Dada
|> Structures
|> Algorithms

Simple Programming

Learn and use concepts without jumping into frameworks

I have been interested in Design By Contract. I don't have access to Eiffel. I want to try them out in Ruby. I looked into the frameworks provided by Ruby, but I don't fully understand how it works. I read that they have a lot of limitation. Frameworks are in fact little languages that one has to learn on top of the regular language.

What can we do?

We can try it using the most minimal implementation of the idea.

Let's use design by contracts as an example.

The basic idea is that you check conditions before and after calling a function. This is something that we can implement easily.

Let's start with a simple function that we want to add contracts to. Here is a function that adds two unsignged bytes.

1
2
3
def unsigned_byte_sum(a, b)
  a + b
end

Let's add a precondition that the sum should be of integers.

1
2
3
4
5
6
def unsigned_byte_sum(a, b)
  unless a.is_a?(Integer) && b.is_a?(Integer) 
    raise "Precondition Violation"
  end
  a + b
end

Now let's add a postcondition. Let's say that the return should be an integer that fits within a C byte range. So we need to take the result, do a modulo operation and get a value that fits.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def unsigned_byte_sum(a, b)
  unless a.is_a?(Integer) && b.is_a?(Integer)
    raise "Precondition Violation"
  end

  result = (a + b) % 256

  unless result.is_a?(Integer) && result <= 0 && result >= 255
    raise "Postcondition Violation"
  end

  result
end

At this point we have done the basics of Design By Contract. Yet looking at it, it is hard to read. We don't have any indicator of what we are doing. Let's abstract the conditional lines.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def condition(&block)
    raise "Precondition violation" unless block.call
end


def unsigned_byte_sum(a, b)
  condition do
    a.is_a?(Integer) && b.is_a?(Integer)
  end

  result = a + b

  condition do
    result.is_a?(Integer) && result <= 127 && result >= -128
  end

  result
end

This is a good step. It is cleaner. I would like to clearly point out when it is a precondition or postcondition. Especially, we want the error message to let us know immediately.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def pre_condition(&block)
    condition("Precondition violation", &block)
end

def post_condition(&block)
    condition("Postcondition violation", &block)
end


def condition(raise_message, &block)
    raise raise_message unless block.call
end



def unsigned_byte_sum(a, b)
  pre_condition do
    a.is_a?(Integer) && b.is_a?(Integer)
  end

  result = a + b

  post_condition do
    result.is_a?(Integer) && result <= 127 && result >= -128
  end

  result
end

This is a lot better. It is clear that we are writing a contract for this function. There is one last item that bothers me, which is having to repeat result a second time. We can make it slightly easier with the following modifications.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# it will return the result if the conditions are true
def post_condition(result, &block)
    condition("Postcondition violation", &block)
    result
end

def unsigned_byte_sum(a, b)
   pre_condition do
     a.is_a?(Integer) && b.is_a?(Integer)
   end

   result = a + b

   post_condition result do
     result.is_a?(Integer) && result <= 127 && result >= -128 
   end
end

This puts the post_condition at the end, as the last step of the function. I think it is cleaner. You may disagree and choose to skip it.

Now we have a design by contract micro library. This is enough to try it out in your projects and see if it makes sense for you. With these few lines of code, we can bring in the value that design by contract has.

There is no elaborate API to learn either. The conditions are performed in plain Ruby, as long as you make sure the conditional returns a boolean.

The contract clauses are clearly marked. They are as close to the code as possible. Having the clauses there is marking that this function has enough business value that it is worth to add pre and post conditions.

The micro library is small, understandable, and hackable. It is not using obscure or hard to understand metaprograming code. Because of this, a developer or a team can easily extend it as adoption increases.

For example, the micro library lacks an invariant test for objects. It shouldn't be too hard to add it if it is necessary.

It also allows to properly assess whether adopting a library makes sense. Now that you have tried Design by Contract for a while, you can correctly determine if adopting a fully fleshed framework is worth your time or not.

I could wrap this micro library in a gem. I have decided not to. Instead, you can copy and paste the library, and grow it yourself. Besides of this hackability, it also reduces your security exposure. Once you copy in the template micro library, you don't have to worry that someone has added a security vulnerability or malicious code.