0

I develop an in-house Java framework. I provide an interface so that my end users can provide their own custom implementation (i.e. plugin/SPI).

public interface SomePlugin {
    SomeResponse doSomething(SomeRequest request);
}

There are a number of small configurations to be made, some required and some optional. For example: plugin name, response formatter.

My initial interface looked like this (V1):

public interface SomePlugin { // V1
    String getName();
    default SomeFormatter getResponseFormatter() { return new DefaultFormatter(); }
    SomeResponse doSomething(SomeRequest request);
}

Over time I learned this is not easily extensible, without breaking existing user implementations or abusing the default keyword. Reasons:

  • What if I want to receive another config value, e.g. plugin description?
  • What if I need to provide an alternative way to receive response formatting?

V2

Now, this blog post suggests that I may use "argument objects" to better achieve this.

It is generally a bad idea to have non-void methods in SPIs as you will never be able to change the return type of a method again. Ideally, you should have argument types that accept “outcomes”.

public interface SomePlugin { // V2
    void configure(SomeConfigurator config);
    SomeResponse doSomething(SomeRequest request);  
}

public interface SomeConfigurator {
    void setName(String name);
    void setDescription(String description);
    void useFormat(SomeFormatter formatter);
    void useFormat(SomeParser parser, SomeOutputBuilder builder);
}
public class MyPlugin { // Usage example
    void configure(SomeConfigurator config) {
        config.setName("Test plugin");
        config.useFormat(new MyFormatter());
    }
    ...
}

This nicely solves the extension/evolution problem that my initial design (V1) had.

  • Deprecation and addition are easier.
  • I can now receive multiple objects at once, i.e. parser and builder, without creating a wrapper object.
  • I can overload some methods because what was a return type is now a parameter.

I lose the required-ness of mandatory configs, i.e. users may forget to provide name and it will still compile. But I think this is an acceptable trade-off for maintainability, with help of Javadoc and runtime checks.

V3

One concern on the concept of "argument objects" with void return type is that it is a foreign idea. When I showed this prototype to my end users, they didn't grasp how to use it unless an example was provided. This leads me to consider:

public interface SomePlugin { // V3
    SomeConfig getConfig();
    SomeResponse doSomething(SomeRequest request);  
}

public final class SomeConfig {
    private String name;
    private String description = "";
    private SomeFormatter formatter = new DefaultFormatter();
    private SomeParser parser;
    private SomeOutputBuilder builder;
    ... getters and setters ...
}
public class MyPlugin { // Usage example
    SomeConfig getConfig() {
        SomeConfig config = new SomeConfig();
        config.setName("Test plugin");
        config.useFormat(new MyFormatter());
        return config;
    }
    ...
}

Now, this feels more like a "traditional" Java and should be more intuitive to my end users. I still get the benefits of V2. It's unlikely that I'd have to worry about deprecating or replacing getConfig() itself.

One small con: SomeConfig is now more verbose with all getters and setters, compared to SomeConfigurator in V2 which looked like a clean "header file". Despite this, I feel like V3 is the way to go.

Does my reasoning make sense? Are there any reasons to favor V2 over V3, or do you have other approaches to suggest? Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.