📜 ⬆️ ⬇️

JVM contract programming

Hi, Habr! I present to your attention the translation of the article " Programming by contract on the JVM " by Nicolas Fränkel.

This week I would like to take 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 proven 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 the conceptual metaphor with the terms and obligations of business contracts.
Wikipedia


In essence, the 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:
')
Pre-conditions:


Constants:


Post conditions:


"Manual" implementation


It is easy to 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... } 

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 with the Java approach:

  1. There is no difference between pre and post conditions.
  2. The code must be run using the -ea start -ea

Oracle's documentation clearly indicates this:
Although the assert construct is not a complete contract construct, it can help support informal contract programming style.

Alternative Java implementation


Starting in Java 8, the Objects class offers three methods that impose restrictions on contract programming:

  1.  public static <T> T requireNonNull(T obj) 
  2.  public static <T> T requireNonNull(T obj, String message) 
  3.  public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) 

The Supplier argument in the last method returns an error message.
All 3 methods throw a NullPointerException if obj is null .

More interesting is what they return if obj not null . This leads to the following code view:

 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 limitations, it also degrades the readability of the code, especially if you add the error message argument.

Implementations for certain frameworks


The Spring Framework provides the Assert class, which offers a variety of state checking methods:



In accordance with custom implementations, pre-conditions checks raise an IllegalArgumentException if the condition is not met, while checks after the state throw an IllegalStateException exception.

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:


Kotlin approach


Contract programming on Kotlin is based on simple method calls, grouped in the Preconditions.kt: file Preconditions.kt:




Rewriting the parent fragment with Kotlin is quite 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. Simply by wrapping the check and throwing exceptions into the method, you can easily use contract-based programming. Although these shells are not available in Java, valid4j and Kotlin offer them.

Thank you for your attention, see you soon!

Source: https://habr.com/ru/post/352672/


All Articles