Lesson 20:適配器模式 - 讓不相容的介面也能合作
這堂課我們正式進入 - 結構型模式 的領域。
如果不把這「創建型」模式(工廠、建造者)搞定,我們很難有東西可以「結構化」。但現在我們已經學會怎麼優雅地產生物件了,接下來會遇到的問題通常是:「這個新來的物件,跟我的舊系統插頭不合怎麼辦?」
這就是 Adapter Pattern 的主場。
核心概念:轉接頭
從台灣帶了筆電(三孔插頭)出國旅行。
牆上的插座 (Client 期待的介面):可能是兩孔圓形,或兩孔扁形。
筆電插頭 (Adaptee 被適配者):兩孔扁型,一孔圓形。
問題:插不進去,無法供電。
解決方案:不可能把牆壁打掉重練,也不會把筆電插頭剪掉。我們會買一個 「萬用轉接頭 」。
程式碼中的定義
適配器模式將一個類別的介面,轉換成客戶端期待的另一個介面。它讓原本因介面不相容而無法一起工作的類別,可以協同運作。
實戰場景:第三方串接的惡夢
假設原本的系統支援「站內信」通知。
Step 1: 既有的標準介面
系統依賴這個 Interface 來運作:
interface NotificationInterface
{
// 系統只認得 send 這個方法
public function send(string $title, string $message): void;
}
class InternalSystem implements NotificationInterface
{
public function send(string $title, string $message): void
{
echo "站內信發送: $title - $message";
}
}
Step 2: 新的需求與不相容的套件
PM說:「我們要串接 LINE!」 於是你找了一個第三方套件 line-bot-sdk-php,但它的方法簽章可能長這樣:
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Model\PushMessageRequest;
class MessagingApiApi
{
public function pushMessage(PushMessageRequest $pushMessageRequest, string $xLineRetryKey = null)
{
// ... SDK 內部實作,發送 HTTP 請求 ...
}
}
問題來了:
原先系統(呼叫端)只知道
send($title, $message)。LINE SDK 需要
pushMessageRequset、xLineRetryKey。參數型別不合 (String vs Request)、參數意義不同,根本接不起來。
Step 3: 製作適配器 (Adapter)
我們建立 LineAdapter,在內部處理掉那些煩人的物件組裝工作。
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Model\PushMessageRequest;
use LINE\Clients\MessagingApi\Model\TextMessage;
class LineAdapter implements NotificationInterface
{
private MessagingApiApi $lineApi;
private string $targetUserId;
public function __construct(MessagingApiApi $lineApi, string $targetUserId)
{
$this->lineApi = $lineApi;
$this->targetUserId = $targetUserId;
}
public function send(string $title, string $message): void
{
// Adapter 的核心價值:轉譯 (Translate)
// 把「簡單的字串」轉譯成「SDK 需要的複雜物件結構」
// 1. 先建立訊息物件
$textMessage = new TextMessage([
'type' => 'text',
'text' => "【$title】\\n$message"
]);
// 2. 再建立 Request 物件 (包裝 UserID 和 訊息)
$request = new PushMessageRequest([
'to' => $this->targetUserId,
'messages' => [$textMessage],
]);
// 3. 呼叫 SDK
$this->lineApi->pushMessage($request);
}
}
在這個例子中:
Client:pushNotification()
Target:NotificationInterface
Adaptee:LINE SDK 的 MessagingApiApi
Adapter:LineAdapter
Step 4: 呼叫端完全沒有變動
// 業務邏輯 (Controller 或 Service)
// 完全不需要 use LINE\Clients\.....
// 依然乾乾淨淨
function pushNotification(NotificationInterface $notifier) {
$notifier->send("訂單通知", "您購買的商品已出貨");
}
// -----------------------------------------
// 初始化 SDK
$client = new \GuzzleHttp\Client();
$config = new \LINE\Clients\MessagingApi\Configuration();
$config->setAccessToken('YOUR_ACCESS_TOKEN');
$lineApi = new MessagingApiApi($client, $config);
// 注入 Adapter
$adapter = new LineAdapter($lineApi, 'USER-12345678');
// 發送!
pushNotification($adapter);
結果:原本的 pushNotification 函式一行都不用改,就能支援 Line。這就是符合 OCP 的展現。
Laravel 中的應用
Adapter 模式是現代框架「可替換驅動 (Driver-based)」架構的核心。
Storage
這是最經典的例子。Laravel 的 Storage Facade 讓你感覺像在操作同一個東西,但底層其實是完全不同的 API。
Target 介面:
Illuminate\\Contracts\\Filesystem\\Filesystem(定義了put,get,delete...)Adaptee 1 (Local):PHP 原生的
file_put_contents,unlink。Adaptee 2 (S3):AWS SDK 的
$s3Client->putObject(...)。Adapter:Laravel 內部的
LocalAdapter和AwsS3Adapter。它們把原生的指令或 SDK 指令,翻譯成統一的put/get。
這就是為什麼你可以只改一行 .env 設定,就讓上傳功能從本機切換到 AWS S3。
Cache & Database
同理,Redis、Memcached、MySQL、PostgreSQL 雖然操作指令不同,但在框架層都被 Adapter 包裝成統一的介面。
進階 - Adapter vs Decorator
| 模式 | 適配器 (Adapter) | 裝飾器 (Decorator) |
| 目的 | 轉換介面,讓無法合作的變成可以合作 | 增強功能,在不改變介面的情況下加料 |
| 改變 | 改變了介面 (A → B) | 不改變介面 (A → A+) |
| 譬喻 | 轉接頭 (讓圓頭插進扁孔) | 手機殼 (讓手機變防摔,但還是手機) |
總結
Adapter 模式是解決「第三方整合」的神器。
何時使用? 想用一個既有的類別,但它的介面跟系統不合時。
好處是什麼? 讓 Client 端程式碼保持單純,不需要為了配合外部套件而改來改去。
關鍵心法: Adapter 是用來「擦屁股」或「翻譯」的,它不應該包含太多的業務邏輯。
NO!Adapter 裡計算金額、判斷權限
NO! Adapter 裡寫 retry / fallback 策略

