Java – In Java, what are some good ways to separate APIs from implementation of entire projects

apidesign-patternsjava

Context: Imagine you're writing a plugin to a program (let's say Foobar Plugin for Eclipse) and you want it to have an API which other people – such as the creators of Barbaz Plugin for Eclipse – can call in order to provide integration. ("Whenever a foo is barred, our plugin also bazzes it automatically! … if the foobar plugin is actually installed, that is.")

If Barbaz just uses classes from Foobar, they'll crash if Foobar isn't actually installed. For licensing or other reasons, Barbaz also wants to be able to work without Foobar. Foobar might not be free-as-in-beer, or it might just be a very large or complicated plugin which users don't want to install.

You might solve this by distributing a small "API plugin", perhaps with just interface declarations IFoobarPlugin. If Foobar is installed, Barbaz can acquire a real instance of IFoobarPlugin – otherwise, they get a null object (or at least a null pointer). Either way, the class IFoobarPlugin is still installed, so Barbaz Plugin doesn't crash with a NoClassDefFoundError.

In short, instead of this dependency graph:

 Barbaz
   |
   V
 Foobar

the developers of both plugins would prefer this:

Barbaz   Foobar
   \       /
    V     V
   FoobarAPI

The question is: How does Barbaz get a reference to the real instance of IFoobarPlugin?

  1. FoobarAPI directly wraps the Foobar classes.

    • This relies on NoClassDefFoundError being thrown in a predictable location (e.g. the first time a method is called which references the non-existent class). It's conceivable that on some platforms, you can't even load FoobarAPI
    • FoobarAPI cannot be compiled from source without Foobar.
    • Typically, the client must not call getFoobarPlugin until it detects that Foobar is actually installed.
    package com.foobar.api;
    import com.foobar.internal.FoobarPlugin;
    public class FoobarPluginAPI {
        public static IFoo createFoo() { // may throw NoClassDefFoundError
            return FoobarPlugin.createFoo();
        }
    }
    
  2. FoobarAPI gets the plugin instance via reflection.

    • The API can be compiled without the implementation.
    • It is easy to predict what happens when Foobar isn't installed.
    • The use of reflection may cause a performance hit, if the function is called repeatedly. (This can be avoided by returning a factory object)
    package com.foobar.api;
    public class FoobarPluginAPI {
        public static IFoo createFoo() {
            try {
                return (IFoobarPlugin)Class.forName("com.pluginx.internal.FoobarPlugin").getMethod("createFoo").invoke(null);
            } catch(ReflectiveOperationException e) {
                return new NullFoobarPluginImpl(); // or null, or throw an exception, etc
            }
        }
    }
    
  3. Foobar registers itself in the API when it initializes.

    • The API is completely separate from the implementation.
    • There is no string-typing with reflection. The compiler can type-check everything here.
    • Alternative plugins ("Open Foobar") can register the same API.
    • If the client doesn't find an instance of Foobar, it might not know whether none is installed, or whether it is installed but hasn't been registered yet.
    package com.foobar.api;
    public abstract class FoobarPluginAPI {
        public abstract IFoo createFoo();
    
        private static FoobarPluginAPI instance;
        public static FoobarPluginAPI getInstance() {return instance;}
        public static void setInstance(FoobarPluginAPI newInstance) {
            if(instance != null)
                throw new IllegalStateException("instance already set");
            else
                instance = newInstance;
        }
    }
    
  4. The same as above, but the client code needs to get the initial reference from the plugin system itself.

    • No special considerations needed in the API module
    • However, the client code is much more verbose.
    // API
    package com.foobar.api;
    public interface FoobarPluginAPI {
        IFoo createFoo();
    }
    
    // Implementation
    package com.foobar;
    import org.eclipse.blahblah.Plugin;
    import com.foobar.api.FoobarPluginAPI;
    public class FoobarPlugin extends Plugin implements FoobarPluginAPI {
        @Override
        public IFoo createFoo() { ... }
    }
    
    // Client code uses it like this
    FoobarPluginAPI foobar = (FoobarPluginAPI)EclipsePluginManager.getPluginByID("com.foobar");
    if (foobar != null) {
        Foo foo = foobar.createFoo();
        // ...
    }
    
  5. Don't. Make it the client's problem. The client (Barbaz Plugin) will probably end up making their own wrapper which acts like one of the above.

Best Answer

The answer to your question depends on the definition of "good" in the question "what are some good ways to separate APIs from implementation of ..."

If "good" means "pragmatic easy to implement for you as manufecturer of the api" this might be helpful:

since you are using java where jar-libraries are loaded at runtime i would suggest a slightly different alternative:

  • The API consists only of interfaces plus a dummy implementation of these interfaces that has no real function.

the customer using your api can compile against this dummy-jar and at runtime you can replace the dummy-jar with the licensed one.

something similar is done with slf4j where the actual logging-implementation to be used with your application will be chosen by replacing the logging-jar