iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
自我挑戰組

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

Day 10|Abstract Factory(抽象工廠) 早餐與下午茶,一次生成組合

  • 分享至 

  • xImage
  •  

昨天用 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),不該把成套組合的約束責任丟給外部呼叫端自己去組合」

Abstract Factory 抽象工廠上場

昨天講的工廠方法(Factory Method)解決的是「單一產品怎麼生」

阿柴遇到的是「一組互相搭配、不能混用的產品,怎麼在型別與架構層面保證一次正確」

這是 Abstract Factory 要處理的核心問題:

提供一個介面,用來建立一系列相關或相依的物件,而不需要指定它們的具體類別

聽起來超饒舌對不對,我也覺得,那把這句話拆看來,我們可以這樣看

  1. 「提供一個介面」 => 開一張「套餐規格表」
  2. 「建立一系列相關物件」=> 規定套餐裡一定要有「飲料 + 點心」
  3. 「不需要指定具體類別」 => 你(點餐客人)不用管內場用什麼品牌的咖啡豆或麵糰

那阿柴該怎麼實作呢?

那我們把「早餐套餐」跟「下午茶套餐」各自封裝成專屬的工廠

呼叫端只需要在一開始選對工廠,之後跟這個工廠索取品項時,拿到的一定是同一個套餐裡的搭配,從源頭杜絕拼錯組合的可能

// ==========================================
// 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 差在哪

阿柴一開始也搞混,兩個看起來都是「介面 + 具體工廠」,差異在哪?

Factory Method 解決的是一個產品怎麼生,重點是讓子類別決定「造出哪一個具體類別」,像昨天的 LatteStoreAmericanoStore,一家工廠只認一種飲料

Abstract Factory 解決的是整組相關產品怎麼一起生對,重點是保證同一個工廠生出來的東西彼此搭配,不會混到別的家族去,BreakfastComboFactory 內部其實就是靠兩個工廠方法(CreateDrinkCreateSide)組成的

也就是說 Abstract Factory 常常是「疊在 Factory Method 之上的一層」

管的是產品之間的成套關係,而不是單一產品怎麼造

Abstract Factory 的取捨

那當然 Abstract Factory 不是沒有成本,阿柴發現,如果哪天小黑想加一項新品項,例如所有套餐都要加「一張抽獎券」,這件事會牽動到整條線

IComboFactory 介面要加一個 CreateLotteryTicket(),所有實作它的具體工廠(BreakfastComboFactoryAfternoonTeaComboFactory……以後可能還有假日限定套餐工廠)都得跟著改

新增「一整組新套餐」(比如之後出了「宵夜套餐」)很輕鬆,多寫一個 LateNightComboFactory 就好,叫端不用動

但新增「套餐裡的一種新品項」,反而要動到介面跟每一個實作

這是 Abstract Factory 的典捨,優勢是換來成套一致性,代價是橫向擴充品項時,牽動範圍變大

邊界在哪

  1. 產品之間,是不是真的有「不能混搭」的強制關係,如果飲料、點心本來就可以自由組合,單點也是各自定價、互不影響,那分開幾個獨立的 Factory Method 就夠,硬包成 Abstract Factory 只是多繞一層

  2. 真的存在兩個以上的產品家族嗎? 如果柴咖啡目前只有早餐套餐一種,下午茶套餐還是小黑腦中的計畫,那先把 Abstract Factory 的骨架搭好,就是在為一個還沒發生的需求先付成本

AI 給得出 Abstract Factory 該怎麼組出這一整套介面跟工廠,但答不出「柴咖啡的飲料跟點心,是不是綁死不能混搭」,這件事得靠人自己盯著套餐規則看,講清楚了,AI 給的架構才會剛好卡進柴咖啡現在的規模,不多也不少


明天客製化飲料要上場了,少冰、半糖、加燕麥奶,這種「同一杯飲料,但選項多到炸開」的組裝問題,工廠方法跟抽象工廠都不太夠用,該換 Builder 出馬了

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

設計模式—工廠與抽象工廠 Factory & Abstract Factory Design Patter

设计模型之抽象工厂模式 含UML完整实例


上一篇
Day 9|Factory Method(工廠方法) 將物件的誕生交給子類別
下一篇
Day 11|Builder(建造者模式) 飲料客製化大亂,救出被參數淹沒的吧台
系列文
一杯咖啡的設計課:30 天 Design Pattern 的自我修煉26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言