Contract Programming
Goals
- Sufficiently document the contract of an API.
- Use semantic versioning.
- Mark non-recommended parts of the API as deprecated.
- Check incoming method arguments with preconditions.
- Use JSR 305 annotations to improve documentation of a method's contract.
Concepts
- annotation
- Application Programming Interface (API)
- contract programming
- coordinates (Maven)
- dependency (Maven)
- deprecated
- Design by Contract™
- fail-fast
- Google findbugs
- Google Guava
- Java Community Process (JCP)
- Java Specification Request (JSR)
- JSR 305
- Maven Central Repository
- method signature
- optional (Maven dependency)
- precondition
- semantic versioning
- scope (Maven dependency)
Language
@Deprecated
Javadoc
@deprecated
Library
java.lang.IllegalArgumentException
java.lang.IllegalStateException
java.lang.IndexOutOfBoundsException
java.lang.NullPointerException
java.lang.Object
java.util.Objects.requireNonNull(T obj)
java.lang.String.format(String format, Object... args)
javax.annotation.Nonnull
javax.annotation.Nullable
com.google.common.base.Preconditions
com.google.common.base.Preconditions.checkArgument(…)
com.google.common.base.Preconditions.checkElementIndex(int index, int size)
com.google.common.base.Preconditions.checkNotNull(T reference)
com.google.common.base.Preconditions.checkState(boolean expression)
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.
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.
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()
.
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
.
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:
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 |
|
The program must be in some expected state. | IllegalStateException |
|
An index must be within some bounds. | IndexOutOfBoundsException |
|
An argument must not be null . | NullPointerException |
|
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.
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 |
|
|
IllegalStateException |
|
|
IndexOutOfBoundsException |
|
|
NullPointerException |
|
|
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:
- annotating a class, method, or variable declaration
- annotating a parameter in a method
- annotating a return type of a method
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:
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.
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.
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!
Gotchas
- Is your next version only a minor version or a patch version release? You should only modify the API in a way that is not backwards compatible in major version release.
- Do you intend to remove this part of the API, or is there now a better method to use instead? Add a
@Deprecated
annotation and add a@deprecated
Javadoc tag if you want to provide more details. - If your method received bad data, don't try to “help” the caller be attempting to massage or “correct” the data. This could obscure the error that generated the bad data, and/or produce apparently good results that are incorrect. Better to “fail fast”.
- The
assert
keyword and associatedAssertionError
is better suited for checking assumptions of a calculation. To enforce the a method contract, it is better to use preconditions that raiseNullPointerException
or argument-related exceptions such asIllegalArgumentException
.
In the Real World
- Google Guava is a very popular library used in many projects. You should use it as much as possible. It is constantly being updated, and you will want to keep up-to-date with its latest additions.
Think About It
- Have I documented, through annotations or by Javadoc, which parameters of my method accept
null
? - Have I documented, through annotations or by Javadoc, whether my method will ever return
null
? - Have I checked the arguments of my method as soon as possible to ensure that they are valid?
Self Evaluation
- What is a precondition?
- What are two common exceptions that might be thrown if a precondition fails?
- What is a project dependency?
- What is the Maven Central Repository?
- Where is Maven's local repository cache usually stored?
- How do you add a dependency to a Maven POM?
- What are two common precondition check methods provided by Guava?
- What does semantic versioning try to do?
- What is the difference between
@deprecated
and@Deprecated
? Why would you use one or the other—or both?
Task
Improve the contract of your Book
class by:
- Expanding the constructor documentation.
- Adding JSR 305 annotations where appropriate.
- Adding precondition checks throughout.
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
- Application programming interface (Wikipedia)
- Building bug-free O-O software: An introduction to Design by Contract(TM) (Eiffel Software Archives)
- Fail-fast (Wikipedia)
- Preconditions Explained (Google Guava User Guide)
- How and When To Deprecate APIs (Oracle - Java™ Platform Overview)
- Annotation Basics (Oracle - The Java™ Tutorials)
References
- The Java® Language Specification, Java SE 11 Edition: 8.4.2. Method Signature (Oracle)
- Semantic Versioning 2.0.0
- POM Reference - Dependencies (Apache Maven Project)
- Dependency Scope (Apache Maven Project)
- Optional Dependencies and Dependency Exclusions (Apache Maven Project)
- Google Guava User Guide
- Predefined Annotation Types (Oracle - The Java™ Tutorials)
- JSR 305: Annotations for Software Defect Detection (Java Community Process)
Resources
- Maven Central Repository
- Google Guava
- The Power of Design by Contract™ (Eiffel Software)
- Java Community Process
Acknowledgments
- “Design by Contract” is a trademark of Eiffel Software, Inc.