【问题标题】:Wrappers/law of demeter seems to be an anti-patternWrappers/demeter 法则似乎是一种反模式
【发布时间】:2010-03-31 06:47:01
【问题描述】:

我一直在阅读“得墨忒耳法则”这个东西,它(以及一般的纯“包装器”类)似乎通常是反模式。考虑一个实现类:

class FluidSimulator {
    void reset() { /* ... */ }
}

现在考虑另一个类的两种不同实现:

class ScreenSpaceEffects1 {
    private FluidSimulator _fluidDynamics;
    public FluidSimulator getFluidSimulator() { return _fluidDynamics; }
}

class ScreenSpaceEffects2 {
    private FluidSimulator _fluidDynamics;
    public void resetFluidSimulation() { _fluidDynamics.reset(); }
}

以及调用上述方法的方法:

callingMethod() {
   effects1.getFluidSimulator().reset(); // Version 1
   effects2.resetFluidSimulation();      // Version 2
}

乍一看,版本 2 似乎更简单一些,并且遵循“Demeter 规则”,隐藏 Foo 的实现等。但这将 FluidSimulator 中的任何更改与 ScreenSpaceEffects 联系在一起。例如,如果一个参数被添加到reset,那么我们有:

class FluidSimulator {
    void reset(bool recreateRenderTargets) { /* ... */ }
}

class ScreenSpaceEffects1 {
    private FluidSimulator _fluidDynamics;
    public FluidSimulator getFluidSimulator() { return _fluidDynamics; }
}

class ScreenSpaceEffects2 {
    private FluidSimulator _fluidDynamics;
    public void resetFluidSimulation(bool recreateRenderTargets) { _fluidDynamics.reset(recreateRenderTargets); }
}

callingMethod() {
   effects1.getFluidSimulator().reset(false); // Version 1
   effects2.resetFluidSimulation(false);      // Version 2
}

在这两个版本中,callingMethod 都需要更改,但在版本 2 中,ScreenSpaceEffects 也需要更改。有人能解释一下使用包装器/外观的优势吗(适配器或包装外部 API 或暴露内部 API 除外)。

编辑:这是我遇到的许多真实示例之一,而不是一个微不足道的示例。

【问题讨论】:

  • 你的意思是“版本 2 似乎更简单一些”?
  • 是的,对不起,会改变
  • 版本 1 不遵循得墨忒耳的规则。输入错误?

标签: design-patterns oop anti-patterns law-of-demeter


【解决方案1】:

主要区别在于,在版本 1 中,作为 Bar 抽象的提供者,您无法控制 Foo 的公开方式。 Foo 的任何更改都会暴露给您的客户,他们将不得不忍受。

在版本 2 中,作为抽象 Bar 的提供者,您可以决定是否以及如何公开演进。它只取决于Bar 抽象,而不是Foo。在您的示例中,您的 Bar 抽象可能已经知道将哪个整数作为参数传递,因此您将能够让您的用户透明地使用新版本的 Foo,而无需进行任何更改。

假设现在 Foo 进化,并要求用户在调用 doSomething 之前调用 foo.init()。在版本 1 中,Bar 的所有用户都需要看到 Foo 发生了变化,并调整他们的代码。对于版本 2,只需更改 Bar,如果需要,它的 doSomething 会调用 init。这导致更少的错误(只有抽象Bar的作者必须知道和理解抽象Foo,并且类之间的耦合更少。

【讨论】:

  • 我不相信这值得额外的复杂性(特别是如果类已经耦合),但无论如何感谢您的回答:-)。
  • 几乎任何时候发生变化,其他事情都必须改变。可以做的最好的事情是尝试安排一切,以便即使可能发生变化的事情发生变化,想要保持不变的部分也能够保持不变。每个给定的设计都将能够比其他设计更容易地适应某些类型的潜在未来 API 更改;哪种设计更好在某种程度上取决于人们如何判断未来各种可能变化的可能性。
【解决方案2】:

这显然是一个人为的例子。在许多真实的情况下,callingMethod(在现实生活中,可以有多个callingMethods)可以保持幸福地不知道Foo.doSomething 已经改变,因为Bar 隔离了它。例如,如果我使用稳定的打印 API,我不必担心我的打印机固件会增加对光泽打印的支持。我现有的黑白打印代码继续工作。我想您会将其归类为“适配器”,我认为这比您暗示的要普遍得多。

你说得对,有时 callMethod 也必须改变。但是如果正确使用得墨忒耳法则,这种情况只会很少发生,通常是为了利用新功能(与新界面不同)。

编辑:callingMethod 似乎很可能不关心渲染目标是否被重新创建(我假设这是性能与准确性的问题)。毕竟,“我们应该忘记小的效率,比如说大约 97% 的时间”(Knuth)。因此 ScreenSpaceEffects2 可以添加一个 resetFluidSimulation(bool) 方法,但通过在幕后调用 _fluidDynamics.reset(true) 让 resetFluidSimulation() 继续工作(无需更改 callMethod)。

【讨论】:

  • 实际上,我问这个问题是因为它在我的爱好项目中出现了太多次
  • 发生了什么?也许是一个真实的例子,而不是所有Foo这个和Bar那个?
【解决方案3】:

问题是,callingMethod() 是否需要知道是否重新创建渲染表?

假设callingMethod() 的某些给定执行需要或不需要重新创建渲染表。在这种情况下,您可以使用新方法扩展包装器。然后你只需要从callingMethod() 的适当实例中调用新方法。

class ScreenSpaceEffects2 {
    private FluidSimulator _fluidDynamics;
    public void resetFluidSimulation() { _fluidDynamics.reset(false); }
    public void resetFluidSimulationWithRecreate() { _fluidDynamics.reset(true); }
}

或者,重新创建的决定可能完全属于其他地方......

class ScreenSpaceEffects2 {
    private FluidSimulator _fluidDynamics;
    public void resetFluidSimulation() { 
             _fluidDynamics.reset( someRuleEngine.getRecreateRenderTables() ); }
}

...在这种情况下,callingMethod() 中的任何内容都不需要更改。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多