Lesson 19: 工廠模式 與 建造者模式
工廠模式
在 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,希望使用者擴充功能時完全不用改你的原始碼,就會用到這個。
核心定義:「定義一個建立物件的介面,但讓子類別決定要實例化哪一個類別。」
架構設計
產品 (Product):
NotifierInterface工廠 (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)」。
Strategy:將「演算法的執行權」反轉(交給介面)。
Factory:將「物件的創建權」反轉(交給工廠)。
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 跟店員說「我要一份潛艇堡」。
選麵包(蜂蜜燕麥)
選肉(火雞胸肉)
選醬料(西南醬)
加菜(不要洋蔥)
- 最後店員把組裝好的潛艇堡交給客人。
在現代後端開發中,我們最常使用的是 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(); // 👈 直到這一刻,真正的物件才被產出來
優點:
可讀性極高:不用去猜第 3 個參數是什麼,方法名稱說明了一切。
順序無關:你可以先設 Header 再設 URL,沒差別。
靈活性:你可以根據邏輯動態決定要不要加某個 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);

