昨天用 Factory Method 把「一種飲料怎麼生出來」交給子類別決定,阿柴不用再對著 if-else 冒冷汗
但日子在走,需求就會來
今天說到 Abstract Factory 抽象工廠
小黑丟了一個新點子過來:「我們來推套餐吧,早上一套早餐,下午茶一套,吸引不同時段的客人」
阿柴一聽,心想這不就是「飲料 + 點心」湊一湊而已嗎,應該很簡單
柴咖啡的套餐其實分兩條線,彼此不能混搭
早餐套餐: 美式 + 貝果
下午茶套餐: 拿鐵 + 蛋糕
阿柴照小黃的老習慣,寫了兩個各自獨立的方法,分別決定飲料跟點心
public interface IDrink
{
string Name { get; }
int Price { get; }
}
public interface ISideItem
{
string Name { get; }
}
public class Bagel : ISideItem
{
public string Name => "貝果";
}
public class Cake : ISideItem
{
public string Name => "蛋糕";
}
public class ComboService // 組合套餐
{
public IDrink GetDrink(string comboType) // 帶入參數 comboType
{
if (comboType == "breakfast") return new Americano(); //如果選早餐 就是提供美式
else return new Latte(); // 不然就是下午茶套餐 提供拿鐵
}
public ISideItem GetSide(string comboType)
{
if (comboType == "breakfast") return new Bagel();
else return new Cake();
}
}
兩個方法各司其職,看起來也符合「集中管理」的精神
但問題在於,這兩個方法是各自獨立判斷 comboType,沒有人保證它們永遠同進退
當業務邏輯在 Controller、外部外送平台 API 串接,或是工程師寫單元測試時,呼叫端必須手動呼叫兩次。只要呼叫端一不小心傳錯參數或漏改變數:
// 呼叫端手動拼裝,留下了出錯的可能
var drink = comboService.GetDrink("breakfast"); // 拿到美式
var side = comboService.GetSide("breakFast"); // fast 寫成大寫,傳錯參數拿到下午茶的蛋糕
語法上合法,C# 編譯器不會報錯,但系統就拼出了一套「美式 + 蛋糕」的幽靈組合
後續的結帳系統要嘛對不上套餐規則、只能以原價單點拆開算;要嘛硬套邏輯導致金額算錯
既然語法合法,那有可能是錯在哪呢?
錯的是「早餐套餐的飲料與點心本來就是綁在一起組合產品(Product Family),不該把成套組合的約束責任丟給外部呼叫端自己去組合」
昨天講的工廠方法(Factory Method)解決的是「單一產品怎麼生」
阿柴遇到的是「一組互相搭配、不能混用的產品,怎麼在型別與架構層面保證一次正確」
這是 Abstract Factory 要處理的核心問題:
提供一個介面,用來建立一系列相關或相依的物件,而不需要指定它們的具體類別
聽起來超饒舌對不對,我也覺得,那把這句話拆看來,我們可以這樣看
那阿柴該怎麼實作呢?
那我們把「早餐套餐」跟「下午茶套餐」各自封裝成專屬的工廠
呼叫端只需要在一開始選對工廠,之後跟這個工廠索取品項時,拿到的一定是同一個套餐裡的搭配,從源頭杜絕拼錯組合的可能
// ==========================================
// 1. 抽象工廠(Abstract Factory)
// ==========================================
// IDrink、ISideItem 跟各家族的產品類別,都沿用前面定義好的內容
public interface IComboFactory
{
IDrink CreateDrink();
ISideItem CreateSide();
}
// ==========================================
// 2. 具體工廠(Concrete Factories):每個工廠只生產同一個套餐裡的品項
// ==========================================
public class BreakfastComboFactory : IComboFactory
{
public IDrink CreateDrink() => new Americano();
public ISideItem CreateSide() => new Bagel();
}
public class AfternoonTeaComboFactory : IComboFactory
{
public IDrink CreateDrink() => new Latte();
public ISideItem CreateSide() => new Cake();
}
呼叫端長這樣
public class ComboService
{
// 只依賴抽象工廠介面,不知道也不需要知道現在是哪一套具體品項
public void ServeCombo(IComboFactory factory)
{
IDrink drink = factory.CreateDrink();
ISideItem side = factory.CreateSide();
Console.WriteLine($"套餐出餐:{drink.Name} + {side.Name}");
}
}
public class Program
{
public static void Main()
{
var comboService = new ComboService();
// 早餐套餐:只要選對 BreakfastComboFactory,兩樣品項自動成套,不用擔心兜錯
comboService.ServeCombo(new BreakfastComboFactory());
// 下午茶套餐:換一顆工廠,整組品項跟著換
comboService.ServeCombo(new AfternoonTeaComboFactory());
}
}
現在店員(或前端)只需要決定「這是早餐套餐還是下午茶套餐」,選擇工廠之後,飲料、點心就會自動配成同一套,不會再有美式配蛋糕這種組合了
阿柴一開始也搞混,兩個看起來都是「介面 + 具體工廠」,差異在哪?
Factory Method 解決的是一個產品怎麼生,重點是讓子類別決定「造出哪一個具體類別」,像昨天的 LatteStore、AmericanoStore,一家工廠只認一種飲料
Abstract Factory 解決的是整組相關產品怎麼一起生對,重點是保證同一個工廠生出來的東西彼此搭配,不會混到別的家族去,BreakfastComboFactory 內部其實就是靠兩個工廠方法(CreateDrink、CreateSide)組成的
也就是說 Abstract Factory 常常是「疊在 Factory Method 之上的一層」
管的是產品之間的成套關係,而不是單一產品怎麼造
那當然 Abstract Factory 不是沒有成本,阿柴發現,如果哪天小黑想加一項新品項,例如所有套餐都要加「一張抽獎券」,這件事會牽動到整條線
IComboFactory 介面要加一個 CreateLotteryTicket(),所有實作它的具體工廠(BreakfastComboFactory、AfternoonTeaComboFactory……以後可能還有假日限定套餐工廠)都得跟著改
新增「一整組新套餐」(比如之後出了「宵夜套餐」)很輕鬆,多寫一個 LateNightComboFactory 就好,叫端不用動
但新增「套餐裡的一種新品項」,反而要動到介面跟每一個實作
這是 Abstract Factory 的典捨,優勢是換來成套一致性,代價是橫向擴充品項時,牽動範圍變大
產品之間,是不是真的有「不能混搭」的強制關係,如果飲料、點心本來就可以自由組合,單點也是各自定價、互不影響,那分開幾個獨立的 Factory Method 就夠,硬包成 Abstract Factory 只是多繞一層
真的存在兩個以上的產品家族嗎? 如果柴咖啡目前只有早餐套餐一種,下午茶套餐還是小黑腦中的計畫,那先把 Abstract Factory 的骨架搭好,就是在為一個還沒發生的需求先付成本
AI 給得出 Abstract Factory 該怎麼組出這一整套介面跟工廠,但答不出「柴咖啡的飲料跟點心,是不是綁死不能混搭」,這件事得靠人自己盯著套餐規則看,講清楚了,AI 給的架構才會剛好卡進柴咖啡現在的規模,不多也不少
明天客製化飲料要上場了,少冰、半糖、加燕麥奶,這種「同一杯飲料,但選項多到炸開」的組裝問題,工廠方法跟抽象工廠都不太夠用,該換 Builder 出馬了
如果看不懂我寫的,可以看看延伸的資料,希望可以幫助到你
設計模式—工廠與抽象工廠 Factory & Abstract Factory Design Patter