迪米特法则(Law of Demeter)

在阅读RDD一书中发现的一个法则:迪米特法则(Law of Demeter)
百度了一下发现和DDD的聚合根概念吻合了,以下是百度内容,和道友们分享。

原文链接:http://terry-yinzhe.spaces.live.com/blog/cns!92560BAA05230C71!277.entry

迪米特法则(Law of Demeter),又称“最少知识原则”(Principle of Least Knowledge),也是主要针对面向对象思想的,可以简单的概括为“talk only to your immediate friends”。

具体来说,在面向对象的方法中,一个方法“M”和一个对象“O”只可以调用以下几种对象的方法:
1. O自己
2. M的参数
3. 在M中创建的对象
4. O的直接组件对象

Law of Demeter来源于1987年荷兰大学的一个叫做Demeter的项目。Craig Larman把Law of Demeter又称作“不要和陌生人说话”。在《程序员修炼之道》中讲LoD的那一章叫作“解耦合与迪米特法则”。可以看出迪米特法则是非常流行的解耦合手段之一。关于迪米特法则有一些很形象的比喻:
如果你想让你的狗狗跑的话,你会对狗狗说还是对四条狗腿说?
如果你去店里买东西,你会把钱交给店员,还是会把钱包交给店员让他自己拿?

让店员自己从钱包里拿钱?这听起来有点荒唐,不过在我们的代码里这几乎是见怪不怪的事情了:
class Clerk {
Store store;
void SellGoodsTo(Client client)
{
money = client.GetWallet().GetMoney();//店员自己从钱包里拿钱了!
store.ReceiveMoney(money);
}
};

在《Clean Code》一书中,作者找到了Apache framework中的一段违反了LoD的代码:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
这么长的一串对其它对象的细节,以及细节的细节,细节的细节的细节......的调用,违反了迪米特法则,增加了耦合,使得代码结构复杂、僵化,难以扩展和维护。

在《重构》一书中的各种“Bad smells of code”中有一种“smell”叫做“Feature Envy”(依恋情结),形象的描述了一种违反了LoC的情况。Feature Envy就是说一个对象对其它对象的内容更有兴趣,也就是说老是羡慕别的对象的成员、结构或者功能,大老远的调用人家的东西。这样的结构显然是不合理的。我们的程序应该写得比较“害羞”。不能像前面例子中的那个不把自己当外人的店员一样,拿过客人的钱包自己把钱拿出来。“害羞”的程序只和自己最近的朋友交谈。这种情况下应该调整程序的结构,让那个对象自己拥有它羡慕的feature,或者使用合理的设计模式(例如Facade和Mediator)。
店员的例子如果是这样就会好一点:
money = client.GetMoney();//客户自己从钱包里拿钱
或者根本不用那么麻烦:
void SellGoods(Money money)
{
store.ReceiveMoney(money);
}

这一法则不仅仅局限于计算机领域,在其他领域也同样适用。据说美国人就在航天系统的设计中采用这一法则。

查看该作者的其他解耦原则受益良多:
1.解耦合手段之一:DRY原则
2.解耦合手段之二:Single Responsibility Principle(单一职责原则)
3.解耦合手段之三:Open/Closed Principle(开闭原则)
4.解耦合手段之四:Law of Demeter
5.解耦合手段之五:Dependency Inversion Principle(依赖注入)
6.解耦合手段之六:并发

2010年05月10日 10:49 "Antinomy"的内容
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); ...

这种典型的把钱包交给店员自己拿是比较普遍,关键问题我在使用DDD时也考虑过,如果我不让店员自己拿,那么在根实体中要增加一个方法如ctxt.getOptions_ScratchDir_AbsolutePath(),这个方法委托子对象去实现。这种重构就太频繁了,每次都要打开根对象,其实是破坏其封装,增加这种虚方法,这种做法显然比"把钱包交给店员自己拿"更荒唐一些,而且不符合“快直接”原则。

所以,出现这样矛盾,肯定是有什么地方出毛病,当然迪米特法则或SOLID原则不应该出问题,还是闲杂语言技术无法帮助我们迅速实现这个目标。

那么畅想一下,以后的语言如何解决这个矛盾呢?
一个想法就是:自动将getOptions().getScratchDir().getAbsolutePath()方法变为根对象ctxt的getOptions_ScratchDir_AbsolutePath(),也就是把一个有层次从抽象到细节的递归方式改为一个平面式的方法?这样好吗?不见得。

或者采取一种动态类型方法,凡是子对象的方法都自动成为根对象的方法?反正接口方式已经不合适,因为你不能让根对象和子对象实现同一个接口,也许Mixin或Trait这种混合组合方式或许可以。

还有一种纯粹以消息通讯,客户端调用根实体对象ctxt,发送的是一个getOptions().getScratchDir().getAbsolutePath()消息,那么ctxt接受到后,不一定是将自己的方法直接暴露给调用,而是在其子对象中自动寻找匹配的方法,委托其执行,这样效率就高多。
[该贴被banq于2010-05-10 12:10修改过]

让钱包自己存钱。
服务员发送收到钱的Event
怎么样?

2010年05月10日 12:01 "banq"的内容
或者采取一种动态类型方法,凡是子对象的方法都自动成为根对象的方法?反正接口方式已经不合适,因为你不能让根对象和子对象实现同一个接口,也许Mixin或Trait这种混合组合方式或许可以。
...
这倒是个直接的方法,换句话说,聚合根可以直接使用根内的对象的方法。让我们直接关注核心的内容,比如说取钱是为了付账,在这个场景内,我们关心的是取多少钱,而不是去关心是从什么牌子的钱包里去取。虽然有掏出钱包的动作,但是并不是我们所要关心的,当然你花个10分钟去掏钱包,店员会怒的~

2010年05月10日 20:47 "Antinomy"的内容
让我们直接关注核心的内容,比如说取钱是为了付账,在这个场景内,我们关心的是取多少钱,而不是去关心是从什么牌子的钱包里去取 ...

是的,这种将做什么和怎么做主次分离策略好像和接口用处类似,但是接口在这里做不了这种事情,这种方式应该使用接口和模板抽象类两种结合起来的方式来实现。可能要模糊对象的类型概念。