☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
設計模式 中級

🏭 常用建立型模式

📌 什麼是建立型模式?

建立型模式(Creational Patterns)專門處理物件怎麼被建立的問題。就像工廠有不同的生產線,每種模式都是一種聰明的「生產方式」。

不要直接 new 物件!讓建立型模式幫你優雅地管理物件的誕生。


🔷 Singleton Pattern(單例模式)

💡 比喻

全公司只有一個總經理,不管誰問「總經理是誰?」,答案永遠是同一個人。

🎯 問題場景

有些物件在系統中應該只存在一個實體,例如:設定檔管理器、資料庫連線池、日誌記錄器。

🏗️ UML 概念

┌──────────────────────┐
│     Singleton         │
├──────────────────────┤
│ - instance: Singleton │  ← 靜態私有欄位,存放唯一實體
│ - Singleton()         │  ← 私有建構子,外部不能 new
│ + GetInstance()        │  ← 公開靜態方法,取得唯一實體
└──────────────────────┘

📝 C# 實作

// 基本版 Singleton — 非執行緒安全(有問題的版本)
public class BasicSingleton
{
    private static BasicSingleton? _instance; // 靜態欄位存放唯一實體

    private BasicSingleton() // 私有建構子:外面不能 new
    {
        Console.WriteLine("Singleton 被建立了"); // 只會印一次
    }

    public static BasicSingleton GetInstance() // 取得唯一實體的方法
    {
        if (_instance == null) // 如果還沒建立過
        {
            _instance = new BasicSingleton(); // 就建立一個
        }
        return _instance; // 回傳唯一的實體
    }
}

⚠️ 多執行緒安全版本

// 執行緒安全的 Singleton — 使用 lock
public sealed class ThreadSafeSingleton
{
    private static ThreadSafeSingleton? _instance; // 唯一實體
    private static readonly object _lock = new();  // 鎖定物件

    private ThreadSafeSingleton() // 私有建構子
    {
        Console.WriteLine("安全的 Singleton 被建立了"); // 只會印一次
    }

    public static ThreadSafeSingleton Instance // 屬性方式取得實體
    {
        get
        {
            if (_instance == null) // 第一次檢查(避免不必要的 lock)
            {
                lock (_lock) // 鎖住!同一時間只有一個執行緒能進入
                {
                    if (_instance == null) // 第二次檢查(Double-Check Locking)
                    {
                        _instance = new ThreadSafeSingleton(); // 建立實體
                    }
                }
            }
            return _instance; // 回傳唯一實體
        }
    }
}

🎯 最推薦的寫法:用 Lazy

// 最簡潔的執行緒安全 Singleton — 用 Lazy<T>
public sealed class ModernSingleton
{
    // Lazy<T> 保證只會在第一次存取時建立,而且執行緒安全
    private static readonly Lazy<ModernSingleton> _lazy =
        new(() => new ModernSingleton()); // 延遲初始化

    private ModernSingleton() // 私有建構子
    {
        Console.WriteLine("Modern Singleton 誕生!"); // 只會執行一次
    }

    public static ModernSingleton Instance => _lazy.Value; // 取得唯一實體
}

✅ 何時用 / ❌ 何時不用

適合使用 不適合使用
全域設定管理 需要多個實體的場景
日誌記錄器 有狀態且會被多執行緒修改
快取管理 單元測試中(難以 Mock)

🔷 Factory Pattern(工廠模式)

💡 比喻

工廠依訂單生產不同產品:你跟工廠說「我要一台筆電」,工廠就生產筆電;說「我要手機」,就生產手機。你不需要知道生產細節。

🎯 問題場景

當你需要根據條件建立不同類型的物件,又不想在呼叫端寫一堆 if-elsenew

📝 C# 實作

// 產品介面:所有通知都要能「發送」
public interface INotification
{
    void Send(string message); // 發送通知
}

// 具體產品一:Email 通知
public class EmailNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine($"📧 Email:{message}"); // 用 Email 發送
    }
}

// 具體產品二:簡訊通知
public class SmsNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine($"📱 簡訊:{message}"); // 用簡訊發送
    }
}

// 具體產品三:推播通知
public class PushNotification : INotification
{
    public void Send(string message)
    {
        Console.WriteLine($"🔔 推播:{message}"); // 用推播發送
    }
}

// 工廠類別:根據類型建立對應的通知物件
public static class NotificationFactory
{
    // 根據傳入的類型字串,回傳對應的通知物件
    public static INotification Create(string type)
    {
        return type.ToLower() switch // 比對類型(轉小寫避免大小寫問題)
        {
            "email" => new EmailNotification(), // 建立 Email 通知
            "sms"   => new SmsNotification(),   // 建立簡訊通知
            "push"  => new PushNotification(),   // 建立推播通知
            _ => throw new ArgumentException(      // 未知類型就丟例外
                $"不支援的通知類型:{type}")
        };
    }
}

// 使用方式
// var notification = NotificationFactory.Create("email"); // 取得 Email 通知
// notification.Send("你的訂單已出貨");                     // 發送通知

✅ 何時用 / ❌ 何時不用

適合使用 不適合使用
建立邏輯複雜且需要集中管理 只有一種產品類型
需要根據設定檔決定建立哪種物件 建立邏輯非常簡單
想要隱藏建立細節 物件不需要多型

🔷 Builder Pattern(建造者模式)

💡 比喻

就像去速食店點餐:先選主餐(漢堡),再加配菜(薯條),最後選飲料(可樂),一步一步組合出你想要的套餐。

🎯 問題場景

當一個物件有很多可選參數,建構子會變得又臭又長(Telescoping Constructor 問題)。

📝 C# 實作

// 產品:一份完整的報表設定
public class ReportConfig
{
    public string Title { get; set; } = "";     // 報表標題
    public string Format { get; set; } = "PDF";  // 輸出格式
    public bool IncludeChart { get; set; }         // 是否包含圖表
    public bool IncludeHeader { get; set; }        // 是否包含頁首
    public bool IncludeFooter { get; set; }        // 是否包含頁尾
    public int FontSize { get; set; } = 12;        // 字體大小
    public string DateRange { get; set; } = "";  // 日期範圍

    // 顯示設定內容(方便除錯)
    public override string ToString()
    {
        return $"報表:{Title},格式:{Format},字體:{FontSize}pt"; // 回傳摘要
    }
}

// 建造者:一步一步建立 ReportConfig
public class ReportConfigBuilder
{
    private readonly ReportConfig _config = new(); // 內部持有要建立的產品

    // 設定標題(回傳 this 以支援鏈式呼叫)
    public ReportConfigBuilder SetTitle(string title)
    {
        _config.Title = title; // 設定報表標題
        return this;           // 回傳自己,支援鏈式呼叫
    }

    // 設定輸出格式
    public ReportConfigBuilder SetFormat(string format)
    {
        _config.Format = format; // 設定格式(PDF、Excel 等)
        return this;              // 回傳自己
    }

    // 加入圖表
    public ReportConfigBuilder WithChart()
    {
        _config.IncludeChart = true; // 啟用圖表
        return this;                  // 回傳自己
    }

    // 加入頁首
    public ReportConfigBuilder WithHeader()
    {
        _config.IncludeHeader = true; // 啟用頁首
        return this;                   // 回傳自己
    }

    // 加入頁尾
    public ReportConfigBuilder WithFooter()
    {
        _config.IncludeFooter = true; // 啟用頁尾
        return this;                   // 回傳自己
    }

    // 設定字體大小
    public ReportConfigBuilder SetFontSize(int size)
    {
        _config.FontSize = size; // 設定字型大小
        return this;              // 回傳自己
    }

    // 最終建立產品
    public ReportConfig Build()
    {
        return _config; // 回傳組裝完成的報表設定
    }
}

// 使用方式 — 鏈式呼叫,清楚易讀
// var config = new ReportConfigBuilder()
//     .SetTitle("月報表")       // 設定標題
//     .SetFormat("Excel")      // 選擇格式
//     .WithChart()              // 要圖表
//     .WithHeader()             // 要頁首
//     .SetFontSize(14)          // 字體 14pt
//     .Build();                 // 組裝完成!

🔷 Abstract Factory(抽象工廠模式)

💡 比喻

想像你在裝潢房子,選了「北歐風格」,那所有家具(沙發、桌子、燈)都會是北歐風的;選了「工業風格」,所有家具就都是工業風的。抽象工廠確保同一個系列的產品風格一致

📝 C# 實作

// 抽象產品一:按鈕
public interface IButton
{
    void Render(); // 渲染按鈕的方法
}

// 抽象產品二:文字輸入框
public interface ITextBox
{
    void Render(); // 渲染輸入框的方法
}

// 具體產品:Windows 風格的按鈕
public class WindowsButton : IButton
{
    public void Render()
    {
        Console.WriteLine("[Windows 按鈕]"); // 渲染 Windows 風格按鈕
    }
}

// 具體產品:Windows 風格的輸入框
public class WindowsTextBox : ITextBox
{
    public void Render()
    {
        Console.WriteLine("[Windows 輸入框]"); // 渲染 Windows 風格輸入框
    }
}

// 具體產品:Mac 風格的按鈕
public class MacButton : IButton
{
    public void Render()
    {
        Console.WriteLine("(Mac 按鈕)"); // 渲染 Mac 風格按鈕
    }
}

// 具體產品:Mac 風格的輸入框
public class MacTextBox : ITextBox
{
    public void Render()
    {
        Console.WriteLine("(Mac 輸入框)"); // 渲染 Mac 風格輸入框
    }
}

// 抽象工廠:定義建立 UI 元件的合約
public interface IUIFactory
{
    IButton CreateButton();   // 建立按鈕
    ITextBox CreateTextBox(); // 建立輸入框
}

// 具體工廠:Windows 風格的 UI 工廠
public class WindowsUIFactory : IUIFactory
{
    public IButton CreateButton() => new WindowsButton();   // 建立 Windows 按鈕
    public ITextBox CreateTextBox() => new WindowsTextBox(); // 建立 Windows 輸入框
}

// 具體工廠:Mac 風格的 UI 工廠
public class MacUIFactory : IUIFactory
{
    public IButton CreateButton() => new MacButton();   // 建立 Mac 按鈕
    public ITextBox CreateTextBox() => new MacTextBox(); // 建立 Mac 輸入框
}

// 使用方式:傳入不同工廠,就能建立不同風格的 UI
// IUIFactory factory = new WindowsUIFactory(); // 選擇 Windows 風格
// var button = factory.CreateButton();          // 建立按鈕
// var textBox = factory.CreateTextBox();        // 建立輸入框
// button.Render();                              // 渲染按鈕
// textBox.Render();                             // 渲染輸入框

🤔 我這樣寫為什麼會錯?

錯誤一:Singleton 在多執行緒環境沒有加鎖

// ❌ 多個執行緒同時進入,可能建立多個實體!
public static Singleton GetInstance()
{
    if (_instance == null)             // 執行緒 A 和 B 同時到這裡
    {
        _instance = new Singleton();   // 兩個都會 new — 不再是 Singleton!
    }
    return _instance; // 回傳的可能不是同一個
}
// ✅ 解法:使用 lock 或 Lazy<T>(參考上面的安全版本)

錯誤二:工廠方法裡用字串比對,打錯字就爆炸

// ❌ 字串容易打錯且沒有編譯期檢查
var service = Factory.Create("emal"); // 打錯字!應該是 "email"
// ✅ 解法:改用 enum 或泛型
// var service = Factory.Create<EmailService>(); // 編譯期就會檢查

錯誤三:Builder 沒有驗證必填欄位

// ❌ 忘記設定必填的 Title 就呼叫 Build()
var config = new ReportConfigBuilder()
    .SetFontSize(14)  // 設定了字體大小
    .Build();          // 但忘了設定標題!報表沒有名字 😱
// ✅ 解法:在 Build() 裡檢查必填欄位
// if (string.IsNullOrEmpty(_config.Title))
//     throw new InvalidOperationException("報表標題為必填!");

📝 建立型模式速查表

模式 一句話記憶 適用場景
Singleton 全公司只有一個總經理 全域唯一的服務
Factory 工廠依訂單生產 根據條件建立不同物件
Builder 點餐式組合 物件有很多可選參數
Abstract Factory 整套風格一致 建立系列相關物件

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊