Blogs:JesseGlick:OfClassesAndInterfaces

From APIDesign

Jump to: navigation, search

Jesse's thoughts about Classes and Interfaces

Someone asked:

I guess that, roughly, a client API should use abstract classes while a support/provider API should use interfaces?

to which I responded:

Generally the client API should use final classes (or static methods where appropriate) and conceal all references to the SPI. For example, say you started with

package javax.whatnot;
public final class Stuff {
  private StuffProvider impl;
  private Stuff(StuffProvider impl) {...}
  public static Stuff forPlace(Place place) {
    return new Stuff(findImplForPlace(place));
  }
  public String name() {
    return impl.name();
  }
}
package javax.whatnot.spi;
public interface StuffProvider {
  String name();
}

and later changed this to:

package javax.whatnot;
public final class Stuff {
  private StuffProvider impl;
  private Stuff(StuffProvider impl) {...}
  public static Stuff forPlace(Place place) {
    return new Stuff(findImplForPlace(place));
  }
  public String name() {
    return impl.name();
  }
  public long distance() {
    return impl instanceof PossiblyDistantStuffProvider ?
      ((PossiblyDistantStuffProvider) impl).distance() :
      /* fallback value */ 0L;
  }
}
package javax.whatnot.spi;
public interface StuffProvider {
  String name();
}
public interface PossiblyDistantStuffProvider extends StuffProvider {
  String name();
  long distance();
}

Compared to

package javax.whatnot;
public abstract class Stuff {
  public Stuff forPlace(Place place) {...}
  protected Stuff() {}
  public abstract String name();
  public long distance() {/*fallback*/ return 0L;}
}

the API/SPI split gives you a lot more options for how to deal with compatibility. You can have different versions of the SPI that have quite different signatures. A single abstract class is hard to make work in such situations - you can add one method with a simple fallback easily enough, but then you can get into trouble if you want to do anything more complicated, like change the return type of a method.

We ran into this very problem long ago when we realized an abstract class in our API was mistakenly using ImageIcon instead of Icon for the return type of a method - oops! It was very hard to fix. (You can have the default impl of both versions of the method delegate to the other one, but then you need to use reflection to make sure at least one method was actually overloaded, else you get a stack overflow...) With an API/SPI split, it would have been trivial, and more comprehensible for SPI users too:

package api;
public Icon getIcon() {
  return implV1 != null ? implV1.getImageIcon() : implV2.getIcon();
}
public ImageIcon getImageIcon() {
  return implV1 != null ? implV1.getImageIcon() :
    new ImageIcon(icon2Image(implV2.getIcon()));
}

In the previous example, consider wanting to add to the SPI

public interface TotallyDifferentStuffProvider {
  long xCoord();
  long yCoord();
}

where in the API

name() -> googleMapsWebServices.locate(impl.xCoord(), impl.yCoord()).
           nameOfMunicipality()
distance() -> sqrt(impl.xCoord()**2 + impl.yCoord()**2)

Personally I don't see any compelling reason to use abstract classes in a public API. There might be weird cases (related to security?) where you can take advantage of the protected access mode.

--JesseGlick 09:33, 15 June 2008 (UTC)

Personal tools