迪米特法則
迪米特法則也稱為最小知識原則,是物件導向 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