引子:電話遙控修電腦#
某天美女嬌嬌來電,說自家電腦藍屏死機。小菜在電話另一頭口頭指導她:
- 拆機箱、找出兩根記憶體
- 拔掉一根、開機測試
- 五分鐘搞定
完全不懂硬體的女生,能透過遠端電話指導順利修好電腦——這之所以可能,是因為 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 類,子類為 Cat、Dog、Cow、Sheep,當需要新增動物時:
Animal animal = new Cat();
animal.吃();
animal.喝();
animal.跑();
animal.叫();classDiagram
class Animal {
+吃()
+喝()
+跑()
+叫()
}
Animal <|-- Cat
Animal <|-- Dog
Animal <|-- Cow
Animal <|-- Sheep只需更改實例化的地方,其他程式不需要改變。
三個原則的關係#
- 里氏代換原則讓繼承複用成為可能
- 因為子類型可替換,使用父類型的模組無需修改即可擴展,這正是開放-封閉原則得以成立的基礎
- 而依賴倒轉原則則告訴我們:高層、低層都應該依賴於抽象(介面或抽象類)
收音機式的強耦合#
收音機由電阻、三極體、電路板焊接在一起,任一問題都可能涉及其他部件——這就是過度耦合的反面教材。
軟體世界裡的「收音機式開發」依然太多。例如某銀行系統因為純 C 面向過程開發,一出問題就要停機大半天排查。
依賴倒轉原則可說是物件導向設計的標誌。
不論用哪種語言,如果程式中所有依賴關係都終止於抽象類或介面,那就是物件導向設計;反之就是面向過程設計。[ASD]