今天開始學習 GoF 設計模式三大分類之一 Creational Pattern 創建型模式
等等等,什麼時候跑出來 GoF 了,這是什麼? 創建模式又是什麼?
GoF 就是所謂的 Gang of Four(四人幫) 的縮寫,就是四位大神
由 1994 年出版的 Design Patterns: Elements of Reusable Object-Oriented Software
去定義 23 個經典設計模式,而分為三大類,也可以說是 GoF Pattern
哇好不容易撐完前面的 SOLID,還有23種要學,突然蹦23個英文單字想搞死我是不是? 不是不是
其實 SOLID 跟 GoF 是不同的知識
SOLID 是評估「怎樣算好設計、什麼時候該重構」,但本身不會具體告訴你該怎麼寫 code
GoF Pattern 則是「遇到相關問題時,已經有人驗證過的具體解法」
所以聰明的你可能會想到,有些 Pattern 剛好是照著 SOLID 的精神做出來的,沒有錯!
但也不是每個模式都這樣,用歪了反而會壞事,像 Singleton 用不好,反而會讓一個類別管太多事、又綁死在固定的實作上
所以接下來我們繼續漫長且有趣的旅程吧(笑),來學習第一個創建型模式: Factory Method(工廠方法)
業界常講的「工廠模式」其實混著三種概念,但只有 2、3 是 GoF 正式收錄的
所以在講工廠方法之前,我們先來聊聊簡單工廠,對我來說也是新手村的入門
雖然沒有被收錄,但應該歸類為創建模式的一種
簡單工廠模式 將「物件的建立」封裝在一個工廠類別中,客戶端無需直接使用 new 關鍵字去實例化物件,而是透過工廠方法來獲得所需的物件
新的一週,柴咖啡店開店了,菜單掛回去,拿鐵、美式
阿柴翻開小黃留下的程式碼,來看看點餐的邏輯
// 建立 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 也不會只被一個地方呼叫
點餐、結帳、庫存扣款,都可能各自需要知道「對應到哪一種飲料」,如果沒有一個共用的地方查,這種字串比對式的判斷,就會在程式碼各處各自重複一份,改一次要抓好幾個地方,漏改一個就會噴錯
那我們提升一些複雜度,換取低風險,工廠方法要處理的問題很單純
定義一個建立物件的介面,但讓子類別決定要實例化哪一個具體類別,將物件的創建延遲到子類別處理
小黃留下的 IDrink、Latte、Americano 產品類別不用動
阿柴要動的只有 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(進入點):負責在最外層根據使用者的選擇做物件組裝與注入
如果阿柴把「幫我寫一個依照飲料種類建立訂單的功能」丟給 AI,時常會見到兩種方式
第一種,AI 直接生出小黃當年那種一大包 if-else,能動,語法正確,但因為自己也不懂,所以也不會考慮到「這串邏輯以後還會不會變」
第二種,AI 反而過度熱心,一開口就是 IDrinkStore、AbstractDrinkStore,外加一個 DrinkStoreRegistry、DrinkStoreFactory,三種飲料包出七八個類別,也會造成設計過頭,因為它不清楚使用者實際的需求
所以我們在查閱程式碼時,一樣先問這自己:現在真的有第二種、第三種產品在排隊,還是只是先包好以防萬一?
如果連小黑都還沒說要加新飲料,AI 卻已經先幫你切好五層抽象、外加一個註冊機制,那多半是它在幫你想一個還沒發生的需求
也是我們先前所提到的 YAGNI(You Aren't Gonna Need It)
AI 可以給出「Factory Method 該長什麼樣子」的語法,甚至能一次生出 DrinkStore、Cashier、Program 這一整組結構,但不了解「產品有沒有這個需求」,最終判斷還是要交由給自己,畢竟需要有人背...
明天繼續繼續加油,業績越來越好了,小黑想推套餐,早餐套餐跟下午茶套餐各自要配一整組不同的品項,一次生成,這時候就輪到 Abstract Factory 上場了
如果看不懂我寫的,可以看看延伸的資料,希望可以幫助到你
Refactoring Guru - Factory Method
初探設計模式 - 工廠方法模式 (Factory Method Patter