🔧 常用結構型與行為型模式
📌 結構型 vs 行為型模式
- 結構型模式:關注類別和物件的組合方式,就像積木怎麼拼在一起
- 行為型模式:關注物件之間的溝通方式,就像人跟人怎麼合作
🔷 Strategy Pattern(策略模式)— 行為型
💡 比喻
就像導航 APP 可以切換路線策略:最短路線、最快路線、避開高速公路。目的地不變,但走法可以隨時切換!
🎯 問題場景
同一個操作有多種演算法可以選擇,而且可能需要在執行時期動態切換。
📝 C# 實作
// 策略介面:定義排序的合約
public interface ISortStrategy
{
void Sort(List<int> data); // 對資料進行排序
}
// 具體策略一:泡沫排序(適合小量資料)
public class BubbleSort : ISortStrategy
{
public void Sort(List<int> data)
{
Console.WriteLine("使用泡沫排序..."); // 印出使用哪種排序
for (int i = 0; i < data.Count - 1; i++) // 外層迴圈
{
for (int j = 0; j < data.Count - i - 1; j++) // 內層迴圈
{
if (data[j] > data[j + 1]) // 如果前面比後面大
{
(data[j], data[j + 1]) = (data[j + 1], data[j]); // 交換位置
}
}
}
}
}
// 具體策略二:快速排序(適合大量資料)
public class QuickSort : ISortStrategy
{
public void Sort(List<int> data)
{
Console.WriteLine("使用快速排序..."); // 印出使用哪種排序
QuickSortRecursive(data, 0, data.Count - 1); // 呼叫遞迴排序
}
private void QuickSortRecursive(List<int> data, int low, int high)
{
if (low < high) // 還有元素需要排序
{
int pivot = data[high]; // 選最後一個當基準點
int i = low - 1; // 小於基準點的區域邊界
for (int j = low; j < high; j++) // 走訪每個元素
{
if (data[j] <= pivot) // 如果比基準點小
{
i++; // 擴大小區域
(data[i], data[j]) = (data[j], data[i]); // 交換到小區域
}
}
(data[i + 1], data[high]) = (data[high], data[i + 1]); // 基準點歸位
QuickSortRecursive(data, low, i); // 排序左半邊
QuickSortRecursive(data, i + 2, high); // 排序右半邊
}
}
}
// 上下文:使用策略的類別
public class DataProcessor
{
private ISortStrategy _strategy; // 持有策略的參考
public DataProcessor(ISortStrategy strategy)
{
_strategy = strategy; // 從建構子注入策略
}
// 可以在執行時期切換策略!
public void SetStrategy(ISortStrategy strategy)
{
_strategy = strategy; // 替換新的排序策略
}
// 處理資料:用當前策略排序
public void Process(List<int> data)
{
_strategy.Sort(data); // 委派給策略物件執行排序
Console.WriteLine(string.Join(", ", data)); // 印出排序結果
}
}
✅ 何時用
- 需要在執行時期切換演算法
- 有一系列相似的行為,用 if-else 切換太醜
- 想避免大量的條件判斷語句
🔷 Observer Pattern(觀察者模式)— 行為型
💡 比喻
就像 YouTube 訂閱:你訂閱了一個頻道,每當頻道上傳新影片,所有訂閱者都會自動收到通知。不用自己每天去檢查有沒有新影片!
📝 C# 實作
// 觀察者介面:所有訂閱者都要實作
public interface ISubscriber
{
void Update(string channel, string videoTitle); // 收到新影片通知
}
// 被觀察者:YouTube 頻道
public class YouTubeChannel
{
private readonly string _name; // 頻道名稱
private readonly List<ISubscriber> _subscribers = []; // 訂閱者清單
public YouTubeChannel(string name)
{
_name = name; // 設定頻道名稱
}
// 訂閱:加入訂閱者清單
public void Subscribe(ISubscriber subscriber)
{
_subscribers.Add(subscriber); // 把訂閱者加進來
Console.WriteLine("新增一位訂閱者"); // 通知有人訂閱了
}
// 取消訂閱:從清單移除
public void Unsubscribe(ISubscriber subscriber)
{
_subscribers.Remove(subscriber); // 把訂閱者移除
Console.WriteLine("移除一位訂閱者"); // 通知有人退訂了
}
// 上傳新影片:通知所有訂閱者
public void UploadVideo(string title)
{
Console.WriteLine($"頻道 [{_name}] 上傳新影片:{title}"); // 印出新影片資訊
foreach (var subscriber in _subscribers) // 走訪每個訂閱者
{
subscriber.Update(_name, title); // 逐一通知
}
}
}
// 具體觀察者一:Email 訂閱者
public class EmailSubscriber : ISubscriber
{
private readonly string _email; // 信箱地址
public EmailSubscriber(string email)
{
_email = email; // 設定信箱
}
public void Update(string channel, string videoTitle)
{
// 收到通知後寄 Email
Console.WriteLine($"📧 寄送到 {_email}:{channel} 上傳了 {videoTitle}");
}
}
// 具體觀察者二:App 推播訂閱者
public class AppSubscriber : ISubscriber
{
private readonly string _userId; // 使用者 ID
public AppSubscriber(string userId)
{
_userId = userId; // 設定使用者 ID
}
public void Update(string channel, string videoTitle)
{
// 收到通知後推播到 App
Console.WriteLine($"🔔 推播給 {_userId}:{channel} 上傳了 {videoTitle}");
}
}
✅ 何時用
- 一個物件狀態改變需要通知多個其他物件
- 不知道有多少物件需要被通知(動態增減)
- 事件驅動架構、發布-訂閱模式
🔷 Decorator Pattern(裝飾者模式)— 結構型
💡 比喻
像俄羅斯套娃(或珍珠奶茶加料):基本款是紅茶,加珍珠變珍珠紅茶,再加椰果變珍珠椰果紅茶,一層一層往外包!
📝 C# 實作
// 基底介面:飲料
public interface IBeverage
{
string GetDescription(); // 取得飲料描述
decimal GetCost(); // 取得飲料價格
}
// 具體元件:基本紅茶
public class BlackTea : IBeverage
{
public string GetDescription()
{
return "紅茶"; // 基本飲料名稱
}
public decimal GetCost()
{
return 30m; // 基本價格 30 元
}
}
// 裝飾者基底類別:所有加料都繼承這個
public abstract class BeverageDecorator : IBeverage
{
protected readonly IBeverage _beverage; // 被裝飾的飲料
protected BeverageDecorator(IBeverage beverage)
{
_beverage = beverage; // 保存被裝飾的飲料
}
public abstract string GetDescription(); // 子類別實作描述
public abstract decimal GetCost(); // 子類別實作價格
}
// 具體裝飾者一:加珍珠
public class PearlDecorator : BeverageDecorator
{
public PearlDecorator(IBeverage beverage) : base(beverage) { } // 傳入被裝飾的飲料
public override string GetDescription()
{
return _beverage.GetDescription() + " + 珍珠"; // 在原本描述後面加上珍珠
}
public override decimal GetCost()
{
return _beverage.GetCost() + 10m; // 原價 + 珍珠加 10 元
}
}
// 具體裝飾者二:加椰果
public class CoconutJellyDecorator : BeverageDecorator
{
public CoconutJellyDecorator(IBeverage beverage) : base(beverage) { } // 傳入被裝飾的飲料
public override string GetDescription()
{
return _beverage.GetDescription() + " + 椰果"; // 加上椰果描述
}
public override decimal GetCost()
{
return _beverage.GetCost() + 15m; // 原價 + 椰果加 15 元
}
}
// 具體裝飾者三:加鮮奶
public class MilkDecorator : BeverageDecorator
{
public MilkDecorator(IBeverage beverage) : base(beverage) { } // 傳入被裝飾的飲料
public override string GetDescription()
{
return _beverage.GetDescription() + " + 鮮奶"; // 加上鮮奶描述
}
public override decimal GetCost()
{
return _beverage.GetCost() + 20m; // 原價 + 鮮奶加 20 元
}
}
// 使用方式:一層一層包起來
// IBeverage drink = new BlackTea(); // 基本紅茶:30 元
// drink = new PearlDecorator(drink); // 加珍珠:40 元
// drink = new MilkDecorator(drink); // 再加鮮奶:60 元
// Console.WriteLine($"{drink.GetDescription()}"); // 紅茶 + 珍珠 + 鮮奶
// Console.WriteLine($"價格:{drink.GetCost()} 元"); // 價格:60 元
🔷 Repository Pattern(倉儲模式)— 結構型
💡 比喻
Repository 就像圖書館的圖書管理員:你不需要知道書放在哪個書架、哪一層,只要跟管理員說「我要借《哈利波特》」,他就會幫你找到。
📝 C# 實作
// 實體類別:產品
public class Product
{
public int Id { get; set; } // 產品編號
public string Name { get; set; } = ""; // 產品名稱
public decimal Price { get; set; } // 產品價格
}
// 泛型倉儲介面:定義通用的 CRUD 操作
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id); // 根據 ID 查詢單筆
Task<IEnumerable<T>> GetAllAsync(); // 查詢全部
Task AddAsync(T entity); // 新增一筆
Task UpdateAsync(T entity); // 更新一筆
Task DeleteAsync(int id); // 刪除一筆
}
// 產品專用的倉儲介面(可擴充特定查詢)
public interface IProductRepository : IRepository<Product>
{
Task<IEnumerable<Product>> GetByPriceRangeAsync( // 根據價格範圍查詢
decimal minPrice, decimal maxPrice);
}
// 具體實作(以記憶體模擬,實際會用 EF Core)
public class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> _products = []; // 用 List 模擬資料庫
private int _nextId = 1; // 自動遞增的 ID
public Task<Product?> GetByIdAsync(int id)
{
var product = _products.FirstOrDefault(p => p.Id == id); // 根據 ID 找產品
return Task.FromResult(product); // 回傳找到的產品(或 null)
}
public Task<IEnumerable<Product>> GetAllAsync()
{
return Task.FromResult<IEnumerable<Product>>(_products); // 回傳所有產品
}
public Task AddAsync(Product product)
{
product.Id = _nextId++; // 指定新的 ID
_products.Add(product); // 加入清單
return Task.CompletedTask; // 完成
}
public Task UpdateAsync(Product product)
{
var index = _products.FindIndex(p => p.Id == product.Id); // 找到索引
if (index >= 0) // 如果找到了
_products[index] = product; // 更新資料
return Task.CompletedTask; // 完成
}
public Task DeleteAsync(int id)
{
_products.RemoveAll(p => p.Id == id); // 移除符合 ID 的產品
return Task.CompletedTask; // 完成
}
public Task<IEnumerable<Product>> GetByPriceRangeAsync(
decimal minPrice, decimal maxPrice)
{
var result = _products.Where( // 篩選價格範圍內的產品
p => p.Price >= minPrice && p.Price <= maxPrice);
return Task.FromResult(result); // 回傳結果
}
}
🔷 Adapter Pattern(轉接器模式)— 結構型
💡 比喻
就像 USB-C 轉 HDMI 的轉接頭:你的筆電只有 USB-C 孔,但螢幕需要 HDMI 線。轉接頭讓兩個不相容的介面可以一起工作!
📝 C# 實作
// 舊系統的介面(不能修改,就像 HDMI 孔不能改)
public class OldPaymentSystem
{
// 舊系統用 XML 格式處理付款
public void ProcessXmlPayment(string xmlData)
{
Console.WriteLine($"舊系統處理 XML 付款:{xmlData}"); // 用 XML 處理
}
}
// 新系統期望的介面(就像筆電的 USB-C)
public interface IModernPayment
{
void Pay(string jsonData); // 新系統用 JSON 格式
}
// 轉接器:讓舊系統配合新介面使用
public class PaymentAdapter : IModernPayment
{
private readonly OldPaymentSystem _oldSystem; // 持有舊系統的參考
public PaymentAdapter(OldPaymentSystem oldSystem)
{
_oldSystem = oldSystem; // 注入舊系統
}
public void Pay(string jsonData)
{
// 把 JSON 轉成 XML(轉接的核心工作!)
var xmlData = ConvertJsonToXml(jsonData); // 格式轉換
_oldSystem.ProcessXmlPayment(xmlData); // 交給舊系統處理
}
private string ConvertJsonToXml(string json)
{
// 簡化的轉換邏輯(實際會用 JSON/XML 解析器)
return $"<payment>{json}</payment>"; // 模擬 JSON 轉 XML
}
}
// 使用方式:新程式碼只認識 IModernPayment
// var oldSystem = new OldPaymentSystem(); // 舊系統實體
// IModernPayment payment = new PaymentAdapter(oldSystem); // 用轉接器包裝
// payment.Pay("{ \"amount\": 100 }"); // 用新介面呼叫
✅ 何時用
- 需要整合第三方套件或舊系統
- 兩個已存在的介面不相容但需要合作
- 想在不修改原始碼的情況下讓不同系統串接
🤔 我這樣寫為什麼會錯?
錯誤一:Strategy 模式中把策略寫死在類別裡
// ❌ 策略寫死,無法動態切換
public class Sorter
{
public void Sort(List<int> data, string method)
{
if (method == "bubble") { /* 泡沫排序 */ } // 寫死在這
else if (method == "quick") { /* 快速排序 */ } // 寫死在這
}
}
// ✅ 把每種排序抽成獨立的 ISortStrategy 實作
錯誤二:Observer 沒有取消訂閱導致記憶體洩漏
// ❌ 訂閱者被銷毀了但沒有取消訂閱
public class SomeComponent
{
public SomeComponent(YouTubeChannel channel)
{
channel.Subscribe(this); // 訂閱了
// 但從來沒有 Unsubscribe — 物件無法被 GC 回收!
}
// ✅ 實作 IDisposable,在 Dispose() 中呼叫 Unsubscribe()
}
錯誤三:Repository 把商業邏輯放在裡面
// ❌ Repository 不應該包含商業邏輯
public class OrderRepository
{
public void CreateOrder(Order order)
{
if (order.Total > 1000) // 商業邏輯不該在這裡!
order.Discount = order.Total * 0.1m; // 折扣計算應該在 Service 層
_context.Orders.Add(order); // 這才是 Repository 該做的
_context.SaveChanges(); // 儲存到資料庫
}
}
// ✅ Repository 只做 CRUD,商業邏輯放在 Service 層
📝 模式速查表
| 模式 | 類型 | 一句話記憶 | 比喻 |
|---|---|---|---|
| Strategy | 行為型 | 動態切換演算法 | 導航 APP 切路線 |
| Observer | 行為型 | 訂閱自動通知 | YouTube 訂閱 |
| Decorator | 結構型 | 一層層加功能 | 珍奶加料 |
| Repository | 結構型 | 資料存取中間層 | 圖書管理員 |
| Adapter | 結構型 | 轉接不相容介面 | USB-C 轉 HDMI |