软件设计

迪米特法则(Law of Demeter):方法链何时会变成糟糕的代码

进行代码评审时,你可能至少遇到过一次这样的指摘。

3 分钟阅读
迪米特法则(Law of Demeter):方法链何时会变成糟糕的代码 封面图

进行代码评审时,你可能至少遇到过一次这样的指摘。

“这一行的点号(.)是不是太多了?”

把它一口气写在同一行里看起来反而更整洁,但问题到底在哪里?

这个故事的核心就是迪米特法则

今天我们会通过示例讲清楚迪米特法则是什么,以及为什么有些方法链没问题,而有些会变成糟糕的代码。

先说结论。

迪米特法则是一条“不要和陌生对象交谈”的规则。

方法链本身并不糟糕;当它深入访问其他对象的内部时,才会变成糟糕的代码。

只要记住这句话,你就已经掌握了本文一半的重点。


什么是迪米特法则

迪米特法则(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 内部。

我亲自这样重构后发现,今后修改结构时需要动的地方明显少了。


最后总结一下:

迪米特法则不是让你数点号,而是提醒你不要随意触碰其他对象的内部。

下次遇到很长的链式调用时,只要问自己一个问题。

“我是不是正在打开别人的抽屉?”

只要提出这个问题,代码就会清爽不少。