Design by Contract: Part Three
This is the final installment of a three-part series on Design by Contract (DbC).
In Part 1 we reviewed the concept and origin of DbC and showed an example in the original DbC-aware language, Eiffel. We also discussed some common misconceptions about DbC.
In Part 2 we embarked on a search for usable and supported DbC libraries for several mainstream programming languages. We learned that there have been numerous attempts to implement DbC libraries, but very few have stood the test of time. We speculated about the reasons for this.
In this instalment, we’ll implement a DbC library in Java. Our goals are:
- Discover empirically how difficult it really is to implement DbC support
- Demonstrate how application code could look if we took the view that contracts are a natural part of the functionality of an application rather than a separate concern
Our approach will be to step through a conventional Detroit school test-driven development process to implement a class similar to the Dictionary class in the Eiffel example from Part 1, and a “library” (probably just a helper class) to implement precondition and postcondition checks.
The “Requirements”
Even if we intend to test-drive the solution using fine-grained microtests, we don’t need to pretend that we know nothing at all about the domain. In a previous post about test-driven development (TDD), I suggested “we’d like to find the optimal place to meet in the middle, between just enough up front design top-down and the beginning of emergent design bottom-up. That optimal place will vary by context.”
In this case, the context is the creation of a Dictionary class similar to the one presented in the Eiffel example in Part 1 as a mechanism to drive out the design of a DbC library for Java. What we know about the domain is summarized in the contract statements in the Eiffel example (key bits snipped out here):
require count <= capacity not key.empty ensure has (x) item (key) = x count = old count + 1 invariant 0 <= count count <= capacity
That’s already a sketch of the key functionality we want to build. Let’s write the same requirements in the form of executable microtests so we can use them to drive out the solution directly. The following are JUnit5 test cases showing the corresponding parts of the Eiffel contract statements that each microtest case partially or fully supports. This will be our “requirements specification.”
invariant count <= capacity invariant 0 <= count @Test void it_creates_dictionary_with_specified_capacity() { assertThat(new Dictionary(5).capacity(), is(equalTo(5))); } invariant count <= capacity invariant 0 <= count @Test void on_creation_the_count_is_zero() { assertThat(new Dictionary(5).count(), is(equalTo(0))); } invariant count <= capacity invariant 0 <= count @Test void it_rejects_a_negative_capacity() { assertThrows(ContractViolationException.class, ()-> new Dictionary(-1)); } ensure has (x) ensure item (key) = x @Test void an_entry_can_be_added_and_looked_up_by_its_key() { Dictionary dict = new Dictionary(1); dict.put("key1", "value1"); assertThat(dict.get("key1"), is(equalTo("value1"))); } ensure count = old count + 1 @Test void after_adding_an_entry_the_count_is_increased_by_one() { Dictionary dict = new Dictionary(1); int expectedCount = dict.count() + 1; dict.put("key1", "value1"); assertThat(dict.count(), is(equalTo(expectedCount))); } require not key.empty @Test void it_will_not_accept_an_entry_with_a_null_key() { Dictionary dict = new Dictionary(1); assertThrows(ContractViolationException.class, ()-> dict.put(null, "value1")); } require not key.empty @Test void it_will_not_accept_an_entry_with_an_empty_key() { Dictionary dict = new Dictionary(1); assertThrows(ContractViolationException.class, ()-> dict.put("", "value1")); } require count <= capacity @Test void it_will_not_accept_an_entry_when_it_is_full() { Dictionary dict = new Dictionary(1); dict.put("key1", "value1"); assertThrows(ContractViolationException.class, ()-> dict.put("key2", "value2"));
We’ve made some initial design assumptions, which are subject to change as we progress:
- Contract checks will be implemented as assertions
- When an assertion fails it will throw an unchecked exception
- The exception will be named ContractViolationException
So far, we have a clear definition of the domain and a set of specifications in an executable form, so they can check themselves as we implement them. Let’s get started.
Create Dictionary with a Specified Capacity
We’ll start by driving out the creation of a Dictionary with a specified capacity:
class DictionaryTest { @Test void it_creates_dictionary_with_specified_capacity() { assertThat(new Dictionary(5).capacity(), is(equalTo(5))); }
This doesn’t compile because there’s no such thing as “Dictionary.” Let’s fix that:
class Dictionary {}
It still doesn’t compile, as there’s no constructor that takes an argument. Let’s fix that:
class Dictionary { Dictionary(int capacity) {} }
Now it’s complaining that there’s no method capacity()
on Dictionary. Let’s fix that:
class Dictionary { Dictionary(int capacity) {} int capacity() { return 0; } }
The compiler is happy, so let’s run our microtest to be sure it’s failing for the right reason. We believe the right reason is that the value returned from the capacity()
method will not match our expectation. If that’s what happens, we’ll know we’re in a position to implement the capacity()
method.
java.lang.AssertionError: Expected: is <5> but: was <0>
Lo and behold, thar she blows, cap’n.
An implementation:
class Dictionary { Dictionary(int capacity) {} int capacity() { return 5; } }
And the test result:
1 test passed
Now, you might be thinking this is a pretty silly implementation. The reason I want to step through it at this level of detail is to illustrate that it’s the way TDD is done. We won’t continue to show these very fine-grained steps going forward. The reason to take it to this level is safety. As a general rule, we’ll commit changes to version control every time we reach “green” with our microtest suite.
The commit workflow includes updating our local copy of the application with whatever changes other people may have made since the last time we committed. This is called continuous integration. It prevents “merge hell” and makes developers’ lives easier. It also ensures that if the changes we make happen to break existing functionality, we’ll know it was due to the change we committed within the past few minutes, so it will be easy to identify and correct. It’s all about making our lives easier, both during development and when we are supporting the application in production.
(Shh! Don’t tell your manager you’re using techniques that make your life easier while also helping assure high quality code! Managers think this is supposed to be work, and work is supposed to be stressful. So, stop smiling and act stressed!)
Above and beyond the specifications we wrote based on the Eiffel example, we know from our weak implementation of capacity()
that we need the solution to be better. The way to make the solution better is to think of a microtest case that will force us to improve the design. Let’s add a case that specifies a different capacity for the Dictionary, and adjust the test method names to be descriptive of our intent:
class DictionaryTest { @Test void it_creates_dictionary_with_capacity_5() { assertThat(new Dictionary(5).capacity(), is(equalTo(5))); } @Test void it_creates_dictionary_with_capacity_1000() { assertThat(new Dictionary(1000).capacity(), is(equalTo(1000))); } }
Now let’s see if the original case still passes while the new one fails for the right reason:
2 tests done: 1 failed java.lang.AssertionError: Expected: is <1000> but: was <5>
Okay, that looks like where we expected to be. Now let’s improve the implementation of capacity()
.
class Dictionary { private int capacity; Dictionary(int capacity) { this.capacity = capacity; } int capacity() { return capacity; } }
And now…
All 2 tests passed
Now we check in to version control and make sure we didn’t break any existing functionality. (There isn’t any yet, of course, but it’s a good habit.)
At this point, we’ve implemented a Dictionary that accepts a capacity value when it’s created. We can see there’s no “meat” behind it yet. We need to drive out the rest of the implementation by mindfully thinking of microtest cases that guide the emergent design.
Our First Contract
I’m going to skip ahead a bit, now that we’ve established the routine for fine-grained TDD steps. Let’s assume we carried on with the same routine to drive out support for the count()
method, and we’ve got something under the covers to hold Dictionary entries.
We’ve moved forward in time to the point that we have these microtest cases in place:
class DictionaryTest { @Test void it_creates_dictionary_with_capacity_5() { assertThat(new Dictionary(5).capacity(), is(equalTo(5))); } @Test void it_creates_dictionary_with_capacity_1000() { assertThat(new Dictionary(1000).capacity(), is(equalTo(1000))); } @Test void on_creation_the_count_is_zero() { assertThat(new Dictionary(5).count(), is(equalTo(0))); } }
…and our Dictionary implementation looks like this:
class Dictionary { private int capacity; private Map<string,object> contents; Dictionary(int capacity) { this.capacity = capacity; contents = new HashMap<>(capacity); } int capacity() { return capacity; } int count() { return contents.size(); } }
We have a little something working now, but we haven’t forced ourselves to implement any sort of DbC functionality. This “requirement” should get us started on that path:
invariant count <= capacity invariant 0 <= count @Test void it_rejects_a_negative_capacity() { assertThrows(ContractViolationException.class, ()-> new Dictionary(-1)); }
When we add this case, we get a compiler error because there’s no such thing as ContractViolationException
. Skipping ahead a bit, we’ve created that class and now we’re ready to run the tests again. We want the new test case to fail because the Dictionary doesn’t check to see whether the specified capacity is greater than zero. We also want to see all the other test cases pass. If those two things are true, then we’ll be in a position to proceed.
4 tests done: 1 failed org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> Expected :<com.neopragma.contracts.ContractViolationException> Actual :<java.lang.IllegalArgumentException>
The HashMap is throwing IllegalArgumentException because it can’t handle a negative capacity. You might say we could live with that, but we don’t want to expose the innards of the Dictionary class like that. We want the code to be clear about intent: This is a violation of the contract with Dictionary (a domain concept), not merely an invalid argument passed to HashMap (an implementation detail).
We get the test passing with this implementation of the Dictionary constructor:
Dictionary(int capacity) { if (capacity < 1) { throw new IllegalArgumentException(); } this.capacity = capacity; contents = new HashMap<>(capacity); }
So, is this okay? Is this Design by Contract?
Well, sure, yeah, it’s a sort of Design by Contract. In effect, it’s enforcing a contract. It’s simple. It works.
But there’s a problem.
In an earler instalment in this series, I mentioned that people could just write “if” statements instead of using a DbC library, but they don’t actually do so in practice.
I speculated a reason for this may be there’s no “name” for contract enforcement, when all you have is an “if” statement here and there. It doesn’t look or feel like anything different from a random “if” statement here and there.
As our Daoist friends reminded us a couple thousand years ago, if something doesn’t have a name it might as well not exist. People don’t have a way to think about it.
There’s value in making this a “thing” with a name, no matter how trivial the implementation. So, let’s refactor it:
Dictionary(int capacity) { Contract.require(capacity > 0, "Capacity must be greater than zero"); this.capacity = capacity; contents = new HashMap<>(capacity); }
I don’t know about you, but for me this minor change makes a huge difference in how I think about the code. The Contract.require
bit looks strikingly different from a naked “if” statement, and causes me to pause and consider what it’s telling me about the solution.
It tells me there’s a contract in effect here, and this method requires a certain condition to be true before it can continue. The message passed into the exception’s constructor tells me the purpose of the contract in plain domain language, with no need for source comments or separate documentation (both of which I would likely ignore…and so would you).
It’s an important part of the solution design, and it’s right there in front of me; not hiding in a javadoc comment or an aspect, and not shifted out of focus by being coded in an annotation a couple of lines away from where my eyeballs are pointed.
The Rest of It
So, let’s skip ahead a bit more. Actually, let’s skip to the end. We have this set of microtests:
class DictionaryTest { @Test void it_creates_dictionary_with_capacity_5() { assertThat(new Dictionary(5).capacity(), is(equalTo(5))); } @Test void it_creates_dictionary_with_capacity_1000() { assertThat(new Dictionary(1000).capacity(), is(equalTo(1000))); } @Test void on_creation_the_count_is_zero() { assertThat(new Dictionary(5).count(), is(equalTo(0))); } @Test void it_rejects_a_negative_capacity() { assertThrows(ContractViolationException.class, ()-> new Dictionary(-1)); } @Test void an_entry_can_be_added_and_looked_up_by_its_key() { Dictionary dict = new Dictionary(1); dict.put("key1", "value1"); assertThat(dict.get("key1"), is(equalTo("value1"))); } @Test void after_adding_an_entry_the_count_is_increased_by_one() { Dictionary dict = new Dictionary(1); int expectedCount = dict.count() + 1; dict.put("key1", "value1"); assertThat(dict.count(), is(equalTo(expectedCount))); } @Test void it_will_not_accept_an_entry_with_a_null_key() { Dictionary dict = new Dictionary(1); assertThrows(ContractViolationException.class, ()-> dict.put(null, "value1")); } @Test void it_will_not_accept_an_entry_with_an_empty_key() { Dictionary dict = new Dictionary(1); assertThrows(ContractViolationException.class, ()-> dict.put("", "value1")); } @Test void it_will_not_accept_an_entry_when_it_is_full() { Dictionary dict = new Dictionary(1); dict.put("key1", "value1"); assertThrows(ContractViolationException.class, ()-> dict.put("key2", "value2")); } }
…and this implementation of Dictionary:
class Dictionary { private int capacity; private Map<string,object> contents; public Dictionary(int capacity) { Contract.require(capacity > 0, "Capacity must be greater than zero"); this.capacity = capacity; contents = new HashMap<>(capacity); } public void put(String key, Object value) { Contract.require(StringUtils.isNotEmpty(key), "Key cannot be empty"); Contract.require(contents.size() < capacity, "Dictionary is full"); contents.put(key, value); } public Object get(String key) { return contents.get(key); } public int capacity() { return capacity; } public int count() { return contents.size(); } }
Our DbC “library” consists of this:
public class Contract { public static void require(boolean expression, String message) { if (!expression) throw new ContractViolationException(message); } }
We could add an ensure()
method to make it visually apparent when we intend to check a postcondition as opposed to a precondition, but the logic would be the same as the require()
method.
We don’t have explicit support for invariants as such, but it’s clear from this example that the precondition and postcondition checks enforce the invariants.
Sometimes, the implementation will also support a contract without any extra effort on our part. For instance, the Map interface automatically enforces:
ensure has (x) item (key) = x count = old count + 1 invariant 0 <= count
The point is that it’s pretty simple to implement DbC support in any language. The visual aspect of it…the fact the code becomes substantially more self-describing…seems valuable to me.
Conclusion
Some take-aways:
- Design by Contract offers another way to improve the resiliency, robustness, and correctness of our code
- Contracts are part of the core functionality of our application, and not a separate concern
- Contract-enforcement method calls tend to make the code more expressive of intent than naked conditional statements
- It is trivially easy to implement DbC support in any programming language