11

I'm working with a team on a new Java API for one of our internal projects. We probably won't be able to take the time to stop and hash out all the details of the Java interfaces and get them 100% perfect at the beginning.

We have some core features that have to be there up front, and others that are likely to be added later over time but aren't important now, + taking the time to design those features now is a luxury we don't have. Especially since we don't have enough information yet to get all the design details right.

The Java approach to APIs is that once you publish an interface, it's effectively immutable and you should never change it.

Is there a way to plan for API evolution over time? I've read this question and I suppose we could do this:

// first release
interface IDoSomething
{
    public void hop();
    public void skip();
    public void jump();
}

// later
interface IDoSomething2 extends IDoSomething
{
    public void waxFloor(Floor floor);
    public void topDessert(Dessert dessert);
}

// later still
interface IDoSomething3 extends IDoSomething2
{
    public void slice(Sliceable object);
    public void dice(Diceable object);
}

and then upgrade our classes from supporting IDoSomething to IDoSomething2 and then IDoSomething3, but this seems to have a code smell issue.

Then I guess there's the Guava way of marking interfaces with @Beta so applications can use these at risk, prior to being frozen, but I don't know if that's right either.

Community
  • 1
  • 1
Jason S
  • 184,598
  • 164
  • 608
  • 970
  • 2
    if your project is internal, I am not sure this statement applies much "The Java approach to APIs is that once you publish an interface, it's effectively immutable...". Being an internal project, you could communicate the changes to your team. If this type of communication is not doable. You can use factories to provide the 'correct' API depending on the client version (for backwards compatibility) so the client needs to know they are using a versioned API and include the version they are using in the request (or provide a different entry point for new versions) – blurfus Oct 08 '14 at 16:17
  • Note than one of the main problems with interfaces has been alleviated with Java 8: You can now add `default` methods to interfaces, which simplifies things a lot, and makes it easier to obey the rule *"When in doubt, leave it out"* that Josh Bloch refers to in his excellent talk about "How to design a good API and why it matters", http://www.infoq.com/presentations/effective-api-design . Apart from that, another pointer: I think that the Wiki (and the book) from http://wiki.apidesign.org/wiki/TheAPIBook is worth a look. – Marco13 Oct 08 '14 at 16:50
  • thanks Marco... yeah, I ordered a copy of Tulach's book about 10 minutes before posting this question. – Jason S Oct 08 '14 at 17:01

7 Answers7

3

If you want flexible code generics can help.

For example, instead of:

interface FloorWaxer
{
    public void waxFloor(Floor floor);
}

You can have:

interface Waxer<T> 
{
    void wax(T t);
}

class FloorWaxer implements Waxer<Floor> 
{
    void wax(Floor floor);
}

Also, Java 8 brought default methods in interfaces which allow you to add methods in already existing interfaces; with this in mind you can make you interfaces generic. This means you should make your interfaces as generic as possible; instead of:

interface Washer<T>
{
    void wash(T what);   
}

and then to later add

interface Washer<T>
{
    void wash(T what);   
    void wash(T what, WashSubstance washSubstance); 
}

and later add

interface Washer<T>
{
    void wash(T what);   
    void wash(T what, WashSubstance washSubstance); 
    void wash(T what, WashSubstance washSubstance, Detergent detergent); 
}

you can add from the beginning

@FunctionalInterface
interface Washer<T>
{
    void wash(T what, WashSubstance washSubstance, Detergent detergent); 

    default wash(T what, WashSubstance washSubstance) 
    {
        wash(what, washSubstance, Detergent.DEFAULT_DETERGENT);
    }

    default wash(T what, Detergent detergent) 
    {
        wash(what, WashSubstance.DEFAULT_WASH_SUBSTANCE, detergent);
    }

    default wash(T what) 
    {
        wash(what, WashSubstance.DEFAULT_WASH_SUBSTANCE, Detergent.DEFAULT_DETERGENT);
    }
}

Also, try to make your interfaces functional (only one abstract method) so you can benefit from lambdas sugaring.

Random42
  • 8,989
  • 6
  • 55
  • 86
2

You could take the approach that tapestry-5 has taken which it dubs "Adaptive API" (more info here).

Instead of locked down interfaces, tapestry uses annotations and pojo's. I'm not entirely sure of your circumstances but this may or may not be a good fit. Note that tapestry uses ASM (via plastic) under the hood so that there is no runtime reflection to achieve this.

Eg:

public class SomePojo {
   @Slice
   public void slice(Sliceable object) {
      ...
   }

   @Dice
   public void dice(Diceable object) {
      ...
   }
}

public class SomeOtherPojo {
   @Slice
   public void slice(Sliceable object) {
      ...
   }

   @Hop
   public void hop(Hoppable object) {
      ...
   }
}
lance-java
  • 25,497
  • 4
  • 59
  • 101
  • interesting.... this is a new concept for me so I'm not sure if we can use annotations in this way... but maybe it would work for us. – Jason S Oct 08 '14 at 16:20
2

You could use a new package name for the new version of API - this would allow old and new API live side-by-side and API users can convert their components to the new API one at a time. You can provide some adaptors to help them with heavy-lifting on boundaries where objects get passed across boundaries between classes using new and old API.

The other option is quite harsh but could work for internal project - just change what you need and make users to adapt.

If you are just adding, providing default implementation (in an abstract class) of the new methods can make the process smoother. Of course this is not always applicable.

Signalling the change by changing major version number a provide detailed documentation about how to upgrade the code base to the new version of API is good idea in both cases.

Rostislav Matl
  • 4,294
  • 4
  • 29
  • 53
1

I would suggest to take a look at these Structural Pattern. I think the Decorator pattern (also known as Adaptive pattern) can fill your needs. See the example in the linked Wikipedia article.

Community
  • 1
  • 1
alain.janinm
  • 19,951
  • 10
  • 65
  • 112
  • I think that this is what the mentioned Tapestry 5 uses. – Rostislav Matl Oct 08 '14 at 16:41
  • @RostislavMatl Maybe, I don't know Tapestry. But from the doc, I can see that it's an "Adaptive API" but maybe it's not an "Adaptive Pattern". I don't know how those annotations works exactly. – alain.janinm Oct 08 '14 at 16:49
1

Here's the way I approach this situation.

First, I'd use abstract classes so that you can plug in default implementations later. With the advent of inner and nested classes in JDK 1.1, interfaces add little; almost all use cases can be comfortably converted to use pure abstract classes (often as nested classes).

First release

abstract class DoSomething {
    public abstract void hop();
    public abstract void skip();
    public abstract void jump();
}

Second release

abstract class DoSomething {
    public abstract void hop();
    public abstract void skip();
    public abstract void jump();

    abstract static class VersionTwo {
        public abstract void waxFloor(Floor floor);
        public abstract void topDessert(Dessert dessert);
    }

    public VersionTwo getVersionTwo() {
        // make it easy for callers to determine whether new methods are supported
        // they can do if (doSomething.getVersionTwo() == null)
        return null;
        // OR throw new UnsupportedOperationException(), depending on specifics
        // OR return a default implementation, depending on specifics
    }

    // if you like the interface you proposed in the question, you can do this:

    public final void waxFloor(Floor floor) {
        getVersionTwo().waxFloor();
    }

    public final void topDessert(Dessert dessert) {
        getVersionTwo().topDessert();
    }
}

Third release would be similar to second, so I'll omit it for brevity.

David P. Caldwell
  • 3,394
  • 1
  • 19
  • 32
  • it's been drilled into my head over the years that abstract classes are bad and interfaces are good... It'll take me a while to process your point + reconcile with my past learning. – Jason S Oct 08 '14 at 17:04
  • There are many instances of such Java insanity, like "wildcard imports are bad because I need to know by looking at the top of my class exactly what classes are depended upon" and various other chestnuts that don't hold up well to scrutiny. But to paraphrase, "the arc of the IT universe is long, but it bends toward sanity." :) – David P. Caldwell Oct 08 '14 at 18:25
0

If you haven't designed the final API, don't use the name you want for it!

Call it something like V1RC1, V1RC2, .. and when it is done, you have V1.

People will see in their code, that they are still using a RC-Version and can remove that to get the real thing when it is ready.

Rostistlav is basically saying the same, but he calls them all real API Versions, so it would be V1, V2, V3, .... Think that's up to your taste.

Community
  • 1
  • 1
0

You could also try an event driven approach and add new event types as your API changes without affecting backwards compatability.

eg:

public enum EventType<T> {
    SLICE<Sliceable>(Sliceable.class),
    DICE<Diceable>(Diceable.class),
    HOP<Hoppable>(Hoppable.class);

    private final Class<T> contextType;

    private EventType<T>(Class<T> contextType) {
       this.contextType = contextType;
    }

    public Class<T> getContextType() {
       return this.contextType;
    }
}

public interface EventHandler<T> {
    void handleEvent(T context);
}

public interface EventHub {
    <T> void subscribe(EventType<T> eventType, EventHandler<T> handler);
    <T> void publish(EventType<T> eventType, T context);
}

public static void main(String[] args) {
    EventHub eventHub = new EventHubImpl(); // TODO: Implement
    eventHub.subscribe(EventType.SLICE, new EventHandler<Sliceable.class> { ... });
    eventHub.subscribe(EventType.DICE, new EventHandler<Diceable.class> { ... });
    eventHub.subscribe(EventType.HOP, new EventHandler<Hoppable.class> { ... });

    Hoppable hoppable = new HoppableImpl("foo", "bar", "baz");
    eventHub.publish(EventType.HOP, hoppable); // fires EventHandler<Hoppable.class>
}
lance-java
  • 25,497
  • 4
  • 59
  • 101