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

🔧 常用結構型與行為型模式

📌 結構型 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

💡 大家的想法 · 0

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