APIvsSPI

From APIDesign

Revision as of 12:48, 21 March 2011 by JaroslavTulach (Talk | contribs)
Jump to: navigation, search

This page provides description of the differencies between so called API and SPI. As the understanding of these terms is often present in minds of many developers (and as it is often hard to find two developers to agree on the difference), let's start with description of ClientAPI and ProviderAPI. Then let see how to combine these together.

Contents

ClientAPI

There is a difference between ClientAPI and ProviderAPI evolution rules. As soon as you publish an API, you, as a publisher of your library, want to have a freedom to evolve it to satisfy additional requirements of users of your library. What additional requirements can you expect? In case when the API can only be called, then the obvious requirement is to add more methods and to allow users to call more of such exposed functionality.

This can be envisioned as an open space which grows with a time. To keep BackwardCompatibility, every method, field or class which has been present in previous releases, needs to stay. New methods can however be added as requested. Those clients that used to call the previously existing elements, don't need to care about the new ones. Clients seeking new functionality will be pleased when it gets exposed in the open space of your ClientAPI.

What is the most suitable coding construct in Java to support an open space? The most safe one is a final class (one cannot cause binary BackwardCompatibility problems by adding new methods into such class). Exposing only final classes to users of a ClientAPI makes it bullet-proof:

public final class Player {
  public void play(File mp3);
  public void stop();
 
  /** @since 2.0 we can also control volume */
  public void setVolume(int volume);
}

The shape of ProviderAPI is quite different. See APIvsSPI to understand what happens when you try to merge the open space with the shape associated with ProviderAPI.

ProviderAPI

Rules for extending API designed for implementors are very different to those for API that can only be called.

As soon as 3rd party author creates an implementation of your interface, the natural expectation of the author is, that it will work forever.

One can visualize such provider interface as a fixed point - a piece of the API that does not change at all. Well, it can change a bit. You may decide to call its methods more often in new version, or call them less frequently. You may pass in arrays of size 4096 instead of previously used 1024, etc. Slight modifications of the contract are allowed, but compared to ClientAPI (where adding methods is good), the interface for providers is really fixed. No new method additions (adding a new method would put additional, unsatisfiable requirement on already existing implementors), no refactorings. Stability is good.

The easiest way to break this promise is to add new non-implemented methods into the type. As such do not add new methods to interfaces or abstract classes implemented by some providers. This immediately breaks the promise of BackwardCompatibility.

The best construct in Java to implement ProviderAPIs is to define these types as Java interfaces. All their methods are abstract by default (thus they have to be implemented) and it is clear that changing the interface method signatures renders existing implementation uncompilable.

As soon as a modification of the contract with providers is needed, one should design new interface to be implemented (as described at ExtendingInterfaces page). The choice between the two interfaces clearly defines version which the provider implements. One can use instanceof to check whether the provider implemented old contract only or also the new one. According to result of the instanceof check, you can switch and behave appropriatelly:

/** @since 1.0 */
public interface Playback {
  public void play(byte[] arr);
 
  /** @since 2.0 we can also control volume */
  public interface Volume extends Playback {
    public void setVolume(int volume);
  }
}

Of course you don't want to spread instanceof calls all over the user's code. But if the Playback interface is used only inside of Player class (representing the ClientAPI), then few additional instanceof checks don't harm any user of the ClientAPI (just the API writer, which is always a limited and thus acceptable demage).

The list of additional interfaces that can be implemented is sometimes hard to find. As the previous code snippet shows, it helps if they are nested interface inside the most general (original) one. When the providers write their implementations, they directly see all available extensions added in later revisions of the API.

Separation & Composition

The worst thing (with respect to evolution) an API design can make is to create a type that serves both purposes - it is a ClientAPI which is supposed to be called by everyone and also ProviderAPI implemented by everyone (everyone here is used in the meaning used while talking about distributed development).

If you try to fit an open space into a fixed point, what will you get? You'll get a fixed point - e.g. no changes allowed. Evolution prevented (only ExtendingInterfaces approach is possible, but tons of instanceof spread around all usages are not beautiful nor sustainable).

Rather than mixing ClientAPI and ProviderAPI, separate them and compose them together! Everytime you design a fixed point, make sure that it is extensible. One example of that is RequestResponse pattern, but in general it is enough if you pass to the fixed point an open space argument. That will guarantee extensibility.

Calculator

Let's go through an example trying to design a bean (e.g. ClientAPI) and its listener (which is a ProviderAPI).

Code from Calculator.java:
See the whole file.

public final class Calculator {
    private final EventSupport listeners;
    private int sum;
 
    private Calculator(EventSupport listeners) {
        this.listeners = listeners;
    }
 
    /** An abstraction over various types of event delivery
     * to listeners. Comes with four different implementations.
     * A trivial one, asynchronous one, one with support for
     * pending events and one for a batch events delivery.
     */
    interface EventSupport {
        public void fireModificationEvent(ModificationEvent ev);
        public void add(ModificationListener l);
        public void remove(ModificationListener l);
    }
 
    public static Calculator create() {
        return new Calculator(new TrivialEventSupport());
    }
 
    public static Calculator createAsynch() {
        return new Calculator(new AsyncEventSupport());
    }
 
    /** @since 2.0 */
    public static Calculator createPending() {
        return new Calculator(new PendingEventSupport());
    }
 
    /** @since 3.0 */
    public static Calculator createBatch() {
        return new Calculator(new PostEventSupport());
    }
 
    public synchronized void add(int add) {
        sum += add;
        listeners.fireModificationEvent(new ModificationEvent(this, add));
    }
 
    public synchronized int getSum() {
        return sum;
    }
 
    public void addModificationListener(ModificationListener l) {
        listeners.add(l);
    }
    public void removeModificationListener(ModificationListener l) {
        listeners.remove(l);
    }
}
 

The calculator has an internal sum which can change by adding numbers to it. Whenever a change happens, listeners are notified. The actual handing of the listeners is abstracted from the bean implementation and we will provide four different ways to deal with them later:

Code from ModificationListener.java:
See the whole file.

public interface ModificationListener extends EventListener {
    public void modification(ModificationEvent ev);
}
 

Using Java interface is appropriate. We want the providers to implement all the methods of the interface. We know this is a fixed point that should not change - e.g. Java interface keyword is perfect. On the other hand, we are sure some evolution will be needed. Thus we associate this fixed point with a ClientAPI to form an open space to help us with future changes:

Code from ModificationEvent.java:
See the whole file.

public final class ModificationEvent extends EventObject {
    private final int delta;
    ModificationEvent(Object source, int delta) {
        super(source);
        this.delta = delta;
    }
 
    public int getChange() {
        return delta;
    }
 
}
 

The event is typical ClientAPI. It is called by the providers of the listener, but its implementation is provided by the API itself. It is made final and as such it may safely grow in the future. Moreover it has package private constructor (another good defensive API design habbit), so only the Calculator class may instantiate the instances.

There are few different implementation of the listeners. The trivial one delivers events synchronously:

Code from TrivialEventSupport.java:
See the whole file.

@Override
public void fireModificationEvent(ModificationEvent ev) {
    for (ModificationListener l : listener) {
        l.modification(ev);
    }
}
 

However as calling foreing code while holding own locks often leads to deadlocks, it maybe be more wise to avoid it. One option is to change the locking scheme. The other is to deliver events asynchronously. Let's try the asynchronous approach:

Code from AsyncEventSupport.java:
See the whole file.

private static final Executor EXEC = Executors.newSingleThreadExecutor();
@Override
public void fireModificationEvent(ModificationEvent ev) {
    EXEC.execute(new Deliverable(
        ev, listeners.toArray(new ModificationListener[0])
    ));
}
 
private static class Deliverable implements Runnable {
    final ModificationEvent ev;
    final ModificationListener[] listeners;
 
    public Deliverable(
        ModificationEvent ev, ModificationListener[] listeners
    ) {
        this.ev = ev;
        this.listeners = listeners;
    }
 
    @Override
    public void run() {
        for (ModificationListener l : listeners) {
            l.modification(ev);
        }
    }
}
 

All such implementations can be used and tested by a common code that looks like this:

Code from CalculatorBase.java:
See the whole file.

public void testSumAndListeners() throws Exception {
    Calculator a = create();
    MockListener l = new MockListener();
    a.addModificationListener(l);
    a.add(5);
    a.add(10);
    a.add(20);
    int ch = allChanges(l.assertEvents("Three changes", 3));
    assertEquals("35 was the change", 35, ch);
    assertEquals("Current value", 35, a.getSum());
    a.add(-5);
    int ch2 = allChanges(l.assertEvents("One change", 1));
    assertEquals("minus five was the change", -5, ch2);
    assertEquals("Final value", 30, a.getSum());
}
 
private static int allChanges(List<ModificationEvent> events) {
    int changes = 0;
    for (ModificationEvent me : events) {
        changes += me.getChange();
    }
    return changes;
}
 
public static class MockListener implements ModificationListener {
    private List<ModificationEvent> events;
 
    @Override
    public synchronized void modification(ModificationEvent ev) {
        if (events == null) {
            events = new ArrayList<ModificationEvent>();
        }
        events.add(ev);
        notifyAll();
    }
 
    public synchronized List<ModificationEvent> assertEvents(
        String msg, int cnt
    ) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            if (events != null && events.size() >= cnt) {
                break;
            }
            wait(1000);
        }
        assertEquals(msg + ":\n" + events, cnt, events.size());
        List<ModificationEvent> res = events;
        events = null;
        return res;
    }
} // end of ModificationListener
 

Extending Open Space

Sometimes, when there is a storm of events, it is important to be able to identify the last one. For example the X Window system supports a kind of reminding counter. Each event comes with a number of known events yet to be delivered. Applications can then mark their state as dirty and wait with any action until the last event in a batch is delivered.

Such storms are unlikely to be real problem in our Calculator example, but still for educational purposes, let's try to enhance the system with a way to find out amount of remaining events:

Code from ModificationEvent.java:
See the whole file.

int pending;
/** @since 2.0 */
public int getPending() {
    return pending;
}
 

The field is initialized by the listener support class before the event is dispatched to registered listeners:

Code from PendingEventSupport.java:
See the whole file.

int pendingCount = pending.length;
for (Deliverable d : pending) {
    d.ev.pending = --pendingCount;
    for (ModificationListener l : d.listeners) {
        l.modification(d.ev);
    }
}
 

We have evolved the ClientAPI - the event class. This is a classical approach used many times in the JDK itself (for example PropertyChangeEvent got additional getPropagationId). For many purposes this is sufficient. However there can be more complicated use-cases that may require more complex evolution.

Mounting new Open Space via a Fixed Point

There is a FileSystem API in NetBeans and it is also capable to deliver events in batches. There were some listeners wanting to be notified about the last modification only. However, due to complex releations between objects and listeners in the system, it was impossible to tell in advance, whether an event comming from the batch is the last one to be delivered to the listener or not (thus we cannot reliably count getRemaining()) value and had to try different solution.

We could add a method lastEventProcessed into the listener interface. However this is not allowed, as the listener (aka ProviderAPI) shall remain immutable for the sake of BackwardCompatibility. We could follow the advice described in ExtendingInterfaces page and create new marker interface LastProccessed with the above metioned method. But rather than that, we resolve to a trick that mounts new open space on top of existing one. First of all let's define new listener (or one could reuse Runnable in this case):

Code from PostModificationListener.java:
See the whole file.

public interface PostModificationListener extends EventListener {
    public void postProcess(PostModificationEvent ev);
}
 

Then add new method to the event class. As the class is ClientAPI it can absorb new methods without any problems:

Code from ModificationEvent.java:
See the whole file.

Collection<PostModificationListener> posts;
/** @since 3.0 */
public void postProcess(PostModificationListener p) {
    posts.add(p);
}
 

The idea is that those willing to find out when batch processing is over, can implement the new listener and register themselves when they receive the first event:

Code from PostTest.java:
See the whole file.

class PostListener extends BlockingListener 
implements PostModificationListener {
    int cnt;
 
    @Override
    public synchronized void modification(ModificationEvent ev) {
        // registers for callback when batch processing is over:
        ev.postProcess(this);
        super.modification(ev);
    }
 
    @Override
    public synchronized void postProcess(PostModificationEvent ev) {
        // called when batch processing is over
        cnt++;
    }
}
 

The implementation just makes sure to collect all registered and to notify them when all regular listeners are processed:

Code from PostEventSupport.java:
See the whole file.

Calculator calc = null;
Set<PostModificationListener> notify;
notify = new HashSet<PostModificationListener>();
int pendingCount = pending.length;
for (Deliverable d : pending) {
    calc = (Calculator)d.ev.getSource();
    d.ev.pending = --pendingCount;
    d.ev.posts = notify;
    for (ModificationListener l : d.listeners) {
        l.modification(d.ev);
    }
    d.ev.posts = null;
}
for (PostModificationListener pml : notify) {
    pml.postProcess(new PostModificationEvent(calc));
}
 

The solution enhances the existing open space (which is allowed), creates new fixed point and make sure it again contains new open space (so it is ready for future evolution). The whole set up is displayed on the picture with two open space arcs. However one does not have stop at two, the extensibility is unlimited...

Personal tools
buy