迪米特法則
迪米特法則 (Law of Demeter, LoD),也被稱為“最少知識原則 (Principle of Least Knowledge)”,是面向對象設計中的一個重要原則。
核心思想:一個對象應該對其他對象有盡可能少的了解。
更具體地說,它規定了一個對象 O 中的一個方法 M 應該只調用以下類型對象的方法:
-
對象 O 本身 (this/self)。
-
作為方法 M 的參數傳遞進來的對象。
-
在方法 M 內部創建的對象 (局部對象)。
-
對象 O 的直接成員/組件對象 (實例變量/屬性)。
-
全局對象 (但應謹慎使用,通常不推薦)。
簡單來說,就是“只和你的直接朋友交談,不要和朋友的朋友交談”。
為什么叫“迪米特法則”?
這個名字來源于 1987 年在美國東北大學 (Northeastern University) 進行的一個名為 "Demeter Project" 的項目。該項目的目標是開發一種更容易維護和演化的面向對象系統。
目的和好處:
遵循迪米特法則的主要目的是降低類之間的耦合度 (Coupling),從而帶來以下好處:
-
提高模塊的獨立性:當一個模塊的內部實現發生改變時,由于它只與直接相關的模塊交互,因此對其他模塊的影響會降到最低。
-
增強系統的可維護性:修改一個類時,不需要關心太多其他類的內部細節,減少了連鎖反應的風險。
-
提高代碼的可復用性:低耦合的模塊更容易被復用到其他系統中。
-
降低復雜性:每個類只需要關注與自己直接相關的交互,使得系統結構更清晰。
-
更容易進行單元測試:由于依賴關系簡單,更容易對模塊進行隔離測試。
違反迪米特法則的常見表現 (壞味道):
當你在代碼中看到一長串的點號調用,例如 object.getA().getB().getC().doSomething(),這通常是違反迪米特法則的信號。這被稱為“鏈式調用”或“消息鏈 (Message Chains)”。
這種寫法的問題在于:
-
當前對象不僅依賴于 object,還間接依賴于 A、B、C 的內部結構。
-
如果 A、B 或 C 的任何一個類的接口發生改變(比如 getB() 方法沒了,或者返回類型變了),當前對象的代碼也可能需要修改,即使它本身并不直接關心 B 或 C 的具體實現。
如何遵循迪米特法則?
當發現違反迪米特法則的情況時,可以考慮以下重構方法:
-
封裝和委托 (Encapsulation and Delegation):
-
如果對象 O 需要調用 object.getA().getB().doSomething(),可以考慮在 object 類中添加一個新方法,比如 doSomethingRelatedToObjectB(),這個新方法內部去處理與 A 和 B 的交互,然后 O 只需要調用 object.doSomethingRelatedToObjectB() 即可。
-
這樣,object 封裝了與 A 和 B 的交互細節,O 只需要與它的直接朋友 object 對話。
例子:
-
不好的設計 (違反 LoD):
class Wallet {private Money money;public Wallet(Money money) { this.money = money; }public Money getMoney() { return money; } } ? class Money {private int amount;public Money(int amount) { this.amount = amount; }public int getAmount() { return amount; }public void setAmount(int amount) { this.amount = amount; } } ? class Person {private Wallet wallet;public Person(Wallet wallet) { this.wallet = wallet; } ?public void pay(int amountToPay) {// 違反了 LoD:Person 通過 Wallet 拿到了 Money,然后操作 Moneyint currentAmount = wallet.getMoney().getAmount();if (currentAmount >= amountToPay) {wallet.getMoney().setAmount(currentAmount - amountToPay);System.out.println("支付成功: " + amountToPay);} else {System.out.println("余額不足");}} }
-
好的設計 (遵循 LoD):
class Wallet { // Wallet 作為 Money 的直接朋友private Money money;public Wallet(Money money) { this.money = money; } ?// Wallet 負責處理支付邏輯,而不是暴露 Money 對象public boolean spend(int amountToSpend) {return money.deduct(amountToSpend);} ?public int getCurrentBalance() {return money.getAmount();} } ? class Money {private int amount;public Money(int amount) { this.amount = amount; }public int getAmount() { return amount; } ?public boolean deduct(int amountToDeduct) { // Money 自己負責扣款if (this.amount >= amountToDeduct) {this.amount -= amountToDeduct;return true;}return false;} } ? class Person { // Person 的直接朋友是 Walletprivate Wallet wallet;public Person(Wallet wallet) { this.wallet = wallet; } ?public void pay(int amountToPay) {// Person 只和它的直接朋友 Wallet 交互if (wallet.spend(amountToPay)) {System.out.println("支付成功: " + amountToPay);} else {System.out.println("余額不足,當前余額: " + wallet.getCurrentBalance());}} }
在這個改進后的例子中,Person 不再需要知道 Wallet 內部是如何管理 Money 的,也不需要直接操作 Money 對象。Person 只與 Wallet 交互,告訴它要支付多少錢。Wallet 負責具體的支付邏輯,它與它的直接朋友 Money 交互。
-
-
移動方法 (Move Method):
-
如果一個方法過多地使用了另一個類的數據和方法,可以考慮將這個方法移動到那個類中。
-
需要注意的平衡:
-
過度應用可能導致問題:如果為了嚴格遵守迪米特法則而創建大量僅僅是進行簡單委托的“包裝方法 (Wrapper Methods)”,可能會導致類的接口膨脹,反而增加系統的復雜性。
-
Getter 方法本身不一定違反 LoD:object.getData() 本身不違反迪米特法則,因為 Data 對象是 object 的一個組件(或者是通過參數傳入的,或者是局部創建的)。問題在于你如何使用這個返回的 Data 對象。如果你只是讀取 Data 的狀態(比如 data.getValue()),通常是可以接受的。但如果你接著調用 data.getAnotherObject().doSomething(),那就可能違反了。
-
數據結構 (Data Structures) vs. 對象 (Objects):對于純粹的數據結構(例如,DTO - Data Transfer Object),其目的就是暴露數據,這時獲取其內部數據是正常的。迪米特法則更多地應用于行為豐富的對象。
總結:
迪米特法則是指導我們設計低耦合、高內聚系統的一個重要原則。它的核心思想是限制對象之間的知識,讓每個對象只與它的直接朋友交互。通過封裝和委托,我們可以有效地應用迪米特法則,從而創建出更易于維護、擴展和理解的軟件系統。但同時也要注意避免過度設計,找到一個合適的平衡點。