進行程式碼審查時,總有一次會被指出這件事。
「這一行的點(.)是不是太多了?」
全部接在同一行寫,看起來反而很整齊,到底問題在哪裡?
這個故事的核心就是迪米特法則。
今天就用範例說明迪米特法則是什麼,以及為什麼有些方法鏈結沒問題,有些卻會變成糟糕的程式碼。
先直接說結論。
迪米特法則是一項「不要和陌生物件交談」的規則。
方法鏈結本身並不糟糕,真正糟糕的是透過鏈結深入別人的內部。
只要記住這一句,就掌握了今天文章的一半。
什麼是迪米特法則
迪米特法則(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)
這類高階函式鏈結或建造者模式,即使有很多點,也不會挖掘別人的內部。
它只是每次回傳自己本身(相同流程)並繼續下去。
把兩段程式碼用表格比較,可以整理如下。
| 區分 | 不好的鏈結 | 好的鏈結 |
|---|---|---|
| 對象 | 持續取出別人的內部物件 | 回傳相同流程/型別 |
| 範例 | order.customer.address | filter { }.map { } |
| 耦合度 | 高(容易受結構變更影響) | 低 |
| 判斷 | 違反迪米特法則 | 不算違反 |
違反規則的程式碼,這樣修正
解法比想像中簡單。
只要想起「不要詢問,直接命令(Tell, Don’t Ask)」即可。
不要取出內部資料,而是請它完成那件事。
以前面的城市範例來說,可以改成這樣。
// Order只詢問所需的結果
let city = order.shippingCity
讓 Order 內部自行經過 Customer 和 Address,回傳城市即可。
如此一來,即使 Address 的結構改變,外部程式碼也不受影響。
因為修改範圍會被限制在 Order 內部。
我親自這樣重構後發現,之後要變更結構時,需要修改的地方大幅減少。
最後總結如下。
迪米特法則不是要你計算點的數量,而是提醒你不要任意碰觸別人的內部。
下次遇到很長的鏈結時,只要問自己一件事。
「我是不是正在打開別人的抽屜?」
只要問出這個問題,程式碼就會清爽許多。

