在他的“API Design for C ++”一书中,Martin Reddy详细阐述了德米特定律。他特别指出:
你永远不应该在通过另一个函数调用获得的对象上调用函数。
他用链接函数调用支持他的语句,如
Func()
{
[...]
m_A.GetObjectB().DoSomething();
[...]
}
相反,他鼓励将B作为参数传递给函数,如:
Func(const ObjectB &B)
{
[...]
B.DoSomething();
[...]
}
我的问题:为什么后一个例子会产生比前者更松散耦合的类?
答案 0 :(得分:10)
经常使用的类比(包括维基百科页面,我注意到)就是要求狗走路 - 你会问狗,你不会要求接触它的腿,然后让它的腿走路
要求狗走路是一个更好的脱钩,因为有一天你可能想要一条腿以外的东西。
在您的具体示例中,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
知之甚少 - 它知道{ {1}}可以实现A
,但不再需要知道它是自己做到了,还是要求其他人这样做。
在实践中,法律通常不被使用,因为它通常只意味着编写数百个包装函数,如DoSomething()
,并且程序的形式语义通常明确规定A::DoSomething() { m_B.DoSomething(); }
将具有{{1}所以你不是通过提供A
来揭示实现细节,因为你只是在履行该对象与整个系统的合同。
第一点也可以用来证明法律增加耦合。假设您最初拥有B
并且已将其折叠为GetObjectB()
。这意味着,因为m_A.GetObjectB().GetObjectC().GetObjectD().DoSomething()
知道m_A.DoSomething()
实现C
,D
必须实现它。然后,因为DoSomething()
现在知道C
实现B
,C
必须实现它。等等。最后,您DoSomething()
必须B
,因为A
会这样做。因此DoSomething()
最终必须以某种方式采取行动,因为D
以某种方式行事,而之前它可能不知道A
。
在第一点上,类似的情况是Java方法传统上声明它们可以抛出的异常。这意味着他们还必须列出他们调用的任何内容如果不能捕获它们就会抛出的异常。所以每次叶子方法添加另一个异常时,你必须走上调用树,将该异常添加到一大堆列表中。因此,一个好的脱钩思想最终会创造出无穷无尽的文书工作。
关于第二点,我认为我们偏离了“是一个”与“有一个”的辩论。 'has a'是一种非常自然的方式来表达一些对象关系,并且教条地隐藏在“我有锁柜钥匙所以如果你想让你的储物柜打开来问我,我将解锁它”的外观背后。谈话只是模糊了手头的任务。
答案 1 :(得分:4)
当你看单元测试时,差异会更大。
假设DoSomething()
有一个副作用,你不想在你的测试代码中发生,因为模拟会很昂贵或烦人,例如数据库访问或网络通信。
在第一种情况下,为了替换测试中的DoSomething()
,您需要伪造ObjectA
和ObjectB
并将伪造的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()
也会对对象做类似的事情。
为了回答你问题的最后一部分,松散耦合得以实现,因为第一个例子意味着你必须得到一个B
到A
将这些类耦合在一起,第二个例子是更一般的并暗示B可以来自任何地方。