C ++ API设计中的Demeter定律

时间:2013-07-05 18:12:57

标签: c++ law-of-demeter

在他的“API Design for C ++”一书中,Martin Reddy详细阐述了德米特定律。他特别指出:

  

你永远不应该在通过另一个函数调用获得的对象上调用函数。

他用链接函数调用支持他的语句,如

Func()
{
    [...]
    m_A.GetObjectB().DoSomething();
    [...]
}

相反,他鼓励将B作为参数传递给函数,如:

Func(const ObjectB &B)
{
    [...]
    B.DoSomething();
    [...]
}

我的问题:为什么后一个例子会产生比前者更松散耦合的类?

5 个答案:

答案 0 :(得分:10)

经常使用的类比(包括维基百科页面,我注意到)就是要求狗走路 - 你会问狗,你不会要求接触它的腿,然后让它的腿走路

要求狗走路是一个更好的脱钩,因为有一天你可能想要一条腿以外的东西。

在您的具体示例中,m_A的实现可能会停止依赖B的实例。


编辑:有些人想要进一步阐述,让我试试这个:

如果对象X包含语句m_A.GetObjectB().DoSomething(),那么X必须知道:

  1. m_A通过B展示了对象GetObject()的实例;和
  2. 对象B的方法为DoSomething()
  3. 所以X需要知道AB的界面,而A必须始终能够出售B

    相反,如果X只需m_A.DoSomething(),那么它需要知道的是:

    1. m_A的方法为DoSomething()
    2. 法律因此帮助脱钩,因为X现在与B完全脱钩 - 它不需要知道任何类别 - 并且对A知之甚少 - 它知道{ {1}}可以实现A,但不再需要知道它是自己做到了,还是要求其他人这样做。

      在实践中,法律通常不被使用,因为它通常只意味着编写数百个包装函数,如DoSomething(),并且程序的形式语义通常明确规定A::DoSomething() { m_B.DoSomething(); }将具有{{1}所以你不是通过提供A来揭示实现细节,因为你只是在履行该对象与整个系统的合同。

      第一点也可以用来证明法律增加耦合。假设您最初拥有B并且已将其折叠为GetObjectB()。这意味着,因为m_A.GetObjectB().GetObjectC().GetObjectD().DoSomething()知道m_A.DoSomething()实现CD必须实现它。然后,因为DoSomething()现在知道C实现BC必须实现它。等等。最后,您DoSomething()必须B,因为A会这样做。因此DoSomething()最终必须以某种方式采取行动,因为D以某种方式行事,而之前它可能不知道A

      在第一点上,类似的情况是Java方法传统上声明它们可以抛出的异常。这意味着他们还必须列出他们调用的任何内容如果不能捕获它们就会抛出的异常。所以每次叶子方法添加另一个异常时,你必须走上调用树,将该异常添加到一大堆列表中。因此,一个好的脱钩思想最终会创造出无穷无尽的文书工作。

      关于第二点,我认为我们偏离了“是一个”与“有一个”的辩论。 'has a'是一种非常自然的方式来表达一些对象关系,并且教条地隐藏在“我有锁柜钥匙所以如果你想让你的储物柜打开来问我,我将解锁它”的外观背后。谈话只是模糊了手头的任务。

答案 1 :(得分:4)

当你看单元测试时,差异会更大。

假设DoSomething()有一个副作用,你不想在你的测试代码中发生,因为模拟会很昂贵或烦人,例如数据库访问或网络通信。

在第一种情况下,为了替换测试中的DoSomething(),您需要伪造ObjectAObjectB并将伪造的ObjectA实例注入包含的类中Func()

在第二种情况下,您只需使用假Func()实例调用ObjectB,这可以大大简化测试。

答案 2 :(得分:3)

更改更灵活。想象一下,m_A是由程序员Bob开发的对象A的实例。如果他决定对其代码进行更改以使A不再有方法返回B类型的对象,则Func的开发人员Alice必须更改她的代码也是。请注意,后一个代码段没有此问题。

在软件开发中,这种类型的耦合会产生所谓的非正交设计,即在某处更改代码的本地部分并且需要在其他地方更改部件的设计类型同样。

答案 3 :(得分:3)

直接回答你的问题:

版本2生成更松散耦合的类,因为第一种情况下的Func取决于m_A类的接口和GetObjectB的返回类型的类(推测{ {1}}),而在第二种情况下,它仅取决于类ObjectB的接口。

也就是说,在第一种情况下,ObjectB的类和m_A之间存在耦合,在第二种情况下,没有。{如果该类的界面应该更改为不具有Func,但是要拥有GetObjectB()GetFirstObjectB(),在第一种情况下,您必须重写GetSecondObjectB()来调用相应的替换函数(甚至可能添加一些要调用的逻辑,可能基于一个额外的函数参数),而在第二个版本中,您可以保留该函数,并让Func的用户关心如何获取Func类型的对象。

答案 4 :(得分:2)

嗯,我认为为什么将函数链接在一起是很糟糕的,因为它更难以维护代码。在顶部示例中,Func()是一个丑陋的函数,因为它似乎只是被称为

Func();

基本上没有告诉你任何关于这个功能的信息。第二个提出的方法调用函数传递给它B,这不仅使它更具可读性,而且意味着你可以为其他类编写Func()而不重命名它(因为它不需要参数你不能为另一个类重写它)。这告诉你即使类不同,Func()也会对对象做类似的事情。

为了回答你问题的最后一部分,松散耦合得以实现,因为第一个例子意味着你必须得到一个BA将这些类耦合在一起,第二个例子是更一般的并暗示B可以来自任何地方。