17

在他的 C++ API 设计一书中,Martin Reddy 详细阐述了得墨忒耳定律。他特别指出:

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

他通过链接函数调用来支持他的声明,例如

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

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

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

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

4

5 回答 5

10

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

让狗走路是一种更好的解耦方式,因为有一天你可能想要一只没有腿的狗。

在您的具体示例中,m_A的实现可能不再依赖于B.


编辑:因为有些人想要进一步说明,让我试试这个:

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

  1. 具有通过 暴露m_A的对象的实例;和BGetObject()
  2. 对象B有方法DoSomething()

所以X需要知道 和 的接口,并且A必须始终能够出售。BAB

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

  1. 那个m_A有方法DoSomething()

因此,法律有助于脱钩,因为X现在已经完全脱钩了B——它不需要任何关于那个阶级的知识——并且对它的了解更少A——它知道A可以实现DoSomething(),但它不再需要知道它是否自己做到了,或者它是否要求某人否则去做。

在实践中,通常不使用该定律,因为它通常只意味着编写数百个包装函数,例如,并且A::DoSomething() { m_B.DoSomething(); }程序的形式语义通常明确规定A将具有履行该对象与整个系统的契约。BGetObjectB()

第一点也可以用来论证该定律增加了耦合。假设您最初拥有m_A.GetObjectB().GetObjectC().GetObjectD().DoSomething()并且您已将其折叠为m_A.DoSomething(). 那意味着因为C知道D实现DoSomething()C所以必须实现它。然后因为B现在知道C实现了DoSomething()B就必须实现它。等等。最后,您必须A实施DoSomething(),因为D确实如此。所以A最终不得不以某些方式行动,因为D以某些方式行动,而以前它可能一无所知D

在第一点上,一个类似的情况是 Java 方法传统上声明它们可以抛出的异常。这意味着他们还必须列出他们调用的任何东西如果没有捕获它可能抛出的异常。因此,每次叶方法添加另一个异常时,您都必须沿着调用树将异常添加到一大堆列表中。因此,一个好的脱钩想法最终会产生无休止的文书工作。

关于第二点,我认为我们误入了“是”与“有”的辩论。'Has a' 是表达一些对象关系的一种非常自然的方式,并且教条地将其隐藏在“我有储物柜钥匙,所以如果你想打开你的储物柜,就来问我,我会解锁它”的表面之下 - 类型谈话只是模糊了手头的任务。

于 2013-07-05T18:31:57.537 回答
5

当您查看单元测试时,差异会更加突出。

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

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

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

于 2013-07-05T22:39:13.400 回答
4

直接回答你的问题:

版本 2 产生了更松散耦合的类,因为Func在第一种情况下,它依赖于类的接口m_A和返回类型的类GetObjectB(可能是ObjectB),而在第二种情况下,它只依赖于类的接口ObjectB

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

于 2013-07-05T20:54:47.900 回答
3

变化更灵活。想象这m_A是一个对象的实例A,由程序员 Bob 开发。如果他决定更改他的代码,以便A不再有返回类型对象的方法B,那么 的开发人员 AliceFunc也将不得不更改她的代码。请注意,后面的代码片段没有这个问题。

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

于 2013-07-05T18:30:28.913 回答
2

好吧,我认为应该很明显为什么将函数链接在一起是不好的,因为它会产生更难维护的代码。在上面的例子Func()中是一个丑陋的函数,因为它看起来就像被称为

Func();

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

为了回答您问题的最后一部分,实现了松散耦合,因为第一个示例意味着您必须B通过A将类耦合在一起,第二个示例更通用,暗示 B 可以来自任何地方。

于 2013-07-05T18:20:44.633 回答