引子:手機軟體何時統一?#

大鳥用 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」關係明確時才考慮,不是任何時候都使用繼承