Hamcrest: That one weird method

Over on Reddit r/programming:

I believe this is exactly how not to design a class hierarchy

package org.hamcrest;

public interface Matcher<T> extends SelfDescribing {

  // [snip]

  /**
   * This method simply acts a friendly reminder not to implement
   * Matcher directly and instead extend BaseMatcher. It's easy
   * to ignore JavaDoc, but a bit harder to ignore compile errors.
   *
   * @see Matcher for reasons why.
   * @see BaseMatcher
   * @deprecated to make
   */
  @Deprecated
  void _dont_implement_Matcher___instead_extend_BaseMatcher_();

}

Oooh that’s my code getting some heat. Response…

/u/joewalnes:

Hi. I wrote that method. It was my design choice. Yep, me. I think about it a lot.

Let me explain the design choice…

Stepping back a little, interfaces are typically used for one of 2 things (not just in Java, but in any staticly typed OO language):

  1. Application Provider Interface (API): users are meant to call this interface.

  2. Service Provider Interface (SPI): users are meant to implement this interface.

Building APIs and SPIs in your own systems is fairly straightforward. But if you’re building for published libraries that you plan to evolve over time without breaking backwards compatibility, you need to think ahead a little…

For APIs, making changes without breaking backwards compatibility is not too bad. Typically you just never remove method signatures - just add new or overloaded versions, and delegate appropriately. Over time you may deprecate.

For SPIs, you cannot add new abstract methods to the signature without breaking upgrades for users who have implemented the old interface. A way to deal with evolution is to ensure SPIs are defined as abstract classes instead of pure interfaces, and provide fallback implementations for the new methods that act as sensible defaults. You can see many examples of these long-lived abstract classes in the wild - for example the Servlet APIs and Collection APIs provide abstract classes.

I’m going to use the Collections API as an example. It is common practice to use the java.util.List interface when you are interacting with a list. But if you were to build your own List implementation, you’d be nuts to implement this interface directly because it has many methods and you’d be tying yourself to a particular JDK. Instead, you’d implement java.util.AbstractList (or maybe a further subclass), which takes care of most of the boring stuff for you, leaving you to just implement a few methods relevant to your implementation.

The same goes for Hamcrest Matchers. Except there was a problem - the Hamcrest Matcher interface was so minimal, that there’s no pain to implementing the interface directly. If users were to do this, it would make it incredibly hard to evolve the interface with subsequent releases.

So that method was added to artificially make it painful to implement the interface.

Five years later… what do I think of this decision? Well, it worked. The Matcher interface had one major revision that would have broken everyone’s implementations had they implemented the interface directly, but because everyone extended the abstract class instead, we could make the change without code breakage. I consider it a success in this area. In contrast, many of my other libraries have had interfaces for SPIs and upgrades have been a never ending pain point.

However… yurrrrrgggg - it’s an eye-soar. Every time I see that method popup as a completion suggestion in my IDE, I throw up a little in the back of my throat.

An alternative approach would have been to have separate interfaces for API and SPI and use the library to glue them together and deal with changes. This middleman approach would have added a small amount of complexity to the library. Maybe this would have worked better, maybe not. I don’t know.

Unfortunately, design decisions aren’t always as clear cut as identifying a chapter in a book that says what you should do - they are about making tradeoffs. Joel Spolsky puts it much better than I ever could.

Anyway, I hope that clarifies why it’s like that. I’m not defending it as the right thing, just trying to explain the rationale.

Nat Pryce also chimed in.