Written on
Contracts in the Domain Model
This blog-post gives a brief introduction to contract-oriented programming aka design by contract in general and in relation to Groovy with gcontracts assertions. The Problem With each Grails application generation of test-cases is simply part of the game. Whenever you create a domain-class the corresponding unit-test is generated directly. With the test-first approach of Test-Driven Development (TDD) each iteration in the development process starts with the task of writing tests specifying the intended behavior of the targeted components. E.g. use-case: a customer in the coffee-house might order several cups of coffee
class Customer {
def Order createOrder() { ... }
}
class Order {
def void addItem(Item someItem) { ... }
}
class ItemTests {
@Test
def void testCreateItem() {
def item = new Item(COFFEE_TYPE.Cappuccino, 1) // (type, amount)
assertEquals COFFEE_TYPE.Cappuccino, item.coffee_type()
assertEquals 1, item.amount()
}
}
new Item(COFFEE_TYPE.Cappuccino, 1)
is nothing more than a function call that generates a new item object, with (COFFEE_TYPE, NUMBER) as parameters. Set theory would map that function to the following expression:



@Requires
or @Ensures
:
class Item {
private CoffeeType coffee_type
...
@Requires({ type != null && amount > 0 })
def Item(final CoffeeType type, final Integer amount) { ... }
@Ensures({ old -> old.coffee_type != coffee_type })
def void change_coffee_type_to(final CoffeeType type) { ... }
}
@Invariant({ coffee_type != null && amount > 0 })
class Item { ... }
- Class-InvariantsWhenever a class B with an invariant B(iv) extends a class A with an invariant A(iv), B(iv) indeed is logically combined with A(iv), meaning that every class-invariant check in B is actually represented by the boolean expression B(iv) AND A(iv).
- PreconditionsWhenever a method B with a precondition B(pre) overrides a method A with a precondition A(pre), B(pre) is logically combined with a boolean OR, resulting in B(pre) OR A(pre) assertions for all method calls of B. The precondition is said to be weakened for redeclared methods.
- Postconditions Whenever a method B with a postcondition B(post) overrides a method A with a postcondition A(post), B(post) is logically combined with a boolean AND, resulting in B(post) AND A(post) for all method calls of B. The postcondition is said to be strengthened for redeclared methods.
- Tests are built-in - especially in complex object-oriented domain models, it is getting hard to (mentally) debug code and getting an impression of dependencies between components and side-effects between them. Contracts help to ensure that at least objects stay in a defined state that satisfies the intention of its supplier.
- Implicit programmer assumptions are made explicit in the class's or method's contract - that's just one step closer to get the domain documented in the source code.
- Contracts enforce correctness - identifying contracts in the domain model is not an easy task and it is surely not done in a single iteration (of course), but once programmers get on it they really have to think about the specification and its mapping in the domain model and its contracts, thus enforcing correctness of understanding how business requirements shall be implemented.
- Contracts allow for rapid changing of business requirements - rapid changing of business requirements is one of the most annoying facts for programmers. Given a set of components and related unit tests, a single business requirement might cause rewriting of all components and their corresponding unit-tests. Especially rewriting the unit-tests might introduce or slip through other bugs since, as we've seen, unit-tests usually cover only a small portion of the problem domain. When applying contract-oriented programming, you might change class-invariants etc. but at least those assertions already cover a large portion of the problem domain.
- Contracts enforce readability - when writing code, a programmer always has a mind-map of all objects and assumptions on objects and their particular state in mind. Those assumptions are said to be implicit, since they are not explicitly stated in source-code. From my experience, when having to switch between multiple projects it gets even worse and the source-code owner itself forgets about its implicit assumptions - which is the best starting point of introducing new bugs.
- Contracts enforce stability - dynamic languages are great for rapid application development, but when it comes to maintainability dynamic code can be hard to read and - due to its dynamic nature and other properties - pretty fragile. Changes in components lead to potential side-effects which might not be spotted by test-suites. Assertions provide a way to determine programming bugs at run-time (mostly in test and not in production-environments).