11

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.

0

3 Answers 3

7

Have you had a look at the ServiceLoader Mechanisms in Java? Basically you can specify the implementation of an interface via the manifest file in a jar. Oracle also provides some further information about plugins in Java programs.

3

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

2
  • To expand on the slf4j example: slf4j contains dummy implementations in the sources which then get removed after compiling. The effect of that is a strong reference (which results in a ClassDefNotFound if there's no implementation provided in your application)
    – dot_Sp0T
    Commented Oct 5, 2015 at 5:55
  • @dot_Sp0T I agree that the "ClassDefNotFound" problem exists for android because in android all jars are merged into one apk file. In ordinary java (withoit using an obfuscator) this should not be a problem. If i am wrong pleas leat me know under wich circumstances happen.
    – k3b
    Commented Oct 5, 2015 at 12:21
1

From what I understand people often use the factory pattern for this.

They put the API interfaces into separate module (say a jar file), and then when the clients want to use the API and has access to an implementation of the API, the implementation of the API will have a factory entry point for the clients to start creating concrete objects implementing that API.

This means that your first idea from the above is the closest resemblance.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.