談及程式中相依性的去除時,迪米特法則(Law of Demeter)經常被提出,此法則經常被簡單描述為「對任何函式傳回的物件,不該再呼叫上頭之方法」,因此程式碼中出現訊息鏈(Message Chain)或方法鏈(Method Chain)時,就有可能是違反迪米特法則的訊號,實際上真的是如此嗎?

迪米特法則

迪米特法則最初,是美國東北大學在1987年由Ian Holland發想,在美國東北大學官網的〈Law of Demeter: Principle of Least Knowledge〉(http://goo.gl/sKqNTn)中記載著,此法則應用在他們的The Demeter Project,迪米特法則又常稱為最少知識原則(Principle of least knowledge),也就是「各單元對其他單元所知應當有限:只瞭解與目前單元最相關之單元」。

迪米特法則具體來說,在一個類別的方法m中,可以再呼叫的方法應只能來自:類別本身、傳給m的引數、m中建立之物件、C實例擁有之物件。使用程式碼來表示的話就是:

private A a;

private void f() {...}

public void m(B b) { // 方法中只能呼叫

  f();               // 類別定義的方法

  b.action();        // 引數的方法

  new D().run();     // 自建物件之方法

  a.execute();       // 實例擁有物件之方法

}

在《Clean Code》中,曾節錄了來自Apache專案中的一段程式碼ctxt.getOptions().getScratchDir().getAbsolutePath(),稱其「可能」違反迪米特法則,這種從一個物件呼叫一堆取值函式的臭味,在《重構:改善既有程式的設計》書中,稱這類情況為依戀情節(Feature Envy),也就是函式對某類別的興趣,高過對自身所處Host類別的興趣。

送報生與錢包的故事

《Clean Code》對於來自Apache專案的程式碼,是否違反迪米特法則做了討論,火車廂式的方法鏈結風格並不是判斷的重點,因此,即便使用暫存變數ctxt、opts、scratchDir將程式碼修改為ctxt.getOptions()、opts.getScratchDir()、scratchDir.getAbsolutePath(),依舊「可能」違反了迪米特法則,書中指出是否違反法則,主要取決於ctxt、opts、scratchDir為物件或者資料結構,類似地,就算不是方法呼叫,ctxt.options.scratchDir.absolutePath這樣的風格,也是有可能違反迪米特法則。

美國東北大學網站上,提供了一份〈The Paperboy, The Wallet, and The Law Of Demeter〉文件,其中以送報生如何取得客戶付款為例,清楚地表達了違反迪米特法則的情況。以下的程式碼,是位於Paperboy類別之中:

Wallet theWallet = myCustomer.getWallet();   

if (theWallet.getTotalMoney() > payment) {       

  theWallet.subtractMoney(payment);   

}

這邊沒有方法鏈串,然而,一個送報生真的可以直接取得客戶的錢包,然後看看裏頭有多少錢,接著從錢包中拿出錢嗎?

在正常的生活經驗中,其實並不會發生這種事,Customer類別中,應該有個如下的getPayment(float bill)方法內容,而Paperboy能做的事,應該只是

myCustomer.getPayment(payment):

if (myWallet.getTotalMoney() > bill) {

  theWallet.subtractMoney(payment);               

  return payment;           

}

別探朋友隱私

迪米特法則並不是要開發者,看到每行程式碼就開始數著出現幾個dot呼叫,雖然在《The ThoughtWorks Anthology》中提到,讓軟體設計更好的九個練習中,第四條是「每行只使用一個dot」,不過,其真正意義在於多個dot出現時,暗示著開發者可能正在透過這些dot破壞封裝(Encapsulation),就像Paperboy中,不該呼叫Customer的getWallet,以取得錢包,同時也暗示開發者應當思考,Customer曝露了Wallet實例是否為正確的設計。

一個連續出現dot呼叫,但不違反迪米特法則的例子是,每次方法呼叫都傳回this,像是Java中的StringBuilder,例如stringBuilder.append(..).delete(..).insert(..)並不違反迪米特法則,因為append、delete、insert仍是在同一個實例上呼叫,仍然是「只跟朋友說話」。

有些資料若本身就是公開的結構,連續的dot呼叫也不算是違反迪米特法則。例如,有個Dimension,本身定義了width、height原本就是公開資訊,那麼canvas.getDimension().getWidth()這樣的程式碼,就不違反迪米特法則,這也就是《Clean Code》中指出的「主要取決於ctxt、opts、scratchDir為物件或者資料結構」之意義。

在〈The Law of Demeter Is Not A Dot Counting Exercise〉(http://goo.gl/dtDRZK),也舉了個C#的例子:

object a = foo.?bar.?baz.?qux;

這樣的程式碼違反迪米特法則嗎?如果單純看dot數就回答「是」的話,那麼,任何程式碼中連續判斷是否為null後取值的動作,就都違反迪米特法則了,而且,在Java 8中,foo.map(Foo::getBar).map(Bar::getBaz).map(Baz::getQux)的新風格,也就都是違法動作了。

實際上必須考量的是,取得bar、baz、qux是否破壞了封裝,對迪米特法則經常可見的一個簡單比喻是「別跟陌生人聊天」,然而,真正的意義應該是「別探朋友隱私」,如果Foo根本不該揭露Bar的存在,或者是Bar不該揭露Baz的存在,或者是Baz不該揭露Qux的存在,上述其中一個情況成立,那才是違反迪米特法則。

其實都是有關於封裝

當然,單看foo.?bar.?baz.?qux這樣的程式碼,確實依賴了Bar、Baz、Qux等類別,透過dot呼叫大老遠取得資料或進行操作的動作,確實也有點依戀情節的味道,這時可以思考取得Qux的真正目的為何,並建立一個方法做適當封裝。

就像《Clean Code》中建議,如果目的是想在路徑中建立一個檔案,可以考慮用ctxt.createScratchFileStream(classFileName),來取代ctxt.getOptions().getScratchDir().getAbsolutePath()。

對於其他比較單純的情況,可以改採委託(Delegate)方式,然而可能產生大量Wrapper方法是其缺點,有時也會感覺多此一舉。

例如在〈Misunderstanding the Law of Demeter〉中,就討論過,透過在Rails定義委託,我們可以讓<%= @order.customer.name %>改為<%= @order.customer_name %>。

不過,作者也思考了用customer_name來交換dot是否必要?畢竟,一個客戶可以有名稱,本來就是很公開的一件事情。

該篇文件底下引來了不少討論,也有人認為可建立一個View模型,來收集畫面繪製時所需的資料,亦即建立公開的資料結構,解決方法視情況而定。畢竟原則之類的東西,本來就沒有絕對的界線。

雖然,迪米特法則經常被視為切斷相依性的重要法則,然而,連續的dot只是個提示。最重要的是,希望開發者能進一步思考,在這個呼叫過程中是否破壞了封裝,以便及時採取手段,來阻止相依性進一步地蔓延,不揭露自身隱私,不探他人隱私,相依性的降低,就只是必然的結果!

專欄作者

熱門新聞

Advertisement