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

# 工廠模式

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

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

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

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

* 沒有工廠模式
    
    想吃漢堡，必須自己走進廚房，自己拿麵包、煎肉排、切生菜、組裝。Client需要知道所有製作細節。
    
* 有工廠模式
    
    走到櫃檯說：「我要一個大麥克」。過幾分鐘，漢堡就出來了。Client完全不需要知道漢堡是怎麼做的，只關心拿到的是不是漢堡。
    

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

## 實際範例

### 不好的範例

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

```php
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 設計模式，但非常實用。 我們把實例化的工作，丟給一個靜態方法去處理。

```php
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("不支援的通知類型");
        }
    }
}
```

```php
//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 不一定是「一個產品一個工廠」，而是「建立流程由子類決定」；本例採用一對一設計，是為了讓責任邊界最清楚。

```php
// 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();
    }
}
```

```php
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

```php
// 這種建構子稱為「伸縮望遠鏡 (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` 來暫存使用者的設定。

```php
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: 呼叫端

```php
$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

```php
$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 模式，可能得這樣寫

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