开发者

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

开发者 https://www.devze.com 2023-01-27 11:08 出处:网络
Suppose I have an interface that supports a few potential operations: interface Frobnicator { int doFoo(double v);

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, defi开发者_运维知识库ned 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.


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.


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.


How about just adding empty non-implementation methods rather than throwing UnsupportedOperationExceptions? This way, calling an unsupported method will have no side-effects, and won't cause a run-time error.

If you do want to be able to tell if a certain object supports a method or not, I would suggest you split your interface up into two with a common super interface, and then type check whatever object you get handed to determine what methods are supported. This is probably cleaner, and more advisable than my first suggestion.

interface Frobnicator {}

interface FooFrobnicator extends Frobnicator {
    void doFoo();
}

interface BarFrobnicator extends Frobnicator {
    void doBar();
}

Edit:

Another way of doing it would be to add a boolean return type to your methods, and make sure that they only return false if the method is not supported. Hence:

interface Frobnicator 
{
    boolean doFoo();
    boolean doBar();
}

class FooFrobnicator implements Frobnicator 
{
    public boolean doFoo() { code, code, code; return true; }
    public boolean doBar() { return false; }
}


The most obvious solution is to just define two interfaces FooFrobnicator and BarFrobnicator. This should be fine as class can implement multiple interfaces.

You could then provide a super (marker) interface if necessary.


The simpler solution is to add supports*() methods,

If you think that is not good style, then just forget about Frobnicator (it gives you very little as it is not an interface you can rely on) and code directly against FooFrobnicator and BarFrobnicator, and if they happen to be implemented by the same object, fine. This also gives you the possibility of requiring a specific interface in client code:

interface FooFrobnicator {
    doFoo();
}

interface BarFrobnicator {
    doBar();
}

public class Client {
     ...
     FooFrobnicator fooFrobnicator;
     public void setFooFrobnicator(FooFrobnicator fooFrobnicator) {
         this.fooFrobnicator = fooFrobnicator;
     }

     BarFrobnicator barFrobnicator;
     public void setBarFrobnicator(BarFrobnicator barFrobnicator) {
         this.barFrobnicator = barFrobnicator;
     }
     ...
     public void doSomething() {
         ...
         if (fooFrobnicator != null) { ... }
         ...
         if (barFrobnicator != null) { ... }
     }
}
...
public class FrobnicatorImpl implements FooFrobnicator, BarFrobnicator { ... }
...
public void doSomething() {
    ...
    FrobnicatorImpl impl = new FrobnicatorImpl();
    Client client = new Client();        
    client.setFooFrobnicator(impl);
    client.setBarFrobnicator(impl);
    ...
}


IMHO, you're making this way too complicated. Do away with the interface. Create a base class with the commonly used methods (they can even do nothing in the base class if you don't know how to implement them there). Then extend the base class & implement/override whichever methods you need.

0

精彩评论

暂无评论...
验证码 换一张
取 消