APIvsSPI
From APIDesign
(→Separation & Composition) |
(→Separation & Composition) |
||
Line 15: | Line 15: | ||
Rather than mixing [[ClientAPI]] and [[ProviderAPI]], separate them and compose them together! | Rather than mixing [[ClientAPI]] and [[ProviderAPI]], separate them and compose them together! | ||
+ | |||
+ | [[Image:OpenFixed.png|thumb|right]] | ||
[[TBD]]: How to compose client and provider [[API]]s? | [[TBD]]: How to compose client and provider [[API]]s? |
Revision as of 06:44, 20 March 2011
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 fix 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!