默认实现的设计模式:空方法实现

22

有没有一个特定的设计模式,描述了这样一种情况:提供非抽象默认实现,它使用空的NO-OP实现来实现接口上的所有或某些方法。这样做是为了减轻子类实现它们自己可能不需要/使用的方法的负担:

public interface MyInterface {
    public void doThis();
    public void doThat();
    public void done();
}

public class MyClass implements MyInterface {
    public void doThis() {
        // NO-OP
    }
    public void doThat() {
        // NO-OP
    }
    public void done() {
        // Some standard implementation
    }
}

public class MuSubClass extends MyClass {
    public void doThat() {
        // Subclass only cares about doThat()
    }
}

我曾多次见到这种模式的使用,包括Java SAX框架中的DefaultHandlerMouseAdapter。在某些情况下,这些类被命名为适配器,但我认为适配器模式是将两个不同的接口进行转换。
由于在这些实例中只声明了一个接口,而该接口被翻译为该接口的未定义子集,因此我不清楚这如何符合适配器模式的精神。
此外,我不太明白这如何符合NullObject模式,因为某些方法可能具有实现,而NullObject通常是单例。

1
我同意这既不是空对象也不是适配器。虽然不知道该怎么称呼它。 - Kristopher Johnson
我认为这就是为什么有些人更喜欢只有一个handle(T event)方法的类型化事件处理程序IEventHandler<T extends Event> - Matthias
9个回答

5

没有默认实现的设计模式。

我通常会在类名前添加DoNothing前缀。根据意图,我还会使用BaseDefault(后者被广泛使用)。可能MouseAdapter应该被称为DefaultMouseListener

如果您关心的话,您可以使用简单的DynamicProxy系统地存根接口,您必须仅返回一个“好”的默认值(Object的null,数字的0等)。

顺便说一句,这是一个非常好的问题。

编辑

此外,这既不是Stub也不是Mock:也许它可以与Stub混淆,但意图是不同的。


3
您应该遵循不同的设计原则:接口隔离原则

接口隔离原则规定,客户端不应强制实现它们不使用的接口。而是基于方法组,每个子模块提供一个小接口,而不是一个臃肿的接口。

您不应该实现更多,也不应该实现更少

请查看相关的SE问题以获取更多细节。

接口隔离原则

接口隔离原则-按接口编程


2

很好的问题。

我已开始使用NoOp作为该模式的类名前缀。这个名称简短,清晰,没有过载(如Empty[包含什么内容?],Null[Null Object模式,这与本模式不同?],Abstract [它提供了一些实现吗?],或Base[它提供了一些实现吗?])。

当我有一个第三方API提供"Hooks"来在复杂操作期间进行插装时,我可能会编写这种类型的类。请考虑该库提供的以下两个类:

public class LongRunningActionRunner {
    public void runSomethingLong(DecisionListener cdh) {
        // ...
    }
}

public interface DecisionListener {
    public void beforeFooHook();
    public void afterFooHook();
    public void beforeBarHook();
    public void afterBarHook();
    public void beforeBazHook();
    public void afterBazHook();
}

在这种情况下,您可以使用以下模式编写一个类,例如:
public class NoOpDecisionListener implements DecisionListener {
    @Override public Something beforeFooHook() {}
    @Override public Something afterFooHook() {}
    @Override public Something beforeBarHook() {}
    @Override public Something afterBarHook() {}
    @Override public Something beforeBazHook() {}
    @Override public Something afterBazHook() {}
}

2
我见过这种设计在春天使用,他们有一个名为FlowExecutionListenerAdapter的类,它可以帮助你保存所有的FlowExecutionListener操作。
然而,听起来也像是空对象模式。但是我觉得它更适合适配器模式,因为它通过允许你只实现你想要的部分来改变接口的行为...但这是个棘手的问题。
我相信这个问题以前已经被问过了?
这听起来相似,值得一读。

谢谢@JamesC - 我已经看过那篇文章了。然而,它仍然提到适配器或NullObject模式,但似乎都不太适合。 - teabot

2
它还在Swing中使用(WindowAdapter实现WindowListener)。这只是一个方便的适配器,您只需要按照此方式定义1-2个方法即可拥有一个有用的窗口监听器。这确实是适配器模式的一个实例,也展示了抽象类的功力。甚至可以用它来说明为什么有时多重实现继承很有用。
至于常规的设计模式,在模板方法中,您可以定义挂钩操作,这些操作可以被重写(与必须的抽象方法不同),但默认行为(通常为NO-OP)也很有意义。

你能解释一下为什么这是一个适配器实例吗?它正在“翻译”的接口是什么?你会说它是适配器的特殊情况,其中目标接口未声明并且具有适配接口方法的子集吗? - teabot
没错。WindowAdapter 类型也是你的目标接口。 - Karl

1

这种模式在旧版Java中很普遍。它是 Java 7接口默认方法的替代方案

Josh Bloch将其称为骨架实现。虽然骨架实现通常是抽象的,但如果骨架本身足够,则无需强制客户端创建子类。

我同意前面的答案指出了接口隔离原则。需要骨架实现可能是一种代码异味,表明接口过于“臃肿”,可能试图完成多个任务。在这种情况下,将接口拆分比创建带有虚拟或noop逻辑的骨架实现更可取。


0

对我来说,这似乎最接近特殊情况空对象模式。

您的更新表明与模板方法类似,但您没有一个单独的方法调用每个模板方法,例如:

public void doEverything()
{
  doThis();
  doThat();
  done();
}

关于空对象模式的修订问题 - teabot

0

我相信马丁·福勒会称之为空对象模式。在他的《重构》一书[1]中,马丁将空对象介绍如下:

多态性的本质是,你不需要询问一个对象它是什么类型,然后根据答案调用某些行为,而是直接调用行为。对象根据其类型做正确的事情。其中一个不太直观的地方是,在字段中有一个空值时。

他随后补充道:“当许多客户端想要做同样的事情时,您受益于可以简单地依赖默认的 null 行为。” 他还为需要变体行为的客户端引入了一个 isNull() 方法。

我同意有时会看到一个(通常是抽象的)实现被称为适配器。例如,在 Android 框架中,AnimatorListenerAdapter(源代码 here)被描述为:

这个适配器类提供了Animator.AnimatorListener方法的空实现。任何只关心该侦听器子集的自定义侦听器都可以简单地子类化此适配器类,而不是直接实现接口。
[1] "重构:改善现有代码的设计",第9章"简化条件表达式","引入Null对象"。

0
你在问空对象模式吗?
除此之外,根据你的编辑,MyClass 对象仅仅是默认实现。我认为并没有特定的设计模式来描述它。

关于空对象模式的修订问题 - teabot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接