Design by Contract is a software design approach that is helpful when we are building solutions from small building blocks that interact with one another through interfaces or APIs. The “contract” is the definition of how an interface or API is meant to be used. There’s a good description on the C2 Wiki, a reasonably good article on Wikipedia, and concise explanations in the Microsoft .NET documentation and on the Java Practices site.
Eiffel language
Computer science pioneer Barbara Liskov wrote about contract-based software design as early as 1974. Building on that work, Bertrand Meyer created the language Eiffel, which has contract functionality built in. He published a book about Eiffel in 1992.
Eiffel was not strictly a proof of concept for DbC. It was a fully-featured programming languages intended for professional software development. That being the case, what happened to Eiffel? Why is it not more widely known and used?
People who wanted to use Eiffel in a serious way reported the main stumbling block initially was licensing costs, which at one time were as high as $5,000 per seat. After the language was open-sourced, tight restrictions on redistribution of Eiffel code made it infeasible to use in commercial software products, or for proprietary software for internal corporate use.
Design by Contract has always seemed like a good idea, but other programming languages lack built-in support for it and developers must depend on libraries or roll their own DbC support. There’s a lot of information about it online. I’ve written about it on LeadingAgile’s blog, when I tried out some available libraries: Design by Contract, Part 1, Part 2, and Part 3. At the time, I was not happy with the libraries available for DbC.
An exploration
In a Twitter discussion in mid-2020, DbC was mentioned in connection with designing microservices. It seems a natural fit for building solutions on that model. I had written a toy implementation of DbC support for Java in 2018, and the discussion inspired me to play with the idea a bit more. That led to an annotation-based DbC library for Java. This is still on the level of a toy or proof of concept implementation. As service-based solutions become increasingly important, I think we would do well to improve support for DbC in languages and tools.
My toy implementations don’t use the “official” terminology of DbC, such as precondition, postcondition, and invariant. I notice the .NET implementation also doesn’t use that terminology. The reason I didn’t use the terminology in the code is that ultimately all three of those concepts are supported by basic conditional logic. The fact a particular check is conceptually part of a “precondition” or a “postcondition” makes no difference in the implementation.
For instance, the .NET documentation shows that in C# you would write:
Contract.Requires(x != null);
Using my 2018 toy implementation for Java, you would write:
Contract.require(x != null);
In my 2020 annotation-based implementation for Java, you would write:
public void someMethod(@NotNull String x) { Constraints.check(x); ... }
That’s a relatively verbose way to check for a null reference. The idea is that as the number of constraints grows, it might be easier to read and understand the code if each constraint is expressed as an annotation than if they were written out as plain “if” statements. Here’s an example:
void processMultipleConstraints( @NotNull @MinimumLength(value=5) @MaximumLength(value=20) @MustMatch(regex="^[a-zA-Z]+$") String someString, @NotNull @NotEmpty @MustContainKey(key="key1") @MustNotContainKey(key="key2") Map<String, String> testMap, @NotNull @MustBeInRangeExclusive(min=50, max=60) Integer someNumber, @NotNull @MinimumValue(value=10) Double someDouble) { Constraints.check(someString, testMap, someNumber, someDouble); }
Well, I only said it might be easier to read.
In these examples, we might be checking x as a precondition, a postcondition, or an invariant. DbC implementations in various languages (at least, the ones I’ve seen and played with) don’t exactly follow the definitions of precondition, postcondition, and invariant given in the Eiffel documentation. If they did, we would attach precondition checks to a “routine” (in Java or C#, a “method”) and invariant checks would be at the “class” level (assuming the language at hand supports the notion of a “class”).
My 2020 toy project, for instance, attaches annotations to method arguments; neither methods nor classes. The idea is to verify a service was called with sensible argument values, and to throw an exception otherwise. It’s a crude and limited subset of the original idea. Other implementations I’ve seen are also in the nature of variations on the original theme. They are not precise implementations of the concepts described in the Eiffel documentation.
In any case, it’s pretty easy to support DbC, or something practical based on the idea of DbC, in any programming language using conditional statements and, if available, other language features such as assertions.
So what?
Why write about this old topic now? There seem to be two misconceptions floating around that might lead to poor outcomes with service-based solutions. I think we should clarify these points when they come up in discussions with colleagues.
First, there’s the idea that DbC is a testing technique or a test automation practice. I’ve read and heard people asking whether we should use DbC or TDD, as if they were mutually-exclusive ways to accomplish the same goals. This is not so. The microtests we write when we use TDD as a design technique are executed in the continuous integration part of our delivery pipeline. They don’t play a role in production. They help assure quality within individual units of code.
DbC checks are part of the production code and they are executed in production. They help avoid runtime errors caused by violating preconditions or postconditions or by modifying invariants. It’s sensible to use both techniques. Each helps in a different way to achieve robust and reliable solutions.
Second, there’s the idea that “anything goes” at the unit level, because interesting problems almost always occur at the “integration” level. People are starting to ignore or dismiss quality-related practices at the unit level in favor of integration and end-to-end testing. In my experience, we need meaningful testing at all levels of abstraction, as well as additional ways to assure quality beyond pre-release testing.
Also, it’s been shown that paying attention to quality in the small translates into fewer production issues in the large. It’s like building things with Lego® bricks. You can assemble the bricks into anything you can imagine, but the individual bricks do not come apart. We should make our software bricks just as solid as that.
So, we want the logic inside our services to be reliable – TDD helps with that. We want our services to reject invalid client requests immediately rather than allowing undefined behavior to occur – DbC helps with that.