iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
自我挑戰組

一杯咖啡的設計課:30 天 Design Pattern 的自我修煉系列 第 9

Day 9|Factory Method(工廠方法) 將物件的誕生交給子類別

  • 分享至 

  • xImage
  •  

今天開始學習 GoF 設計模式三大分類之一 Creational Pattern 創建型模式

等等等,什麼時候跑出來 GoF 了,這是什麼? 創建模式又是什麼?

GoF 就是所謂的 Gang of Four(四人幫) 的縮寫,就是四位大神
由 1994 年出版的 Design Patterns: Elements of Reusable Object-Oriented Software
去定義 23 個經典設計模式,而分為三大類,也可以說是 GoF Pattern

  1. 創建型(Creational): 處理物件怎麼被生出來
    Factory Method、Abstract Factory、Builder、Singleton、Prototype
  2. 結構型(Structural): 怎麼把物件包起來變成更大的結構
    Adapter、Decorator、Facade、Proxy、Composite、Bridge、Flyweight
  3. 行為型(Behavioral): 處理物件之間怎麼溝通協作
    Strategy、Observer、Command、Template Method、State、Chain of Responsibility、Mediator、Iterator、Visitor、Interpreter、Memento

哇好不容易撐完前面的 SOLID,還有23種要學,突然蹦23個英文單字想搞死我是不是? 不是不是

其實 SOLID 跟 GoF 是不同的知識

SOLID 是評估「怎樣算好設計、什麼時候該重構」,但本身不會具體告訴你該怎麼寫 code
GoF Pattern 則是「遇到相關問題時,已經有人驗證過的具體解法」

所以聰明的你可能會想到,有些 Pattern 剛好是照著 SOLID 的精神做出來的,沒有錯!

但也不是每個模式都這樣,用歪了反而會壞事,像 Singleton 用不好,反而會讓一個類別管太多事、又綁死在固定的實作上

所以接下來我們繼續漫長且有趣的旅程吧(笑),來學習第一個創建型模式: Factory Method(工廠方法)

業界常講的「工廠模式」其實混著三種概念,但只有 2、3 是 GoF 正式收錄的

  1. Simple Factory (簡單工廠)
  2. Factory Method (工廠方法)
  3. Abstract Factory (抽象工廠)

所以在講工廠方法之前,我們先來聊聊簡單工廠,對我來說也是新手村的入門

雖然沒有被收錄,但應該歸類為創建模式的一種

什麼是簡單工廠?

簡單工廠模式 將「物件的建立」封裝在一個工廠類別中,客戶端無需直接使用 new 關鍵字去實例化物件,而是透過工廠方法來獲得所需的物件

解決了什麼問題?

  1. 降低耦合度:客戶端不需要了解具體產品類別的細節,只需向工廠請求物件即可
  2. 集中管理創建邏輯:當產品的創建過程發生改變時,只需要修改工廠內部的邏輯,而無需動及客戶端程式碼

那我們來回到故事來看例子吧

新的一週,柴咖啡店開店了,菜單掛回去,拿鐵、美式

阿柴翻開小黃留下的程式碼,來看看點餐的邏輯
https://ithelp.ithome.com.tw/upload/images/20260907/20183470fWygvMCRfD.jpg

// 建立 interface
public interface IDrink
{
    string Name { get; }
    int Price { get; }
}

// 建立物件邏輯,去實作 interface
public class Latte : IDrink
{
    public string Name => "拿鐵";
    public int Price => 90;
}

public class Americano : IDrink
{
    public string Name => "美式";
    public int Price => 70;
}

// 建立 Factory -> 實作相同介面 -> 丟入參數 type -> 回傳建立物件
public class DrinkFactory
{
    public IDrink CreateDrink(string type)
    //使用者不需要知道物件的建立細節,只要傳入對應參數 (type) 就能拿到需要的物件
    {
        if (type == "latte") // 我要拿鐵
        {
            return new Latte(); // 就拿到拿鐵
        }
        else if (type == "americano")
        {
            return new Americano();
        }
        else
        {
            throw new Exception("沒有這個飲料");
        }
    }
}

小黃把產品拆成 IDrink 介面 + 兩個實作類別,生產邏輯也集中放進一個專門的 DrinkFactory去管理

這就是業界常聽到的簡單工廠(Simple Factory),也是常見的入門寫法:一個工廠類別,裡面用 if-else / switch 依參數決定要生產哪一種產品

阿柴這幾天複習完 SOLID,默默地盯著這段程式碼看,腦中浮現 Day 4 開放封閉原則的那句話:

「對擴充開放,對修改封閉」

小黃把產品拆開、生產邏輯也集中管理,方向是對的

這時候小黑提了需求: 我們再賣「燕麥拿鐵好了!」,但阿柴知道,當新增飲料類別時,也會影響到 DrinkFactory 的變更

若以 SOLID 來看,這個動作是違反了 OCP 的原則

短期內看起來或許沒關係,但假如此方法會一直長:三種變五種,五種變十種,每次改動都要在同一個方法裡動刀,而改壞原有邏輯的機率也會往上疊

然而 DrinkFactory 也不會只被一個地方呼叫

點餐、結帳、庫存扣款,都可能各自需要知道「對應到哪一種飲料」,如果沒有一個共用的地方查,這種字串比對式的判斷,就會在程式碼各處各自重複一份,改一次要抓好幾個地方,漏改一個就會噴錯

Factory Method 工廠方法上場

那我們提升一些複雜度,換取低風險,工廠方法要處理的問題很單純

定義一個建立物件的介面,但讓子類別決定要實例化哪一個具體類別,將物件的創建延遲到子類別處理

小黃留下的 IDrinkLatteAmericano 產品類別不用動

阿柴要動的只有 DrinkStore,原本一個類別用 if-else 生產全部產品,現在改用一個抽象的工廠基底類別,每種產品各自有自己的工廠子類

工廠方法的實現方式很多,我們這裡使用 abstract 去示範

//  抽象工廠基底類別(Creator): 飲料工廠
public abstract class DrinkStore
{
    // 1. 定義合約:子類別「保證」會實作這個方法並回傳 IDrink
    public abstract IDrink CreateDrink(); // 製作飲料

    // 2. 具體流程:父類別先寫好流程骨架
    public IDrink OrderDrink() // 點飲料
    {
        // 雖然父類別現在不知道 CreateDrink 會做什麼,但因為有上面 1. 的合約,父類別「確定」執行時一定能拿到某個 IDrink
        IDrink drink = CreateDrink(); // 有人呼叫點飲料() 就會製作飲料()

        Console.WriteLine($"--- 柴咖啡點餐系統 ---");
        Console.WriteLine($"正在製作:{drink.Name}");
        Console.WriteLine($"收取金額:{drink.Price} 元");
        Console.WriteLine($"【{drink.Name}】製作完成,已交付給客人!\n");

        return drink;
    }
}

// 2. 具體工廠(Concrete Creators):每種飲料繼承飲料工廠
public class LatteStore : DrinkStore
{
    public override IDrink CreateDrink() => new Latte();
}

public class AmericanoStore : DrinkStore
{
    public override IDrink CreateDrink() => new Americano();
}

那現在要新增「燕麥拿鐵」,阿柴該怎麼做? 他只需要擴充:

// 1. 新增燕麥拿鐵類別,跟上述拿鐵一樣
public class OatMilkLatte : IDrink
{
    public string Name => "燕麥拿鐵";
    public int Price => 110;
}

// 2. 新增燕麥拿鐵工廠
public class OatMilkLatteStore : DrinkStore
{
    public override IDrink CreateDrink() => new OatMilkLatte();
}

那完整的的流程會是什麼?

// ==========================================
// 1. 產品層(Product)
// ==========================================
public interface IDrink
{
    string Name { get; }
    int Price { get; }
}

public class Latte : IDrink
{
    public string Name => "拿鐵";
    public int Price => 90;
}

public class Americano : IDrink
{
    public string Name => "美式";
    public int Price => 70;
}

public class OatMilkLatte : IDrink
{
    public string Name => "燕麥拿鐵";
    public int Price => 110;
}

// ==========================================
// 2. 工廠方法層(Creator & Concrete Creators)
// ==========================================
public abstract class DrinkStore
{
    // Factory Method:交由子類別決定實際建立哪種飲料
    public abstract IDrink CreateDrink();

    // 飲料製作的核心流程
    public IDrink OrderDrink()
    {
        IDrink drink = CreateDrink();

        Console.WriteLine($"--- 柴咖啡點餐系統 ---");
        Console.WriteLine($"正在製作:{drink.Name}");
        Console.WriteLine($"收取金額:{drink.Price} 元");
        Console.WriteLine($"【{drink.Name}】製作完成,已交付給客人!\n");

        return drink;
    }
}

public class LatteStore : DrinkStore
{
    public override IDrink CreateDrink() => new Latte();
}

public class AmericanoStore : DrinkStore
{
    public override IDrink CreateDrink() => new Americano();
}

public class OatMilkLatteStore : DrinkStore
{
    public override IDrink CreateDrink() => new OatMilkLatte();
}

// ==========================================
// 3. 業務服務層(Client / Consumer)
// ==========================================
public class Cashier
{
    // 收銀機只認識抽象的 DrinkStore,不依賴任何具體飲料或具體工廠類別
    public void ProcessCustomerOrder(DrinkStore store)
    {
        IDrink drink = store.OrderDrink(); // 還記得嗎,OrderDrink 裡面就有 createDrink 了
        // 這裡可以處理其他通用流程,例如印發票、累計營業額等等
    }
}

// ==========================================
// 4. 程式進入點 Entry Point
// ==========================================
public class Program
{
    public static void Main()
    {
        Cashier cashier = new Cashier();

        // 情境一:客人 A 點了燕麥拿鐵
        // 這裡省略前端傳過來並透過 Controller 下達指令,但因這樣寫就會變複雜且長,主要專注於在 factory method 的用法
        DrinkStore oatLatteStore = new OatMilkLatteStore();
        cashier.ProcessCustomerOrder(oatLatteStore);

        // 情境二:客人 B 點了美式
        DrinkStore americanoStore = new AmericanoStore();
        cashier.ProcessCustomerOrder(americanoStore);
    }
}

這段整合也符合所謂的關注點分離

Cashier(業務層):只知道呼叫 store.OrderDrink(),未來就算新增 100 種飲料,Cashier 也不需要修改或增加 if-else

OatMilkLatteStore(工廠層):只專注在「如何生出燕麥拿鐵」

Main(進入點):負責在最外層根據使用者的選擇做物件組裝與注入

產出程式碼時,Factory Method 能拿來檢查什麼

如果阿柴把「幫我寫一個依照飲料種類建立訂單的功能」丟給 AI,時常會見到兩種方式

第一種,AI 直接生出小黃當年那種一大包 if-else,能動,語法正確,但因為自己也不懂,所以也不會考慮到「這串邏輯以後還會不會變」

第二種,AI 反而過度熱心,一開口就是 IDrinkStoreAbstractDrinkStore,外加一個 DrinkStoreRegistryDrinkStoreFactory,三種飲料包出七八個類別,也會造成設計過頭,因為它不清楚使用者實際的需求

所以我們在查閱程式碼時,一樣先問這自己:現在真的有第二種、第三種產品在排隊,還是只是先包好以防萬一?

如果連小黑都還沒說要加新飲料,AI 卻已經先幫你切好五層抽象、外加一個註冊機制,那多半是它在幫你想一個還沒發生的需求

也是我們先前所提到的 YAGNI(You Aren't Gonna Need It)

AI 可以給出「Factory Method 該長什麼樣子」的語法,甚至能一次生出 DrinkStoreCashierProgram 這一整組結構,但不了解「產品有沒有這個需求」,最終判斷還是要交由給自己,畢竟需要有人背...


明天繼續繼續加油,業績越來越好了,小黑想推套餐,早餐套餐跟下午茶套餐各自要配一整組不同的品項,一次生成,這時候就輪到 Abstract Factory 上場了

如果看不懂我寫的,可以看看延伸的資料,希望可以幫助到你

Refactoring Guru - Factory Method

20. 簡單工廠模式 - Simple Factory

簡單工廠模式 SimpleFactor

初探設計模式 - 工廠方法模式 (Factory Method Patter


上一篇
Day 8|先別急著 SOLID,用 KISS、YAGNI、DRY 幫架構踩煞車
下一篇
Day 10|Abstract Factory(抽象工廠) 早餐與下午茶,一次生成組合
系列文
一杯咖啡的設計課:30 天 Design Pattern 的自我修煉26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言