对于带有可选组件的界面来说,什么是好的设计?

发布于 2024-10-05 09:05:01 字数 2065 浏览 1 评论 0原文

假设我有一个支持一些潜在操作的接口:

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

现在,某些实例将仅支持这些操作中的一个或另一个。他们可能两者都支持。客户端代码不一定知道,直到它通过依赖项注入或从任何地方获取实例实际从相关工厂获取实例为止。

我看到了一些处理这个问题的方法。其中一种似乎是 Java API 中采用的通用策略,即仅具有如上所示的接口,并让不支持的方法引发 UnsupportedOperationException。然而,这有一个缺点,即不能快速失败 - 客户端代码在尝试调用 doFoo 之前无法判断 doFoo 是否会工作。

这可以通过 supportsFoo()supportsBar() 方法进行增强,定义为当相应的 do 方法有效时返回 true。

另一种策略是将 doFoodoBar 方法分别分解为 FooFrobnicatorBarFrobnicator 方法。如果不支持该操作,这些方法将返回 null。为了使客户端代码不必执行 instanceof 检查,我定义了一个 Frobnicator 接口,如下所示:

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();
}

或者,FooFrobnicatorBarFrobnicator< /code> 可以扩展 Frobnicator,并且 get* 方法可能被重命名为 as*

其中一个问题是命名:Frobnicator 实际上不是一个 frobnicator,它是一种获取 frobnicator 的方法(除非我使用 as* 命名)。它也变得有点笨拙。命名可能会更加复杂,因为将从 FrobnicatorEngine 服务中检索 Frobnicator

有谁对这个问题有一个好的、最好是被广泛接受的解决方案有任何见解吗?有合适的设计模式吗?在这种情况下,访问者是不合适的,因为客户端代码需要特定类型的接口(如果无法获取它,最好应该快速失败),而不是分派它所获取的对象类型。是否支持不同的功能可能因各种因素而异 - Frobnicator 的实现、该实现的运行时配置(例如,仅当某些情况下它才支持 doFoo)系统服务的存在是为了启用Foo)等。

更新:运行时配置是这项业务中的另一个难题。可能可以携带 FooFrobnicator 和 BarFrobnicator 类型来避免这个问题,特别是如果我大量使用 Guice-modules-as-configuration,但它给其他周围接口(例如生产 Frobnicators 的工厂/构建器)带来了复杂性首先)。基本上,生成 frobnicator 的工厂的实现是在运行时配置的(通过属性或 Guice 模块),我希望它能够让用户很容易地说“将此 frobnicator 提供程序连接到此客户端” 。我承认这是一个潜在的固有设计问题,而且我也可能过度考虑了一些泛化问题,但我会寻求最不丑陋和最不令人惊讶的某种组合。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

路弥 2024-10-12 09:05:01

我看到了一些处理这个问题的方法。其中一种似乎是 Java API 中采用的通用策略,即仅具有如上所示的接口,并让不支持的方法引发 UnsupportedOperationException。然而,这有一个缺点,即不能快速失败 - 客户端代码在尝试调用 doFoo 之前无法判断 doFoo 是否会工作。

正如您所说,一般策略是使用模板方法设计模式。一个很好的例子是 HttpServlet

您可以通过以下方法实现同样的目标。

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;
    }
}

客户端只需阅读文档并相应地处理 UnsupportedOperationException 即可。由于它是一个 RuntimeException,因此它是“程序员错误”的完美案例。确实,它不是快速失败(即不是编译时),但这就是您作为开发人员获得的报酬。只要阻止它或抓住并处理它即可。

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.

王权女流氓 2024-10-12 09:05:01

如果您说类 FooImpl 实现了接口 Foo,但它并没有真正实现所有 Foo,我认为您有问题。该接口应该是一个契约,指示实现类(至少)实现哪些方法。

如果有东西调用 FooFactory 来获取 Foo 对象,则该对象应该能够执行 Foos 所做的事情。

那么,你要从那里去哪里呢?我建议你的组合想法是一个很好的想法,使用继承稍微少一些。

组合的工作方式与使用 getFooFrobnicator()getBarFrobnicator() 方法完全相同。如果您不喜欢您的 Frobnicator 不能真正进行 frobnicate,请将其称为 FrobnicatorHolder

继承将使您的 Frobnicator 仅包含所有 Frobnicator 具有的方法,并且子接口将具有附加方法。

public interface Frobnicator{
   public void frobnicate();
}

public interface FooFrobnicator{
   public void doFoo();
}

public interface BarFrobnicator{
   public void doBar();
}

然后,您可以使用 if (frobnicator instanceof FooFrobnicator) { ((FooFrobnicator) frobnicator).doFoo() } 生活继续。

优先考虑组合而不是继承。你会更快乐。

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.

撧情箌佬 2024-10-12 09:05:01

只添加空的非实现方法而不是抛出 UnsupportedOperationExceptions 怎么样?这样,调用不受支持的方法将不会产生副作用,也不会导致运行时错误。

如果您确实希望能够判断某个对象是否支持某个方法,我建议您使用公共超级接口将您的接口分成两个,然后键入检查您收到的任何对象以确定支持哪些方法。这可能比我的第一个建议更干净、更可取。

interface Frobnicator {}

interface FooFrobnicator extends Frobnicator {
    void doFoo();
}

interface BarFrobnicator extends Frobnicator {
    void doBar();
}

编辑:

另一种方法是向您的方法添加布尔返回类型,并确保它们仅在该方法不受支持时返回 false。因此:

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

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

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; }
}
我还不会笑 2024-10-12 09:05:01

最明显的解决方案是定义两个接口 FooFrobnicator 和 BarFrobnicator。这应该没问题,因为类可以实现多个接口。

如果需要的话,您可以提供一个超级(标记)接口。

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.

寒尘 2024-10-12 09:05:01

更简单的解决方案是添加 supports*() 方法,

如果你认为这不是好的风格,那么就忘记 Frobnicator (它给你的东西很少,因为它不是一个您可以依赖的接口)并直接针对 FooFrobnicatorBarFrobnicator 进行编码,如果它们碰巧由同一​​个对象实现,那就很好。这也使您可以在客户端代码中需要特定的接口:

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);
    ...
}

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);
    ...
}
趁微风不噪 2024-10-12 09:05:01

恕我直言,你让这种方式太复杂了。取消界面。使用常用方法创建一个基类(如果您不知道如何在基类中实现它们,它们甚至可以在基类中什么也不做)。然后扩展基类&实现/覆盖您需要的任何方法。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文