重新理解Law of Demeter
最近開始繼續拜讀Uncle Bob的大作Clean Code,讀到第六章講述Law of Demeter的部分時,發現自己其實根本就沒有真正地理解過它的含義,也因此沒有辦法抱持著足夠的意識在自己平日撰寫的程式中。
Law of Demeter (LoD),又稱為Least Knowledge,其目的是為了儘可能地減少class之間的耦合度,使得在日後替換implementation時,能降低系統中其它程式修改的需要。
根據Clean Code [1],LoD要求一個class所能引用的method來源只能是下列4種:
1. class自身
2. 在method裡面創建的物件
3. 作為method參數傳遞進來的物件
4. class自身的內部物件
也就是,一個class A只能使用跟它有直接關聯性的class B的method。如果A想要透過class B來取用class C所提供的method,那麼A不應該直接藉由B拿到class C的物件來呼叫C的method,因為這是class B的內部結構,A去了解B的實作細節就破壞了B的封裝。所以我們就需要思考B要怎麼提供method讓A能夠間接使用C的method。
以Clean Code中的例子來說:
這段程式想要透過ctxt物件來找出output目錄,所以它先取得了Options物件,再透過Options物件取得ScratchDir物件,再透過ScratchDir物件繼而取得output目錄。使用ctxt的method知道了太多它不應該知道的細節了,因為它需要去了解有哪些物件需要逐層被取出,然後才能找到output目錄,所以這樣的鏈式呼叫會與執行它的上層程式產生緊密的耦合度,日後不好修改。
對於這樣的狀況,我們就需要去思考寫這段程式的目的是為了解決什麼問題,是不是根本可以換個方式設計。回到剛剛Clean Code的例子,該段鏈式呼叫的最終目的其實是想要為output目錄下的某檔案創建一個BufferedOutputStream。所以呼叫端的那串鏈式呼叫可以被替換為:
把取得SratchDir()和absolute path的邏輯封裝在createScratchFileStream()裡,這樣呼叫端就不用再關心細節。
良葛格[2]也補充說明,不是看到鏈式呼叫就表示違反LoD,要看鏈式呼叫過程中所返回的物件是什麼。以Java的StringBuilder為例:
雖然有很多dot運算在當中,但是每次取回的物件都是一開始創建的StringBuilder物件本身,沒有引入其它更多的關聯性,這樣的呼叫仍是符合LoD的。
因此,我覺得鏈式呼叫其實是個提醒,每當直覺要寫下一段這樣風格的程式碼時,就是告訴自己要加倍留意method的呼叫過程,還有設計上是不是有需要進行調整的地方。
----
Reference
[1] Clean Code, https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882
[2] 封裝與迪米特法則, https://www.ithome.com.tw/voice/98670
[3] Law of Demeter, https://en.wikipedia.org/wiki/Law_of_Demeter
Law of Demeter (LoD),又稱為Least Knowledge,其目的是為了儘可能地減少class之間的耦合度,使得在日後替換implementation時,能降低系統中其它程式修改的需要。
根據Clean Code [1],LoD要求一個class所能引用的method來源只能是下列4種:
1. class自身
2. 在method裡面創建的物件
3. 作為method參數傳遞進來的物件
4. class自身的內部物件
也就是,一個class A只能使用跟它有直接關聯性的class B的method。如果A想要透過class B來取用class C所提供的method,那麼A不應該直接藉由B拿到class C的物件來呼叫C的method,因為這是class B的內部結構,A去了解B的實作細節就破壞了B的封裝。所以我們就需要思考B要怎麼提供method讓A能夠間接使用C的method。
Bad Smell?
連續以dot運算來串接而成的鏈式呼叫 (又稱為train wreck),常常可能是違反LoD的bad smell。以Clean Code中的例子來說:
這段程式想要透過ctxt物件來找出output目錄,所以它先取得了Options物件,再透過Options物件取得ScratchDir物件,再透過ScratchDir物件繼而取得output目錄。使用ctxt的method知道了太多它不應該知道的細節了,因為它需要去了解有哪些物件需要逐層被取出,然後才能找到output目錄,所以這樣的鏈式呼叫會與執行它的上層程式產生緊密的耦合度,日後不好修改。
對於這樣的狀況,我們就需要去思考寫這段程式的目的是為了解決什麼問題,是不是根本可以換個方式設計。回到剛剛Clean Code的例子,該段鏈式呼叫的最終目的其實是想要為output目錄下的某檔案創建一個BufferedOutputStream。所以呼叫端的那串鏈式呼叫可以被替換為:
把取得SratchDir()和absolute path的邏輯封裝在createScratchFileStream()裡,這樣呼叫端就不用再關心細節。
良葛格[2]也補充說明,不是看到鏈式呼叫就表示違反LoD,要看鏈式呼叫過程中所返回的物件是什麼。以Java的StringBuilder為例:
雖然有很多dot運算在當中,但是每次取回的物件都是一開始創建的StringBuilder物件本身,沒有引入其它更多的關聯性,這樣的呼叫仍是符合LoD的。
因此,我覺得鏈式呼叫其實是個提醒,每當直覺要寫下一段這樣風格的程式碼時,就是告訴自己要加倍留意method的呼叫過程,還有設計上是不是有需要進行調整的地方。
----
Reference
[1] Clean Code, https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882
[2] 封裝與迪米特法則, https://www.ithome.com.tw/voice/98670
[3] Law of Demeter, https://en.wikipedia.org/wiki/Law_of_Demeter
留言
張貼留言