One of the possible implementations of programming by contract (aka design by contract) is use of informally defined invariants. Apart from being a useful theoretical concept, they have great impact on code quality and readability.
Nasty complexity and uncertainty
Suppose our system performs and tracks notifications. When one notification fails for some reason, it is retried. Each attempt is performed asynchronously by a third party process. In our system we record that an attempt was started, then call the 3rd party service. Callback from that service is supposed to update our attempt status. So we need record attempt state as one of: executing, successful, failed.
The callback is not 100% reliable, and the notification will only be retried if the last attempt failed. What we decided to do is assume that not hearing back in 15 minutes means a failure.
We could code it like this:
List<Notification> getNotifications(Date start, Date end) { if(start == null || end == null) { return null; } return repository.listNotifications(start, end); } List<Notification> filterExecutingNotifications(List<Notification> notifications) { if (notifications == null) { return null; } else { List<Notification> result = new LinkedList<Notification>(); for (Notification candidate : notifications) { for (Attempt attempt : candidate.getAttempts()) { if(attempt.isExecuting()) { result.add(candidate); break; } } } return result; } } public void discardHungAttempts(Date start, Date end) { List<Notification> notifications = getNotifications(start, end); notifications = filterExecutingNotifications(notifications); if(notifications != null { for(Notification notification : notifications) { List<Attempt> attempts = notification.getAttempts(); if(!attempts.isEmpty()) { Attempt attempt = attempts.get(attempts.size()-1); discardIfOldEnough(attempt); } } } }
How does it work? What can we tell about properties of each variable? What if it’s null? What properties does it satisfy? Can this collection be empty? It may seem that all these conditions are necessary. You can’t be absolutely sure that all variables aren’t nulls. Even then, you can’t be sure about their properties. You couldn’t call attempts.get() in the last loop if the collection was empty, so there’s the isEmpty() condition. And what if getNotifications() returns null instead of an empty list…?
This is just a simplistic self-contained blog sample. Normally it would most likely be spread across several classes and even modules. In a real project conditional complexity would explode. Number of paranoid if(this == null) and if(that.isEmpty()) conditions and possible scenarios would grow. At the same time readability and maintainability would shrink dramatically. And you still could not be 100% sure about everything.
The complexity is not only present in code. It’s a bit harder to read, but that may be bearable. What’s worse is it’s mental complexity. In any non-trivial project it would mean a lot of uncertainty, possible bugs and maintenance headache. Moreover, this is not domain complexity. It’s not a part of application logic. It produces no value. All this code is there for technical reasons only. And it makes the actual logic blurry.
Global contract: don’t pass nulls
What if the team agreed on the following contract: “We never pass nulls as arguments”? This is our invariant. At the beginning of each method we know that each argument is not null. That code would become:
List<Notification> getNotifications(Date start, Date end) { return repository.listNotifications(start, end); } List<Notification> filterExecutingNotifications(List<Notification> notifications) { List<Notification> result = new LinkedList<Notification>(); for (Notification candidate : notifications) { for (Attempt attempt : candidate.getAttempts()) { if(attempt.isExecuting()) { result.add(candidate); break; } } } return result; } public void discardHungAttempts(Date start, Date end) { List<Notification> notifications = getNotifications(start, end); notifications = filterExecutingNotifications(notifications); for(Notification notification : notifications) { List<Attempt> attempts = notification.getAttempts(); if(!attempts.isEmpty()) { Attempt attempt = attempts.get(attempts.size()-1); discardIfOldEnough(attempt); } } }
A little bit better. Lost 9 lines along with a few code blocks and forks. The logic is exactly the same, but now it’s a lot easier to grasp.
Local contracts
Let’s have a look at filterExecutinglNotifications() method. What it returns is a list of notifications with an executing attempt. It implies that each of the returned notifications is guaranteed to have non-empty collection of attempts, one of which is executing. This is a useful invariant.
Taking this into consideration you could rewrite the last method to:
public void discardHungAttempts(Date start, Date end) { List<Notification> notifications = getNotifications(start, end); notifications = filterExecutingNotifications(notifications); for(Notification notification : notifications) { List<Attempt> attempts = notification.getAttempts(); Attempt attempt = attempts.get(attempts.size()-1); discardIfOldEnough(attempt); } }
Conclusion
Even simple chunks of code and interactions can benefit greatly from proper use of contracts / invariants. This trivial example was shrunk from 37 down to 26 lines. Cyclomatic complexity dropped by an order of magnitude. What’s more important, we can be sure that everything is as robust as it can be.
Using such conventions and invariants is really simple regardless of project size. On the other hand, the benefits are truly invaluable. It’s puzzling that many very large projects still don’t do it.