Skip to content
flbulgarelli edited this page Jan 28, 2012 · 1 revision

Check by example

The complete example code can be found here

The problem

Lest suppose we are implementing a juice recipes system, that can produce juice using ingredients. In particular, we are now implementing a TropicalJuiceRecipe, based on bananas and pineapples. We are going to inject dependencies through constructor, so our recipe will look like the following:

 class TropicalJuiceRecipe implements JuiceRecipe {
    private final int suger;
    private final OrangeJuice juice;
    private final Collection<Banana> bananas;
    private final Collection<Pineapple> pineapples;
    /**
     * Creates a new {@link TropicalJuiceRecipe}
     * It takes orange juice, some sugar, some bananas and at least 2 pinaples.
     * Juice must not have expired and that it must not be too acid.
     */
    public TropicalJuiceRecipe(int sugar, OrangeJuice juice, Collection<Banana> bananas,
        Collection<Pineapple> pineapples) {
        this.suger = sugar;
        this.juice = juice;
        this.bananas = bananas;
        this.pineapples = pineapples;

    }
    public Juice prepare() {
        // Don't care....
        return null;
    }
}

As you see in the constructor Javadoc, our recipe has some constraints regarding its input ingredients, such restrictions are called preconditions. Our recipe will not work unless they are meet, so it is good practice to check them as soon as possible, and fail fast, if necessary. So just add the preconditions check, our constructor will look like the following:

    /**
     * Creates a new {@link TropicalJuiceRecipe}
     * It takes orange juice, some sugar, some bananas and at least 2 pinaples.
     * Juice must not have expired and that it must not be too acid.
     */
    public TropicalJuiceRecipe(int sugar, OrangeJuice juice, Collection<Banana> bananas,
        Collection<Pineapple> pineapples) {
        if (juice == null)
            throw new IllegalArgumentException("juice must not be null");

        if (!juice.isNotTooAcid())
            throw new IllegalArgumentException("juice.isNotTooAcide must not be true");

        if (juice.getExpirationDate().compareTo(new Date()) >= 0)
            throw new IllegalArgumentException("juice.expirationDate must be greater than today");

        if (sugar <= 0)
            throw new IllegalArgumentException("sugar must be be greather than zero");

        if (bananas == null)
            throw new IllegalArgumentException("bananas must not be null");

        if (bananas.isEmpty())
            throw new IllegalArgumentException("bananas must not be empty");

        if (pineapples == null)
            throw new IllegalArgumentException("pineapples must be null");

        if (pineapples.size() < 2)
            throw new IllegalArgumentException("pineapples must be of at least size 2");
            
        this.suger = sugar;
        this.juice = juice;
        this.bananas = bananas;
        this.pineapples = pineapples;

    }

All such checks are necessary, but boilerplate, code. We are forced to manually write each condition, negate it, generate a failure message, and throw an exception on failure. It works, but is not the Panacea.

The solution

Here is where Staccatissimo-Check comes to rescue:

    /**
     * Creates a new {@link TropicalJuiceRecipe}
     * It takes orange juice, some sugar, some bananas and at least 2 pinaples.
     * Juice must not have expired and that it must not be too acid.
     */
    public TropicalJuiceRecipe2(int sugar, OrangeJuice juice, Collection<Banana> bananas,
        Collection<Pineapple> pineapples) {
        Ensure
            .that()
            .isTrue("juice.notTooAcid", juice.isNotTooAcid())
            .isGreaterThan("juice.expirationDate", juice.getExpirationDate(), new Date())
            .isPositive("sugar", sugar)
            .isNotEmpty("bananas", bananas)
            .isMinSize("pineapples", pineapples, 2);
        this.suger = sugar;
        this.juice = juice;
        this.bananas = bananas;
        this.pineapples = pineapples;
    }

The latter code is completely equivalent to previous one, but evidently shorter and easier to understand and maintain. If any of such condition is not met, an IllegalArgumentException is thrown. Messages are generated automatically when needed

However, this is not the whole story. Throwing IllegalArgumentException's is generally meaningful enough and indicates a precondition violation. But what happens if we want throw exceptions of a different class?

Ensure is just one of the entry points to the API - it is only meant for preconditions validation. For postconditions validation, there is another class, Assert, which shares the same interface that Ensure, but throws AssertionErrors instead. Finally, if you want to throw arbitrary exceptions, even checked - not inheriting RuntimeException -, there is the Validate class.

Lets see a second example showing all those three implementations in action in a phone calls system:

public class PhoneCall {
    private final PhoneLine line;
    private final CallDestination destination;
    private CallLog log;

    /**
     * Creates a new {@link PhoneCall}. Line and destination must being nonnull
     * and destination must be included in allowed destinations set of the
     * PhoneLine
     */
    public PhoneCall(PhoneLine line, CallDestination destination) {
        if (line == null)
            throw new IllegalArgumentException("line must not be null");

        if (destination == null)
            throw new IllegalArgumentException("Destinantion must not be null");

        if (!line.getAllowedDestinations().contains(destination))
            throw new IllegalArgumentException("line.allowedDestinantions must contain" + destination);

        this.line = line;
        this.destination = destination;
    }

    /**
     * Starts the call. In order to succeed, the line must be free, the
     * destination reachable, and the calllog set. If not of such conditions is
     * met, a {@link PhoneCallException} is thrown.
     * 
     * A postcondition of connect id that, if succeeds, the line must be busy.
     **/
    public void connect() throws PhoneCallException {
        if (line.isBusy())
            throw new PhoneCallException("The line " + line + " is busy");

        if (!destination.isReachable())
            throw new PhoneCallException("The destination " + destination + " is unreachable");

        if (log == null)
            throw new PhoneCallException("The log has not been set");

        // ...implementation of connect...

        if (!line.isBusy())
            throw new AssertionError("The line " + line + " should be busy now");
    }

    public void setLog(CallLog log) {
        if (log == null)
            throw new IllegalArgumentException("log must not be null");

        this.log = log;
    }

    @SuppressWarnings("serial")
    public static class PhoneCallException extends Exception {
        /**
         * Creates a new {@link PhoneCall.PhoneCallException}
         */
        public PhoneCallException(String message) {
            super(message);
        }
    }
}

Again, code is clumsy, so let's refactor it using Staccatissimo-Check. The main difference with previous example is that now we need to throw exceptions other than IllegalArgument: we need to throw an AssertionError if a postcondition is not met - for example, because of a bug - and throw checked PhoneCallException's under some circumstances when starting the connection. Nothing of this is a problem.

public class PhoneCall2 {
   //...
    /**
     * Creates a new {@link PhoneCall}. Line and destination must being nonnull
     * and destination must be included in allowed destinations set of the
     * PhoneLine
     */
    public PhoneCall2(PhoneLine line, CallDestination destination) {
        Ensure.that()
            .isNotNull("line", line)
            .isNotNull("destination", destination)
            .contains("line.allowedDestinations", line.getAllowedDestinations(), destination);
        this.line = line;
        this.destination = destination;
    }

    /**
     * Starts the call. In order to succeed, the line must be free, the
     * destination reachable, and the calllog set. If not of such conditions is
     * met, a {@link PhoneCallException} is thrown.
     * 
     * A postcondition of connect id that, if succeeds, the line must be busy.
     **/
    public void connect() throws PhoneCallException {
        Validate
            .throwing(PhoneCallException.class)
            .isFalse("line.busy", line.isBusy())
            .isTrue("destination.reachable", destination.isReachable())
            .isNotNull("log", log);

        // implementation of connect

        Assert.that().isTrue("line.busy", line.isBusy());
    }

    public void setLog(CallLog log) {
        Ensure.isNotNull("log", log);
        this.log = log;
    }
    //...
}

However, messages are now not as human readable as before. For example, isTrue("destination.reachable", destination.isReachable()) will produce the message "destionation.reachable must be true", which is clear, but not the best message. In many situations this is not a problem, but if you want better messages, Staccatissimo-Check allows you to still build them by hand.

So, finally, using custom messages in the connect method, code will look like the following:

public class PhoneCall3 {

   //...

    /**
     * Starts the call. In order to succeed, the line must be free, the
     * destination reachable, and the calllog set. If not of such conditions is
     * met, a {@link PhoneCallException} is thrown.
     * 
     * A postcondition of connect id that, if succeeds, the line must be busy.
     **/
    public void connect() throws PhoneCallException {
        Validate
            .throwing(PhoneCallException.class)
            .that(!line.isBusy(), "The line %s is busy", line)
            .that(destination.isReachable(), "The destination %s is unreachable", destination)
            .that(log != null, "The log has not being set");

        // implementation of connect

        Assert.that(line.isBusy(), "The line %s should be busy", line);
    }

    //...
}
Clone this wiki locally