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.

Answers


I think you have a problem if you are saying that class FooImpl implements interface Foo, but it doesn't really implement ALL of Foo. The interface is supposed to be a contract indicating what methods the implementing class (at minimum) implement.

If something is calling the FooFactory to get a Foo object, the object should be able to do what Foos do.

So, where do you go from there? I'd suggest your composition idea is a good one, using inheritance slightly less so.

Composition would work exactly as you have it with the getFooFrobnicator() and getBarFrobnicator() methods. If you don't like that your Frobnicator doesn't really frobnicate, call it a FrobnicatorHolder.

Inheritance would have your Frobnicator containing only the methods that all Frobnicators have, and the sub-interfaces would have the additional methods.

public interface Frobnicator{
   public void frobnicate();
}

public interface FooFrobnicator{
   public void doFoo();
}

public interface BarFrobnicator{
   public void doBar();
}

You could then use if (frobnicator instanceof FooFrobnicator) { ((FooFrobnicator) frobnicator).doFoo() } and life goes on.

Favor composition over inheritance. You'll be happier.


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.


Need Your Help

SOLVED: RubyFiddle issue - NameError: undefined local variable or method 'gets' for #

ruby input io gets

I apologise if this has been asked before. I have looked through Stack Overflow and I have tried some potential solutions but to no avail.