🏭 常用建立型模式
📌 什麼是建立型模式?
建立型模式(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-else 和 new。
📝 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 | 整套風格一致 | 建立系列相關物件 |