---
tags: 設計模式
---
# 轉接器模式(Adapter Pattern)
## 前言
- 當你想使用某些現有類別, 但它的接口與你的客戶端程式碼不兼容, 此時可考慮使用 Adapter Pattern.
- 某些現有類別缺乏一些常見的功能, 此時可考慮使用 Adapter Pattern 去擴充這些類別.
- 兩個以上不同介面的類別所做的事情, 概念上是類似的, 此時若統一介面可以讓客戶端程式使用上更簡潔, 也可以考慮使用 Adapter Pattern.
#### 問題需求
假設現有機器會定期產生 XML 格式的資料, 然後我們希望將這些資料轉換為圖表輸出在網頁上.
所以我們找了一套繪圖用的 Nuget 套件. 但是這個套件只能接受 JSON 格式的資料...
此時我們可能有以下三種解法
1. 放棄使用 Nuget 套件, 由自己寫一套繪圖用的程式
2. 修改機器上的程式, 讓其改產生 JSON 格式的資料
3. 寫一個 Adapter 負責轉換 XML 成 JSON. 當 XML 資料被 Adapter 轉換成 JSON 格式後, 我們就可以使用 Nuget 套件了.

- 如果開發時間允許, 或許可以考慮使用自己寫一套繪圖用程式, 減少依賴外部套件的成分.
- 假設開發時間不夠, 或許也可以考慮改變機器上的程式, 但可能會影響到其他接受 XML 格式資料的程式, 或者我們可能沒有權限去存取或改變機器上的程式.
- 假設上述選擇皆不切實際, 那可以考慮額外寫一個 Adapter 類別去專門負責將 XML 轉換成 Nuget 套件看得懂的 JSON 格式.
## Adapter 介紹
### 定義
> Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.
> The adapter pattern convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
> * Applicability
> * Need to using an existing class, but its interface doesn't match
> * Need to make use of incompatible classes
> * (object adapter) Need to use several existing subclasses, but don't what to subclass them all
> * Consequences
> * Class adapter commits to the concrete Adapter class: it won't work with Adapter's subclasses, but object adapter does.
> * Class adapter introduces only one object and no ponter indirection
> * Object adapter requires quite a bit of work, since all of Adaptee's interface must be duplicated.
- 建立一個與 Client 端可兼容的新介面/接口, 此介面可以讓原來因為介面/接口不合, 而無法協同運作的兩個類別可以順利工作.
- Adapter 擁有一個與 Client 類別可兼容的介面/接口, Client 類別可透過這個介面/接口使用 Adaptee 的方法. 一旦 Client 類別使用 Adapter 所提供的方法, Adapter 會將訊息傳遞給 Adaptee, 但在傳遞之前, 會先轉換成 Adaptee 能接受的格式.
### 名詞定義
```mermaid
graph BT;
Client--使用-->ITarget
Adapter-.->|繼承|ITarget
Adapter--使用-->Adaptee
```
- Client
- 呼叫 ITarget 介面的物件
- ITarget
- 能夠解決 Client 端需求的公開介面, 通常以介面 (Interface) 或抽象類別的形式呈現. ( 還是可以是類別啦XD )
- Adapter
- 實作 ITagert 介面的類別, 通常擁有 Adaptee 型別的物件. 透過這種方式讓外界自認為在使用 ITarget, 但實際是操作 Adaptee (將 Adaptee 轉換成 Client 可以使用的 ITarget)
- Adaptee
- 現存但不滿足 ITarget 定義的類別, 需要被 Adapter 轉換後才能讓 Client 使用
### 兩種實作方式
- Class Adapter (多重繼承)
- C# 僅允許單一繼承, 所以不建議使用這種方式來實作 Adapter
- Adapter 透過繼承來得到 Adaptee
```mermaid
graph BT;
Client--使用-->ITarget
Adapter-.->|繼承|ITarget
Adapter--繼承-->Adaptee
```

- Pseudo Code
```C#
public interface ITarget
{
void DoSomeThing();
}
public class Adaptee
{
public void Execute() => throw new NotImplementedException();
}
// 會暴露實作來自於 Adaptee.
public class Adapter : Adaptee, ITarget
{
public void DoSomeThing() => Execute();
}
// Client 端程式
internal class Program
{
private static void Main(string[] args)
{
ITarget target = new Adapter();
target.DoSomeThing();
}
}
```
- Object Adapter (物件合成)
- Adapter 透過物件合成來得到 Adaptee (通常只具有一個 Adaptee 成員, 但具有多個也被允許, 定義上並沒有去限制 Adapter 能擁有的 Adaptee 數量)
```mermaid
graph BT;
Client--使用-->ITarget
Adapter-.->|繼承|ITarget
Adapter--使用-->Adaptee
```

- Pseudo Code
```C#
public interface ITarget
{
void DoSomeThing();
}
public class Adaptee
{
public void Execute() => throw new NotImplementedException();
}
public class Adapter : ITarget
{
private readonly Adaptee _adaptee = new Adaptee();
public void DoSomeThing() => => _adaptee.Execute();
}
// Client 端程式
internal class Program
{
private static void Main(string[] args)
{
ITarget target = new Adapter();
target.DoSomeThing();
}
}
```
### Pluggable Adapter Pattern
一般 Adapter Pattern 的使用情境**是 Adaptee 先存在**, 之後有新的 Client, 有新的對 Adaptee 的使用需求, 或有多個 Client 但其對於 Adaptee 的使用需求不同. 因此需要分別建立專屬於他們的 ITarget & Adapter 去銜接 Client & Adaptee, 使其能偕作.
```mermaid
graph TB;
New-Client--使用-->ITarget
New-Client2--使用-->ITarget2
Adapter-.->|繼承|ITarget
Adapter2-.->|繼承|ITarget2
Adapter--使用-->Adaptee
Adapter2--使用-->Adaptee
subgraph 已經先存在
Adaptee
end
subgraph 新建立
New-Client
Adapter
ITarget
end
subgraph 新建立2
Adapter2
ITarget2
New-Client2
end
```
Pluggable Adapter Pattern 則是反過來, 先存在 Client & ITarget, 之後才開始新增 Adapter & Adaptee.
```mermaid
graph TB;
Client--使用-->ITarget
Adapter-.->|繼承|ITarget
Adapter--使用-->Adaptee
Adapter2-.->|繼承|ITarget
Adapter2--使用-->Adaptee2
subgraph 新建立
Adapter
Adaptee
end
subgraph 已經先存在
Client
ITarget
end
subgraph 新建立2
Adapter2
Adaptee2
end
```
##### 舉例
- 若需要將資料轉化成樹狀結構 但資料來源可以是 Json 或是 Xml 或...
- 你在實作 TreeView 時, 亙本不知道未來會使用哪種資料, 因此先定義好 IDataProvider. TreeView 只要使用 IDataProvider 傳回的資料就好(不需要知道資料來源是 Json 還是什麼鬼...)
```mermaid
graph TB;
TreeView--使用-->IDataProvider
JsonParser-.->|繼承|IDataProvider
JsonParser--使用-->JsonData
XmlParser-.->|繼承|IDataProvider
XmlParser--使用-->XmlData
subgraph 新建立
JsonParser
JsonData
end
subgraph 已經先存在
TreeView
IDataProvider
end
subgraph 新建立2
XmlParser
XmlData
end
```
## Code Example
### 令介面/接口不合的類別可以偕同運作
- Client 需要取得 XML 格式的資料(也許會做一些資料的處理), 傳給 Client.
- Client 不應該被修改 (假設這是前提)
- XmlData 很早就被實作完成.**(Adaptee 先存在)**
- GetXmlString() - XmlData & XmlInfo - IXmlDataProvider . 兩者對外的介面/接口並不合, 但我們又不想或沒辦法更改它們.
- 可能情境 :
1. 你依據客戶需求定義出 IXmlDataProvider 介面, 並且建立一個實作此介面的類別.
2. 之後客戶有新的需求, 且你希望沿用原本定義的介面(IXmlDataProvider).
3. 原本你打算建立一個新的類別(實作 IXmlDataProvider), 但你突然發現這個新需求已經被實作在類別 XmlData.
4. 你不想建立一個類別(實作 IXmlDataProvider) 再把 XmlData 的程式碼複製貼上
5. 你不想修改 XmlData 的對外簽章, 再讓其實作 IXmlDataProvider. 因為這個修改可能會讓原本使用 XmlData 的程式碼出現問題.
6. 你不想修改 IXmlDataProvider 的對外簽章, 因為你不希望修改 Client 的程式碼.
##### Adaptee
```C#
// 這個類別已經被實作完成很久了.
public class XmlData
{
public string GetXmlString() => "default XML";
}
```
##### ITarget
```C#
// 依據 Client 需求所定義出的新介面
public interface IXmlDataProvider
{
string XmlInfo { get; }
}
```
##### Adapter
```C#
public class XmlDataProvider : IXmlDataProvider
{
private readonly XmlData _data = new XmlData();
public string XmlInfo
{
get
{
var xmlData = $"回傳 \"{_data.GetXmlString()}\" 前 , 或許可以做一些處理.";
return xmlData;
}
}
}
```
##### Client
```C#
internal static class Program
{
private static void Main(string[] args)
{
IXmlDataProvider xmlDataProvider = new XmlDataProvider();
Console.WriteLine(xmlDataProvider.XmlInfo);
Console.ReadKey();
}
}
```
##### 輸出結果
```
回傳 "default XML" 前 , 或許可以做一些事情.
```
- XmlDataProvider 令 Client 可與 XmlData 偕同運作
- Client 並不需要認識 XmlData, 更不用知道資料來源為 XmlData,
- Client 只需要認識 IXmlDataProvider
- **Adaptee 先存在 , 之後若新的 Client 對於 Adaptee 有新的使用需求, 此時可為 Adaptee 建立一個專屬於此 Client 的 Adapter & ITarget**
### 替類別補上其不具有的功能
- 可能情境
1. 目前有兩個古老的類別 拿著狙擊槍的人 & 拿著手槍的人, 因為歷史等因素幾乎沒辦法修改.
2. 這兩個古老的類別都缺乏常見功能 (e.g. 裝子彈)
3. 你不想為了這兩個類別都各建造一個子類並且在子類內實作相同的功能.(複製&貼上)
```C#
// 在子類實作所需要的功能
public class 會裝子彈的拿狙擊槍的人 : 拿著狙擊槍的人
{
private void 裝子彈() => Console.WriteLine("裝填子彈...");
}
public class 會裝子彈的拿手槍的人 : 拿著手槍的人
{
private void 裝子彈() => Console.WriteLine("裝填子彈...");
}
```
##### Adaptee
```C#
// 假設這些類別已經被實作完成很久了. 被非常多類別參考使用, 幾乎完全沒辦法修改.
public class 拿著狙擊槍的人
{
public void 狙擊() => Console.WriteLine("狙擊");
}
public class 拿著手槍的人
{
public void 開槍() => Console.WriteLine("開槍");
}
```
##### ITarget
```C#
public abstract class 持有武器的人
{
public abstract void 手槍攻擊();
public abstract void 狙擊槍攻擊();
protected void 裝子彈() => Console.WriteLine("裝填子彈...");// 也可以等到 Adapter 再實作
}
```
##### Adapter
```C#
public class 壞人 : 持有武器的人
{
private readonly 拿著手槍的人 _拿著手槍的人 = new 拿著手槍的人();
private readonly 拿著狙擊槍的人 _拿著狙擊槍的人 = new 拿著狙擊槍的人();
public override void 手槍攻擊()
{
裝子彈();
_拿著手槍的人.開槍();
}
public override void 狙擊槍攻擊()
{
裝子彈();
_拿著狙擊槍的人.狙擊();
}
}
```
##### Client
```C#
internal static class Program
{
private static void Main(string[] args)
{
// 實務上, 可由 Factory or IOC 取得物件
持有武器的人 持有武器的人 = new 壞人();
持有武器的人.手槍攻擊();
持有武器的人.狙擊槍攻擊();
Console.ReadKey();
}
}
```
##### 輸出結果
```
裝填子彈...
開槍
裝填子彈...
狙擊
```
- 將 Adaptee 所不具有的"常見功能"實作在 ITarget or Adapter 內.
- Adapter 可依據需求決定是否使用 "常見功能"
- 盡量在架構設計階段上多下工夫, 減少使用這個方法的機會. (這算是事後補救)
### 統一介面可以讓客戶端使用上更方便
- 可能情境
- 對於客戶端來說, TCP 和 File 的操作, 在概念上是類似的.
- 客戶端希望可以透過多型的方式決定要操作 TCP or File
##### Adaptee
- Socket
- File
##### ITarget
```C#
public interface ICommunication
{
void Send(byte[] buffer);
byte[] Receive();
bool Connect(string path);
void Disconnect();
}
```
##### Adapter
```C#
public class FileCommunication : ICommunication
{
public bool Connect(string path)
{
try
{
using var fs = File.Create(path);
_path = path;
return true;
}
catch
{
return false;
}
}
public void Disconnect() => File.Delete(_path);
public void Send(byte[] buffer) => File.WriteAllBytes(_path, buffer);
public byte[] Receive() => File.ReadAllBytes(_path);
private string _path;
}
public class TcpCommunication : ICommunication, IDisposable
{
public bool Connect(string path)
{
var data = path.Split(':');
if (IPAddress.TryParse(data[0], out var ip) && int.TryParse(data[1], out var port))
{
_client.Connect(ip, port);
}
return _client.Connected;
}
public void Disconnect()
{
if (_client.Connected)
{
_client.Disconnect(true);
}
}
public byte[] Receive()
{
if (!_client.Connected)
{
return null;
}
var buffer = new List<byte>();
while (_client.Available > 0)
{
var bytes = new byte[1];
var byteCounter = _client.Receive(bytes, bytes.Length, SocketFlags.None);
var isReceive = byteCounter == 1;
if (isReceive)
{
buffer.Add(bytes[0]);
}
}
return buffer.ToArray();
}
public void Send(byte[] buffer)
{
if (_client.Connected)
{
_client.Send(buffer);
}
}
public void Dispose() => _client?.Dispose();
private readonly Socket _client;
public TcpCommunication() => _client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
```
##### Client
```C#
internal static class Program
{
private static void Main(string[] args)
{
var data = Encoding.UTF8.GetBytes("this is data");
TestMethod(new FileCommunication(), @"C:\TestFile\QOO.txt", data);
Console.WriteLine("==========================");
// connect and send data to echo server and receive it from echo
TestMethod(new TcpCommunication(),"127.0.0.1:7",data);
Console.ReadLine();
}
private static void TestMethod(ICommunication adapter, string path, byte[] data)
{
adapter.Connect(path);
adapter.Send(data);
var receiveData = adapter.Receive();
adapter.Disconnect();
Console.WriteLine(Encoding.UTF8.GetString(receiveData));
}
}
```
##### 輸出結果
```
this is data
==========================
this is data
```
- 提供 ICommunication 介面給客戶端使用, 未來客戶端可以透過多型的方式切換功能.(e.g. 透過 IOC 取得某個型別的物件)
## 總結
- 將一個類別的介面, 轉換成另一個介面以供客戶使用. Adapter 讓原本介面不相容的類別可以合作.
- 實作上跟 Facade 很像, 但重視的地方不一樣. Adapter 重視的是透過新增一個中間層. **"這個中間層是否可以讓兩個接口不同的介面或類別能夠偕同運作."**
- Client 和 Adaptee 彼此之間並不認識彼此. Client 呼叫 Adapter 的方法, 以及接收其結果. 但 Client 並不需要認識實際執行的 Adaptee.
- 遵守單一職責原則(Single Responsibility Principle). Client 端程式能抽離介面需求或是資料轉換相關的邏輯到 Adapter.
- 遵守開放封閉原則(Open-Closed Principle). 若有新的需求, 可以產生新的 Adapter 類別對應, Client 端程式不需要修改. 因為 Client 認識的是 ITarge 這個介面.
- 橋接模式和轉接器模式(Adapter)的 UML 很像, 轉接器模式通常在開發前期設計類別的時候使用, 因為其不希望程式設計師設計出抽象和實作混用的類別(不方便未來擴充), 而轉接器模式則通常在已有的程式中使用, 其目標是讓相互不兼容的類別能夠很好的合作.
- Adapter 模式會改變現存物件對外的介面, 而 Decorator 模式會在不改變對外界面的前提之下試著附加一些功能.
- Adapter 為 Adaptee(已存在類別)提供不同的對外界面, 與之不同的是 Proxy 仍然提供相同的介面.
- Facade 為了已存在的類別定義新的介面/接口/Facade, 反之 Adapter 試著讓已存在的介面/接口能再被利用.
- Adapter 經常只包一個類別在其內部使用, 但 Facade 常常是包整個子系統(複數類別).
- Adapter 會增加新的 Class/Interface, 過度使用可能會導致程式的整體複雜性提升(增加很多奇怪的類別). 有時候, 若是簡單修改原來的程式就可以解決需求, 那麼就不需要使用 Adapter 模式.
## 參考
[Adapter](https://refactoring.guru/design-patterns/adapter)
[轉接器模式 (Adapter Pattern)](http://corrupt003-design-pattern.blogspot.com/2016/07/adapter-pattern.html)
[轉接器模式(Adapter Pattern)](https://dotblogs.com.tw/pin0513/2010/05/30/15497)
[[Design Pattern] Adapter 配接器模式](https://ithelp.ithome.com.tw/articles/10219666)
[Adapter Pattern](https://www.geeksforgeeks.org/adapter-pattern/)
[Programming Patterns Overview Adapter]()
---
###### Thank you!
You can find me on
- [GitHub](https://github.com/s0920832252)
- [Facebook](https://www.facebook.com/fourtune.chen)
若有謬誤 , 煩請告知 , 新手發帖請多包涵
# :100: :muscle: :tada: :sheep:
<iframe src="https://skilltree.my/c67b0d8a-9b69-47ce-a50d-c3fc60090493/promotion?w=250" width="250" style="border:none"></iframe>