Yep. I've designed several APIs in the past and it is all about a clean definition of the interfaces involved. Hiding implementation details where ever possible, but being sure to make them explicite when they really matter. Always imagine at least two completely different ways the library could be used (i.e. put yourself in the shoes of two potential users, each with different goals) and use that as a basis for making the interface reasonably generic in the context of the library.

If you want to make an expandable library you might condsider splitting up your library in three parts:

- API- SPI- Implementatation

The API (Application Prorgamming Interface) consists of the interfaces and basic data type classes you would like to provide. To create real instances of the implementing classes you should consider using the abstract factory pattern (meaning creating the instances by a "factory"-class with some createXXX()-Method instead of directly calling constructors).

The SPI (Service Provider Interface) contains all the neccesary bits for implementing modules in your library. It mainly consists of abstract base classes that implement one or more interfaces from the API and a set of helper utilities.

The Implementation is what it's named: the concrete (standard)-implementation of your API ;-)

// To create an instance of your Foo classFooaNewFoo= Factory.getInstance().createFoo();

As you can see, the separation of API and SPI enables the library author to add more interface methods over time without breaking the module developer contract, since they only depend on the AbstractFoo base class. A similar effect can be seen for the "getIdentity()"-method in ConcreteFoo, which was an abstract method of the SPI but was removed from the base class at some version to be replaced by another identification mechanism, that does not depend on this info from the concrete implementation anymore. Both modifications did not break the contract between the user and the library nor between the module vendor and the library, so long term binary compatibility can be archived.

Since the API-user should not need to know anything about the concrete implementation, the Factory just deliveres an instance of the API Interface. Which class is instanciated can be configured using a system property, so you can replace the API-Implementation by specifying a different concrete class in the property without touching the users code. Normally you would use a configuration file or something instead of a system property, but this is just an example

Also keep in mind, that using a factory for every single type in your library is probably not the right way. There can be a lot of simple classes in your API, that don't need the abstraction and exchangability feature, as well. However, the factory pattern as described above is best suited for component/module instanciation.

You don't have to provide a SPI layer for everything, but having it for a limited set of module-/component-interfaces is a Good Thing (TM). For example it might be a good idea to use an SPI layer for something like a plugable scenegraph renderer, but it makes little sense to use one for new geometry-types to be added to a scenegraph, for which simply implementing a generic Geometry interface is sufficient (and probably much better).

From my point of view, SPI are usually a design error ; they promote implementation inheritance as a good design practice which, I think, is completely wrong.

The more simple design pattern with interface on one side and implementation on the other seems the good way to go for me.

Your client should never be in a situation were it has to read your implementation to use it. SPI will create such a situation.

For the API I design, I tend to try to apply this design rules ; . keep it simple and readable . keep classes and interfaces "short" (i.e. avoid interfaces with 30 methods, classes with hundreds of lines of code,...) . build a readable package architecture (avoid packages wih too many elements, avoid mixing definition package with interfaces and implementation packages with classes) . prohibit package cycles . restrict class cycles to the minimum . keep it flat (i.e. limit the number of levels of inheritance) . discourage implementation inheritance (in fact, implementation inheritance is only allowed for ultra-stable classes fo which I am completely sure that the contract willnot evolve, and even in this situation, it is only allowed in the internals of the library) . prefer composition to inheritance

One thing I would love to have in Java is "design by contract" (search for Bertrand Meyer book on the subject) wich gives a very good and safe way to design your library but it's still missing.

Vincent

Edit : the example of the pluggable scenegraph renderer is clearly a situation where I would not (and I do not in my 3d engine) use a SPI. If you want your scenegraph renderer to be pluggable, I think you should clearly define the contract of your extension point in extension point interfaces and provide an extension registering mechanism.

example :

SPI :

public abstract class PluggableRenderer {

[...]

protected void visitNode(INode node);

[...]

}

Result : everything is mixed, the resulting design willnot be reusable, your plugin won't be reusable for another renderer.

Composition with interfaces :

public interface INodeVisitor {

public void visitNode(INode node);

}

public class PluggableRenderer {

public PluggableRenderer(INodeVisitor visitor) { [...] }

}

Result : the design is readable, your plugin is reusable.

Note : I know this example is very simple but you can go further this way. For example, my renderer use plugins for visiting nodes based on their node Id. The overhead for this is near zero since the design use a simple array access to get the plugin for the appropriate node.This lets my plugin be shared between the different scenegraph visitors.

Good library design is as much an esthetic as anythign esle, and exaclty what consittutes "good" chnages depending on your audiance.

Game develoeprs want lean, mean and efficient. If you cna make my job easier great but NEVER totally take away control from me or force me to use up CPU cycles where I don't want to.

Eneterprise develoerps want it as automated, idiot proof, and simple to use as possible and don't care much about control and only limitedly about performance, really.

A good general rule of thumb for any tool is this:"Doing the simpelst 80% of the use-cases should be easy. Doing the last, hard 20% shouldn't be any harder then doing it without your tool and ideally shoudl be easier."

Got a question about Java and game programming? Just new to the Java Game Development Community? Try my FAQ. Its likely you'll learn something!

java-gaming.org is not responsible for the content posted by its members, including references to external websites,
and other references that may or may not have a relation with our primarily
gaming and game production oriented community.
inquiries and complaints can be sent via email to the info‑account of the
company managing the website of java‑gaming.org