引子:在 NBA 我需要翻譯#

姚明剛去 NBA 時,一名記者問他「在 CBA 與 NBA 最大的區別是什麼?」,他答:

在 NBA 我需要翻譯,而在 CBA 我不需要。

姚明身高夠、球技夠,但聽不懂英文。教練的戰術、隊員的溝通完全沒辦法理解。要讓姚明在 NBA 打球,有三個辦法:

  1. 讓姚明立刻學會英文 → 不切實際
  2. 讓教練與球員學會中文 → 不可能
  3. 找一個翻譯

翻譯就是適配器

電源適配器也是同樣道理:美國 110V,台灣 110V,中國 220V,筆電不能直接使用——適配器負責把電壓轉成需要的形式。

適配器模式#

適配器模式(Adapter Pattern):將一個類別的介面轉換成客戶希望的另外一個介面。Adapter 模式使得原本由於介面不相容而不能一起工作的那些類別可以一起工作。[DP]

主要解決:「需要的東西就在面前但卻不能使用,又無法改造它」的場景——透過包裝來適配

結構(物件適配器)#

GoF 提出兩種適配器:類別適配器與物件適配器。由於類別適配器需要多重繼承(C++ 才支援),通常用物件適配器

  • Target(目標):客戶所期待的介面
  • Adaptee(被適配者):現存的、需要適配的類別
  • Adapter(適配器):通過在內部包裝一個 Adaptee 物件,把源介面轉換成目標介面
classDiagram
    class Client
    class Target {
        +Request()
    }
    class Adapter {
        -Adaptee adaptee
        +Request()
    }
    class Adaptee {
        +SpecificRequest()
    }
    Client --> Target
    Target <|-- Adapter
    Adapter o--> Adaptee
class Target
{
    public virtual void Request() => Console.WriteLine("普通請求!");
}

class Adaptee
{
    public void SpecificRequest() => Console.WriteLine("特殊請求!");
}

class Adapter : Target
{
    private Adaptee adaptee = new Adaptee();
    public override void Request() => adaptee.SpecificRequest();
}

客戶端:

Target target = new Adapter();
target.Request();

範例:火箭隊的翻譯#

球員抽象類及具體球員:

abstract class Player
{
    protected string name;
    public Player(string name) { this.name = name; }
    public abstract void Attack();
    public abstract void Defense();
}

class Forwards : Player { /* 前鋒,懂英文 */ }
class Center   : Player { /* 中鋒,懂英文 */ }
class Guards   : Player { /* 後衛,懂英文 */ }

但姚明剛來 NBA 時不懂英文,需要適配。先定義「外籍中鋒」(被適配者):

class ForeignCenter
{
    public string Name { get; set; }
    public void 進攻() { Console.WriteLine($"外籍中鋒 {Name} 進攻"); }
    public void 防守() { Console.WriteLine($"外籍中鋒 {Name} 防守"); }
}

再定義「翻譯者」(適配器):

class Translator : Player
{
    private ForeignCenter wjzf = new ForeignCenter();
    public Translator(string name) : base(name) { wjzf.Name = name; }
    public override void Attack()  => wjzf.進攻();
    public override void Defense() => wjzf.防守();
}

客戶端:

Player b  = new Forwards("巴蒂爾");      b.Attack();
Player m  = new Guards("麥克格雷迪");    m.Attack();
Player ym = new Translator("姚明");       ym.Attack(); ym.Defense();

教練不必學中文、姚明不必學英文,靠 Translator 適配器就能一起工作。

何時使用適配器模式?#

  • 想使用一個已存在的類別,但其介面與你的要求不相同
  • 兩個類別所做的事情相同或相似,但具有不同的介面
  • 由於類別共享同一介面,客戶端可以以一種一致的方式呼叫

適配器模式有「亡羊補牢」的味道——用在後期維護階段為宜。

在設計階段,若功能類似的類介面不同,應該透過重構統一介面,而不是直接堆適配器。

設計階段也有適合的場景:使用第三方開發元件,元件介面與系統介面不一致時,完全沒必要為了元件而改動自己的介面——適配器模式正合適。

.NET 的應用:DataAdapter#

.NET 類庫中重要的適配器之一:DataAdapter

DataAdapter 用作 DataSet 和資料來源之間的適配器,以便檢索和保存資料。

透過 Fill(更改 DataSet 以匹配資料來源)與 Update(更改資料來源以匹配 DataSet)提供這一適配器功能。

不論資料來源是 SQL Server、Oracle、Access 還是 DB2,組織方式可能不同,但我們希望得到統一的 DataSet(XML 資料)——這正是適配器的價值。

模式不可亂用#

扁鵲三兄弟的故事:

  • 長兄治病於病情發作前,名氣最小
  • 中兄治病於病情初起時,名氣只及本鄉
  • 扁鵲治病於病情嚴重時(穿針放血、開大手術),名氣最大

事前控制 > 事中控制 > 事後控制

適配器模式是好模式,但如果能事前預防介面不一致,何必事後再去彌補?盲目使用反而是本末倒置。