[設計案例] 生命遊戲 #6, 抽像化 (Abstraction)

以下內容基於原文(C#、生命遊戲抽象化設計與擴充感染規則)抽取並結構化為 15 個可落地的實戰案例,涵蓋問題、根因、解法、程式碼與實際效益,方便教學、練習與評估。

Case #1: 用抽象型別 Life 隔離 World 與具體 Cell

Problem Statement(問題陳述)

  • 業務場景:生命遊戲需要從單一細胞模型擴展到容納多種生物(未來可能是草、羊、虎),並能承受不確定的規則增減(如新增病毒感染規則),系統要能持續演進但不推翻現有架構。
  • 技術挑戰:避免 World 直接依賴具體 Cell,使主體演算法不因新生物/新規則而牽連修改;需有穩定的互動介面與動態聯結支持。
  • 影響範圍:若耦合過深,新增一種 Life 或規則將迫使 World 與多處程式變更,增加風險與成本。
  • 複雜度評級:中

Root Cause Analysis(根因分析)

  • 直接原因:
    1. World 直接操作具體 Cell,導致編譯期綁定與高耦合。
    2. 缺乏穩定的抽象協議,World 無法面向抽象開發。
    3. 規則變更注入點不清,改一處牽動多處。
  • 深層原因:
    • 架構層面:沒有以抽象契約作為邊界,類別間責任不清。
    • 技術層面:缺少繼承與多型的設計應用。
    • 流程層面:開發順序未先定義穩定依賴,導致後續擴充成本高。

Solution Design(解決方案設計)

  • 解決策略:引入抽象型別 Life 作為唯一讓 World 溝通的契約(位置、所屬世界、顯示、生命週期迭代),World 只認得 Life;所有具體規則在衍生類(如 Cell)中多型實作。此舉以動態聯結保證執行期根據實體類別運作,消除對具體 Cell 的編譯期依賴。

  • 實施步驟:
    1. 建立抽象基底類 Life
      • 實作細節:包含 CurrentWorld, PosX, PosY, DisplayText, WholeLife()
      • 所需資源:C# 繼承、多型、yield
      • 預估時間:0.5 天
    2. 修改 World 只操作 Life
      • 實作細節:World.PutOn(Life, x, y)、FindNeighborsOf(Life)
      • 所需資源:資料結構(MxN 棋盤)
      • 預估時間:0.5 天
    3. 將 Cell 改為 Life 的衍生類
      • 實作細節:覆寫 DisplayText、WholeLife
      • 所需資源:原 Cell 規則
      • 預估時間:0.5 天
  • 關鍵程式碼/設定: ```csharp public abstract class Life { public World CurrentWorld { get; internal set; } public int PosX { get; internal set; } public int PosY { get; internal set; } public virtual string DisplayText => “?”; protected abstract IEnumerable WholeLife(); protected IEnumerable FindNeighbors() => CurrentWorld.FindNeighborsOf(this); }

public class World { public void PutOn(Life life, int x, int y) { /* 只認 Life,不認 Cell */ } public IEnumerable FindNeighborsOf(Life life) { /* 回傳鄰居 */ } }


- 實際案例:原文中 World 程式碼完全未出現 Cell;只有 GameHost 在啟動時建立 Cell 並放進 World。
- 實作環境:C# 3.0 / .NET 3.5(yield 支援)、Console/UI 任一
- 實測數據:
  - 改善前:World 依賴 Cell,新增規則需改 World
  - 改善後:World 僅依賴 Life,新增規則無需改 World
  - 改善幅度:主程式修改範圍 1 類別 -> 0 類別(100% 消除)

Learning Points(學習要點)
- 核心知識點:
  - 抽象化是穩定邊界,細節在邊界後方演進
  - 多型提供動態聯結,消除編譯期耦合
  - 合約先行設計(interface/abstract class)
- 技能要求:
  - 必備技能:C# OOP、抽象類/虛擬方法
  - 進階技能:契約式設計、封裝與邊界識別
- 延伸思考:
  - 此方案可應用於插件式系統、策略模式場景
  - 風險:抽象定義不當會導致重構成本
  - 優化:以 interface 明確最小契約,增進測試可替換性

Practice Exercise(練習題)
- 基礎練習:把現有 Cell 的共有欄位上移到 Life(30 分鐘)
- 進階練習:讓 World 支援 Life 的清單與鄰居查詢(2 小時)
- 專案練習:加入第二種 Life 並證明 World 不改仍可運作(8 小時)

Assessment Criteria(評估標準)
- 功能完整性(40%):World 僅依賴 Life 抽象即可運行
- 程式碼品質(30%):清晰的抽象、命名與界面
- 效能優化(20%):鄰居查詢與資料結構合理
- 創新性(10%):抽象契約可覆用於其他模擬題

---

## Case #2: 抽取父類(Generalization)與職責上移

### Problem Statement(問題陳述)
- 業務場景:生命遊戲需支援更多生物與規則,若每種生物都重複維護位置、所屬世界、顯示介面等欄位,易造成重複與不一致。
- 技術挑戰:如何識別「所有生命的共通特性」並上移到父類以利擴充和維護一致性。
- 影響範圍:重複程式、易出錯、修改成本高。
- 複雜度評級:中

### Root Cause Analysis(根因分析)
- 直接原因:
  1. 共通欄位/行為遺留在子類,導致重複。
  2. 規則與展示/定位邏輯混雜。
  3. 測試時需多處修改。
- 深層原因:
  - 架構層面:未執行一般化(Generalization)
  - 技術層面:未善用繼承與虛擬成員
  - 流程層面:缺乏「先抽象再專精」的開發序

### Solution Design(解決方案設計)
- 解決策略:將 CurrentWorld、PosX、PosY、DisplayText、FindNeighbors 等共通責任上移至 Life;保留子類(Cell)只專注於其特有規則(WholeLife)。

- 實施步驟:
  1. 盤點共通欄位/方法
     - 實作細節:以 Life 定義且受控於 World
     - 所需資源:重構工具/測試
     - 預估時間:0.5 天
  2. 子類移除重複,覆寫必要虛擬成員
     - 實作細節:DisplayText、WholeLife 保持在子類
     - 所需資源:原始 Cell 程式
     - 預估時間:0.5 天

- 關鍵程式碼/設定:
```csharp
public abstract class Life
{
    public World CurrentWorld { get; internal set; }
    public int PosX { get; internal set; }
    public int PosY { get; internal set; }
    public virtual string DisplayText => "?";
    protected IEnumerable<Life> FindNeighbors() => CurrentWorld.FindNeighborsOf(this);
}

public class Cell : Life
{
    public override string DisplayText => /* 覆寫顯示符號 */;
    protected override IEnumerable<TimeSpan> WholeLife() { /* Cell 特有生命週期 */ }
}
  • 實際案例:原文將 Cell 程式碼部分搬移至 Life,並建立繼承關係。
  • 實作環境:C# 3.0 / .NET 3.5
  • 実測數據:
    • 改善前:共通欄位散落於子類
    • 改善後:共通欄位集中於 Life,子類僅保留特有規則
    • 改善幅度:重複碼顯著降低(重複欄位 3->0,重複方法 1->0)

Learning Points(學習要點)

  • 核心知識點:Generalization/Specialization、上移欄位/方法、虛擬覆寫
  • 技能要求:
    • 必備技能:識別共通性
    • 進階技能:重構(抽取父類)與回歸測試
  • 延伸思考:抽象若過度會僵硬;可先以最小集合開始,逐步上移

Practice Exercise(練習題)

  • 基礎:把座標與世界引用上移到 Life(30 分鐘)
  • 進階:讓 FindNeighbors 在 Life 提供,再由子類使用(2 小時)
  • 專案:將另一種生命型別也繼承 Life,避免重複(8 小時)

Assessment Criteria

  • 功能完整性(40%):重構後功能保持一致
  • 程式碼品質(30%):無重複、命名清楚
  • 效能優化(20%):鄰居查詢不退化
  • 創新性(10%):抽象層設計合理

Case #3: 在不改 World 的前提下新增「感染規則」

Problem Statement(問題陳述)

  • 業務場景:助教臨時修改需求:在原四條生命遊戲規則上新增「病毒感染」機制(感染機率與鄰居相關,感染 3 次狀態後痊癒,且感染中有 10% 機率死亡)。
  • 技術挑戰:快速導入新規則,保持原有模擬運作,避免影響 World 與既有架構。
  • 影響範圍:變更風險、工期、相容性。
  • 複雜度評級:中

Root Cause Analysis(根因分析)

  • 直接原因:
    1. 規格臨時變更需擴充規則引擎。
    2. 若 World 耦合規則,擴充成本高。
    3. 規則含機率與時序,複雜度提升。
  • 深層原因:
    • 架構層面:需依賴抽象層來接納未知變更
    • 技術層面:需要可插拔的行為(多型)
    • 流程層面:必須在不完整需求下先定抽象

Solution Design(解決方案設計)

  • 解決策略:在 Cell(Life 衍生類)中實作感染邏輯,維持 World 完全不變;藉由 DisplayText 一併呈現感染狀態,並使用 WholeLife 的時序驅動。

  • 實施步驟:
    1. 在 Cell 新增 IsInfected 與 InfectedCount
      • 實作細節:InfectedCount > 0 代表感染中;每次 tick 減 1
      • 所需資源:欄位/屬性
      • 預估時間:0.5 天
    2. 擴充 WholeLife 規則
      • 實作細節:計算鄰居存活/感染數、查表決定生死、機率感染/死亡
      • 所需資源:_table、InProbability
      • 預估時間:0.5 天
    3. 顯示感染狀態
      • 實作細節:覆寫 DisplayText 映射符號
      • 所需資源:UI/Console 輸出
      • 預估時間:0.5 天
  • 關鍵程式碼/設定: ```csharp public bool IsInfected => this.InfectedCount > 0; private int InfectedCount = 0;

public override string DisplayText { get { if (this.IsAlive == true) return “●”; else if (this.IsInfected == true) return “◎”; else return “○”; } }

protected override IEnumerable WholeLife() { yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200)); for (int index = 0; index < int.MaxValue; index++) { int livesCount = 0, infectsCount = 0; foreach (Cell item in this.FindNeighbors()) { if (item.IsAlive == true) livesCount++; if (item.IsInfected == true) infectsCount++; }

    bool? value = _table[this.IsAlive ? 1 : 0, livesCount];
    if (value.HasValue == true) this.IsAlive = value.Value;

    if (this.IsInfected == true)
    {
        this.InfectedCount--;
        if (this.InProbability(10) == true) this.IsAlive = false;
    }
    else
    {
        if (this.InProbability(1 + infectsCount * 5) == true) this.InfectedCount = 3;
    }
    yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));
}
this.Dispose(); } ```
  • 實際案例:畫面可區分「感染」、「正常」、「死亡」,觀察到病毒擴散態勢。
  • 實作環境:C# 3.0 / .NET 3.5
  • 實測數據:
    • 改善前:無感染模型
    • 改善後:支援感染/痊癒/死亡機率、World 無變更
    • 改善幅度:新增規則改動面只在 Cell(1 類別),World 0 變更

Learning Points

  • 核心知識點:多型擴充行為、以抽象隔離變更、機率式規則
  • 技能要求:
    • 必備技能:集合與迭代、基本機率
    • 進階技能:行為集中化與可觀測性設計
  • 延伸思考:顯示順序與狀態優先級要一致(可考慮感染優先於一般活狀態)

Practice Exercise

  • 基礎:調整感染週期(InfectedCount 初值)並觀察擴散(30 分鐘)
  • 進階:將感染死亡機率從固定 10% 改為與感染強度相關(2 小時)
  • 專案:加入另一種傳染病規則(不同機率與痊癒條件)(8 小時)

Assessment Criteria

  • 功能完整性(40%):感染/痊癒/死亡流程正確
  • 程式碼品質(30%):變更集中於子類,命名清楚
  • 效能優化(20%):鄰居統計計算合理
  • 創新性(10%):可配置化的機率/週期

Case #4: 動態聯結(多型)確保執行期正確行為

Problem Statement(問題陳述)

  • 業務場景:World 不應知道每個生命的具體類別;執行期需根據生命實際型別運行相對應規則(包含未來新增)。
  • 技術挑戰:消除編譯期對具體類別的依賴;確保新增類別不需改 World 仍能被正確呼叫。
  • 影響範圍:擴充性、可維護性、相容性。
  • 複雜度評級:中

Root Cause Analysis

  • 直接原因:
    1. 編譯期綁定會限制擴充。
    2. 缺乏虛擬函式導致呼叫靜態化。
    3. World 操作具體型別造成硬耦合。
  • 深層原因:
    • 架構層面:未面向抽象開發
    • 技術層面:未利用虛擬/抽象成員與覆寫
    • 流程層面:組件邊界未定義清楚

Solution Design

  • 解決策略:以 Life 抽象定義生命週期(WholeLife)與顯示(DisplayText);World 迭代 Life 集合並呼叫抽象成員,由 CLR 在執行期動態聯結到具體子類。

  • 實施步驟:
    1. Life 定義抽象/虛擬成員
    2. World 僅儲存/遍歷 Life
    3. 子類覆寫行為,新增類別即插即用
  • 關鍵程式碼/設定:
    // World 只依賴 Life,呼叫虛擬/抽象成員
    foreach (var life in world.AllLives)
    {
      foreach (var wait in life.Run()) { /* 排程下一次tick */ }
      Console.Write(life.DisplayText);
    }
    
  • 實際案例:原文指出主程式可在未有 Cell 細節時先開發,後續加入 Cell 後即可執行。
  • 實作環境:C# 3.0 / .NET 3.5
  • 實測數據:新增類別數:可由 0 擴充至 N;World 修改次數:0

Learning Points

  • 核心知識點:動態聯結、多型調度、面向抽象編程
  • 技能要求:抽象類/介面、覆寫、集合操作
  • 延伸思考:可以 interface ILife 降低耦合並支援多重繼承場景

Practice Exercise

  • 基礎:建立另一個 Life 子類並確認 World 無須修改(30 分鐘)
  • 進階:將 Life 抽象改為 interface,驗證行為(2 小時)
  • 專案:做一個可裝載多種 Life 的模組化模擬(8 小時)

Assessment Criteria

  • 功能完整性:新子類無縫加入
  • 程式碼品質:呼叫面向抽象
  • 效能:動態聯結迭代效率
  • 創新性:介面化/插件化設計

Case #5: 組合根(Composition Root)—在 GameHost 建立具體類別

Problem Statement

  • 業務場景:系統需在一處集中配置「要放入世界的實體」;避免於 World 內 new 具體類別。
  • 技術挑戰:在哪裡知道具體型別的存在,同時不污染核心邏輯。
  • 影響範圍:可測試性、可替換性、部署配置彈性。
  • 複雜度評級:低

Root Cause Analysis

  • 直接原因:
    1. new 對象分散於各層,耦合難以管理。
    2. World 負責創建會破壞其單一職責。
  • 深層原因:
    • 架構層面:組裝與運行職責未分離
    • 技術層面:缺乏依賴注入概念
    • 流程層面:部署/組態無集中管理

Solution Design

  • 解決策略:把組裝責任放在應用程式進入點(GameHost),由這層 new 出具體 Cell 並放入 World;World/ Life 保持純粹。

  • 實施步驟:
    1. 建立 GameHost/Program.Main 作為組合根
    2. 在此 new 出 Cell,呼叫 world.PutOn
    3. 將 World/ Life 完全與具體型別解耦
  • 關鍵程式碼/設定:
    static void Main(string[] args)
    {
      int worldSizeX = 30, worldSizeY = 30;
      World realworld = new World(worldSizeX, worldSizeY);
      for (int x = 0; x < worldSizeX; x++)
          for (int y = 0; y < worldSizeY; y++)
              realworld.PutOn(new Cell(), x, y);
      // ...
    }
    
  • 實際案例:原文僅 GameHost 知道 Cell;World/Life 不知道。
  • 實作環境:C# Console 專案
  • 實測數據:具體型別知識分散點數:多處 -> 1 處(組合根)

Learning Points

  • 核心知識點:組合根、控制反轉
  • 技能要求:專案分層與職責界定
  • 延伸思考:未來可替換為 DI 容器以載入不同 Life

Practice Exercise

  • 基礎:把 new Cell() 移到組合根(30 分鐘)
  • 進階:以工廠委派 Func 產生生命(2 小時)
  • 專案:實作可由設定檔決定放入的 Life 種類(8 小時)

Assessment Criteria

  • 功能完整性:World/ Life 無 new 具體類別
  • 程式碼品質:明確的組合根
  • 效能:初始化成本合理
  • 創新性:DI/工廠應用

Case #6: 抽象化鄰居計算(FindNeighbors)統一規則依據

Problem Statement

  • 業務場景:多數規則(包括感染)都依賴鄰居狀態統計;需穩定、可重用的鄰居取得方式。
  • 技術挑戰:避免子類各自實作鄰居查詢造成重複與錯誤;支援多型。
  • 影響範圍:規則正確性、維護性。
  • 複雜度評級:中

Root Cause Analysis

  • 直接原因:
    1. 鄰居查找若分散於各子類,易出現邊界/越界錯誤。
    2. 缺乏共用 API 導致一致性問題。
  • 深層原因:
    • 架構層面:資料讀取責任未集中
    • 技術層面:未善用基底類統一定義
    • 流程層面:沒有可重用的查詢介面

Solution Design

  • 解決策略:在 Life 提供 FindNeighbors() 封裝 World 的鄰居查詢,回傳 IEnumerable,子類直接於規則迴圈中使用。

  • 實施步驟:
    1. World:實作 FindNeighborsOf(Life)
    2. Life:提供 this.FindNeighbors() 包裝 World 呼叫
    3. 子類:純使用 API,僅負責統計/判斷
  • 關鍵程式碼/設定: ```csharp // Life protected IEnumerable FindNeighbors() => CurrentWorld.FindNeighborsOf(this);

// Cell int livesCount = 0, infectsCount = 0; foreach (Cell n in this.FindNeighbors()) { if (n.IsAlive) livesCount++; if (n.IsInfected) infectsCount++; }


- 實際案例:感染規則依賴鄰居感染數、存活數。
- 實作環境:C# / .NET 3.5
- 實測數據:鄰居查詢實作點:多處 -> 1 處;越界/重複風險降低

Learning Points
- 核心知識點:查詢邏輯集中化、迭代與計數
- 技能要求:IEnumerable、LINQ(可選)
- 延伸思考:回傳 IEnumerable<Life>(而非 Cell)更具通用性

Practice Exercise
- 基礎:實作 FindNeighborsOf,處理邊界(30 分鐘)
- 進階:以 LINQ 統計鄰居狀態(2 小時)
- 專案:支援不同鄰居定義(Moore/Neumann)(8 小時)

Assessment Criteria
- 功能完整性:鄰居統計正確
- 程式碼品質:統一 API,無重複
- 效能:鄰居查詢效率可接受
- 創新性:可配置鄰居拓撲

---

## Case #7: 用產生器(yield)驅動非阻塞的生命週期(WholeLife)

### Problem Statement
- 業務場景:每個生命隨時間演化,需要定期更新但不可阻塞主迴圈或 UI;節奏可有隨機性。
- 技術挑戰:如何在單執行緒或簡單排程下,讓多個生命互不阻塞地演進。
- 影響範圍:使用者體驗、可觀察性。
- 複雜度評級:中

### Root Cause Analysis
- 直接原因:
  1. while(true) 迴圈易阻塞。
  2. 多執行緒成本高、同步複雜。
- 深層原因:
  - 架構層面:缺乏非阻塞迭代模型
  - 技術層面:未活用 C# 產生器
  - 流程層面:模擬排程器未定義

### Solution Design
- 解決策略:以 IEnumerable<TimeSpan> 的 WholeLife 產生器,每次 yield 回時間間隔;由外部排程器根據時間間隔驅動下一步,避免阻塞。

- 實施步驟:
  1. 在子類覆寫 WholeLife,插入規則與 yield
  2. 排程器讀取 yield 的 TimeSpan 以等待或安排下一次
  3. 支援隨機時間間隔(提高模擬自然性)

- 關鍵程式碼/設定:
```csharp
protected override IEnumerable<TimeSpan> WholeLife()
{
    yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));
    // 規則更新...
    yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));
}
  • 實際案例:原文展示用 Random 於 800~1200ms 之間節奏更新。
  • 實作環境:C# 產生器
  • 實測數據:阻塞呼叫:避免;UI/主迴圈流暢度提高

Learning Points

  • 核心知識點:產生器模式、合作式排程
  • 技能要求:yield、Enumerator、時間排程
  • 延伸思考:可將 TimeSpan 改為命令物件,整合更多事件

Practice Exercise

  • 基礎:把固定 while(true) 改為 yield 節奏(30 分鐘)
  • 進階:寫一個簡單排程器驅動多個 WholeLife(2 小時)
  • 專案:加入優先級或事件通知(8 小時)

Assessment Criteria

  • 功能完整性:多個生命可交錯演進
  • 程式碼品質:清晰的迭代/排程分工
  • 效能:無不必要阻塞
  • 創新性:可擴展的排程策略

Case #8: 機率式規則的設計(InProbability 與感染週期)

Problem Statement

  • 業務場景:感染規則涉及機率(與鄰居感染數相關)、感染期、機率死亡;需可讀可調。
  • 技術挑戰:在迭代模型中整合機率事件,保持行為可預期且易於調整。
  • 影響範圍:模擬真實度、測試穩定性。
  • 複雜度評級:中

Root Cause Analysis

  • 直接原因:
    1. 機率事件若散落,難以理解與調整。
    2. 感染期/死亡機率需與狀態機配合。
  • 深層原因:
    • 架構層面:缺乏機率/狀態管理封裝
    • 技術層面:隨機數使用分散
    • 流程層面:測試需可重現

Solution Design

  • 解決策略:集中使用 InProbability(prob%) 封裝隨機判定;以 InfectedCount 作為簡單狀態機(>0 表示感染中,逐步遞減至痊癒);在 WholeLife 單點統一處理。

  • 實施步驟:
    1. 建立 InfectedCount 與 IsInfected
    2. 將機率邏輯放入 WholeLife 迭代中
    3. 以鄰居感染數計算感染機率
  • 關鍵程式碼/設定:
    if (this.IsInfected)
    {
      this.InfectedCount--;
      if (this.InProbability(10)) this.IsAlive = false;
    }
    else
    {
      if (this.InProbability(1 + infectsCount * 5))
          this.InfectedCount = 3; // 感染期
    }
    
  • 實際案例:感染期為 3 次狀態變化,感染中有 10% 死亡。
  • 實作環境:C# Random
  • 實測數據:調參成本低(單點改);測試可透過固定種子提升重現性(建議)

Learning Points

  • 核心知識點:機率事件封裝、簡易狀態機
  • 技能要求:隨機數、百分比機率運算
  • 延伸思考:抽象出 IRandom 以注入固定種子便於測試

Practice Exercise

  • 基礎:修改感染期長度,觀察擴散(30 分鐘)
  • 進階:把 InProbability 改為可注入隨機來源(2 小時)
  • 專案:多種感染株與不同機率/致死率(8 小時)

Assessment Criteria

  • 功能完整性:機率行為符合規格
  • 程式碼品質:機率計算集中清晰
  • 效能:隨機呼叫頻率合理
  • 創新性:可測試性提升設計

Case #9: 用規則矩陣(_table)表達生命遊戲決策

Problem Statement

  • 業務場景:原始生命遊戲四條規則需清晰且可調整;在程式中避免多層 if/else。
  • 技術挑戰:將規則以資料化方式表達,便於調整與閱讀。
  • 影響範圍:可維護性、可配置性。
  • 複雜度評級:低

Root Cause Analysis

  • 直接原因:
    1. 硬編碼條件造成理解困難。
    2. 調整規則易牽動多處。
  • 深層原因:
    • 架構層面:缺乏規則資料化
    • 技術層面:條件邏輯過度分支
    • 流程層面:缺少單點調整策略

Solution Design

  • 解決策略:使用二維矩陣 bool?[,] _table 代表(當前存活/死亡 × 鄰居數)對下一狀態的決策,簡化為查表。

  • 實施步驟:
    1. 設計 2×9 的 bool? 表(0..8 鄰居)
    2. WholeLife 以 _table[alive?1:0, livesCount] 查表
    3. value.HasValue == false 表示保持不變
  • 關鍵程式碼/設定:
    bool? value = _table[this.IsAlive ? 1 : 0, livesCount];
    if (value.HasValue) this.IsAlive = value.Value;
    
  • 實際案例:原文示範以查表決定生死。
  • 實作環境:C# 陣列
  • 實測數據:條件分支數顯著下降;規則變更改為更新一張表

Learning Points

  • 核心知識點:規則資料化、查表優化
  • 技能要求:陣列/索引、狀態映射
  • 延伸思考:將表外部化(設定檔)提供運行期調整

Practice Exercise

  • 基礎:用表重現原始 4 條規則(30 分鐘)
  • 進階:將表改為可讀配置檔載入(2 小時)
  • 專案:支援多套規則集切換(8 小時)

Assessment Criteria

  • 功能完整性:規則正確映射
  • 程式碼品質:分支精簡、可讀性高
  • 效能:查表高效
  • 創新性:規則外部化

Case #10: 顯示邏輯與規則邏輯分離(DisplayText 覆寫)

Problem Statement

  • 業務場景:需在畫面中區分「受感染」、「正常」、「死亡」三種狀態,避免規則與 UI 互相污染。
  • 技術挑戰:在不改 World 的情況下,讓不同 Life 自行定義顯示。
  • 影響範圍:UI 可維護性、擴展性。
  • 複雜度評級:低

Root Cause Analysis

  • 直接原因:
    1. 若 World 決定顯示,會與規則耦合。
    2. 不同 Life 可能有不同視覺映射。
  • 深層原因:
    • 架構層面:缺乏展示責任劃分
    • 技術層面:未利用多型覆寫 UI 映射
    • 流程層面:展示需求易變化

Solution Design

  • 解決策略:在 Life 提供虛擬 DisplayText,由子類依自身狀態覆寫;World 僅讀取該屬性輸出。

  • 實施步驟:
    1. Life:定義 virtual DisplayText
    2. Cell:依狀態回傳符號(●/◎/○)
    3. World:統一輸出,不含條件判斷
  • 關鍵程式碼/設定:
    public override string DisplayText
    {
      get
      {
          if (this.IsAlive == true) return "●";
          else if (this.IsInfected == true) return "◎";
          else return "○";
      }
    }
    
  • 實際案例:原文圖例展示不同符號。
  • 實作環境:Console 或 UI 繫結
  • 實測數據:UI 汙染點數:World 0、Cell 1(集中)

Learning Points

  • 核心知識點:多型化 UI 映射、單一責任
  • 技能要求:屬性覆寫與呈現
  • 延伸思考:可改為輸出狀態列舉,由 UI 決定圖示

Practice Exercise

  • 基礎:依狀態輸出文字/色彩(30 分鐘)
  • 進階:支援多種字元主題(2 小時)
  • 專案:UI 層用資料繫結呈現 DisplayText(8 小時)

Assessment Criteria

  • 功能完整性:三態正確顯示
  • 程式碼品質:UI 與規則分離
  • 效能:輸出效率
  • 創新性:可替換的視覺主題

Case #11: 面向未知的擴充(多生物共存)的開放封閉實踐

Problem Statement

  • 業務場景:下一篇會把「細胞」換成「草/羊/虎」;要求 World/Life 架構不變即可應對新生態。
  • 技術挑戰:如何使核心引擎對擴充開放、對修改封閉。
  • 影響範圍:長期演進能力、重用性。
  • 複雜度評級:中

Root Cause Analysis

  • 直接原因:
    1. 核心若耦合「細胞」概念,無法換域。
    2. 規則多樣化很快超出最初假設。
  • 深層原因:
    • 架構層面:缺乏穩定抽象層
    • 技術層面:未使用繼承/介面抽象契約
    • 流程層面:需求不完整,需演化式設計

Solution Design

  • 解決策略:維持 Life/World 合約穩定,將「物種」與「規則」全部實現在衍生 Life;World 無須變更即可接納新物種。

  • 實施步驟:
    1. 確認 Life 合約(位置、顯示、週期、鄰居)
    2. 為新物種各自建立子類
    3. 組合根載入不同物種配置
  • 關鍵程式碼/設定:
    public class Grass : Life { /* 草的規則與顯示 */ }
    public class Sheep : Life { /* 羊的規則與顯示 */ }
    public class Tiger : Life { /* 虎的規則與顯示 */ }
    // World 不需修改
    
  • 實際案例:原文聲明將以相同架構套用到草原生態。
  • 實作環境:C#
  • 實測數據:跨領域重用 World/Life = 100%;核心改動:0

Learning Points

  • 核心知識點:開放封閉原則、可插拔規則
  • 技能要求:抽象契約設計、子類實作
  • 延伸思考:用組件/插件讓物種以外掛方式加入

Practice Exercise

  • 基礎:新增一種 Life 並顯示不同符號(30 分鐘)
  • 進階:兩種物種間互動規則(2 小時)
  • 專案:草-羊-虎簡化生態互動(8 小時)

Assessment Criteria

  • 功能完整性:新物種可運行
  • 程式碼品質:核心零修改
  • 效能:多物種運行效率
  • 創新性:規則組合的靈活度

Case #12: 需求變更的改動面控管(Change Risk Management)

Problem Statement

  • 業務場景:使用者臨時增加「感染」規則;需在短時間交付且風險低。
  • 技術挑戰:侷限變動範圍、快速驗證、不破壞既有功能。
  • 影響範圍:交付速度、品質保證。
  • 複雜度評級:中

Root Cause Analysis

  • 直接原因:
    1. 變更點定位不清易擴散。
    2. 缺乏測試切入點。
  • 深層原因:
    • 架構層面:若無抽象隔離,風險外溢
    • 技術層面:行為分散難以回歸測試
    • 流程層面:需求頻變需設計對策

Solution Design

  • 解決策略:以 Life/World 抽象隔離變更,將感染規則完全放入 Cell;驗證 World 行為不變、Display 層可識別感染。

  • 實施步驟:
    1. 定位修改點:Cell.WholeLife 與 DisplayText
    2. 實作感染規則
    3. 驗證:World 無修改、執行畫面如預期
  • 關鍵程式碼/設定:同 Case #3(WholeLife/DisplayText)
  • 實際案例:原文指出 World 未改,畫面能辨識感染擴散。
  • 實作環境:C#
  • 實測數據:
    • 修改檔案數:1
    • World 變更:0
    • 回歸範圍:Display 與規則局部

Learning Points

  • 核心知識點:變更隔離、回歸測試重點
  • 技能要求:差異化測試、冒煙測試
  • 延伸思考:加上小型規則表單元測試,保障日後改表安全

Practice Exercise

  • 基礎:列出此次變更受影響模組清單(30 分鐘)
  • 進階:為 WholeLife 規則增加單元測試(2 小時)
  • 專案:建立變更清單與回歸測試劇本(8 小時)

Assessment Criteria

  • 功能完整性:新規則可運作
  • 程式碼品質:改動面集中
  • 效能:無明顯退化
  • 創新性:測試切入策略

Case #13: 避免過度設計(YAGNI)但保留抽象彈性

Problem Statement

  • 業務場景:新手傾向預留一堆未確認的功能(列印、通用運算等),導致架構複雜與成本上升。
  • 技術挑戰:在不知道未來需求下,如何拿捏抽象與不過度實作的平衡。
  • 影響範圍:維護成本、設計複雜度、專案風險。
  • 複雜度評級:低

Root Cause Analysis

  • 直接原因:
    1. 預先實作未被需求驗證的功能。
    2. 以為「多做準備」等於「更靈活」。
  • 深層原因:
    • 架構層面:未定義「穩定抽象」與「可延後決策」
    • 技術層面:混淆「抽象化」與「功能堆疊」
    • 流程層面:需求不確定卻提前實作

Solution Design

  • 解決策略:僅建立穩定抽象(Life/World 合約、FindNeighbors、WholeLife),避免實作未需求的功能;將多型擴充視為未來插點而非當下實作。

  • 實施步驟:
    1. 萃取不變性(抽象契約)
    2. 延遲決策(只保留插點)
    3. 僅當需求明確時才補具體功能
  • 關鍵程式碼/設定(反例 vs 正例): ```csharp // 反例:預留無需求功能 if (enablePrint) Print(); switch(mode) { /* +-/ / 預留 */ }

// 正例:面向抽象設計 public abstract class Life { protected abstract IEnumerable WholeLife(); }


- 實際案例:原文諷刺過度準備 1+1→通用運算器;主張抽象而非預做功能。
- 實作環境:通用
- 實測數據:不可度量的過度功能由多→無,複雜度下降

Learning Points
- 核心知識點:YAGNI、抽象與實作的取捨
- 技能要求:需求辨識、最小可行設計
- 延伸思考:以擴充點保留彈性(介面/事件/策略),而非預先實作

Practice Exercise
- 基礎:審視並移除未使用的預留功能(30 分鐘)
- 進階:列出抽象層「不變性清單」(2 小時)
- 專案:重構一段過度設計的程式為最小抽象(8 小時)

Assessment Criteria
- 功能完整性:核心需求滿足
- 程式碼品質:簡潔、無死碼
- 效能:更少開銷
- 創新性:抽象插點設計

---

## Case #14: 單一職責分離(World=環境、Life=行為)

### Problem Statement
- 業務場景:World 需管理棋盤、放置、鄰居;Life 需負責自己的狀態與規則。角色混淆將導致難以擴充。
- 技術挑戰:分離環境管理與行為決策,避免互相干擾。
- 影響範圍:維護性、可讀性。
- 複雜度評級:低

### Root Cause Analysis
- 直接原因:
  1. World 內若含規則會過重。
  2. Life 若操縱環境也會破壞封裝。
- 深層原因:
  - 架構層面:未遵循單一職責
  - 技術層面:邊界責任不清
  - 流程層面:需求混淆導致責任漂移

### Solution Design
- 解決策略:World 僅負責座位/鄰居/放置;Life 僅負責狀態、規則、顯示。雙方透過受控 API 互動。

- 實施步驟:
  1. World:實作 PutOn、FindNeighborsOf
  2. Life:實作 WholeLife、DisplayText
  3. 組合根:建立/組裝物種

- 關鍵程式碼/設定:
```csharp
public class World
{
    public void PutOn(Life life, int x, int y) { /* 管理位置與世界引用 */ }
    public IEnumerable<Life> FindNeighborsOf(Life life) { /* 邊界處理 */ }
}
  • 實際案例:原文強調 World 程式中不含 Cell 規則。
  • 實作環境:C#
  • 實測數據:責任交疊點:多 -> 0;變更影響面縮小

Learning Points

  • 核心知識點:單一職責原則、封裝邊界
  • 技能要求:API 設計、職責劃分
  • 延伸思考:以不變性(1 格 1 生命、MxN)為 World 的責任

Practice Exercise

  • 基礎:分離一段混雜邏輯至正確類別(30 分鐘)
  • 進階:為 World/Life 畫責任對照圖(2 小時)
  • 專案:用單元測試確保責任不跨界(8 小時)

Assessment Criteria

  • 功能完整性:角色分工清楚
  • 程式碼品質:耦合低、內聚高
  • 效能:結構簡潔
  • 創新性:邊界設計

Case #15: 世界不變條件與安全放置(MxN、每格一個生命)

Problem Statement

  • 業務場景:世界是 M×N 棋盤,每格只能有一個生命;放置與查詢需保證邏輯安全。
  • 技術挑戰:邊界檢查、佔用檢查、生命與世界間參照一致性。
  • 影響範圍:資料一致性、錯誤率。
  • 複雜度評級:低

Root Cause Analysis

  • 直接原因:
    1. 無邊界檢查導致例外或錯誤行為。
    2. 重覆放置造成資料衝突。
  • 深層原因:
    • 架構層面:不變條件未顯式表達
    • 技術層面:缺少守衛子句
    • 流程層面:測試場景涵蓋不足

Solution Design

  • 解決策略:World.PutOn 內實作邊界/佔用守衛;成功放置後設定 Life 的世界與座標;FindNeighborsOf 處理邊界。

  • 實施步驟:
    1. 實作 PutOn 守衛與賦值
    2. 在 FindNeighborsOf 處理四/八方向與邊界
    3. 撰寫基本測試覆蓋邊界/佔用
  • 關鍵程式碼/設定:
    public void PutOn(Life life, int x, int y)
    {
      if (x < 0 || y < 0 || x >= _sizeX || y >= _sizeY)
          throw new ArgumentOutOfRangeException();
      if (_grid[x, y] != null)
          throw new InvalidOperationException("Occupied");
      _grid[x, y] = life;
      life.CurrentWorld = this;
      life.PosX = x; life.PosY = y;
    }
    
  • 實際案例:原文明示 M×N、每格能放一個生命。
  • 實作環境:C#
  • 實測數據:邊界/佔用錯誤數:顯著下降;一致性提高

Learning Points

  • 核心知識點:不變條件、守衛子句
  • 技能要求:例外處理、集合管理
  • 延伸思考:可支援換位/移動 API 時亦需維護不變性

Practice Exercise

  • 基礎:為 PutOn 加入測試(30 分鐘)
  • 進階:實作 Remove/Move 並確保不變性(2 小時)
  • 專案:支援包裝座標系與不同鄰居拓撲(8 小時)

Assessment Criteria

  • 功能完整性:正確放置與守衛
  • 程式碼品質:清晰、可維護
  • 效能:O(1) 放置
  • 創新性:座標抽象

案例分類

1) 按難度分類

  • 入門級:
    • Case #5 組合根
    • Case #9 規則矩陣化
    • Case #10 顯示分離
    • Case #14 單一職責
    • Case #15 世界不變條件
  • 中級:
    • Case #1 抽象型別隔離
    • Case #2 抽取父類
    • Case #3 新增感染規則
    • Case #4 動態聯結
    • Case #6 抽象化鄰居計算
    • Case #7 產生器驅動生命週期
    • Case #8 機率式規則設計
    • Case #12 變更改動面控管
    • Case #13 YAGNI 與抽象
  • 高級:
    • Case #11 開放封閉的跨領域擴充(多物種共存)

2) 按技術領域分類

  • 架構設計類:#1, #2, #4, #11, #12, #13, #14
  • 效能優化類:#7, #9(查表優化側重可維護也具效能)
  • 整合開發類:#5, #6, #15
  • 除錯診斷類:#6, #12, #15
  • 安全防護類(邏輯安全/不變性視為防護):#15

3) 按學習目標分類

  • 概念理解型:#1, #2, #4, #11, #13, #14
  • 技能練習型:#5, #6, #7, #9, #10, #15
  • 問題解決型:#3, #8, #12
  • 創新應用型:#11

案例關聯圖(學習路徑建議)

  • 先學: 1) Case #14(單一職責)→ 打基礎的職責分工觀念 2) Case #1(抽象型別隔離)→ 建立穩定抽象 3) Case #2(抽取父類)→ 學會一般化與上移責任 4) Case #15(世界不變條件)→ 確立資料與環境安全

  • 依賴關係:
    • Case #6(鄰居計算)依賴 #1/#2/#15
    • Case #7(產生器生命週期)依賴 #1
    • Case #9(規則矩陣)依賴 #6
    • Case #10(顯示分離)依賴 #2
    • Case #3(感染規則)依賴 #6/#7/#9/#10
    • Case #8(機率規則)依賴 #3
    • Case #5(組合根)與 #1 協同
    • Case #4(動態聯結)依賴 #1/#2
    • Case #12(變更控管)依賴 #3/#4/#5
    • Case #13(YAGNI)綜觀 #1-#12 經驗
    • Case #11(開放封閉擴充)依賴 #1/#2/#4/#5/#6/#7/#9/#10/#15
  • 完整學習路徑建議:
    • 基礎設計:#14 → #1 → #2 → #15
    • 核心機制:#6 → #7 → #9 → #10 → #4 → #5
    • 規則擴充:#3 → #8
    • 工程治理:#12 → #13
    • 跨域擴張:最後進入 #11,將同一套架構應用到草/羊/虎等新領域

以上 15 個案例皆直接對應原文的問題、根因、解法與實際效益描述,並補足教學所需的實作與評估框架,便於課程、練習與專案演練使用。






Facebook Pages

AI Synthesis Contents

- 原始文章內容
- 問答集
- 文章摘要
- 解決方案 / Case Study

Edit Post (Pull Request)

Post Directory