This week, I’d like to tackle an interesting approach that I’ve rarely seen used, but is quite useful.
Design by contract, also known as contract programming, programming by contract and design-by-contract programming, is an approach for designing software. It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.
https://en.wikipedia.org/wiki/Design_by_contract
In essence, conditions allow to fail fast. It’s no use to run code if at the end, a computation will fail because of a wrong assumption.
Let’s use the example of a transfer operation between two bank accounts. Here are some conditions:
- Pre-conditions
-
- The transferred amount must be positive
- Invariants
-
- The source bank account must have a positive balance
- After the transfer
-
- The source bank account balance must be equal to the initial balance minus the transfered amount
- The target bank account balance must be equal to the initial balance plus the transfered amount
Naive implementation
One can easily implement pre- and post-conditions "manually":
public void transfer(Account source, Account target, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgument("Amount transferred must be higher than zero (" + amount + ")";
}
if (source.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgument("Source account balance must be higher than zero (" + source.getBalance() + ")";
}
source.transfer(target, amount);
if (source.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalState("Source account balance must be higher than zero (" + source.getBalance() + ")";
}
// Other post-conditions...
}
It’s quite cumbersome to write and quite hard to read.
Checking for invariants translates as checking both as a pre-condition and a post-condition |
Java implementation
You might already be familiar with pre- and post-conditions via the assert
keyword:
public void transfer(Account source, Account target, BigDecimal amount) {
assert (amount.compareTo(BigDecimal.ZERO) <= 0);
assert (source.getBalance().compareTo(BigDecimal.ZERO) <= 0);
source.transfer(target, amount);
assert (source.getBalance().compareTo(BigDecimal.ZERO) <= 0);
// Other post-conditions...
}
There are several problems with the Java approach:
- It makes no difference between pre- and post-conditions
- It has to be activated through the
-ea
launch flag
The Oracle documentation states it explicitly:
While the assert construct is not a full-blown design-by-contract facility, it can help support an informal design-by-contract style of programming.
Alternative Java implementation
Since Java 8, the Objects
class offers three methods that provide some very limited programming by contract approach:
public static <T> T requireNonNull(T obj)
public static <T> T requireNonNull(T obj, String message)
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier)
The Supplier
argument in the last method returns the error message
All 3 methods throw NullPointerException
if obj
is null.
More interestingly, they all return obj
if it’s not.
This leads to this kind of code:
public void transfer(Account source, Account target, BigDecimal amount) {
if (requireNonNull(amount).compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgument("Amount transferred must be higher than zero (" + amount + ")";
}
if (requireNonNull(source).getBalance().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgument("Source account balance must be higher than zero (" + source.getBalance() + ")";
}
source.transfer(target, amount);
if (requireNonNull(source).getBalance().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalState("Source account balance must be higher than zero (" + source.getBalance() + ")";
}
// Other post-conditions...
}
Not only this is pretty limited, it doesn’t really improve readability, especially if you add the error message argument.
Framework-specific implementations
The Spring framework provides the Assert
class that offers a lot of condition checking methods.
As per our own naive implementations, pre-condition checks throw an IllegalArgumentException
if the condition is not met, while post-condition checks throw an IllegalStateException
.
The Wikipedia page above also lists several frameworks dedicated to programming by contract:
Most of the above frameworks are based on annotations.
Pro and cons of annotations
Let’s start with the pro: annotations make conditions obvious.
On the other hand, they suffer from some drawbacks:
- They require bytecode manipulation, either at compile time or at runtime
- They either:
- Are pretty limited in their scope (e.g.
@Email
) - Or delegate to an external language, which is configured as an annotation string attribute and the opposite of typesafe
- Are pretty limited in their scope (e.g.
Kotlin’s approach
Kotlin’s programming by contract is based on simple method calls, grouped in the Preconditions.kt
file:
require
type methods implement pre-conditions and throw anIllegalArgumentException
if not metcheck
type methods implement post-conditions and throw anIllegalStateException
if not met
Rewriting the above snippet with Kotlin is quite straightforward:
fun transfer(source: Account, target: Account, amount: BigDecimal) {
require(amount <= BigDecimal.ZERO)
require(source.getBalance() <= BigDecimal.ZERO)
source.transfer(target, amount);
check(source.getBalance() <= BigDecimal.ZERO)
// Other post-conditions...
}
The valid4J project listed has the same approach. |
Conclusion
As it is often (always?) the case, simpler is better. By just wrapping the check and the exception throwing instructions into a method, one can readily use programming by contract concepts. While no such wrappers are available out-of-the-box in Java, valid4j and Kotlin offer them.