C++ API 设计中的得墨忒耳定律
Law of Demeter in API Design for C++
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
必须知道:
- 该
m_A
具有对象的实例B
通过GetObject()
公开;和 - 对象
B
具有方法DoSomething()
。
所以X
需要知道A
和B
的接口,A
必须始终能够B
相反,如果X
只需要做m_A.DoSomething()
那么它需要知道的是:
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()
,您需要伪造ObjectA
和ObjectB
,并将伪造的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 可以来自任何地方。
- 用于访问容器<T>数据成员的正确 API
- 如何使用Luacneneneba API正确读取字符串和表参数
- C++MySQL C api用户输入行
- 如何使用 AWS Transcribe C++ API 中的'StartTranscriptionJobRequest'?
- 将std::string传递给WriteConsole API
- EvtExportLogneneneba API正在将远程计算机的事件日志保存到远程PC本身.如何将其保存到主机
- 在gtest.中使用fff.h模拟系统API
- 有没有任务栏API可以立即应用注册表更改
- C++win32 API创建多个类似视口的窗口
- 使用libcurl提交批量url的正确BING Api POST url是什么
- 如何将真正的字符串从python c-api转换为python脚本
- 使用Qt框架在c ++类中创建API调用
- Libreoffice API (UNO):需要更改用户的 xTextField 文本
- 使用 WIN32 API (C/C++) 对特定树视图项进行着色
- 使用 Python Extension API 包装复杂C++类
- 如何使用 samtools C API 构建一个简单的主.cpp文件
- 第三方 API 中的编译错误 - Visual Studio
- Tensorflow c++ api undefined reference to 'tflite::D efaultErrorReporter()'
- clang 的 libFuzzer 可以在同一二进制文件中测试超过 1 个 API 吗?
- C++ API 设计中的得墨忒耳定律