Contracts in the Domain ModelThis 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 When executing the test-first approach we would start with writing a unit-test for class Item, going on to class Order and finally writing one for class Customer. e.g. ItemTests As you might have already noticed it is pretty hard to find tests that maximize the covering of business requirements. Most of the time, unit-tests are written in the first iteration, in subsequent iterations that tests are simply rewritten to "get green". Your mileage may vary, but the main problem with test-cases are that each single test-case only tests a small fraction of the input domain, that is, the cartesian product of the current function: e.g. the constructor call to
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: Meaning that the overall set of parameter tuples is mapped by the cartesian product between all COFFEE_TYPE and all INTEGER values, the result is a single tuple out of all cartesian product tuples. Just imagine the cartesian product as a set that contains all possible combinations of this function's parameter values. The unit-test tests exactly one single pair out of the cartesian product: In this simple example it gets pretty obvious that even if tests have a high code coverage, they hardly never can satisfy testing a large portion of the problem domain. That is where contracts come into play. An Introduction to Contract-Oriented Programming Contracts are the clue between the specification and the implementation of a certain software artifact, in object-oriented speak: classes. Formally, contracts are based on so-called Hoare rules which provide a formal system towards logical rules of reasoning on the correctness of computer programs . The main feature of this formal system is the so-called Hoare triple: where P and Q are assertions and C represents a set of commands. A concrete Hoare triple states that before execution of C, the precondition P must be true, after the execution the postcondition Q must hold. Pre- and postconditions represent so-called assertion types. In a gcontracts enabled Groovy program you might use pre- and postconditions by using either
@Ensures: As you can see pre- and postconditions are applied on constructors or methods only. In addition, there is the need to express assertions which must be preserved by all methods and every instance of that class; these assertion types are called class-invariants: A class-invariant must be preserved after every constructor call and before/after each method call. When executing a method with a precondition P, a postcondition Q and a class-invariant C, before a method call the assertion expression P AND C and after a method call expression Q AND C must be true. When inheritance comes into play we'll have to have a separate look at all assertion types:
- 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).