以下內容基於原文(C#、生命遊戲抽象化設計與擴充感染規則)抽取並結構化為 15 個可落地的實戰案例,涵蓋問題、根因、解法、程式碼與實際效益,方便教學、練習與評估。
Case #1: 用抽象型別 Life 隔離 World 與具體 Cell
Problem Statement(問題陳述)
- 業務場景:生命遊戲需要從單一細胞模型擴展到容納多種生物(未來可能是草、羊、虎),並能承受不確定的規則增減(如新增病毒感染規則),系統要能持續演進但不推翻現有架構。
- 技術挑戰:避免 World 直接依賴具體 Cell,使主體演算法不因新生物/新規則而牽連修改;需有穩定的互動介面與動態聯結支持。
- 影響範圍:若耦合過深,新增一種 Life 或規則將迫使 World 與多處程式變更,增加風險與成本。
- 複雜度評級:中
Root Cause Analysis(根因分析)
- 直接原因:
- World 直接操作具體 Cell,導致編譯期綁定與高耦合。
- 缺乏穩定的抽象協議,World 無法面向抽象開發。
- 規則變更注入點不清,改一處牽動多處。
- 深層原因:
- 架構層面:沒有以抽象契約作為邊界,類別間責任不清。
- 技術層面:缺少繼承與多型的設計應用。
- 流程層面:開發順序未先定義穩定依賴,導致後續擴充成本高。
Solution Design(解決方案設計)
-
解決策略:引入抽象型別 Life 作為唯一讓 World 溝通的契約(位置、所屬世界、顯示、生命週期迭代),World 只認得 Life;所有具體規則在衍生類(如 Cell)中多型實作。此舉以動態聯結保證執行期根據實體類別運作,消除對具體 Cell 的編譯期依賴。
- 實施步驟:
- 建立抽象基底類 Life
- 實作細節:包含 CurrentWorld, PosX, PosY, DisplayText, WholeLife()
- 所需資源:C# 繼承、多型、yield
- 預估時間:0.5 天
- 修改 World 只操作 Life
- 實作細節:World.PutOn(Life, x, y)、FindNeighborsOf(Life)
- 所需資源:資料結構(MxN 棋盤)
- 預估時間:0.5 天
- 將 Cell 改為 Life 的衍生類
- 實作細節:覆寫 DisplayText、WholeLife
- 所需資源:原 Cell 規則
- 預估時間:0.5 天
- 建立抽象基底類 Life
- 關鍵程式碼/設定:
```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
- 實際案例:原文中 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(根因分析)
- 直接原因:
- 規格臨時變更需擴充規則引擎。
- 若 World 耦合規則,擴充成本高。
- 規則含機率與時序,複雜度提升。
- 深層原因:
- 架構層面:需依賴抽象層來接納未知變更
- 技術層面:需要可插拔的行為(多型)
- 流程層面:必須在不完整需求下先定抽象
Solution Design(解決方案設計)
-
解決策略:在 Cell(Life 衍生類)中實作感染邏輯,維持 World 完全不變;藉由 DisplayText 一併呈現感染狀態,並使用 WholeLife 的時序驅動。
- 實施步驟:
- 在 Cell 新增 IsInfected 與 InfectedCount
- 實作細節:InfectedCount > 0 代表感染中;每次 tick 減 1
- 所需資源:欄位/屬性
- 預估時間:0.5 天
- 擴充 WholeLife 規則
- 實作細節:計算鄰居存活/感染數、查表決定生死、機率感染/死亡
- 所需資源:_table、InProbability
- 預估時間:0.5 天
- 顯示感染狀態
- 實作細節:覆寫 DisplayText 映射符號
- 所需資源:UI/Console 輸出
- 預估時間:0.5 天
- 在 Cell 新增 IsInfected 與 InfectedCount
- 關鍵程式碼/設定: ```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
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
- 直接原因:
- 編譯期綁定會限制擴充。
- 缺乏虛擬函式導致呼叫靜態化。
- World 操作具體型別造成硬耦合。
- 深層原因:
- 架構層面:未面向抽象開發
- 技術層面:未利用虛擬/抽象成員與覆寫
- 流程層面:組件邊界未定義清楚
Solution Design
-
解決策略:以 Life 抽象定義生命週期(WholeLife)與顯示(DisplayText);World 迭代 Life 集合並呼叫抽象成員,由 CLR 在執行期動態聯結到具體子類。
- 實施步驟:
- Life 定義抽象/虛擬成員
- World 僅儲存/遍歷 Life
- 子類覆寫行為,新增類別即插即用
- 關鍵程式碼/設定:
// 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
- 直接原因:
- new 對象分散於各層,耦合難以管理。
- World 負責創建會破壞其單一職責。
- 深層原因:
- 架構層面:組裝與運行職責未分離
- 技術層面:缺乏依賴注入概念
- 流程層面:部署/組態無集中管理
Solution Design
-
解決策略:把組裝責任放在應用程式進入點(GameHost),由這層 new 出具體 Cell 並放入 World;World/ Life 保持純粹。
- 實施步驟:
- 建立 GameHost/Program.Main 作為組合根
- 在此 new 出 Cell,呼叫 world.PutOn
- 將 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
- 直接原因:
- 鄰居查找若分散於各子類,易出現邊界/越界錯誤。
- 缺乏共用 API 導致一致性問題。
- 深層原因:
- 架構層面:資料讀取責任未集中
- 技術層面:未善用基底類統一定義
- 流程層面:沒有可重用的查詢介面
Solution Design
-
解決策略:在 Life 提供 FindNeighbors() 封裝 World 的鄰居查詢,回傳 IEnumerable
,子類直接於規則迴圈中使用。 - 實施步驟:
- World:實作 FindNeighborsOf(Life)
- Life:提供 this.FindNeighbors() 包裝 World 呼叫
- 子類:純使用 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
- 直接原因:
- 機率事件若散落,難以理解與調整。
- 感染期/死亡機率需與狀態機配合。
- 深層原因:
- 架構層面:缺乏機率/狀態管理封裝
- 技術層面:隨機數使用分散
- 流程層面:測試需可重現
Solution Design
-
解決策略:集中使用 InProbability(prob%) 封裝隨機判定;以 InfectedCount 作為簡單狀態機(>0 表示感染中,逐步遞減至痊癒);在 WholeLife 單點統一處理。
- 實施步驟:
- 建立 InfectedCount 與 IsInfected
- 將機率邏輯放入 WholeLife 迭代中
- 以鄰居感染數計算感染機率
- 關鍵程式碼/設定:
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
- 直接原因:
- 硬編碼條件造成理解困難。
- 調整規則易牽動多處。
- 深層原因:
- 架構層面:缺乏規則資料化
- 技術層面:條件邏輯過度分支
- 流程層面:缺少單點調整策略
Solution Design
-
解決策略:使用二維矩陣 bool?[,] _table 代表(當前存活/死亡 × 鄰居數)對下一狀態的決策,簡化為查表。
- 實施步驟:
- 設計 2×9 的 bool? 表(0..8 鄰居)
- WholeLife 以 _table[alive?1:0, livesCount] 查表
- 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
- 直接原因:
- 若 World 決定顯示,會與規則耦合。
- 不同 Life 可能有不同視覺映射。
- 深層原因:
- 架構層面:缺乏展示責任劃分
- 技術層面:未利用多型覆寫 UI 映射
- 流程層面:展示需求易變化
Solution Design
-
解決策略:在 Life 提供虛擬 DisplayText,由子類依自身狀態覆寫;World 僅讀取該屬性輸出。
- 實施步驟:
- Life:定義 virtual DisplayText
- Cell:依狀態回傳符號(●/◎/○)
- 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
- 直接原因:
- 核心若耦合「細胞」概念,無法換域。
- 規則多樣化很快超出最初假設。
- 深層原因:
- 架構層面:缺乏穩定抽象層
- 技術層面:未使用繼承/介面抽象契約
- 流程層面:需求不完整,需演化式設計
Solution Design
-
解決策略:維持 Life/World 合約穩定,將「物種」與「規則」全部實現在衍生 Life;World 無須變更即可接納新物種。
- 實施步驟:
- 確認 Life 合約(位置、顯示、週期、鄰居)
- 為新物種各自建立子類
- 組合根載入不同物種配置
- 關鍵程式碼/設定:
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
- 直接原因:
- 變更點定位不清易擴散。
- 缺乏測試切入點。
- 深層原因:
- 架構層面:若無抽象隔離,風險外溢
- 技術層面:行為分散難以回歸測試
- 流程層面:需求頻變需設計對策
Solution Design
-
解決策略:以 Life/World 抽象隔離變更,將感染規則完全放入 Cell;驗證 World 行為不變、Display 層可識別感染。
- 實施步驟:
- 定位修改點:Cell.WholeLife 與 DisplayText
- 實作感染規則
- 驗證: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
- 直接原因:
- 預先實作未被需求驗證的功能。
- 以為「多做準備」等於「更靈活」。
- 深層原因:
- 架構層面:未定義「穩定抽象」與「可延後決策」
- 技術層面:混淆「抽象化」與「功能堆疊」
- 流程層面:需求不確定卻提前實作
Solution Design
-
解決策略:僅建立穩定抽象(Life/World 合約、FindNeighbors、WholeLife),避免實作未需求的功能;將多型擴充視為未來插點而非當下實作。
- 實施步驟:
- 萃取不變性(抽象契約)
- 延遲決策(只保留插點)
- 僅當需求明確時才補具體功能
- 關鍵程式碼/設定(反例 vs 正例): ```csharp // 反例:預留無需求功能 if (enablePrint) Print(); switch(mode) { /* +-/ / 預留 */ }
// 正例:面向抽象設計
public abstract class Life { protected abstract IEnumerable
- 實際案例:原文諷刺過度準備 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
- 直接原因:
- 無邊界檢查導致例外或錯誤行為。
- 重覆放置造成資料衝突。
- 深層原因:
- 架構層面:不變條件未顯式表達
- 技術層面:缺少守衛子句
- 流程層面:測試場景涵蓋不足
Solution Design
-
解決策略:World.PutOn 內實作邊界/佔用守衛;成功放置後設定 Life 的世界與座標;FindNeighborsOf 處理邊界。
- 實施步驟:
- 實作 PutOn 守衛與賦值
- 在 FindNeighborsOf 處理四/八方向與邊界
- 撰寫基本測試覆蓋邊界/佔用
- 關鍵程式碼/設定:
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 個案例皆直接對應原文的問題、根因、解法與實際效益描述,並補足教學所需的實作與評估框架,便於課程、練習與專案演練使用。