Skip to main content

Command Palette

Search for a command to run...

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

Updated
5 min read

工廠模式

在 SOLID 的課程後,我們已經知道「依賴注入」的重要性:我們不應該在類別內部直接 new 依賴的物件。

但問題來了:「那到底誰負責 new?」 總得有人負責把物件生出來吧?如果到處散落著 new, 當需求變更時,我們還是要改一堆地方。

在Lesson 17 開放封閉 (OCP) 的時候我們有說道:利用策略模式來進行解偶,並利用”工廠模式”來決定策略的選擇(實作 實例化物件),但當時對於工廠模式並沒有去詳細說明,這裡我們一起來看看工廠模式的相關細節。

核心概念:為什麼需要「工廠」

  • 沒有工廠模式

    想吃漢堡,必須自己走進廚房,自己拿麵包、煎肉排、切生菜、組裝。Client需要知道所有製作細節。

  • 有工廠模式

    走到櫃檯說:「我要一個大麥克」。過幾分鐘,漢堡就出來了。Client完全不需要知道漢堡是怎麼做的,只關心拿到的是不是漢堡。

工廠模式所要解決的就是 解決的是 「我要 new 誰?」這個問題,更精確地說,工廠模式不是為了消滅 new,而是為了集中並隔離「建立物件的決策邏輯」,避免這些決策污染核心業務流程。

實際範例

不好的範例

假設我們有一個「通知服務」,可以發送 Email 或 SMS。

class NotificationService
{
    public function send(string $type, string $message)
    {
        $notifier = null;

        // 違反 OCP:每次加一種新通知,都要來改這個 Service
        // 違反 SRP:Service 應該只管「發送」,不該管「怎麼 new 物件」
        if ($type === 'email') {
            $notifier = new EmailNotifier();
        } elseif ($type === 'sms') {
            $notifier = new SmsNotifier();
        }

        $notifier->send($message);
    }
}

第一階段調整:簡單工廠 (Simple Factory)

這是最常見的重構手法,雖然嚴格來說它不算標準 GoF 設計模式,但非常實用。 我們把實例化的工作,丟給一個靜態方法去處理。

class NotifierFactory
{
    // 靜態方法,負責生產物件
    // 回傳的是介面 (Interface),這符合 DIP (依賴反轉)
    public static function create(string $type): NotifierInterface
    {
        switch ($type) {
            case 'email':
                return new EmailNotifier(); // 這裡可能包含複雜的 SMTP 設定
            case 'sms':
                return new SmsNotifier();
            default:
                throw new Exception("不支援的通知類型");
        }
    }
}
//Service就變乾淨
class NotificationService
{
    public function send(string $type, string $message)
    {
        // ✅ Service 不再關心物件怎麼產生的
        // 就像跟工廠下訂單一樣
        $notifier = NotifierFactory::create($type);
        $notifier->send($message);
    }
}

/**
優點:Service 層與「具體通知實作與建立細節」解耦,符合 SRP。 
缺點:如果明天要加 "Line" 通知,還是要回去改 NotifierFactory 的 switch,微幅違反 OCP。
但在中小型專案中,這是可接受的妥協。
*/

第二階段調整:工廠方法 (Factory Method)

這才是正宗的 GoF 設計模式。當系統非常複雜,或者開發的是框架/Library,希望使用者擴充功能時完全不用改你的原始碼,就會用到這個。

核心定義:「定義一個建立物件的介面,但讓子類別決定要實例化哪一個類別。」

架構設計

  1. 產品 (Product):NotifierInterface

  2. 工廠 (Creator):NotifierFactory (介面或抽象類別)

我們不再用一個巨大的 switch,而是「一種產品,配一個專屬工廠」。

注:實務上 Factory Method 不一定是「一個產品一個工廠」,而是「建立流程由子類決定」;本例採用一對一設計,是為了讓責任邊界最清楚。

// 1. 定義工廠合約
interface NotifierFactory
{
    public function createNotifier(): NotifierInterface;
}

// 2. 實作:Email 專屬工廠
class EmailFactory implements NotifierFactory
{
    public function createNotifier(): NotifierInterface
    {
        // 這裡可以處理很複雜的建構邏輯
        return new EmailNotifier('smtp.gmail.com', 587);
    }
}

// 3. 實作:SMS 專屬工廠
class SmsFactory implements NotifierFactory
{
    public function createNotifier(): NotifierInterface
    {
        return new SmsNotifier();
    }
}
class NotificationService
{
    private $factory;

    // 依賴注入:給我一個工廠,隨便哪個工廠都行
    public function __construct(NotifierFactory $factory) 
    {
        $this->factory = $factory;
    }

    public function send(string $msg)
    {
        // 我不知道會產生什麼,反正工廠會給我一個能用的 Notifier
        $notifier = $this->factory->createNotifier();
        $notifier->send($msg);
    }
}

// 使用時:由外部決定要注入哪個工廠
$service = new NotificationService(new EmailFactory());

/**
優點 (OCP):如果明天要加 "Line",只需要新增 LineNotifier 和 LineFactory。
舊的 Service 和舊的 Factory 完全不用改!
*/

Laravel 實戰場景

在 Laravel 中,為了符合 OCP 而實作成工廠模式的範例無處不在,通常被包裝成 Manager 的形式。

場景 1:驅動 (Drivers)

當你切換 Database 或 Storage 時:

  • Storage::disk('s3')

  • Storage::disk('local')

Laravel 內部有一個 FilesystemManager (這就是一個超級工廠)。它根據傳入的字串,去讀取 config/filesystems.php,然後 new 出對應的 S3 Adapter 或 Local Adapter。

注:此範例在OCP章節中亦有提及。

場景 2:Auth Guards

Auth::guard('web') vs Auth::guard('api')。 這也是工廠模式。它根據設定產生不同的驗證物件 (SessionGuard vs TokenGuard)。

觀念重置

是否覺得鬼打牆?

回顧我們最近的旅程:

  • Lesson 14: 物件導向的靈魂:介面 (Interface) 與抽象類別 (Abstract Class)

  • Lesson 15: 解耦的關鍵:依賴注入 (Dependency Injection) 與 IoC Container

  • Lesson 17: SOLID 實戰篇 (2):開放封閉 (OCP) - 擁抱變化而不修改舊碼

  • Lesson 18: SOLID 實戰篇 (3):完結LSP、ISP 與 魔王DIP

不斷的出現介面(Interface),在各種情境下的例子也都是不斷地用Interface來做舉例,這麼多篇幅只為了說明Interface嗎?

重新審視 SOLID:介面的雙重身份

我們先把 SOLID 分類成 「利用介面解偶 (SRP, OCP, DIP)」 與 「定義介面邊界 (LSP, ISP)」。

第一組:目的與手段 (SRP, OCP, DIP)

  • 本質:這三者都在告訴我們 「如何切分系統」 以及 「如何處理依賴」

  • 手段:確實都是靠 實作 Interface

    • SRP:透過 Interface 把不同的職責隔開,避免一個類別包山包海。

    • OCP:透過 Interface 讓舊程式碼(Client)對新功能(New Implementation)封閉,但對擴充開放。

    • DIP:透過 Interface 讓高層模組不依賴低層模組,而是雙方都依賴抽象。

第二組:品質與契約 (LSP, ISP)

  • 本質:這兩者是在規範 「Interface 應該長什麼樣子」 以及 「實作與介面的契約關係」

  • 手段:定義 Scope (範圍)Contract (契約)

    • ISP:Interface 不要太肥(範圍),要切得夠細,讓 Client 不需要依賴它不需要的方法。

    • LSP:實作 Interface 的人不能「掛羊頭賣狗肉」,子類別必須能完美替換父介面,不能丟出預期外的 Exception 或改變行為本質。

結論

Interface在不同情境下的運用是為了解決SOLID不同的問題。

Interface只是一個工具但在 SOLID 的五個原則中,我們是為了達成不同的戰略目的而去使用這個工具,Interface可以滿足這五個維度問題的工具,但不是用Interface就完全可以避免這五個問題,Interface 只是語法上的工具,它無法保證「設計」是好的。 如果介面設計得很爛,不但解決不了問題,還會變成「為了介面而介面」的 Boilerplate code。

實戰演練:Strategy 與 Factory 的連鎖反應

在真實架構中,我們會發現一個有趣的現象:「解決 OCP 通常用 Strategy,但為了把 Strategy 再解耦所以用 Factory,而Factory 實際上也是定義 Interface 後再做一次」。

這個過程其實就是 「推延依賴 (Deferring Dependency)」 的極致表現。

第一層:為了 OCP,引入 Strategy

我們不想在 OrderService 裡寫死 if (type == 'VIP'),所以我們定義了 DiscountStrategy 介面。

  • 結果OrderService 依賴於 DiscountStrategy (Interface),商業邏輯解偶了

  • 殘留問題:但是在某個地方(可能是 Controller),還是得寫 new VipDiscountStrategy()。「創建」的動作還是違反 OCP,因為加新策略時,創建的地方要改。

第二層:為了創建物件的 OCP,引入 Factory

為了不讓 Controller 髒掉,我們把 new 的動作丟給 DiscountFactory

  • 結果:Controller 也不用管怎麼 new 了,它只管呼叫 Factory。

  • 殘留問題:現在 DiscountFactory 變成了那個「違反 OCP」的地方(因為 Factory 裡面的 switch/case 還是要改)。

第三層:Factory 也是 Interface?

如果連 Factory 都要解偶(例如我們要換不同的 Factory 實作),我們就會定義一個 DiscountFactoryInterface

  • 邏輯:用一層 Interface 包裝了「行為」(Strategy),再用一層 Interface 包裝了「行為的產生」(Factory)。

誰來終結這個無限套娃?

如果依照這個邏輯,Factory 上面還可以有 FactoryProducer,Producer 上面還可以有 Builder... 這會變成無限的 Interface 實作。

在現代後端開發,這個「Factory 的 Factory」最終通常由 Dependency Injection Container (IoC Container) 來終結。

  • 手動工廠 (Manual Factory):我們自己寫 class PaymentFactory

  • 終極工廠 (DI Container):Laravel 的 Service Container (app()) 本身就是一個巨型的、通用的 Factory。

我們其實一直在做 「控制反轉 (IoC)」。

  1. Strategy:將「演算法的執行權」反轉(交給介面)。

  2. Factory:將「物件的創建權」反轉(交給工廠)。

  3. DI Container:將「依賴的組裝權」徹底反轉(交給框架配置)。

建造者模式

建造者模式所要解決的是:這個物件參數太多、太複雜,我要怎麼new?

是否看過這樣的Code

// 這種建構子稱為「伸縮望遠鏡 (Telescoping Constructor)」
// 參數多到你根本記不住第 5 個 false 代表什麼意思
$request = new HttpRequest(
    '<https://api.example.com>', 
    'POST', 
    ['Content-Type' => 'application/json'], 
    '{"data": 123}', 
    30,   // timeout
    true, // isAsync
    false // verifySsl
);

這就是 Builder 模式要消滅的敵人。

核心概念:組裝大於製造

建造者模式將一個複雜物件的「建構過程」與它的「表示」分離,使得同樣的建構過程可以建立不同的表示。

走進Subway 跟店員說「我要一份潛艇堡」。

  1. 選麵包(蜂蜜燕麥)

  2. 選肉(火雞胸肉)

  3. 選醬料(西南醬)

  4. 加菜(不要洋蔥)

  • 最後店員把組裝好的潛艇堡交給客人。

在現代後端開發中,我們最常使用的是 Builder 的變體:流暢介面 (Fluent Interface) / 方法鏈 (Method Chaining)。

實戰演練:拯救 HTTP Request

Step 1: 建立 Builder 類別

我們不直接 new HttpRequest,而是建立一個 HttpRequestBuilder 來暫存使用者的設定。

class HttpRequestBuilder
{
    // 設定預設值
    private string $method = 'GET';
    private string $url = '';
    private array $headers = [];
    private string $body = '';
    private int $timeout = 30;

    // 1. 設定 URL
    public function setUrl(string $url): self
    {
        $this->url = $url;
        return $this; // 👈 關鍵!回傳 $this 才能繼續串接 (Method Chaining)
    }

    // 2. 設定 Method
    public function setMethod(string $method): self
    {
        $this->method = $method;
        return $this;
    }

    // 3. 設定 Header (可以多次呼叫,慢慢加)
    public function addHeader(string $key, string $value): self
    {
        $this->headers[$key] = $value;
        return $this;
    }

    // ... 其他 setter 省略 ...

    // 4. 最終步驟:建造!(Build)
    public function build(): HttpRequest
    {
        // 這裡可以做最後的驗證 (Validation)
        if (empty($this->url)) {
            throw new Exception("URL cannot be empty");
        }

        // 把蒐集好的參數,一次傳給 HttpRequest 的建構子
        return new HttpRequest(
            $this->url,
            $this->method,
            $this->headers,
            $this->body,
            $this->timeout
        );
    }
}

Step 2: 呼叫端

$request = (new HttpRequestBuilder())
    ->setUrl('<https://api.example.com>')
    ->setMethod('POST')
    ->addHeader('Content-Type', 'application/json')
    ->addHeader('Authorization', 'Bearer token123')
    ->build(); // 👈 直到這一刻,真正的物件才被產出來

優點:

  1. 可讀性極高:不用去猜第 3 個參數是什麼,方法名稱說明了一切。

  2. 順序無關:你可以先設 Header 再設 URL,沒差別。

  3. 靈活性:你可以根據邏輯動態決定要不要加某個 Header。

Laravel 中的實戰場景

大家可能沒意識到,其實Laravel開發者每天都在寫 Builder 模式。Laravel 的 Eloquent ORM 就是全世界最著名的 Builder 範例之一。

SQL Query Builder

$users = User::query()            // 1. 拿到 Builder 實例
    ->where('is_active', 1)       // 2. 設定條件 (相當於 setWhere)
    ->orderBy('created_at', 'desc') // 3. 設定排序 (相當於 setOrder)
    ->limit(10)                   // 4. 設定限制
    ->get();                      // 5. Build! (執行 SQL 並回傳結果)

如果沒有 Builder 模式,可能得這樣寫

// ❌ 假設的寫法:參數地獄
$users = new UserQuery(null, ['is_active' => 1], null, 'created_at', 'desc', 10);

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

Part 9 of 31

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

Up next

Lesson 18: SOLID 實戰篇 (3):完結LSP、ISP 與 魔王DIP

關於LSP 接下來進到L了,這裡的L指的就是里氏替換原則 (LSP , Liskov Substitution Principle),這是許多後端工程師覺得最「抽象」的一個原則,但它其實是判斷「繼承 (Inheritance) 是否被濫用」最重要的標準。 什麼是里式替換 (LSP)? 「子類別 (Subclass) 必須能夠替換掉它們的父類別 (Base Class),且程式的行為不會發生錯誤。」 簡單來說,如果程式碼依賴於一個父類別(或介面),那麼隨便塞一個該父類別的「子類別」進去,程式都...

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

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