When we try to pick out anything by itself, we find it hitched to everything else in the Universe.
— John Muir, My First Summer in the Sierra
核心概念#
在 Topic 8 The Essence of Good Design 中,作者主張使用良好的設計原則可以讓程式碼容易修改。耦合是變更的敵人,因為它把必須同時改變的東西連結在一起。這使得變更更加困難——你要麼花時間追蹤所有需要改的地方,要麼花時間搞清楚為什麼你「只改了一個東西」就壞了其他東西。
設計橋梁時,你要它們剛性;設計軟體時,你要它們靈活。要靈活,個別元件應該盡可能少地耦合到其他元件。
耦合是可傳遞的:如果 A 耦合到 B 和 C,而 B 耦合到 M 和 N,C 耦合到 X 和 Y,那麼 A 實際上耦合到了 B、C、M、N、X 和 Y。
Tip 44 - Decoupled Code Is Easier to Change(去耦合的程式碼更容易修改)
耦合的症狀#
- 不相關模組或函式庫之間出現古怪的相依性
- 對一個模組的「簡單」修改傳播到系統中不相關的模組或破壞其他地方
- 開發者害怕修改程式碼,因為不確定會影響什麼
- 每個人都必須參加會議,因為沒人確定誰會受到某個變更的影響
火車殘骸(Train Wrecks)#
像這樣的方法呼叫鏈就是火車殘骸:
public void applyDiscount(customer, order_id, discount) {
totals = customer
.orders
.find(order_id)
.getTotals();
totals.grandTotal = totals.grandTotal - discount;
totals.discount = discount;
}這段程式碼穿越了五層抽象,從 customer 到 total amounts。頂層程式碼必須知道 customer 暴露了 orders、orders 有 find 方法、order 有 totals 物件等等。這些隱含知識都不能在未來改變。
Tip 45 - Tell, Don’t Ask(命令,不要詢問)
Tell, Don’t Ask(TDA) 原則說你不應該根據物件的內部狀態做決策然後更新它。這樣做完全破壞了封裝的好處。修正方式是將責任委派給相關物件:
public void applyDiscount(customer, order_id, discount) {
customer
.findOrder(order_id)
.applyDiscount(discount);
}TDA 不是自然法則,只是幫助我們辨識問題的模式。在每個應用中都有某些頂層概念是通用的,適度暴露這些概念是務實的決定。
Demeter 法則#
Demeter 法則(LoD)規定一個類別 C 中定義的方法只應呼叫:
- C 的其他實例方法
- 它的參數
- 它所建立的物件中的方法
- 全域變數
作者現在建議一個更簡單的表達方式:
Tip 46 - Don’t Chain Method Calls(不要鏈式呼叫方法)
盡量讓存取某樣東西時不要有超過一個「.」。這也涵蓋使用中間變數的情況。
例外: 如果你鏈接的東西真的、真的不太可能改變(例如語言內建的函式庫),那麼鏈式呼叫是可以的。
鏈式呼叫 vs. 管線#
在 Topic 30 Transforming Programming 中討論的函式管線(pipelines),雖然看起來像是方法呼叫的鏈式串接,但本質不同。管線是轉換資料,從一個函式傳遞到下一個,不依賴隱藏的實作細節。這種形式的耦合對程式碼變更的阻礙遠小於火車殘骸。
全域化的邪惡#
全域可存取的資料是應用元件之間耦合的隱蔽來源。每一筆全域資料就像是你應用中每個方法突然多了一個額外的參數。
全域資料使程式碼難以拆分和測試——寫單元測試時你會發現自己需要一大堆設定程式碼來建立全域環境。
Tip 47 - Avoid Global Data(避免全域資料)
全域資料包括 Singleton#
如果你只是把全域變數包裝在一個 singleton 物件或全域模組中,它仍然是全域資料,只是有了更長的名字。較好的做法是將資料隱藏在方法後面(如 Config.getLogLevel()),但你仍然只有一份設定資料。
全域資料包括外部資源#
任何可變的外部資源都是全域資料——資料庫、檔案系統、服務 API 等。解決方案是始終將這些資源包裝在你控制的程式碼後面。
Tip 48 - If It’s Important Enough to Be Global, Wrap It in an API(如果重要到需要全域化,就用 API 包裝它)
繼承增加耦合#
子類化的濫用所帶來的耦合問題非常重要,以至於有專門的章節討論:Topic 31, Inheritance Tax。
一切都關乎變更#
耦合的程式碼很難改變:一個地方的改動可能在程式碼的其他地方產生副作用,而且往往出現在難以發現的地方,直到上線一個月後才浮現。
讓你的程式碼保持害羞——只處理它直接知道的事情——將幫助保持應用程式的去耦合,讓它們更容易改變。
相關章節#
- Topic 8,好設計的本質
- Topic 9,DRY——邪惡的重複
- Topic 10,正交性
- Topic 11,可逆性
- Topic 29,行走江湖
- Topic 30,轉換式程式設計
- Topic 31,繼承稅
- Topic 32,設定
- Topic 33,打破時間耦合
- Topic 34,共享狀態是不正確的狀態
- Topic 35,Actor 與 Process
- Topic 36,黑板