# Lesson 20：適配器模式 - 讓不相容的介面也能合作

這堂課我們正式進入 - 結構型模式 的領域。

如果不把這「創建型」模式（工廠、建造者）搞定，我們很難有東西可以「結構化」。但現在我們已經學會怎麼優雅地產生物件了，接下來會遇到的問題通常是：「這個新來的物件，跟我的舊系統插頭不合怎麼辦？」

這就是 Adapter Pattern 的主場。

# 核心概念：轉接頭

從台灣帶了筆電（三孔插頭）出國旅行。

牆上的插座 (Client 期待的介面)：可能是兩孔圓形，或兩孔扁形。

筆電插頭 (Adaptee 被適配者)：兩孔扁型，一孔圓形。

問題：插不進去，無法供電。

解決方案：不可能把牆壁打掉重練，也不會把筆電插頭剪掉。我們會買一個 「萬用轉接頭 」。

## 程式碼中的定義

適配器模式將一個類別的介面，轉換成客戶端期待的另一個介面。它讓原本因介面不相容而無法一起工作的類別，可以協同運作。

# 實戰場景：第三方串接的惡夢

假設原本的系統支援「站內信」通知。

### Step 1: 既有的標準介面

系統依賴這個 `Interface` 來運作：

```php
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`，但它的方法簽章可能長這樣：

```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 請求 ...
    }
}
```

問題來了：

1. 原先系統（呼叫端）只知道 `send($title, $message)`。
    
2. LINE SDK 需要 `pushMessageRequset`、 `xLineRetryKey`。
    
3. 參數型別不合 (String vs Request)、參數意義不同，根本接不起來。
    

### Step 3: 製作適配器 (Adapter)

我們建立 `LineAdapter`，在內部處理掉那些煩人的物件組裝工作。

```php
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: 呼叫端完全沒有變動

```php
// 業務邏輯 (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 模式是解決「第三方整合」的神器。

1. 何時使用？ 想用一個既有的類別，但它的介面跟系統不合時。
    
2. 好處是什麼？ 讓 Client 端程式碼保持單純，不需要為了配合外部套件而改來改去。
    
3. 關鍵心法： Adapter 是用來「擦屁股」或「翻譯」的，它不應該包含太多的業務邏輯。
    
    1. NO！Adapter 裡計算金額、判斷權限
        
    2. NO！ Adapter 裡寫 retry / fallback 策略
