JVM contracted programming
Hello, Habr! I present to you the translation of the article " Programming by contract on the JVM " by Nicolas Fränkel.
This week I would like to tackle an interesting approach that I rarely saw, but it is very useful.
In essence, conditions interrupt work. It makes no sense to run the code if, at the end, the calculation fails due to an incorrect assumption.
Let's look at an example of a transfer operation between two bank accounts. Here are some conditions:
Preconditions:
Constants:
Post-conditions:
It is easy to implement the pre- and post-conditions “manually”:
Such code is cumbersome and difficult to read.
You may have already worked with pre and post conditions using the assert keyword :
There are several problems when using the Java approach:
Oracle documentation explicitly points to this:
Starting with Java 8, the class
More interesting is what they return, if
Not only does this impose restrictions, it also degrades the readability of the code, especially if you add an error message argument.
The Spring Framework provides a class
According to its own implementations,
The Wikipedia page above also lists several contract programming frameworks:
Most of the above frameworks are based on annotations.
Let's start with the pros: annotations make the conditions obvious.
On the other hand, annotations are not without flaws:
Kotlin contract programming is based on simple method calls grouped in a file
Rewriting a parent snippet with Kotlin is pretty simple:
Since this is a frequent case, the simpler the better. Just wrapping the check and throwing exceptions into the method, you can easily use programming on the concepts of the contract. Although such shells are not available in Java, valid4j and Kotlin offer them.
Thank you for your attention, see you soon!
This week I would like to tackle an interesting approach that I rarely saw, but it is very useful.
Wikipedia
Contract design, also known as contract programming, is an approach to software development. It requires software developers to define formal, accurate, and verified interface specifications for software components that extend the usual definition of abstract data types with preconditions, postconditions, and invariants. These specifications are called “contracts,” in accordance with a conceptual metaphor for the terms and conditions of business contracts.
Wikipedia
In essence, conditions interrupt work. It makes no sense to run the code if, at the end, the calculation fails due to an incorrect assumption.
Let's look at an example of a transfer operation between two bank accounts. Here are some conditions:
Preconditions:
- The amount transferred must be positive.
Constants:
- The original bank account must have a positive balance.
Post-conditions:
- The balance of the account of the source bank should be equal to the initial balance, minus the amount of the transfer.
- The balance of the target bank account must be equal to the initial balance plus the amount transferred.
Manual implementation
It is easy to implement the 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...
}
Such code is cumbersome and difficult to read.
Java implementation
You may have already worked with pre and post conditions using 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 when using the Java approach:
- There is no difference between pre- and post-conditions
- Code must be run using the run flag
-ea
Oracle documentation explicitly points to this:
Although the assert construct is not a complete contract construct, it can help maintain an informal contract programming style.
Alternative Java implementation
Starting with Java 8, the class
Objects
offers three methods that impose restrictions on contract programming:public static
T requireNonNull(T obj) public static
T requireNonNull(T obj, String message) public static
T requireNonNull(T obj, Supplier messageSupplier)
The argument Supplier
in the last method returns an error message
All 3 methods throw NullPointerException
, if obj
equal null
. More interesting is what they return, if
obj
not equal null
. This leads to the following 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 does this impose restrictions, it also degrades the readability of the code, especially if you add an error message argument.
Implementations for specific frameworks
The Spring Framework provides a class
Assert
that offers many stateful methods:According to its own implementations,
IllegalArgumentException
precondition checks throw an exception if the condition is not met, while post-state checks throw an exceptionIllegalStateException
. The Wikipedia page above also lists several contract programming frameworks:
Most of the above frameworks are based on annotations.
Pros and cons of annotations
Let's start with the pros: annotations make the conditions obvious.
On the other hand, annotations are not without flaws:
- They require bytecode manipulation either at compile time or at run time.
- They are quite limited in scope (e.g. Email )
- Translated into an external language that is configured as an attribute of the annotation string
Kotlin approach
Kotlin contract programming is based on simple method calls grouped in a file
Preconditions.kt:
require
methods implement preconditions, and if they are not, it will be thrownIllegalArgumentException
check
methods implement post-conditions, and if they are not, then it will be thrownIllegalStateException
Rewriting a parent snippet with Kotlin is pretty simple:
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...
}
Conclusion
Since this is a frequent case, the simpler the better. Just wrapping the check and throwing exceptions into the method, you can easily use programming on the concepts of the contract. Although such shells are not available in Java, valid4j and Kotlin offer them.
Thank you for your attention, see you soon!