Adapter Pattern(適配器模式)

是什麼?

Adapter Pattern 將一個類別的既有介面轉換成客戶端期望的目標介面,讓原本因為介面不相容而無法合作的類別能夠一起工作。

ℹ️GoF 分類

Adapter 屬於結構型模式(Structural Pattern),重點在於如何組合類別和物件以形成更大的結構。

什麼時候用?

用以下 if/then 條件判斷:

什麼時候不該用?

⚠️過度設計警告

如果你可以直接修改來源類別的介面,或者兩邊的介面差異極小(只是方法名稱不同),直接重構比加 Adapter 更乾淨。Adapter 最適合用在你無法修改的既有程式碼上。

執行流程

1

辨識不相容介面

Client 期望介面 A,但既有類別提供介面 B

2

定義目標介面

定義 Client 真正需要的方法簽名

3

建立 Adapter

Adapter 實作目標介面,內部持有 Adaptee 的實例

4

轉換呼叫

Adapter 的方法內部將呼叫轉譯為 Adaptee 的方法

5

Client 使用

Client 透過目標介面使用 Adapter,完全不知道背後是 Adaptee

流程解讀:Adapter 的核心工作是「翻譯」。它實作 Client 期望的目標介面(Target),內部持有 Adaptee 的實例。當 Client 呼叫目標介面的方法時,Adapter 將呼叫轉譯為 Adaptee 能理解的方法。這種方式稱為 Object Adapter(透過組合實現),另一種是 Class Adapter(透過多重繼承,較少使用)。

程式碼範例

C# 版本

csharp
// 既有的第三方 XML 分析服務(Adaptee)
public class LegacyXmlParser
{
    public string ParseXmlToString(string xml)
        => $"Parsed XML: {xml.Length} characters processed";
}
 
// Client 期望的目標介面
public interface IDataParser
{
    string Parse(string data);
}
 
// Adapter:讓 LegacyXmlParser 符合 IDataParser
public class XmlParserAdapter : IDataParser
{
    private readonly LegacyXmlParser _legacyParser;
 
    public XmlParserAdapter(LegacyXmlParser legacyParser)
    {
        _legacyParser = legacyParser;
    }
 
    public string Parse(string data)
        => _legacyParser.ParseXmlToString(data);
}
 
// 使用
IDataParser parser = new XmlParserAdapter(new LegacyXmlParser());
Console.WriteLine(parser.Parse("<root><item>Hello</item></root>"));

TypeScript 版本

typescript
// 既有的第三方 XML 分析服務(Adaptee)
class LegacyXmlParser {
  parseXmlToString(xml: string): string {
    return `Parsed XML: ${xml.length} characters processed`;
  }
}
 
// Client 期望的目標介面
interface DataParser {
  parse(data: string): string;
}
 
// Adapter
class XmlParserAdapter implements DataParser {
  constructor(private legacyParser: LegacyXmlParser) {}
 
  parse(data: string): string {
    return this.legacyParser.parseXmlToString(data);
  }
}
 
// 使用
const parser: DataParser = new XmlParserAdapter(new LegacyXmlParser());
console.log(parser.parse("<root><item>Hello</item></root>"));

Python 版本

python
from abc import ABC, abstractmethod
 
# 既有的第三方 XML 分析服務(Adaptee)
class LegacyXmlParser:
    def parse_xml_to_string(self, xml: str) -> str:
        return f"Parsed XML: {len(xml)} characters processed"
 
# Client 期望的目標介面
class DataParser(ABC):
    @abstractmethod
    def parse(self, data: str) -> str:
        pass
 
# Adapter
class XmlParserAdapter(DataParser):
    def __init__(self, legacy_parser: LegacyXmlParser):
        self._legacy_parser = legacy_parser
 
    def parse(self, data: str) -> str:
        return self._legacy_parser.parse_xml_to_string(data)
 
# 使用
parser: DataParser = XmlParserAdapter(LegacyXmlParser())
print(parser.parse("<root><item>Hello</item></root>"))

Java 版本

java
// 既有的第三方 XML 分析服務(Adaptee)
public class LegacyXmlParser {
    public String parseXmlToString(String xml) {
        return "Parsed XML: " + xml.length() + " characters processed";
    }
}
 
// Client 期望的目標介面
public interface DataParser {
    String parse(String data);
}
 
// Adapter
public class XmlParserAdapter implements DataParser {
    private final LegacyXmlParser legacyParser;
 
    public XmlParserAdapter(LegacyXmlParser legacyParser) {
        this.legacyParser = legacyParser;
    }
 
    public String parse(String data) {
        return legacyParser.parseXmlToString(data);
    }
}
 
// 使用
DataParser parser = new XmlParserAdapter(new LegacyXmlParser());
System.out.println(parser.parse("<root><item>Hello</item></root>"));

結構圖

Client
uses
Target Interface (IDataParser)
Adapter (XmlParserAdapter)
implements
Adaptee (LegacyXmlParser)

結構解讀:Client 只依賴 Target Interface(IDataParser),完全不知道 Adaptee 的存在。Adapter 實作 Target Interface,同時內部持有 Adaptee 的參考。當 Client 呼叫 Parse() 時,Adapter 將呼叫轉譯為 Adaptee 的 ParseXmlToString()。如果未來要換一個新的 XML 解析器,只需建立新的 Adapter,Client 程式碼零修改。

實戰補充

💡資深開發者經驗

在 .NET 生態系中,Adapter 最常見的場景是整合第三方 SDK。例如你的系統定義了 IEmailSender 介面,但要接 SendGrid、Mailgun 等不同服務商。每個服務商的 SDK 介面都不同,各寫一個 Adapter 就能無縫切換。搭配 DI Container 註冊,切換服務商只需要改一行設定。另一個常見場景是 Legacy 系統對接:舊系統回傳 XML,新系統期望 JSON DTO,中間放一個 Adapter 做轉換。

理解測驗

🤔 Adapter Pattern 的核心目的是什麼?

🤔 以下哪個場景最適合用 Adapter Pattern?

🤔 Adapter 和被適配物件(Adaptee)之間的關係是什麼?

面試常見問題

Q: Object Adapter 和 Class Adapter 有什麼差別?實務上用哪個?

A: Object Adapter 透過組合(持有 Adaptee 的參考),Class Adapter 透過多重繼承(同時繼承 Target 和 Adaptee)。實務上幾乎都用 Object Adapter,因為:(1) 大部分語言不支援多重繼承(C#、Java);(2) 組合比繼承更靈活,可以在執行時切換 Adaptee。

Q: Adapter 和 Facade 都是「包一層」,差在哪裡?

A: Adapter 的目的是「介面轉換」— 讓不相容的介面能合作,通常是一對一的轉換。Facade 的目的是「簡化使用」— 把多個子系統的複雜操作封裝成簡單的高層介面,通常是一對多的封裝。

Q: 什麼時候 Adapter 會退化成不必要的間接層?

A: 當 Adapter 的方法只是直接呼叫 Adaptee 的方法而沒有任何轉換邏輯時,它就是不必要的。判斷標準:如果 Adapter 每個方法都只有一行 return adaptee.xxx()、且參數和回傳值完全相同,那就直接讓 Adaptee 實作目標介面即可。

相關模式

| 模式 | 關係 | |------|------| | Facade | 都是包裝層,但 Adapter 做介面轉換(一對一),Facade 做簡化封裝(一對多) | | Decorator | 結構相似(都包裝另一個物件),但 Adapter 改變介面、Decorator 保持介面不變並增加功能 | | Proxy | 結構也相似,但 Proxy 提供相同介面並控制存取,Adapter 提供不同介面 | | Bridge | Bridge 在設計階段就分離抽象與實作,Adapter 是事後補救不相容的介面 |

重點整理

💡一句話記住

Adapter Pattern = 介面轉接頭:你改不了插座也改不了插頭,就加個轉接器。 口訣:「改不動別人的碼,就包一層自己的」

| 概念 | 說明 | |------|------| | Target(目標介面) | Client 期望使用的介面 | | Adaptee(被適配者) | 既有的、介面不相容的類別 | | Adapter(適配器) | 實作目標介面,內部委派給 Adaptee | | 核心好處 | 不修改既有程式碼就能整合不同系統 | | 代價 | 多一層間接呼叫,過多 Adapter 會讓系統變複雜 |

你可能也想看

Prototype PatternBridge Pattern

按 ← → 鍵切換課程