2010-03-31 44 views
5

我一直在读这个“demeter法”的东西,它(和纯粹的“包装”类一般)似乎通常是反模式。考虑一个实现类:包装/ demeter定律似乎是反模式

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。例如,如果一个参数添加的复位,然后我们有:

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)。

编辑:其中一个我碰到这个,而不是一个微不足道的例子的实例。

+0

你的意思是“版本2似乎更简单”? – 2010-03-31 06:54:07

+0

是的,很抱歉,会改变 – 2010-03-31 06:57:14

+0

版本1不遵循德米特的规则。输错? – Corwin 2010-03-31 06:58:24

回答

13

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

对于版本2,作为抽象的提供者Bar,您可以决定是否以及如何公开演变。它仅取决于Bar抽象,而不取决于Foo。在你的例子中,你的Bar抽象可能已经知道哪个整数作为参数传递,因此你将能够让你的用户透明地使用新版本Foo,完全没有任何改变。

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

+0

我不相信这值得额外的复杂性(特别是如果类已经耦合),但感谢无论如何答案:-)。 – 2010-04-02 00:37:24

+0

几乎任何时候都会发生变化,其他事情将不得不改变。最好的做法是尽量安排一切,以便即使可能发生变化的事情,即使可能会发生变化,那些希望保持不变的部分也能保持不变。每个给定的设计将能够比其他设计更容易地适应某些潜在的未来API变化;哪种设计更好取决于如何判断各种可能的未来变化的可能性。 – supercat 2013-11-11 23:03:53

2

这显然是一个人为的例子。在许多现实情况下,callingMethod(在现实生活中,可以有多个如果我使用稳定的打印API,我不必担心我的打印机固件增加了对光面打印的支持。我现有的黑色我认为你会将它归入“适配器”下面,我认为它比你暗示的要普遍的多。

你说得对,有时候调用方法一定是变也是。但是,当德米特法得到正确使用时,这种情况很少发生,通常利用新功能(与新界面不同)。

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

+0

其实,我在问这个问题,因为它在我的爱好项目中出现过很多次 – 2010-03-31 07:05:52

+1

出现了什么问题?也许是一个真实的例子,而不是所有的'Foo'这个和'Bar'那个? – 2010-03-31 07:07:54

+1

增加了一个真实的例子。 – 2010-03-31 10:30:07

2

问题是,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()需要在所有改变。