引子:手機軟體何時統一?#
大鳥用 N 牌手機玩「魂斗羅」,小菜想在自己的 M 牌手機上玩——不行,不同品牌軟體不相容。
PC 因為有 Windows 等通用作業系統,硬體與軟體可以分別由不同廠商開發。
手機若也能像 PC 那樣軟硬解耦,發展會更快。
緊耦合的演化#
如果用繼承來組織「品牌 + 功能」:
手機品牌
├── 手機品牌 M
│ ├── 手機品牌 M 通訊錄
│ └── 手機品牌 M 遊戲
└── 手機品牌 N
├── 手機品牌 N 通訊錄
└── 手機品牌 N 遊戲每新增一個品牌,要寫對應的「品牌 + 通訊錄」、「品牌 + 遊戲」、「品牌 + MP3」……
每新增一個功能(MP3、輸入法、拍照),要對所有品牌都加一個子類。
類別爆炸——是繼承被濫用的典型反例。
也可以反向,按「功能 → 品牌」分類,但問題一樣。
繼承的副作用#
「有了新錘子,所有東西看上去都成了釘子。」[DPE]
繼承的問題:
- 對象的繼承關係在編譯時就定義好了,無法在運行時改變
- 子類與父類有非常緊密的依賴——父類任何變化都導致子類發生變化
- 父類實作不適合解決新問題時,必須重寫或被其他類替換
- 這種依賴限制了靈活性,最終限制了複用性[DP]
合成/聚合複用原則(CARP)#
合成/聚合複用原則(Composite/Aggregate Reuse Principle, CARP):
盡量使用合成/聚合,盡量不要使用類繼承。[J&DP]
- 合成(Composition):強的「擁有」關係,部分與整體生命週期一致(如鳥與翅膀)
- 聚合(Aggregation):弱的「擁有」關係,A 包含 B,但 B 不是 A 的一部分(如雁群與大雁)
優先使用對象的合成/聚合,將有助於:
- 保持每個類被封裝並集中在單個任務上
- 類及其繼承層次保持較小規模,不易膨脹
橋接模式#
橋接模式(Bridge Pattern):將抽象部分與它的實作部分分離,使它們都可以獨立地變化。[DP]
「實作」指的是:抽象類及其派生類用來實現自己的對象。
對手機例子而言:
- 品牌是一個抽象維度
- **軟體(功能)**是另一個抽象維度
- 把兩者分開,透過聚合連接
classDiagram
class HandsetBrand {
<<abstract>>
-HandsetSoft soft
+SetHandsetSoft(HandsetSoft)
+Run()*
}
class HandsetBrandN
class HandsetBrandM
class HandsetSoft {
<<abstract>>
+Run()*
}
class HandsetGame
class HandsetAddressList
HandsetBrand o--> HandsetSoft
HandsetBrand <|-- HandsetBrandN
HandsetBrand <|-- HandsetBrandM
HandsetSoft <|-- HandsetGame
HandsetSoft <|-- HandsetAddressList改寫範例#
abstract class HandsetSoft
{
public abstract void Run();
}
class HandsetGame : HandsetSoft
{
public override void Run() => Console.WriteLine("運行手機遊戲");
}
class HandsetAddressList : HandsetSoft
{
public override void Run() => Console.WriteLine("運行手機通訊錄");
}
abstract class HandsetBrand
{
protected HandsetSoft soft;
public void SetHandsetSoft(HandsetSoft soft) => this.soft = soft;
public abstract void Run();
}
class HandsetBrandN : HandsetBrand
{
public override void Run() => soft.Run();
}
class HandsetBrandM : HandsetBrand
{
public override void Run() => soft.Run();
}客戶端:
HandsetBrand ab = new HandsetBrandN();
ab.SetHandsetSoft(new HandsetGame());
ab.Run();
ab.SetHandsetSoft(new HandsetAddressList());
ab.Run();新增 MP3 功能 → 只新增一個
HandsetMP3類(不影響其他類)新增 S 品牌 → 只新增一個
HandsetBrandS類(不影響其他類)完全符合開放-封閉原則。
結構#
abstract class Implementor
{
public abstract void Operation();
}
class ConcreteImplementorA : Implementor { /* ... */ }
class ConcreteImplementorB : Implementor { /* ... */ }
class Abstraction
{
protected Implementor implementor;
public void SetImplementor(Implementor i) => implementor = i;
public virtual void Operation() => implementor.Operation();
}
class RefinedAbstraction : Abstraction
{
public override void Operation() => implementor.Operation();
}何時使用?#
當系統可能有多角度分類,每一種分類都有可能變化,就應該把這些角度分離出來讓它們獨立變化,減少耦合。
簡言之:當發現需要多角度分類實作對象,只用繼承會造成大量類別增加、不滿足開放-封閉原則時,就考慮橋接模式。
繼承使用準則#
繼承是一種強耦合的結構:父類變,子類就必須變。
用繼承時,一定要在「is-a」關係明確時才考慮,不是任何時候都使用繼承。