Skip to main content

Command Palette

Search for a command to run...

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

Updated
2 min read

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

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

這就是 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 請求 ...
    }
}

問題來了:

  1. 原先系統(呼叫端)只知道 send($title, $message)

  2. LINE SDK 需要 pushMessageRequsetxLineRetryKey

  3. 參數型別不合 (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 內部的 LocalAdapterAwsS3Adapter。它們把原生的指令或 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 策略

從Rookie到Junior,一個後端成長的30堂課

Part 8 of 31

“從 Rookie 到 Junior:一個後端成長的 30 堂課” 是一套專為後端新手所設計的成長型技術系列文章。內容以實務為導向,逐步拆解後端工程的核心能力,包括程式語言基礎、架構思維、框架運作原理、業務邏輯設計、資料庫操作、以及常見的開發模式。 本系列的目標是協助讀者從零散的學習堆疊,建立成體系的後端知識框架。讀者能夠理解各語言背後不變的工程思維與設計原則。這套內容旨在讓學習者從「會寫程式」進入「能理解系統設計」的階段,逐步具備勝任 Junior Backend Engineer 的能力。

Up next

Lesson 19: 工廠模式 與 建造者模式

工廠模式 在 SOLID 的課程後,我們已經知道「依賴注入」的重要性:我們不應該在類別內部直接 new 依賴的物件。 但問題來了:「那到底誰負責 new?」 總得有人負責把物件生出來吧?如果到處散落著 new, 當需求變更時,我們還是要改一堆地方。 在Lesson 17 開放封閉 (OCP) 的時候我們有說道:利用策略模式來進行解偶,並利用”工廠模式”來決定策略的選擇(實作 實例化物件),但當時對於工廠模式並沒有去詳細說明,這裡我們一起來看看工廠模式的相關細節。 核心概念:為什麼需要「工廠」 ...

More from this blog

Lesson 26 : 系統韌性的守護者-限流、熔斷與背壓的設計模式

當這幾個名詞出現後,代表我們進到了一個高併發/大流量的系統了。在這個章節中,我們一起來看看如何透過一些方式來避免高併發導致我們的系統crash掉。 限流(Rate Limiting) 相信大家對這個名詞並不陌生,限流其實就是字面上的含意,限制流量。 限流的目的是保護「接收方」,確保系統不會因為瞬間的高併發請求而癱瘓。 限流通常發生在 API Gateway 或服務的最前端。它像是一個夜店門口的保全

Mar 26, 20262 min read

Lesson 25: 淺談 單體架構、微服務架構與單/多租戶架構

過去我們討論了 要把程式寫在哪、程式要怎麼拆的題目,接下來我們來看看「如何服務不同客戶」,這些架構反映了軟體開發在擴充性與複雜度之間的權衡。 軟體架構深度解析:從系統拆分到商業規模化 在軟體工程的演進中,架構的選擇往往是在「開發效率」、「系統擴充性」與「營運成本」之間尋求平衡。我們可以從兩個核心維度來觀察這些架構:系統如何運行(單體 vs. 分散式) 以及 如何服務客戶(單租戶 vs. 多租戶)。

Mar 26, 20262 min read

Lesson 24: 資料庫擴展術-讀寫分離、複寫機制與快取一致性挑戰

為什麼要讀寫分離? 大多數的 Web 應用都是 「讀多寫少」(例如:看文的人多,發文的人少,Heavy Read System)。當所有的請求都塞給同一台資料庫時,磁碟 I/O 和連線數會成為瓶頸。 Master (主庫): 負責寫入 (Insert/Update/Delete),確保數據一致性。 Slave (從庫): 負責讀取 (Select),可以有多個從庫來分擔讀取壓力。 為什麼讀

Mar 25, 20262 min read

面試經驗談 2025-2026

從2025年3月開始,我陸陸續續參與了從新創到上市櫃公司的Senior - Tech Lead的相關面試,其中有不乏 尊重面試者、展現高度專業的企業(公司),當然也有遇到幾場面試鬼故事,這篇文章主要分享我對於軟體工程師面試的方向分享,以及部分鬼故事,以此警惕自己不要成為這樣的面試官。 AI的洪流,改變了SWE的生態 LLM的發展確確實實的影響到了軟體工程師的生態。 過去受限於算力與資料規模,深度學

Mar 23, 20262 min read

Lesson 23: 系統的緩衝區-Queue 佇列與非同步處理 (Asynchronous)

佇列 佇列的實作工具非常多,舉凡AWS SQS、RabbitMQ、Kafka…等。 佇列的特性,其實是一個非常強大的系統緩衝區,應用層面非常廣。 什麼是佇列? 佇列可以想像成,在既有流程中外,有另一個”水管”,來連接原有的資料流(或邏輯過程),其中 呼叫方將資料 推(Push)到水管中,接受方(監聽) 從水管中將資料拉(Pull)出處理 為什麼佇列是「強大的緩衝區」? 在同步處理中,系統像是一

Mar 23, 20262 min read

Bennett's Tech Blog | 後端架構、系統設計

32 posts

來自台灣的軟體工程師,相信軟體可以改變世界