Contract Programming

Goals

Concepts

Language

Javadoc

Library

Dependencies

Lesson

Design by Contract™

Now that you know how to make classes and methods public so that they are accessible by the “world at large” outside their package, you must put some thought into which things you make public. When your class or its members are marked public, they comprise the official way for others to access your class—the Application Programming Interface (API) for your class. It is especially important that these classes and methods be documented in the Javadoc comments, so that developers who use your methods will know what those methods accept, and what the caller should expect in return. A method signature (its name and parameter types), its return type, and exception throws declarations also aid in explaining how your method promises to work.

It is useful to think of a method's declaration and related documentation as forming a “contract” between the caller and the method implementation. The method (by way of the developer who wrote and documented it) in effect tells anyone who may want to use it, If you give me data that I claim to support, I hereby agree to process your data in the following manner and provide you such and such results. This metaphor of an API being the equivalent of an agreement between parties has even been turned into a formal system by Eiffel Software called Design by Contract™.

Though we may not use Eiffel Software's formal specification and software tools, the core idea of contract programming is extremely important in creating understandable, maintainable, and (most critically) correctly functioning software. An API should be clear about what it accepts and what it does not, and it should be rigorous in adhering to those rules and ensuring that those rules are adhered to by a caller.

Semantic Versioning

When discussing Maven we touched on semantic versioning, which is a good set of rules to follow when releasing updates to your software. Semantic versioning says in essence that you are committing to support your public API with minor version changes (x.?) and even patch version changes (x.x.?). Only when the major version changes (e.g. from 2.0.1 to 3.0) are you allowed to “break” the API be removing methods or changing what the methods are supposed to do. Therefore choose wisely what functionality you expose with public. It is a good idea to make information hidden by default only allow access to it if needed.

Deprecation

Even when you intend to remove a method in a future version, it is good practice to provide fair warning to developers who may be using your class by indicating that the method is deprecated. You can place @Deprecated above the method to inform others that the method may be removed at some point. This Java feature, a name beginning with an at @ sign, called an annotation. The @Deprecated annotation, besides providing documentation, will cause the compiler to issue a warning if someone tries to call the method. In the example below, we note that the two Point.calculateSlope(…) methods are deprecated, and that going forward users should use Geometry.slope(…) instead.

Deprecating a method.
package com.example;

public class Point
{

  …

  /**Calculates and returns the slope of a line between this point.
   * and the given coordinates.
   * @param x2 The other x coordinate.
   * @param y2 The other y coordinate.
   * @return The slope of the resulting line, or {@link Double#POSITIVE_INFINITY} if the resulting line would be vertical.
   * @deprecated Isn't something a {@link Point} should calculate; will be removed in favor of {@link Geometry#slope(Point, Point)}.
   */
  @Deprecated  //NOTE: annotation
  public double calculateSlope(final int x2, final int y2)
  {
    …
  }

  /**Calculates and returns the slope of a line between this point
   * and another one.
   * @param point2 The other point.
   * @return The slope of the resulting line, or {@link Double#POSITIVE_INFINITY} if the resulting line would be vertical.
   * @deprecated Isn't something a {@link Point} should calculate; will be removed in favor of {@link Geometry#slope(Point, Point)}.
   */
  @Deprecated  //NOTE: annotation
  public double calculateSlope(final Point point2)
  {
    …
  }

  …
}

Fail-Fast

Sometimes programmers have a tendency to “help” other developers, when presented with incorrect or incomplete data, by attempting to correct or complete the data. For example, a developer might detect a null argument in a method and assume the caller intended the empty string "" (or use the empty string because nothing seemed better, as after all nothing was given). The problem with that approach is that, if the user provided incorrect data, there is usually no way to know what the “correct” data was or the intention of the caller. Trying to correct the problem or ignoring the problem only pushes the problem down the road to be discovered later. Even worse, it may return meaningless data that was based on incorrect or “guessed” information, and the caller would be none the wiser.

Around 1905 George Parker Bidder wanted to test ocean currents in the North Sea, so he released a series of sealed bottles containing postcards, requesting that anyone who found a bottle to return it to the Marine Biological Association (MBA) in Plymouth, England. Approximately 100 years later in 2015 someone found one of the bottles and returned it to the MBA, making it one of the world's oldest known messages in a bottle. See Adrift for over 100 years – the world’s oldest message in a bottle.

What if someone performing such an experiment were to accidentally grabbed a blank postcard and, without noticing, seal it in a bottle and toss it into the sea. Imagine the disappointment of finding a bottle with no message—not to mention that the purpose of sending out the bottle would have been entirely defeated. The Bottle class below provides a demonstration of this situation in Java programming terms.

Example Bottle class representing P. Bidder's message in a bottle around 1905.
package uk.ac.mba;

/**
 * Bottle with a message.
 * @author George Parker Bidder
 * @see <a href="https://www.mba.ac.uk/">The Marine Biological Association</a>
 /
public class Bottle
{

  private final String message;

  /**
   * Constructor.
   * @param message The message to place in the bottle.
   */
  public Bottle(String message) {
    this.message = message;  //what if someone accidentally passes null?
  }

  /** @return The message placed in the bottle. */
  public String getMessage() {
    return message;
  }

}

Rather than waiting 100 years for an error to be found, a better approach is to constantly check provided values and “fail fast” when incorrect data is discovered. This alerts the caller as soon as possible that some invalid value was somehow present. This sort of fail-fast approach is just as important during development, because it may alert other developers that they are inadvertently using your class or method incorrectly.

Preconditions

One of the best ways to fail fast is to check incoming values to a method before any work is done and throw an exception if some expectation is not met. This is known as a precondition, and is an essential part of contract programming.

A common problem with method arguments is passing a null reference value for an object when a method does not recognize or a accept null value for the corresponding parameter. It may be that the method was not well documented and the developer implementing the caller did not know whether null was allowed. Whatever the reason, methods should very quickly check for the presence of null as soon as possible, and throw a NullPointerException if not.

Consider a utility method which returns the full name of a Person by combining the values of Person.getFirstName() and Person.getLastName().

Attempting to use an object immediately, which may result in a NullPointerException.
public static String getFullName(final Person person)
{
  return String.format("%s %s", person.getFirstName(), person.getLastName());
}

The very nature of this method will result in a NullPointerException if a null was passed for person, because the method immediately tries to access the object referenced by person. But what if the method merely stored the value away for later use, as was done with the message in the Bottle class above? If this were done here the class might not attempt to access the Person object until seconds or days later, and only then would a NullPointerException be thrown—and the cause of the problem would likely be made harder to track down, because the actual exception would have been thrown from a different area of the program. In such a case it would be better to add an explicit precondition check as soon as you enter the method, throwing a NullPointerException manually if the argument is null.

A fail-fast precondition check for null.
/**
 * Remembers the pet for later, so that we can walk the dog, etc.
 * @param pet The pet to remember; must not be <code>null</code>.
 * @throws NullPointerException if the reference to the pet is <code>null</code>.
 */
public void setPet(final Animal pet)
{
  if(pet == null)
  {
    throw new NullPointerException("Pet cannot be null.");
  }
  this.pet = pet; //this in itself will not check for null
}

That this sort of null check could potentially be repeated in various places should bring to your mind the concept of “check methods”—small methods for simply checking a condition and throwing an exception if the condition does not hold. You could easily write a “check method” for null values:

A “check method” to use as a precondition check for preventing null.
/**
 * Throws an exception if the given animal reference is <code>null</code>; otherwise does nothing.
 * @param animal The animal reference to check.
 * @throws NullPointerException if the given animal is <code>null</code>.
 */
public static void checkNotNull(final Animal animal)
{
  if(animal == null)
  {
    throw new NullPointerException();
  }
}

Besides null checks there are several other preconditions that are common, and for which there exist standard Java exceptions that are appropriate to throw if the precondition does not hold:

Precondition Exception Example
An argument must be valid and supported. IllegalArgumentException
if(ingredients.length > 5)
{
  throw new IllegalArgumentException("No more than five ingredients supported.");
}
The program must be in some expected state. IllegalStateException
if(!car.isRunning())
{
  throw new IllegalStateException("Cannot change gears with the car turned off.");
}
An index must be within some bounds. IndexOutOfBoundsException
if(index < 0 || index >= size)
{
  throw new IndexOutOfBoundsException();
}
An argument must not be null. NullPointerException
if(author == null)
{
  throw new NullPointerException("Missing author");
}

Google Guava

The Google Guava Java library provides a plethora of classes, utilities, and constants. Guava's support for preconditions will prove especially useful throughout your program. Merely include Guava in your build as a Maven dependency, and begin to use Guava's preconditions liberally as explained in the following sections.

Maven Dependencies

One indispensable feature of Maven is its ability to automatically pull in dependencies (libraries and other things on which your program relies) when needed. Every Maven build has access to the Maven Central Repository (and you can configure Maven to access other repositories). By specifying the coordinates (the groupId, artifactId, and version) of a library you would like to access, Maven will be able to download the library from its repository automatically when needed.

You should begin to see why Maven coordinates are so crucial, and why they must never change once they are made public. Maven needs to be able to find a dependency in a Maven repository by the dependency's coordinates. Moreover Maven stores a copy of all dependencies in a local repository cache.

When Maven needs a dependency during a build, it first checks the local cache. If the dependency has been downloaded already, based upon the recorded coordinates, Maven will have no need to retrieve it again. Otherwise, if no dependency with the correct coordinates is found in the local repository cache, Maven searches for them in the Maven Central Repository by default.

Dependencies are declared in the Maven POM in a section named <dependencies>. (See POM Reference - Dependencies.) Each dependency will be listed inside a <dependency> element, which identifies the dependency coordinates in individual <groupId>, <artifactId>, and <version> elements. The Google Guava dependency (assuming the latest version is 27.0.1-jre; search the Maven Central Repository for com.google.guava:guava to find the latest version) would therefore appear in the POM as shown below. Note that Guava adds a -jre suffix to the artifact ID for normal JDK programming, and uses the -android suffix to indicate the version for use with the Android mobile operating system.

Declaring Google Guava as a dependency in the Maven POM.
…
  <dependencies>
    …
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>27.0.1-jre</version>
    </dependency>
    …
  </dependencies>
…

Once this is added to your Maven POM, the next time you do a build Maven will check to see if the Google Guava library has already been downloaded. If not, Maven will download it as if by magic, solely based upon the provided coordinates, and store it for later so that it does not have to be downloaded for the next build.

Guava Preconditions

Google Guava provides many utilities methods to use as preconditions. They are available as static methods of the appropriately-named com.google.common.base.Preconditions class. Here are a few of them:

Exception Example Guava Precondition
IllegalArgumentException
if(ingredients.length > 5)
{
  throw new IllegalArgumentException(
      "No more than five ingredients supported.");
}
Preconditions.checkArgument(ingredients.length <= 5,
    "No more than five ingredients supported.");
IllegalStateException
if(!car.isRunning())
{
  throw new IllegalStateException(
      "Cannot change gears with the car turned off.");
}
Preconditions.checkState(car.isRunning(),
    "Cannot change gears with the car turned off.");
IndexOutOfBoundsException
if(index < 0 || index >=length)
{
  throw new IndexOutOfBoundsException();
}
Preconditions.checkElementIndex(index, size);
NullPointerException
if(author == null)
{
  throw new NullPointerException("Missing author");
}
Preconditions.checkNotNull(author);

Annotations for Software Defect Detection

It has been repeatedly stressed that one way to prevent developers from making errors is to make sure that code is sufficiently documented so that the developer will not be in doubt of how to use the code. Good documentation can never prevent all errors, but no or poor documentation is sure to promote them. If it gets tedious indicating in the Javadoc comments which variables can be null and which cannot, the Java Community Process (JCP) has created a set of annotations for indicating in a method's contract whether null is allowed.

Annotations

Besides the core language constructs such as classes, variables, and methods; Java allows you to add metadata about the program in the form of annotations. This metadata may be used by the compiler for special features or behavior; or if made available at runtime, it may provide flags or other information that can be examined by other classes when the program is running. The @Deprecated annotation, discussed above, is one of many predefined annotations (see the References section for more), and Java also allows you to create custom annotations. You will learn how to create your own annotations in a future lesson. For now you need to know how to use them.

If the annotations you want to use are in another library, you'll want to make sure that dependency is included in your build (see below). Then you'll include them in your source code as as illustrated as in the example above. Where you can put annotations depends on how the annotations were defined when they were written, but popular locations include:

An annotation will always begin with the at @ sign. Some annotations will allow (or require) parameters, in which case you will add them inside parentheses. Annotation parameters may be unnamed (so that they look like arguments passed to a method), or they may be named; if named, you will list the names of the parameters, along with the values you are providing, separated by the equals = sign. As an imaginary example, if there existed an annotation named @Course to annotate your homework, it might look like this:

Hypothetical @Course annotation for annotating homework.
package com.example;

import com.example.education.annotations.Course;

@Course(title = "GlobalMentor Java Course", lesson = "12")
public class Booker
{
  …
}

The library that implements an annotation must be available at compile time, unless it is a built-in annotation. Depending on how the annotation was written, the annotation information may stay around and be available at runtime. But even if it is available in the .class file at runtime, if no code tries to examine the annotation, you won't need the library files to support them.

Annotations usually don't make the code they are annotating run any differently. But usually they will cause other code to run differently based upon the presence of annotations in the code. For example, an annotation might indicate to some other class that certain methods should be treated specially, when accessed by some other program or tool framework such as JUnit (discussed in an upcoming lesson).

JSR 305

The Java Community Process allows for a specification called a Java Specification Request (JSR) for adding new features to the Java programming language. JSR 305 defined annotations to be used as part of an extensive set of tools that would help a compiler and other tools verify certain constraints of a program before it is even run. Some of those annotations indicate to an external tool whether a method parameter may be null, and whether a method may return null. These annotations, primarily javax.annotation.Nonnull and javax.annotation.Nullable, might be useful to a smart compiler, but more importantly in the contact of contract programming is that they provide to the developer information that may not be included in the Javadocs, or that may be too tedious to indicate in the Javadocs.

Using JSR 305 annotations.
import static com.google.common.base.Preconditions.*;
import javax.annotation.*;

...

public @Nullable Foo doSomething(@Nonnull final Bar bar) {
  checkNotNotNull(bar); //precondition
  final int calculatedNumber = calculateSomething(bar);
  doSomethingElse();
  return foo > 0 ? new Foo(calculatedNumber) : null;
}

Google findbugs

JSR 305 only specifies an API (e.g. which annotations to use, and what they mean). To actually use JSR 305 annotations in your code, you must add some JSR 305 implementation to your dependencies. One such JSR 305 is Google findbugs, which you can provide in the dependencies of your Maven POM.

Declaring Google findbugs as a dependency in the Maven POM.
…
  <dependencies>
    …
    <dependency>
      <groupId>com.google.code.findbugs</groupId>
      <artifactId>jsr305</artifactId>
      <version>3.0.2</version>
      <optional>true</optional>
    </dependency>
    …
  </dependencies>
…

Review

If George Parker Bidder were to create a Java program for sending out a message in a bottle, he would want to use preconditions to ensure that no bottle contained an empty message!

Example Bottle class representing P. Bidder's message in a bottle around 1905, with preconditions.
package uk.ac.mba;

import static com.google.common.base.Preconditions.*;
import javax.annotation.*;

/**
 * Bottle with a message.
 * @author George Parker Bidder
 * @see <a href="https://www.mba.ac.uk/">The Marine Biological Association</a>
 /
public class Bottle
{

  private final String message;

  /**
   * Constructor.
   * @param message The message to place in the bottle.
   * @throws NullPointerException if the given message is <code>null</code>.
   */
  public Bottle(@Nonnull final String message) {
    this.message = checkNotNull(message);
  }

  /** @return The message placed in the bottle. */
  public @Nonnull String getMessage() {
    return message;
  }

}

Gotchas

In the Real World

Think About It

Self Evaluation

Task

Improve the contract of your Book class by:

Add a precondition that prevents a book from being created with a publication date in the future. This precondition will eliminate the need for checking the state of this value in Book.getYearsInPrint(), so you can remove the code from that method that throws an IllegalStateException. You may wish to convert the check into an assert, however, to indicate that the developer is assuming that the value has been checked elsewhere.

See Also

References

Resources

Acknowledgments