Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is a good design for an interface with optional components?

Suppose I have an interface that supports a few potential operations:

interface Frobnicator {
    int doFoo(double v);
    int doBar();
}

Now, some instances will only support one or the other of these operations. They may support both. The client code won't necessarily know until it actually gets one from the relevant factory, via dependency injection, or wherever it is getting instances from.

I see a few ways of handling this. One, which seems to be the general tactic taken in the Java API, is to just have the interface as shown above and have unsupported methods raise UnsupportedOperationException. This has the disadvantage, however, of not being fail-fast - client code can't tell whether doFoo will work until it tries to call doFoo.

This could be augmented with supportsFoo() and supportsBar() methods, defined to return true iff the corresponding do method works.

Another strategy is to factor the doFoo and doBar methods into FooFrobnicator and BarFrobnicator methods, respectively. These methods would then return null if the operation is unsupported. To keep the client code from having to do instanceof checks, I define a Frobnicator interface as follows:

interface Frobnicator {
    /* Get a foo frobnicator, returning null if not possible */
    FooFrobnicator getFooFrobnicator();
    /* Get a bar frobnicator, returning null if not possible */
    BarFrobnicator getBarFrobnicator();
}

interface FooFrobnicator {
    int doFoo(double v);
}

interface BarFrobnicator {
    int doBar();
}

Alternatively, FooFrobnicator and BarFrobnicator could extend Frobnicator, and the get* methods possibly be renamed as*.

One issue with this is naming: the Frobnicator really isn't a frobnicator, it's a way of getting frobnicators (unless I use the as* naming). It also gets a tad unwieldy. The naming may be further complicated, as the Frobnicator will be retrieved from a FrobnicatorEngine service.

Does anyone have any insight into a good, preferably well-accepted solution to this problem? Is there an appropriate design pattern? Visitor is not appropriate in this case, as the client code needs a particular type of interface (and should preferably fail-fast if it can't get it), as opposed to dispatching on what kind of object it got. Whether or not different features are supported can vary on a variety of things - the implementation of Frobnicator, the run-time configuration of that implementation (e.g. it supports doFoo only if some system service is present to enable Foo), etc.

Update: Run-time configuration is the other monkey wrench in this business. It may be possible to carry the FooFrobnicator and BarFrobnicator types through to avoid the problem, particularly if I make heaver use of Guice-modules-as-configuration, but it introduces complexity into other surrounding interfaces (such as the factory/builder that produces Frobnicators in the first place). Basically, the implementation of the factory that produces frobnicators is configured at run-time (either via properties or a Guice module), and I want it to make it fairly easy for the user to say "hook up this frobnicator provider an this client". I admit that it's a problem with potential inherent design problems, and that I may also be overthinking some of the generalization issues, but I'm going for some combination of least-ugliness and least-astonishment.

like image 570
Michael Ekstrand Avatar asked Nov 30 '10 16:11

Michael Ekstrand


1 Answers

I see a few ways of handling this. One, which seems to be the general tactic taken in the Java API, is to just have the interface as shown above and have unsupported methods raise UnsupportedOperationException. This has the disadvantage, however, of not being fail-fast - client code can't tell whether doFoo will work until it tries to call doFoo.

As you said, the general tactic is to use the template method design pattern. An excellent example is the HttpServlet.

Here's how you could achieve the same.

public interface Frobnicator {
    int doFoo(double v);
    int doBar();
}

public abstract class BaseFrobnicator implements Frobnicator {
    public int doFoo(double v) {
        throw new UnsupportedOperationException();
    }
    public int doBar() {
        throw new UnsupportedOperationException();
    }
}

/**
 * This concrete frobnicator only supports the {@link #doBar()} method.
 */
public class ConcreteFrobnicator extends BaseFrobnicator {
    public int doBar() {
        return 42;
    }
}

The client has just to read the docs and handle the UnsupportedOperationException accordingly. Since it's a RuntimeException, it's a perfect case for a "programmer error". True, it's not fail-fast (i.e. not compiletime), but that's what you get paid for as developer. Just prevent it or catch and handle it.

like image 97
BalusC Avatar answered Oct 08 '22 22:10

BalusC