設計模式—六大原則—迪特米法則

在面向對象設計的世界裏,有一個尋常卻又常常爲人所忽略的原則——“迪米特(Law of Demeter)”法則。這個原則認爲,任何一個對象或者方法,它應該只能調用一下對象:
• 該對象本身
• 作爲參數傳進來的對象(也可以是該對象的字段)
• 在方法內創建的對象

這個原則用以指導正確的對象協作,分清楚哪些對象應該產生協作,哪些對象則對於該對象而言,又應該是無知的。

本文所涉及的信息

1、迪特米法則
2、最小知識法則
3、信息專家模式
4、代碼重構—Move Method

案例

假設一個超市購物的場景,顧客(Customer)到收銀臺結賬,收銀員(Paper Boy)負責收錢。

public class Customer {
    private String firstName;
    private String lastName;
    private Wallet myWallet;
    public String getFirstName(){
        return firstName;
    }
    public String getLastName(){
        return lastName;
    }
    public Wallet getWallet(){
        return myWallet;
    }
}

public class Wallet {
    private float value;
    public float getTotalMoney() {
        return value;
    }
    public void setTotalMoney(float newValue) {
        value = newValue;
    }
    public void addMoney(float deposit) {
        value += deposit;
    }
    public void subtractMoney(float debit) {
        value -= debit;
    }
}

public class Paperboy {
    public void pay(Customer myCustomer, double payment) {
        Wallet theWallet = myCustomer.getWallet();
        if (theWallet.getTotalMoney() > payment) {
            theWallet.subtractMoney(payment);
        } else {
            //money not enough
        }
    }
}

我們將收銀員的收款方法翻譯一下是否可你想的一致:

“把錢包交出來!”收銀員算出顧客要買的商品總價後,“溫柔”地對顧客說道。
顧客言聽計從,趕緊將錢包掏出來,恭恭敬敬地遞給收銀員。
接過錢包,收銀員毫不客氣地打開,檢查裏面的錢夠不夠。噢,不錯,錢夠了。收銀員從錢包取出錢,心滿意足地笑了。
(看完是不是感覺很恐怖?如果你是顧客,你敢去這樣的超市購物嗎?)

案例疑問

對於PaperBoy而言,Wallet的調用不滿足迪米特法則三個條件中的任何一個,因此讓PaperBoy與Wallet對象直接交互是錯誤的行爲。
若從擬人化的角度思考,則Wallet其實屬於Customer的隱私。如此重要的隱私,怎麼能直接交給收銀員這個陌生人呢?這裏所謂的“隱私”,可以視爲是“數據”,是“信息”,是“知識”,因此我們往往又將迪米特法則稱之爲“最小知識法則”。

最小知識法則

當我們理解“最小知識法則”時,又可以從職責的角度去思考以上代碼。對於收銀員角色,他的職責應該是負責收錢,而不用去管錢包裏的錢夠不夠,如果夠了怎麼辦,如果不夠又該怎麼辦,這些統統都不屬於他的職責。設想一下,當超市裏人流如織,大家都在購買商品時,如果每一個收銀員都要承擔這般的職責時,會出現什麼樣的景象?所以“最小知識法則”乃善法,在對象社區中,我們就應該刻意減少對象之間彼此深入的瞭解。瞭解最小的知識,就意味着依賴最小,彼此產生的影響就會最小。這實際上是KISS(keep it simple and stupid)原則的體現。

信息專家模式

信息專家模式告訴我們:“信息的持有者即爲操作該信息的專家。”對於對象,所謂信息就是該對象內部的字段。在前面的例子中,Wallet是Customer的字段,那麼操作Wallet的行爲自然就應該分配給Customer了。這是題中應有之義。“信息專家模式”其實是面向對象最重要原則“數據與行爲應該封裝在一起”的別名。若在領域建模時能遵循該原則,則可以規避我們設計出貧血模型。

代碼重構

public class Paperboy {

    public void charge(Customer myCustomer, double payment) {
        pay(myCustomer, payment)
    }
    private void pay(Customer myCustomer, double payment) {
        Wallet theWallet = myCustomer.getWallet();
        if (theWallet.getTotalMoney() > payment) {
            theWallet.subtractMoney(payment);
        } else {
            //money not enough
        }
    }
}

提取的pay()的方法體與charge()方法完全相同,但是在PaperBoy類中卻保留了charge()方法,只是這個方法什麼也沒有做,在接收方法請求後,轉而將請求委派給了pay()方法。我們可以這樣理解:在抽象層面,收款是收銀員的職責;在實現層面,是pay()方法支持了收款行爲,該實現歸屬於顧客。
觀察pay()方法,我們發現該方法操作的數據皆來自Customer。我們嗅到了一種壞味道,即Martin Fowler所謂的“特性依戀(Feature Envy)”。對於該壞味道,Martin Fowler是這樣闡釋的:“函數對某個類的興趣高過對自己所處類的興趣。”不要再嫉妒了,橋歸橋,路歸路,讓方法回到自己最喜歡的地方吧。運用“Move Method”重構手法,將pay()方法移動到Customer中

代碼重構

public class Customer {
    private String firstName;
    private String lastName;
    private Wallet myWallet;
    public String getFirstName(){
        return firstName;
    }
    public String getLastName(){
        return lastName;
    }
    private void pay(double payment) {
    
        if (theWallet.getTotalMoney() > payment) {
            theWallet.subtractMoney(payment);
        } else {
            //money not enough
        }
    }
}
public class Paperboy {

    public void charge(Customer myCustomer, double payment) {
       myCustomer.pay(payment);
    }
    
}

在將方法移到正確的位置後,我們發現暴露的getWallet()方法根本就沒有意義。更何況,將錢包裸露出去,難道是想要炫富嗎?還是低調一點爲好,隱藏自己的“隱私”,總好過被人覬覦而招來飛來橫禍之險。於是,內聯(inline)之。

判斷是否違背法則

判斷一段代碼是否違背了迪米特法則,有一個小竅門,就是看調用代碼是否出現形如a.m1().m2().m3().m4()之類的代碼。這種代碼在Martin Fowler《重構》一書中,被名爲“消息鏈條(Message Chain)”,有人更加誇張地名其爲“火車殘骸”。車禍現場啊,真是慘不忍睹。
那麼,如下代碼是否這樣的殘骸呢?

str.split("&")
    .stream()
    .map(str -> str.contains(elementName) ? str.replace(elementName + "=", "") : "")
    .filter(str -> !str.isEmpty())
    .reduce("", (a, b) -> a + "," + b);

不是的。這樣的代碼我們一般稱之爲“流暢接口或連貫接口(Fluent Interface)”。二者的區別在於觀察形成鏈條的每個方法返回的是別的對象,還是對象自身。如果返回的是別的對象,就是消息鏈條。所謂m1().m2().m3().m4()的調用,其實是調用者不需要也不想知道的“知識”,把這些中間過程的細節暴露出來沒有意義,調用者關心的是最終結果;而上述代碼中的map()與filter()等方法其實返回的還是Stream類。這一調用方式其初衷並非告知中間過程的細節,而是一種聲明式的DSL表達,調用者可以自由地組合它們。

參考文章demeter

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章