måndag 22 februari 2016

Preconditions in Google Guava

While the Guava project (https://code.google.com/p/guava-libraries) from Google comes packed with goodies for Java developers there is one thing in particular that I tend to use over, over and over again. Actually there used to be two of those, but since Java 8 comes with their own implementation of Optional (https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) I have shifted over towards that implementation instead. Anyway, the topic of this short text is Preconditions in Google Guava (https://github.com/google/guava/wiki/PreconditionsExplained).

You can think of preconditions in Guava (as the word suggests) as a way to express and enforce preconditions that must be true in order for the execution to continue. A natural fit for these would be at the beginning of a method or a constructor, acting as guard statements (http://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html) of sorts that throw exceptions when violated.

To make things a bit more concrete we turn to code. First off we take a look at the "normal" way to achieve precondition-like functionality without relying on Guava:


public class SomeClass {  
   private final String aString;  
   private final String anotherString;  
   private final int anInteger;  
   public SomeClass(String aString, String anotherString, int anInteger) {  
     if (aString == null) {  
       throw new NullPointerException("aString was null");  
     }  
     if (anotherString == null) {  
       throw new NullPointerException("anotherString was null");  
     }  
     if (anInteger <= 0) {  
       throw new IllegalArgumentException(  
           String.format("Expected anInteger to be > 0, got %s", anInteger));  
     }  
     this.aString = aString;  
     this.anotherString = anotherString;  
     this.anInteger = anInteger;  
   }  
   // more code here..  
 }  


In the SomeClass constructor we want to a) enforce that the strings passed as arguments are not null and that the integer argument is greater than zero and b) assign the arguments to their respective class member variable. Throughout the 16 lines of code in the constructor body we spend 12 lines of code just enforcing the correctness of the supplied arguments. One word that comes to my mind is verbose.

Another thing to note is that in each of the if-statements we check if the argument has a value that we do not accept. In plain english we can say "if aString is null, then throw an exception" or "if anInteger is equal to or lower than zero, throw an exception".

We now turn to preconditions in Guava. An implementation of the constructor with the same functionality would then look as follows:



import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

public class SomeClass {  
   private final String aString;  
   private final String anotherString;  
   private final int anInteger;  
   public SomeClass(String aString, String anotherString, int anInteger) {  
     checkArgument(anInteger > 0, "Expected anInteger to be > 0, got %s", anInteger);  
     this.anInteger = anInteger;  
     this.aString = checkNotNull(aString);  
     this.anotherString = checkNotNull(anotherString);  
   }  
   // more code here..  
 }  


In this version of the class constructor we make use of two static methods in the Preconditions class, namely checkArgument and checkNotNull. In order to make the code more readable these two methods have been statically imported, as seen above the class definition. Let's walk through the lines in the constructor one by one.

The first line calls the checkArgument precondition to check that anInteger is greater than zero, otherwise an IllegalArgumentException will be thrown. A second and third argument can be supplied to the method - this is optional as the method is overloaded - that provides the exact same functionality as the String.format method used in the former version of the constructor. The most important thing to note on this line is that we check if the argument has a value that we accept. This is the complete opposite to what we did in the former version of the constructor, where we checked if the argument had a value we did not accept. In plain english we can now say "anInteger must be greater than 0 or an exception will be thrown".

The second line in the constructor is just an assignment, nothing more to say about that. The fourth and the fifth lines are however much more interesting. Here we use a precondition on each line to check that the string is not null, otherwise a NullPointerException will be thrown. If the precondition is enforced successfully, the argument supplied to the checkNotNull method is assigned to its member variable. In plain english we can now say "aString cannot be null or an exception will be thrown". Again, the way we express this is completely inverted from the former version of the constructor, where we say "if aStirng is null, then throw an exception".

Aside from making the code more compact - we have now effectively reduced the code from 16 lines down to 5 - it is also in my strong opinion that the latter version of our constructor is easier to understand. With preconditions in Guava we have the ability to state how things must be rather than how things must not be in order to proceed with the execution. This might seem like a trivial improvement to code readability, but with each small improvement to readability we add to our code base we also make it more maintainable.

After all, code is a form of communication, and code that communicates well is easier to understand and maintain (http://www.casualsemantics.se/koden-ska-kommunicera-bra).

P.S. An alternative to the Preconditions.checkNotNull method is the Objects.requireNonNull method available in Java 7.

This blog post was authored by Martin Moberg.

Inga kommentarer:

Skicka en kommentar