引子:電話遙控修電腦#

某天美女嬌嬌來電,說自家電腦藍屏死機。小菜在電話另一頭口頭指導她:

  • 拆機箱、找出兩根記憶體
  • 拔掉一根、開機測試
  • 五分鐘搞定

完全不懂硬體的女生,能透過遠端電話指導順利修好電腦——這之所以可能,是因為 PC 是模組化、可插拔的。

對照軟體:

  • PC 可以理解為大型系統
  • CPU、記憶體、硬碟、顯卡都是封裝好的「類別」或「程式集」
  • 易插拔的方式 → 任一部件出問題,換一個即可,不影響其他部件

這正是物件導向追求的「強內聚、鬆耦合」。

介面的力量#

CPU 全世界沒幾家能做(Intel、AMD……),但每塊主機板都能裝上去,原因在於:

  • CPU 對外是針腳式或觸點式的標準介面
  • 內部多複雜外界都不必知道
  • 主機板只需預留與 CPU 針腳對應的插槽

介面的最大好處:把複雜性留在內部,對外只暴露穩定的契約。

依賴倒轉原則(DIP)#

依賴倒轉原則(Dependency Inversion Principle, DIP):[ASD]

A. 高層模組不應該依賴低層模組。兩者都應該依賴抽象

B. 抽象不應該依賴細節。細節應該依賴抽象

白話說,就是「針對介面編程,不要對實現編程」。

為什麼叫「倒轉」?#

傳統的面向過程開發:

  • 為了複用,將常用程式寫成函式庫(低層模組)
  • 新專案直接呼叫這些低層函式
  • 高層模組依賴低層模組

問題:

  • 業務邏輯(高層)相同,但客戶想換資料庫或儲存方式(低層)
  • 高層模組與低層緊綁,無法複用
  • 就像 PC 若 CPU、記憶體、硬碟都依賴具體主機板,主機板一壞所有部件就都廢了

解法:讓高層與低層都依賴於抽象(介面或抽象類)。只要介面穩定,任何一邊更換都不影響另一邊。

classDiagram
    direction LR
    class HighLevelModule
    class IAbstraction {
        <<interface>>
        +Operation()
    }
    class LowLevelModule {
        +Operation()
    }
    HighLevelModule ..> IAbstraction
    IAbstraction <|.. LowLevelModule

里氏代換原則(LSP)#

要理解 DIP,必須先理解里氏代換原則。

里氏代換原則(Liskov Substitution Principle, LSP):子類型必須能夠替換掉它們的父類型。[ASD]

由 Barbara Liskov 在 1988 年提出。

意思是:在軟體中,把父類別都替換成它的子類別,程式的行為沒有變化

經典範例:企鵝是鳥嗎?#

  • 生物學上:企鵝是一種特殊的鳥
  • 物件導向設計上:若 Bird 類別有 Fly() 方法,Penguin 卻不會飛
  • 子類擁有父類所有非 private 的行為和屬性
  • 因此 Penguin 不能繼承 Bird——它無法以「鳥」的身份出現

LSP 是繼承複用的基礎#

  • 只有當子類可以替換掉父類且功能不受影響時,父類才能真正被複用
  • 子類則能在父類基礎上增加新的行為

例如有 Animal 類,子類為 CatDogCowSheep,當需要新增動物時:

Animal animal = new Cat();
animal.();
animal.();
animal.();
animal.();
classDiagram
    class Animal {
        +吃()
        +喝()
        +跑()
        +叫()
    }
    Animal <|-- Cat
    Animal <|-- Dog
    Animal <|-- Cow
    Animal <|-- Sheep

只需更改實例化的地方,其他程式不需要改變。

三個原則的關係#

  • 里氏代換原則讓繼承複用成為可能
  • 因為子類型可替換,使用父類型的模組無需修改即可擴展,這正是開放-封閉原則得以成立的基礎
  • 依賴倒轉原則則告訴我們:高層、低層都應該依賴於抽象(介面或抽象類)

收音機式的強耦合#

收音機由電阻、三極體、電路板焊接在一起,任一問題都可能涉及其他部件——這就是過度耦合的反面教材。

軟體世界裡的「收音機式開發」依然太多。例如某銀行系統因為純 C 面向過程開發,一出問題就要停機大半天排查。

依賴倒轉原則可說是物件導向設計的標誌。

不論用哪種語言,如果程式中所有依賴關係都終止於抽象類或介面,那就是物件導向設計;反之就是面向過程設計。[ASD]