Adapter Pattern(適配器模式)
是什麼?
Adapter Pattern 將一個類別的既有介面轉換成客戶端期望的目標介面,讓原本因為介面不相容而無法合作的類別能夠一起工作。
ℹ️GoF 分類
Adapter 屬於結構型模式(Structural Pattern),重點在於如何組合類別和物件以形成更大的結構。
什麼時候用?
用以下 if/then 條件判斷:
- 如果你需要使用一個既有類別,但它的介面(方法名稱、參數、回傳值)跟你系統定義的不同,且你無法修改該類別 → 用 Adapter
- 如果你正在整合第三方 SDK 或 Legacy 系統,且未來可能切換供應商 → 用 Adapter 包一層,切換時只改 Adapter
- 如果兩邊的介面只差一個方法名稱 → 直接重構比加 Adapter 更乾淨
- 如果你可以修改來源類別 → 直接修改而非加 Adapter
什麼時候不該用?
⚠️過度設計警告
如果你可以直接修改來源類別的介面,或者兩邊的介面差異極小(只是方法名稱不同),直接重構比加 Adapter 更乾淨。Adapter 最適合用在你無法修改的既有程式碼上。
執行流程
辨識不相容介面
Client 期望介面 A,但既有類別提供介面 B
定義目標介面
定義 Client 真正需要的方法簽名
建立 Adapter
Adapter 實作目標介面,內部持有 Adaptee 的實例
轉換呼叫
Adapter 的方法內部將呼叫轉譯為 Adaptee 的方法
Client 使用
Client 透過目標介面使用 Adapter,完全不知道背後是 Adaptee
流程解讀:Adapter 的核心工作是「翻譯」。它實作 Client 期望的目標介面(Target),內部持有 Adaptee 的實例。當 Client 呼叫目標介面的方法時,Adapter 將呼叫轉譯為 Adaptee 能理解的方法。這種方式稱為 Object Adapter(透過組合實現),另一種是 Class Adapter(透過多重繼承,較少使用)。
程式碼範例
C# 版本
// 既有的第三方 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 版本
// 既有的第三方 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 版本
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 版本
// 既有的第三方 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 只依賴 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 會讓系統變複雜 |