Design by Contract (DBC) is a mindset to design software using Contract. It has several benefits:

  • better maintainability: easier to read and understand code.
  • easier to review code.
  • easier to write unit tests.
  • prevent technical debt.

Contract

A contract is an agreement between 2 or more parties.
Example:

If you give me $5
I will give you a sandwich.

The ultimate benefit of a contract is to avoid surprises. Additionally, a contract helps us to ignore the detailed implementation. For example, consider the above contract, the seller and buyer do not need to care about how a sandwich is made. The contract is independent of the implementation.

Contract in programming

Legal contracts are often long and confusing. To avoid long and confusing programming contracts, we should use logical thinking by asking the following question when reasoning about code: What must be true before (precondition), and what will be true after (postcondition) we execute the code?

A programming contract has 2 parts:

  • precondition: what the caller must be or must possess to execute the contract.
  • postcondition: what is guaranteed to happen after the contract is executed.

The programming contract must have the following properties:

1. Do not reveal the implementation.

Consider the following method:

double sqrt(int x) {

}

Example of a bad contract of this method:

Precondition: x is a positive integer.
Postcondition: return the square root of x using the binary search algorithm.

It reveals the implementation of "using binary search algorithm", which is unnecessary.

2. Unambiguous

Ambiguous preconditions Well-written preconditions
If I give you enough money ... If I give you $5
If the buyer is a child ... If the buyer's age is < 17.
If x is a small number ... If x < 10

A good contract should not confuse reader. Good conditions usually contain the verb is or has.

3. The condition should cover all cases.

// Precondition: 
//    - None.
// Postcondition: 
//    - return true if the list has at least 1 element.
bool isEmtpy(std::vector<int> list) {
	...
}

This contract does not specify the output when the list has zero elements.
We can modify the contract a bit to make it covers all possible cases:

Precondition: None.
Postcondition: Return true if and only if the list has at least 1 element.

4. The contract should have the right balance between precision and flexibility.

Consider the sandwich example

If you give me $5
I will give you a sandwich.

What happens if I give you an amount of bitcoin that is worth $5 or any different type of payment like Credit card, gold ...
Now we have 2 choices:

  1. Make the precondition more precise:

If I give you 5 USD in the form of USD bills or in Bitcoin or Credit card ...

  1. Make the precondition more flexible:

If I give you $5 in an acceptable form of payment ...

In general, the caller wants a more flexible precondition.
The same tradeoff happens with postcondition, if the precision increases, it is more difficult to meet the postcondition. So it is the contract's job maker, i.e SWE, to decide the right balance between precision and flexibility.

DBC in practice

Normally, when engineers first create new methods, they are unlikely to make design mistakes. However, when adding new behaviors to existing methods, they make this mistake all the time and add more technical debt to the project. So before modifying an existing function, ask yourself if you will break the current contract.

I often use DBC mindset when designing functions and I often write the contract as a comment before each public function. For example:

// Precondititon:
// 	- None.
// Postcondition:
//	- Return square root of x if x is >= 0; otherwise throw an exception. 
double sqrt(int x) {
}

When you find it very hard to write contracts, it is a warning.

DBC can't tell you what the beautiful design should be, but DBC can alert you to an ugly design.

Reference