Last week I watched a video showing a talk by Kevin Bourrillion introducing Guava, a set of open source core libraries used internally by Google.

Kevin Bourrillion mentioned the library's Preconditions class that is closely related to Apache's Validate or Java 7's Objects. Basically all three implementations provide means to check preconditions. This is especially useful to check passed in arguments to fail-fast. At smartics we provided our own version of argument checking some time ago. It is called Arguments and is especially helpful to check for non-null or non-blank values.

The Idea for a Change

There is an interesting feature in Google's Preconditions class that we do not cover in our library: It returns the checked value. This makes it possible to reformulate the following

public MyConstructor(final String string, final Integer integer) {
  Arguments.checkNotBlank("string", string);
  Arguments.checkNotNull("integer", integer);
 
  this.string = string;
  this.integer = integer;
}

to

public MyConstructor(final String string, final Integer integer) {
  this.string = Arguments.checkNotBlank("string", string);
  this.integer = Arguments.checkNotNull("integer", integer);
}

This makes the code half as long.

Returning the value also allows to use the check in constructor chaining situations. It may get ugly, but in some situations it may make the failure condition more clear:

public MyConstructor(final String myDomainIdentifier) {
  this(new Whatever(
     checkNotBlank("myDomainIdentifier", myDomainIdentifier)));
}

This might get a better message in the exception if Whatever simply refers to a string, since the constructor above still ‘knows’ the name of the parameter to include it in the error message.

Now go for it!

Both scenarios I want to support in the next version of our library. I still want to stick to our version of the argument checking helper class, because I often have to check for argument values not being blank.

So all I have to do is return the passed in argument? “Wrong!” you shout and you are right.

If I change

public static void checkNotBlank(final String name,
  final String value, final String message) 
  throws BlankArgumentException {
    if (StringUtils.isBlank(value)) {
      throw new BlankArgumentException(name, message);
    }
  }
}

to

public static String checkNotBlank(final String name,
  final String value, final String message)
  throws BlankArgumentException {
    if (StringUtils.isBlank(value)) {
      throw new BlankArgumentException(name, message);
    }
 
    return value;
  }
}

This change does not break the source code. But I introduce an incompatible change with the previous version that occurs at runtime. This is because changing the return value requires the compiler to create a new method and removing the old. So the change is not binary compatible.

The relevant part in the Java Language Specification:

Changing the result type of a method, replacing a result type with void, or replacing void with a result type has the combined effect of deleting the old method and adding a new method with the new result type or newly void result (see §13.4.12).

For purposes of binary compatibility, adding or removing a method or constructor m whose return type involves type variables (§4.4) or parameterized types (§4.5) is equivalent to the addition (respectively, removal) of the an otherwise equivalent method whose return type is the erasure (§4.6) of the return type of m.

Java Language Specification, 13.4.15 Method Result Type

For more information about this topic, please refer to polygenelubricants‘s answer to the question Retrofitting void methods to return its argument to facilitate fluency: breaking change? on StackOverflow.

This problem is reported by the clirr-maven-plugin that can easily be integrated into any Maven build process.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>clirr-maven-plugin</artifactId>
  <version>2.3</version>
  <configuration>
    <minSeverity>info</minSeverity>
  </configuration>
</plugin>

The report for the incompatible change looks like this on the Clirr report on the project’s Maven Site:

The fix in our case is quite easy if you control all dependent projects: You simply have to recompile. No source code change is required. But in reality this is not feasible. Either you do not have all dependent binaries in your control or you just do not have the resources to recompile and test them all. So the solution to this problem is to be found elsewhere.

Do not break Things

For libraries we have to be very sensitive to changes that break the API. In larger projects there may be a couple of modules that depend on the same library and if there is a change that broke the API all modules have to grade up to work properly together again. Therefore for e.g. Apache makes it easy to use their version 3 of commons-lang alongside with their version 2.

First they provided a new artifact ID:

<dependency>
  <groupId>commons-lang</groupId>
  <artifactId>commons-lang</artifactId>
  <version>2.6</version>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.1</version>
</dependency>

This allows Maven to resolve a dependency set that includes both libraries. This way some modules can use the new version and other modules can update as soon as they want to.

To succeed in this, the package name has also be adjusted. Where in Version 2 the package for e.g. the Validate class mentions above was

org.apache.commons.lang.Validate

in Version 3 it is called:

org.apache.commons.lang3.Validate

So the library in version 3 can live along with any previous version, since Validate is effectively a new class.

Mark as deprecated

But in our case we do not have to go this far. We are not planning to release a new major version any time soon. Therefore we have to introduce a new class with the new behavior. It is called Arg. The old Arguments class is now marked deprecated.

/**
 * Utility class to check arguments.
 *
 * @deprecated Due to a breaking change a new class called {@link Arg} has been
 *             designed to be used from now on. It has the same methods as this
 *             one, but it returns the value passed in to check. This allows to
 *             do the check and an initialization (e.g. in a constructor) in one
 *             line. It also allows to check arguments if another constructor is
 *             called (such as in <code>this()</code> and <code>super()</code>).
 *             This class is planned to be removed with the release of version 
 *             2.0.
 */
@Deprecated
public final class Arguments {
  ...
}

We inform our API users

  1. where the new version can be found
  2. why we made the change
  3. when this class will go out of service

The deprecation mark is translated into a warning by the compiler. If a developer is in doubt whether to use Arguments or Arg she simply has not remember which is newer. This helps to move the code towards the new version supported by the compiler.

The non-breaking change is reported in the Clirr report like this:

 

Everything green again. No headaches for anyone in the future.


Link

Link

Posts

Tagcloud

Loading tagcloud ...