迪米特法則
迪米特法則也稱為最小知識原則,是物件導向 SOLID
原則中的 L
其中之一 LKP
( Least Knowledge Principle),是 1987 年 Ian Holland 在美國東北大學所提出,此法則應用在其 The Demeter Project 而得名,是物件導的基本原則。
Motivation
很多人沒有認清迪米特法則的本質,只要看到類似 Clean Code p. 110 書中提到的 Train Wreck
風格的程式碼
1 | final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); |
就認為違反的迪米特法則,真的是這樣嗎?
定義
高階模組不應該知道低階模組的內部如何運作。
低階模組不應該暴露內部物件,不應該暴露實踐細節,應僅提供方法給高階模組使用。
白話就是
Controller 不應該知道 service 的內部如何運作。
Service 應該將內部所用的其它 service 封裝起來,提供 method 給 controller 使用,而非直接提供內部 service 給 controller 呼叫。
Train Wreck
SMSService
1 | namespace App\Services; |
SMSService
僅有一個 getMessage()
。
NotificationService
1 | namespace App\Services; |
NotificationService
相依了 SMSService
,直接使用 getSMSService()
將 SMSService
物件傳出去。
PostController
1 | namespace App\Http\Controllers; |
因為 getSMSService()
傳回 SMSService
物件,導致 controller 必須寫出 Train Wreck
:
1 | $this->notificationService->getSMSService()->getMessage(); |
這種寫法有幾個缺點 :
PostController
與NotificationService
內部的SMSService
強烈耦合,若想要換掉SMSService
物件,則PostController
必須跟著修改,也就是暴露內部物件。PostController
為了要顯示 message,竟然還必須知道NotificationService
內部使用了SMSService
物件,先使用getSMSService()
才行,也就是暴露實踐細節。- 違反了物件導向的封裝原則,
PostController
竟然可以將手伸進去執行SMSService
的方法。
簡單來說,迪米特法則就是物件導向封裝特性的具體實現。
建議將以上程式碼重構成以下寫法
NotificationService
1 | namespace App\Services; |
PostController
1 | namespace App\Http\Controllers; |
重構之後,PostController
不再出現 Train Wreck
。
1 | $this->notificationService->getMessage(); |
PostController
完全不知道NotificationService
的內部物件,若想要換掉SMSService
物件,則PostController
完全不用修改。PostController
為了要顯示 message,不必再知道實踐細節, 直接使用getMessage()
就可以抓到資料。- 符合了物件導向的封裝原則,
PostController
無法將手伸進去執行SMSService
的方法。
當 service 直接將內部使用的 service 傳出後,逼 controller 必須先了解其內部實踐細節,使得 controller 與 service 的內部其它 service 強烈耦合,這違反了物件導向封裝特性,也違反了迪米特法則。
違反迪米特法則,通常會寫出
Train Wreck
,因此可使用Train Wreck
檢查是否違反迪米特法則。
最小知識原則
物件導向 SOLID
原則的最小知識原則 LKP
(Least Knowledge Principle),事實上與迪米特法則講的是同一件事情。
一個物件應該對其他物件有最少的了解。
白話就是
Controller 應該以最簡單的方式使用 service。
之前的 PostController
,因為違反了迪米特法則
1 | $this->notificationService->getSMSService()->getMessage(); |
因此必須知道NotificationService
與 SMSService
之後才能 getMessage()
。
但重構之後
1 | $this->notificationService->getMessage(); |
只要知道 NotificationService
就可以 getMessage()
了。
符合最小知識原則,自然符合迪米特法則。
再論 Train Wreck
我們知道違法迪米特法則會寫出 Train Wreck
,但寫出 Train Wreck
一定違反迪米特法則嗎?
1 | final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); |
Clean Code p.111 認為,要看 getOptions()
、getScratchDir()
與 getAbolutePath()
回傳的是 object 還是 data structure?
這裡的 data structure 不是我們在學校念書時所謂的資料結構,如 linked list、tree 那些,而是指一個物件只有資料,沒有任何商業邏輯。
1 | class Student { public $id; public $name; } |
這種完全用 public field 的物件算 data structure。
1 | class Student |
儘管多了 constructor 與 getter,它還是 data structure,因為沒有任何商業邏輯。
如果回傳的只是一種無其它行為的 data structure,那它們在本質上必然會揭露內部的結構,所以迪米特法則在這種狀況下並不適用。
Clean Code p.111
如前例回傳 SMSService
, 因為包含商業邏輯,所以回傳算是 object,而非 data structure,只要包含商業邏輯,就會暴露實踐細節,而導致商業邏輯無法抽換,因此 controller 與 service 就必須解耦合,遵守迪米特法則。
Train Wreck
不見得違反迪米特原則,要看回傳的是 data structure 還是 object。
Fluent Interface
在 Laravel 的 Eloquent,我們會這樣寫
1 | $flights = Flight::where('active', 1) |
這也是 Train Wreck
,也違反迪米特法則嗎?
where()
、orderBy()
與 take()
這些,並沒有回傳其內部物件,而是傳回 $this
,因此沒有暴露內部物件與暴露實踐細節的問題,也沒有與內部物件強烈耦合問題,因此 fluent interface 並沒有違反迪米特法則。
並不是
Train Wreck
一定違反迪米特法則,關鍵在於有沒有暴露內部物件與暴露實踐細節,而不在於Train Wreck
。
Conclusion
- 違反迪米特法則會寫出
Train Wreck
,但Train Wreck
不一定會違反迪米特法則。 - 迪米特法則重點在於強調物件導向的封裝特性,關鍵在於不該暴露內部物件,進而暴露實踐細節,導致使用端與內部物件強烈耦合而無法抽換商業邏輯。
- 迪米特法則要求所有的動作都必須透過物件本身的方法操作,而不能傳出內部物件,讓使用端直接操作內部物件,而不在於是否使用
Train Wreck
。
Reference
Robert C. Martin, 無瑕的程式碼
良葛格, 封裝與迪米特法則
Martin Fowler, Fluent Interface