軟體設計

迪米特法則(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)

這類高階函式鏈結或建造者模式,即使有很多點,也不會挖掘別人的內部。

它只是每次回傳自己本身(相同流程)並繼續下去。

把兩段程式碼用表格比較,可以整理如下。

區分 不好的鏈結 好的鏈結
對象 持續取出別人的內部物件 回傳相同流程/型別
範例 order.customer.address filter { }.map { }
耦合度 高(容易受結構變更影響)
判斷 違反迪米特法則 不算違反

違反規則的程式碼,這樣修正

解法比想像中簡單。

只要想起「不要詢問,直接命令(Tell, Don’t Ask)」即可。

不要取出內部資料,而是請它完成那件事。

以前面的城市範例來說,可以改成這樣。

// Order只詢問所需的結果
let city = order.shippingCity
不要詢問,直接命令;原來只要這一行就夠了
不要詢問,直接命令;原來只要這一行就夠了

讓 Order 內部自行經過 Customer 和 Address,回傳城市即可。

如此一來,即使 Address 的結構改變,外部程式碼也不受影響。

因為修改範圍會被限制在 Order 內部。

我親自這樣重構後發現,之後要變更結構時,需要修改的地方大幅減少。


最後總結如下。

迪米特法則不是要你計算點的數量,而是提醒你不要任意碰觸別人的內部。

下次遇到很長的鏈結時,只要問自己一件事。

「我是不是正在打開別人的抽屜?」

只要問出這個問題,程式碼就會清爽許多。