进行代码评审时,你可能至少遇到过一次这样的指摘。
“这一行的点号(.)是不是太多了?”
把它一口气写在同一行里看起来反而更整洁,但问题到底在哪里?
这个故事的核心就是迪米特法则。
今天我们会通过示例讲清楚迪米特法则是什么,以及为什么有些方法链没问题,而有些会变成糟糕的代码。
先说结论。
迪米特法则是一条“不要和陌生对象交谈”的规则。
方法链本身并不糟糕;当它深入访问其他对象的内部时,才会变成糟糕的代码。
只要记住这句话,你就已经掌握了本文一半的重点。
什么是迪米特法则
迪米特法则(Law of Demeter)也被称为“最少知识原则”。
简单来说就是:
一个对象应该只和自己直接了解的“近邻朋友”通信。
一旦开始使用朋友的朋友,甚至朋友的另一个朋友,就会产生问题。
通常,在方法中可以调用的对象可以归纳为以下几类:
- 自身(self)的方法
- 作为方法参数传入的对象
- 在方法中直接创建的对象
- 自身持有的属性(成员)对象
也就是说,只在这个范围内进行通信。
因此也产生了“一行最多一个点”的著名经验法则。
当然,点号的数量终究只是提示,并不是绝对规则。
为什么方法链会变成糟糕的代码?
我用自己实际遇到过的代码来说明。
当时的场景是从订单中取出客户的城市名称。
// 从订单一路深入到客户、地址和城市
let city = order.customer
.address
.city
.name
乍一看是不是很整洁?
但这行代码意味着 Order 了解 Customer 的内部,以及其中的 Address 和 City。
问题就在这里出现了。
如果 Address 的结构发生变化,或者 City 消失,这段代码也会一起崩溃。
因为我们已经把手伸进别人家的抽屉里,所以那家的结构一变,我的代码就会坏掉。
这种沿着对象图一路向下的链式调用,通常被称为“火车事故(train wreck)”。
因为它看起来就像一节节车厢首尾相连。
那么,如何区分好的链式调用和坏的链式调用呢?
这是本文最重要的部分。
方法链并不都是坏的。
判断标准只有一个。
“链式调用时,是否一直在取出其他对象的内部对象?”
像前面的示例一样,如果不断深入访问不同的外部对象——customer → address → city——就是违反规则。
相反,像下面这样处理同类对象并不断返回自身的链式调用是没问题的。
// filter·map 这种链式调用每次都返回“相同上下文”,因此不违反规则
let names = users
.filter { $0.isActive }
.map(\.name)
这种高阶函数链式调用或 Builder 模式无论有多少个点,都不会翻动其他对象的内部。
它们只是每次返回自身,也就是同一条流程,以便继续调用。
把两段代码放在表格中比较,可以整理如下。
| 分类 | 坏的链式调用 | 好的链式调用 |
|---|---|---|
| 对象 | 不断取出其他对象的内部对象 | 返回相同的流程/类型 |
| 示例 | order.customer.address | filter { }.map { } |
| 耦合度 | 高(容易受结构变化影响) | 低 |
| 判断 | 违反迪米特法则 | 不违反规则 |
如何修复违反规则的代码
解决方法比想象中简单。
只要想起“不要询问,要命令(Tell, Don’t Ask)”即可。
不要取出内部对象,而是请它完成这项工作。
前面获取城市的示例可以改成这样。
// Order只询问所需的结果
let city = order.shippingCity
让 Order 内部自行经过 Customer 和 Address,并返回城市即可。
这样即使 Address 的结构发生变化,外部代码也能保持不变。
修改范围被严格限制在 Order 内部。
我亲自这样重构后发现,今后修改结构时需要动的地方明显少了。
最后总结一下:
迪米特法则不是让你数点号,而是提醒你不要随意触碰其他对象的内部。
下次遇到很长的链式调用时,只要问自己一个问题。
“我是不是正在打开别人的抽屉?”
只要提出这个问题,代码就会清爽不少。

