迪米特法则与重构

标签 | 面向对象设计 重构

字数 | 2973字

阅读 | 8分钟

在面向对象设计的世界里,有一个寻常却又常常为人所忽略的原则——“迪米特(Law of Demeter)”法则。这个原则认为,任何一个对象或者方法,它应该只能调用下列对象:

  • 该对象本身
  • 作为参数传进来的对象(也可以是该对象的字段)
  • 在方法内创建的对象

这个原则用以指导正确的对象协作,分清楚哪些对象应该产生协作,哪些对象则对于该对象而言,又应该是无知的。如何理解这个原则?我们可以看看David Bock就该原则给出的一个绝佳案例。假设一个超市购物的场景,顾客(Customer)到收银台结账,收银员(Paper Boy)负责收钱。我们来看看代码:

public class Customer {
    private String firstName;
    private String lastName;
    private Wallet myWallet;
    public String getFirstName(){
        return firstName;
    }
    public String getLastName(){
        return lastName;
    }
    public Wallet getWallet(){
        return myWallet;
    }
}

public class Wallet {
    private float value;
    public float getTotalMoney() {
        return value;
    }
    public void setTotalMoney(float newValue) {
        value = newValue;
    }
    public void addMoney(float deposit) {
        value += deposit;
    }
    public void subtractMoney(float debit) {
        value -= debit;
    }
}

public class Paperboy {
    public void pay(Customer myCustomer, double payment) {
        Wallet theWallet = myCustomer.getWallet();
        if (theWallet.getTotalMoney() > payment) {
            theWallet.subtractMoney(payment);
        } else {
            //money not enough
        }
    }
}

让我们将PaperBoy中charge()方法的代码翻译成这幕小话剧的对白。

“把钱包交出来!”收银员算出顾客要买的商品总价后,“温柔”地对顾客说道。 顾客言听计从,赶紧将钱包掏出来,恭恭敬敬地递给收银员。 接过钱包,收银员毫不客气地打开,检查里面的钱够不够。噢,不错,钱够了。收银员从钱包取出钱,心满意足地笑了。

如果你是顾客,你敢去这样的超市shopping吗?

对于PaperBoy而言,Wallet不满足迪米特法则三个条件中的任何一个,因此让PaperBoy与Wallet对象直接交互是错误的行为。若从拟人化的角度思考,则Wallet其实属于Customer的隐私。如此重要的隐私,怎么能直接交给收银员这个陌生人呢?这里所谓的“隐私”,可以视为是“数据”,是“信息”,是“知识”,因此我们往往又将迪米特法则称之为“最小知识法则”

当我们理解“最小知识法则”时,又可以从职责的角度去思考以上代码。对于收银员角色,他的职责应该是负责收钱,而不用去管钱包里的钱够不够,如果够了怎么办,如果不够又该怎么办,这些统统都不属于他的职责。设想一下,当超市里人流如织,大家都在购买商品时,如果每一个收银员都要承担这般的职责时,会出现什么样的景象?所以“最小知识法则”乃善法,在对象社区中,我们就应该刻意减少对象之间彼此深入的了解。了解最小的知识,就意味着依赖最小,彼此产生的影响就会最小。这实际上是KISS(keep it simple and stupid)原则的体现。

信息专家模式告诉我们:“信息的持有者即为操作该信息的专家。”对于对象,所谓信息就是该对象内部的字段。在前面的例子中,Wallet是Customer的字段,那么操作Wallet的行为自然就应该分配给Customer了。这是题中应有之义。“信息专家模式”其实是面向对象最重要原则“数据与行为应该封装在一起”的别名。若在领域建模时能遵循该原则,则可以规避我们设计出贫血模型。

如何修改以上代码?注意,charge()行为仍然属于PaperBoy的职责,因此我们不应该将该方法整体搬迁到Customer中,而应该先进行方法的提取:

提取的pay()的方法体与charge()方法完全相同,但是在PaperBoy类中却保留了charge()方法,只是这个方法什么也没有做,在接收方法请求后,转而将请求委派给了pay()方法。我们可以这样理解:在抽象层面,收款是收银员的职责;在实现层面,是pay()方法支持了收款行为,该实现归属于顾客。

观察pay()方法,我们发现该方法操作的数据皆来自Customer。我们嗅到了一种坏味道,即Martin Fowler所谓的“特性依恋(Feature Envy)”。对于该坏味道,老马是这样阐释的:“函数对某个类的兴趣高过对自己所处类的兴趣。”不要再嫉妒了,桥归桥,路归路,让方法回到自己最喜欢的地方吧。运用“Move Method”重构手法,将pay()方法移动到Customer中:

在将方法移到正确的位置后,我们发现暴露的getWallet()方法根本就没有意义。更何况,将钱包裸露出去,难道是想要炫富吗?还是低调一点为好,隐藏自己的“隐私”,总好过被人觊觎而招来飞来横祸之险。于是,内联(inline)之。

判断一段代码是否违背了迪米特法则,有一个小窍门,就是看调用代码是否出现形如a.m1().m2().m3().m4()之类的代码。这种代码在Martin Fowler《重构》一书中,被名为“消息链条(Message Chain)”,有人更加夸张地名其为“火车残骸”。车祸现场啊,真是惨不忍睹。

那么,如下代码是否这样的残骸呢?

str.split("&")
	.stream()
	.map(str -> str.contains(elementName) ? str.replace(elementName + "=", "") : "")
	.filter(str -> !str.isEmpty())
	.reduce("", (a, b) -> a + "," + b);

不是的。这样的代码我们一般称之为“流畅接口或连贯接口(Fluent Interface)”。二者的区别在于观察形成链条的每个方法返回的是别的对象,还是对象自身。如果返回的是别的对象,就是消息链条。所谓m1().m2().m3().m4()的调用,其实是调用者不需要也不想知道的“知识”,把这些中间过程的细节暴露出来没有意义,调用者关心的是最终结果;而上述代码中的map()filter()等方法其实返回的还是Stream类。这一调用方式其初衷并非告知中间过程的细节,而是一种声明式的DSL表达,调用者可以自由地组合它们。

本文链接: http://zhangyi.xyz/demeter-law-and-refactoring/

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2018-05-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏take time, save time

Think in 递归

     网上写递归的文章可以用汗牛充栋来形容了,大多数都非常清晰而又细致的角度上讲解了递归的概念,原理等等。以前学生的时候,递归可以说一直是我的某种死穴,原理...

41112
来自专栏C语言及其他语言

初学C语言的学习计划

背景:很多同学在学习C语言的过程中,常常会遇到这样的问题,即“教材看完了,知识点也懂,但写不出来程序”,这段时间,我们通过长期与有多年C语言研究经验的教授、教师...

3624
来自专栏小詹同学

Python系列之六——拿什么拯救你?我的大脑

我一定是智障了,话不多说,上图上图~ ? 就是这样10个选择题,你没有看错,我一定是个智障了~佩服不用穷举,也不用参考网上的大...

3654
来自专栏深度学习计算机视觉

工厂方法模式

简单定义### 定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。 工厂方法(Factory Method)模式的意义...

3389
来自专栏阮一峰的网络日志

为什么Lisp语言如此先进?(译文)

上周,《黑客与画家》总算翻译完成,已经交给出版社了。 翻译完这本书,累得像生了一场大病。把书稿交出去的时候,心里空荡荡的,也不知道自己得到了什么,失去了什么。 ...

2976
来自专栏程序员互动联盟

【答疑解惑第三十八讲】初学者做项目需要掌握哪些东西?

疑惑一 【答疑解惑】初学必须掌握的数据结构有哪些? 数据结构有很多,难以程度也不相同,初学者应该掌握哪些基本的数据结构呢?作为一个过来人,我觉得作为一个初学者应...

3578
来自专栏python学习之旅

测试工程师的一些面试题目(python)和总结

    1、输入:JSON {"a":"aa","b":"bb","c":{"d":"dd","e":"ee"}}   输出:字典 {'a': 'aa', 'b...

4102
来自专栏ACM小冰成长之路

HDU-6008-Worried School

ACM模版 描述 ? 题解 简单的模拟题,题意不是特别容易翻译,但是模拟的规则十分简单,和 WFWF 晋级资格相似,大致是一共 X+Y=GX + Y = G 个...

1988
来自专栏贾志刚-OpenCV学堂

图形图像算法中必须要了解的设计模式(2)

AI越来越火热,人工智能已然成风!而人工智能最重要是各种算法,因此机器学习越来越受到追捧,算法越来越被重视。

1072
来自专栏怀英的自我修炼

Java漫谈1

对于接触编程的人来说,Java更多地代表了一门编程语言。 Java是一门通用的计算机编程语言,它是并行的,基于类的,面向对象的,可以一次编写到处运行的一门语言。...

37714

扫码关注云+社区

领取腾讯云代金券