C++ API 设计中的得墨忒耳定律

Law of Demeter in API Design for C++

本文关键字:定律 API C++      更新时间:2023-10-16

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

切勿对通过另一个函数调用获得的对象调用函数。

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

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

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

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

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

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

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

在您的特定示例中,m_A 的实现可能不再依赖于 B 的实例。


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

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

  1. m_A具有对象的实例B通过GetObject()公开;和
  2. 对象B具有方法DoSomething()

所以X需要知道AB的接口,A必须始终能够B

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

  1. m_A有方法DoSomething().

因此,法律有助于脱钩,因为X现在与B完全脱钩——它不需要对那个类有任何了解——而且对A的了解也更少——它知道A可以实现DoSomething()但它不再需要知道它自己是这样做还是要求别人这样做。

在实践中,通常

不使用定律,因为它通常只是意味着编写数百个包装器函数,如A::DoSomething() { m_B.DoSomething(); },并且程序的形式语义通常明确规定A将具有B因此您不是通过提供GetObjectB()来揭示实现细节,而只是履行该对象与整个系统的契约。

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

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

关于第二点,我认为我们误入了"是"与"有"的辩论。"有一个"是一种非常自然的方式来表达一些对象关系,教条地将其隐藏在"我有储物柜钥匙,所以如果你想打开你的储物柜,就来问我,我会解锁它"的外表后面——类型的对话只是掩盖了手头的任务。

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

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

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

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

直接回答您的问题:

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

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

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

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

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

Func();

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

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