Design by Contract: How can this approach help us build more robust software?

Design by Contract: How can this approach help us build more robust software?

It is hard enough to find an error in your code when you are looking for it; it is even harder when you assume that your code is error-free. - Steve M

It is common today in any business relationship for contracts to be made between two parties. In fact we see contracts everywhere. When you buy something on an e-commerce there is a contract between the two parties. One party expects to receive the product they paid for and the other party expects the payment method to be valid. What we will talk about in this article is more about contracts, but in the context of software. We will see that to have good contracts it is important to understand the requirements. So let's get started right away!

What is a requirement?

The dictionary has this definition:

something that you must do, or something you need

It is an excellent definition. But how does it fit in the software engineering context? A requirement is a feature of some functionality in software engineering that someone needs to add facilities and solve common everyday problems. Furthermore the requirement describes what each part of the software should or should not do. This phase is a user-dominated phase and translates the ideas or views into a requirements document.

So if a software house has been hired to develop a digital solution for a company. The two parties need to get together and understand the problem. Once they understand the problem scenarios, they start planning the solution in theory, and with the planning they come up with the requirements that solve the problem and will guide the software development. If these requirements are not followed by the developers, the system will not work as the company expects.

For this reason, as we mentioned at the beginning of the article, it is common to have agreements or contracts that seek to consolidate the duty between two parties. This way both parties can work without unforeseen events and following the requirements. But what is the point of planning, understanding the problem, and finding a solution, if the developer cannot clearly understand the requirements? This is why many companies today understand the importance of documenting these important points. And we can say that when we read these documents with the step-by-step requirements of each functionality, what it should or should not do, then we can say that we have contracts. Now we need to transform these contracts into code, and that is when we get into design by contract. Let's understand more about this approach to software development.

What is DbC?

Created by Bertrand Meyer in the 1980, Design by Contract (DbC) allows modules of a system to establish a contractual relationship with each other, which is characterized by the specification of obligations and benefits. Based on the contracts described, it is checked whether the code satisfies what is written in the contracts. Don't worry, we will see code examples about this soon. But let's focus on understanding the obligations and benefits better. Every contract in real life requires an obligation and a benefit if everything is satisfied. See this example:

Obligations (Must ensure precondition)

Client: Must be with the ticket and at the theater before 7 pm to watch the theater performance.

Benefit (May benefit from postcondition)

Client: Watch the theater performance.

On the other side, the supplier of the event, which in this case is the theater (establishment), also has obligations and benefits:

Obligations (Must ensure postcondition)

Supplier: Should allow the client to see the theater performance.

Benefit (May assume precondition)

Supplier: Should not allow the customer to enter without paying the ticket or after 7 pm!

Both sides, client and supplier, have well-defined benefits and obligations! What if one side does not agree with the contract? Then the flow does not occur and the customer is barred. In case this analogy got you a bit confused, take a look at this one from the Eiffel language documentation:

image.png

So we can understand that what one party sees as an obligation is a benefit to the other. A contract document protects both the client, by specifying how much should be done, and the supplier, by stating that the supplier is not liable for failing to carry out tasks outside of the specified scope.

Did you notice these words in the above situations? Two very common ones when we talk about design by contract, precondition and postcondition. Let's understand each one.

Precondition

The dictionary defines a word precondition as:

something that must happen or be true before it is possible that something else can happen;

Let's focus on the being true part before it is possible for something else to happen.

Let's think about the case of the customer who wants to see the theater play. In order for it to be possible for him to watch it, the customer needs to be at the theater before 7 pm. Must be with your ticket and before 7 pm at the theater. If do not meet these requirements, not in compliance with the contract and the customer will not be able to watch the performance. In other words, the vendor tells customers, "this is what I expect from you".

We can now bring this concept more into the software context. So imagine that the method we are calling expects something to be in place before or at the point the method is being called. There is no guarantee that the operation will execute as it should unless the precondition has been met.

A more real world example, suppose the method you are creating does not accept negative numbers, so you might be imagining something like this:


    if(value < 0) {
      throw new RuntimeException("Value cannot be negative.");
    }

The precondition ensures that the input values received by a method meet what it requires. So preconditions mean what the method needs to function correctly.

See another example in code. Let's imagine that you are working on a calculator. What would be a precondition for this method? We can imagine, well, if the method is the sum of two values, we first expect these two values to be integers and their values to be above zero. In most programming languages we don't need to create a conditional if statements to check the types, because this is an inferred feature already. The only thing we need to do is to check if the value is greater than zero:

using System;
using System.Globalization;

namespace app
{
    public class HelloWorld
    {
        public static void Main(string[] args)
        {
            var test = sum(1, 2);
            Console.WriteLine(test);
        }
        // method with a precondition
        public static int sum(int a, int b)
        {
              if (a <= 0 || b <= 0)
            {
                throw new ArgumentException("a and b must be positive numbers");
            }
            return a + b;
        }
    }
}

I'll be tedious in repeating, if the precondition is not met, the contract is not being followed, so we throw an ArgumentException in this method that indicates that something is wrong. Remembering that this is for didactic purposes, in more robust software, there are much more complex checks. For now we are in the basics of design by contract.

I hope it has been clear by now what a precondition is. But now let's talk about postconditions and why they are so important!

Postcondition

In wikipedia the definition found for postcondition is this:

A postcondition is a condition or predicate that must always be true immediately after the execution of some section of code or after an operation.

To make the definition simpler. Postconditions: what the method guarantees as results. The method says, "this is what I promise as a result, this is what I guarantee". So, in the calculator example, what we guarantee as a final result is the sum of two integers that were entered by input. So if in the middle of the process, after the precondition is met, the developer needs to manipulate some input value in the method, the postcondition should guarantee that all is well. Let's look at an example:

// namespace app

using System;
using System.Globalization;

namespace app
{
    public class HelloWorld
    {
        public static void Main(string[] args)
        {
            int a = 2;
            int b = 2;
            DateTime? date = DateTime.Now;
            var test = sum(a, b);
            Console.WriteLine(test);
        }
        // method with a precondition and postcondition
        public static int sum(int a, int b)
        {
            if (a <= 0 || b <= 0)
            {
                throw new ArgumentException("a and b must be positive numbers");
            }

            int c =  subtract(a);

            if(c <= 0)
            {
                throw new ArgumentException("c must be positive number");
            }
            var result = c + b;
            return result;
        }

        // subtract method
        public static int subtract(int a)
        {
            // note that the precondition and postcondition is not checked in the method
            var result = a - 1;
            return result;
        }
    }
}

So if something happens and in the process of manipulating the input values, some input gets negative integer, the code will return an error in the postcondition.

In the calculator example, we have a manipulation of the value of the input represented by the letter "a " this input is manipulated and its value subtracted by another method. Now the letter "c" receives the manipulated value and adds it to "b". The postcondition of this method is do not return negative numbers. In addition the postcondition of this method is ensuring that subtract did not return something invalid, i.e. a negative number, if the subtract method returned something invalid we have the postcondition to warn us about this condition that should not occur in the contract. So the difference is preconditions ensure that the input values received by a method conform to what the method requires. Postconditions ensure that the method returns what it promises to other methods. Why we need a postcondition? Our code is simple, it is just the sum of two values. But in more complex situations, with calculations that require more mathematical operators and need the help of other methods or classes, some bug may appear and if it appears and our input becomes negative, we will have problems and even return invalid values! The postcondition ensures that if there is a bug in the implementation, the method throws an exception instead of returning an invalid value.

Maybe you are wondering: Why not add a null check? What if these values come back as null?

In the industry, possibly some properties could actually be null, because they are not required to be persisted in the database. Not in our didactic case. I am showing examples with C#, the language itself already infers that I don't need to worry about checking if an int type equals null because it is not of type nullable, as the output below shows:


The result of the expression is always 'false' since a value of type 'int' is never equal to 'null' of type 'int?' csharp(CS0472)

There are situations where we need to check whether some property is null or not, for example:

// a little bit of logic here
var userExist = await _usersRepository.GetById(userId, token);
        //Postconditions 
        if (userExist == null)
        {
            return CustomResponse<bool>.Failed(new DomainException("This user does not exist."));
        }

This method doesn't show it, but we see that it receives a userId as a parameter. Also it is important to emphasize GetById(), if it doesn't find a matching id, probably the method will return null, not the best scenario, but we see this a lot in the industry. We see that it is possible to guarantee postconditions by checking for null values as well and throwing exceptions to stop the flow of the program. This can help avoid bugs!

How many times have you come across a new flow within a program, that after some changes in the code the method returned null? I have encountered many such situations. The return of another method affects the behavior of another method, mainly because these returns, if not analyzed carefully, can break the normal flow of the software. You have probably spent hours debugging a software until you understand that the method that queries the base is returning null by some parameter, since most of the time we are not expecting this to happen, the software has unexpected behaviors.

Postconditions have the important role of enforcing the contract and ensuring that the entire flow continues as normal.

Now it is time to talk more about the assert keyword in Java.

Assert a great ally in Java

If you have worked or are working with Java, you probably know the assert keyword. However, it remains a little-known keyword that can drastically reduce boilerplate and make our code more readable.

Often times in our code we need to verify certain conditions that might prevent our application from working properly.

Therefore, for backward compatibility, the JVM disables assertion validation by default. They must be explicitly enabled using either the -enableassertions command line argument, or its shorthand -ea:

java -ea com.baeldung.assertion.assertion

Assert is useful because it allows you to do quick and simple checks without large if structures. This makes the code readable and the unit tests easy to understand.

Here is an example:


 public Customer registerNewCustomer(Customer customer) {
    String name = customer.getName();
    boolean isValidName = customer.isStringOnlyAlphabet(name);

    assert isValidName : "Name should contain only alphabets"; // the magic occurs here, if return false the code throw a Error.

    return customerRepository.save(customer);
  }

Okay, but what happens if the method returns an invalid value? See the error thrown:

Captura de Tela 2022-11-16 às 20.35.20.png

The Java Virtual Machine (JVM) throw an AssertionError. If you want a more informative message, use the assert statement expressed below:

assert BooleanExpr : expr;

A good reason to use assert is when you want to stop the program immediately, rather than proceeding with an undesired state. This is usually related to the design philosophy of the fail-fast system.

Assertions are very similar to exceptions; in fact, like exceptions, they signal a problem, but unlike exceptions - they do not suggest any alternative execution path, but simply fail.

Assertions should be used to reflect the system specification at a certain point in the code. Ultimately they say "for this requirement to be met, this property must hold at this point in the code".

Exceptions are different because you know that unexpected conditions have arisen due to external system failure (incorrect parameters, lack of resources, etc.). They should be used to handle abnormal code behavior at production time, usually caused by external events that prevent the code from continuing its normal execution. Exceptions must be handled in such a way that the program recovers from the failure to exhibit acceptable behavior.

Assertions are no substitute for exceptions. Unlike exceptions, assertions don’t support error recovery (assertions typically halt program execution immediately — AssertionError isn’t meant to be caught) they are often disabled in production code and they typically don’t display user-friendly error messages (although this isn’t an issue with assert). It’s important to know when to use exceptions rather than assertions.

It is very important to understand these differences, and to be clear, you need to understand these concepts and continue to use Exceptions or CustomExceptions in your software. Just using AssertionError will not make your life easier!

When to use assert in Java for pre and post condition?

This is a very important question, and a difficult one at the same time. It is often said that the assert keyword is very useful at development time, but should not go into production. Others say that this is an excuse of those who don't know how to use assert well.

Everything that I studied and put into practice showed me that it is a very useful resource but to validate errors in programming logic. It is also possible to take it into production, but obviously one has to be careful. We should never replace all our if/else structures in the code base, just because we think this is a nice feature in Java. Throwing custom exceptions when an argument is invalid is still better.

It is also often said that it is not appropriate to use an assertion to validate an argument in this public method.

But why? The reason many point out is that the focus of assertions is to validate the logic and tell us: look we seem to have a bug here, a seemingly impossible situation just occurred, better check it out quick! In particular, an assertion failure indicates "there is a bug here", but "here" is somewhere internal to your code, and the cause of the failure can only really be determined by debugging.

I believe that we should understand each situation well and extract the features that each programming language provides in a balanced way. In my tests I have not seen a problem when using it to check arguments passed in public methods. But obviously we should be careful. I understand that checking arguments in public methods is sometimes necessary, in certain moments we know that argument will not arrive invalid in that specific flow of the program. Also, checking arguments of public methods is in the general scope of bug checking, so it should be treated as such, and the mechanism we already have for detecting bugs is assertions.

In conclusion, it doesn't matter what means you use to check preconditions and postconditions. The important thing is to be aligned with the rest of the team and understand the pros and cons of each feature!

Invariants

The Eiffel language documentation which is fully adherent to preconditions and postconditions defines invariants in the following words:

An assertion that describes a property that contains all instances of a class is called a class invariant.

Perhaps not very helpful, perhaps a dictionary definition to better understand the meaning of the word:

not changing;

Right so we know that an Invariant, literally, means something that does not change or vary. Based on this we can bring it more into the software context. Invariants are conditions that must always occur before and after the execution of a method. An invariant is therefore a condition that holds throughout the lifetime of an object. It represents something that is always true and can never change. The method tells clients, "if this was true before you called me, I promise it will still be true when I'm done". We can say that an invariant is a combined pre-condition and post-condition. It has to be valid before and after a call to a method.

Can we use asserts in Java to check invariants? Yes you can. But it would be interesting to check if the state remains correct only at the end. You can create helper methods for this or using library. We can see an example in Spring Boot Java and another in C#:

  public Customer selectCustomerByPhone(String phone) {
    Customer customer = customerRepository.selectCustomerByPhone(phone);

    // Invariant: this method should never return null
    Assert.notNull(customer, "Customer with phone " + phone + " does not exist");
    return customer;
  }

In the code above, it is clearly stated that the method should never return null if it does not find a user with that phone number. Obviously, this is a very didactic example to understand the invariants.

This feature is available in Java, specifically for the Spring Boot framework, so let's take a closer look with another example:


public class Car {
    private String state = "stop";

    public void drive(int speed) {
        Assert.isTrue(speed > 0, "speed must be positive");
        this.state = "drive";
        // ...
    }
}

Assert class is a library, and we know that many developers like libraries, others hate libraries. But something interesting is that within the Java ecosystem, there are solid packages that can make your day to day life easier. In the example above, using the Assert.isTrue method, we are practically declaring a precondition. See that we have many methods to be used, we don't need to replace all the conditional structures in the software with this feature, but we can slowly use it at the right moments, mainly because it makes it easier to read and also helps us to slowly adhere to the DbC:

public void сhangeOil(String oil) {
    Assert.notNull(oil, "oil mustn't be null");
    // code here...
}

But unlike the native java assert keyword approach, on failure an IllegalArgumentException or IllegalStateException is thrown. Check out more in the documentation and see if this makes sense for your project.

Continuing with our invariant check, instead of using a library, you can create your own method as in the example below:

public class Basket {

  public void addItem(Product product, int qtyToAdd) {
    // another code ...
    assert invariant() : "Invariant does not hold";
  }

  public void removeItem(Product product) {
    // another code ...
    assert invariant() : "Invariant does not hold";
  }

  private boolean invariant() {
    return totalValue.compareTo(BigDecimal.ZERO) >= 0;
  }
}

A shopping cart can never have negative numbers, this would cause serious problems in production for an ecommerce site. This is clearly an invariant, a shopping cart can never have negative numbers. And following the design by contract we should check this before returning and finishing the method flow. So the solution to avoid repeating code in the methods that manipulate the cart, is to centralize this in a private method that checks the state of the cart. Pretty simple isn't it?

Now in .NET, for those who work with C#, we don't have many facilities to work with contracts. But there are resources. This I will present in part two of this series of articles about design by contracts. But first, I will present a code approach that is interesting to verify invariants:

public static void Check(bool condition, string conditionDescription)
        {
            if (!condition)
            {
                throw new AssertException(ExceptionDescription("Invariant", conditionDescription));
            }
        }

        private static string ExceptionDescription(string assertionType, string description)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0} failed. The expectation was '{1}', but this is false.", assertionType, description)
        }

The method is simple, but its purpose is to check if that condition is still valid. But it is not the only approach. You can create methods like, for example, RequireIsNotNull. The approach of checking for invariants, in the case of the calculator would look like this:

using System;
using System.Globalization;

namespace app
{
    public class HelloWorld
    {
        public static void Main(string[] args)
        {
            int a = 1;
            int b = 2;
            DateTime? date = DateTime.Now;
            var test = sum(a, b);
            Console.WriteLine(test);
        }
        // method with a precondition
        public static int sum(int a, int b)
        {
            if (a <= 0 || b <= 0)
            {
                throw new ArgumentException("a and b must be positive numbers")
            }

            int c =subtract(a);

            if(c <= 0)
            {
                throw new ArgumentException("c must be positive number")
            }
            var result = c + b;
            Assertion.Check(result > 0, "result must be positive");
            return result;
        }

        // subtract method
        public static int subtract(int a)
        {
            // note that the precondition and postcondition is not checked in the method
            var result = a - 1;
            return result;
        }

        public static void Check(bool condition, string conditionDescription)
        {
            if (!condition)
            {
                throw new AssertException(ExceptionDescription("Invariant", conditionDescription));
            }
        }

        private static string ExceptionDescription(string assertionType, string description)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0} failed. The expectation was '{1}', but this is false.", assertionType, description)
        }
    }
}

Benefits of DbC

  • DbC states what should be met by a customer (caller) in preconditions, what should be met by a provider (received) in postconditions, and what is always true in all conditions. Based on these facts, we can quickly understand who should take responsibility for repairing defects. So we can take a bug and say, "class B, it's returning something that it shouldn't!".

  • Writing contracts defines business specifications for modules. The code gets documented in a way that is much easier to understand the logic.

  • Preconditions and postconditions also play a role in separating responsibilities.

  • Preconditions allow the vendor to rely on guarantees provided by the customer.

  • Post-conditions allow the customer to rely on warranties provided by the supplier.

  • A systematic approach to building object-oriented systems with fewer bugs (every system will always have bugs).

  • Testing is much easier, because it involves understanding the contracts and their requirements and benefits.

Conclusion

This technique helps us to better understand the requirements and to think about the behavior of our code if something does not go as expected. It assumes that based on preconditions and postconditions, the code starts to stay more solid and less susceptible to bugs. Perhaps a more real-world approach makes it easier to understand in the corporate world. So I hope to return soon with an approach using spring boot features to better exemplify its advantages and facilities. Design by contract showed me that we developers think of code more as random functionality than in contracts with benefits and obligations of interacting classes. That's why I will leave at the end the references of articles I read that helped me a lot to understand that programming is not only about coding, but also about understanding these contracts that the objects establish between themselves and what each one expects in order to execute their tasks.

Thanks for reading!

References:

Eiffel Documentation

Effective Software Testing - Book by Mauricio Aniche

Applying Design by Contract - Bertrand Meyer