[SDD] SDD 實作 #1 - 個人開發也適用的 SDD

  • 67
  • 0
  • SDD
  • 2025-10-29

本文探討 SDD (規格驅動開發) 的原理與實作, 並使用一個 Winform 專案 (使用 C#) 做為範例。

在 AI 時代以前, TDD (測試驅動開發) 或 BDD (行為驅動開發) 已經算是蠻有討論度的開發概念; 但是 SDD (規格驅動開發) 絕對是從 AI 時代才開始的。

開宗明義, 我們先來看一下到底什麼叫做 SDD。用嚴格的角度來看「規格驅動開發」(Specification-Driven Development) 的定義, 它是一種「以精確且可執行的規格文件作為開發核心的方法論」。其核心精神為:以規格優先 - 規格文件是唯一的真相來源, 它驅動程式碼的生成與驗證; 其目的在於透過消除歧義, 確保軟體品質, 讓程式碼依賴於規格 (而不是個人見解和習慣)。

以下我用一張表來比較有 SDD 和沒有 SDD 可能有什麼不同:

圖1. 有無 SDD 的差異

從上表中可以看出 SDD 概念能為開發團隊帶來什麼好處。

不過在本文中我想要來示範的是 SDD 概念的簡單「實作」。因為如果你是在團隊中而且已經使用了 Spec Kit, 那麼其實也沒什麼太多可以講的了。我想示範的是不依賴工具, 只需使用 Copilot, 就能達成的 SDD。

當然, 不使用工具就不會有工具所能帶來的好處, 例如統一的風格和嚴謹的語法。但我在這裡只打算示範「SDD」本身能為你帶來什麼好處, 而不是工具。這裡產出的文件只需要你能看懂就好, 而且你必須手動調整, 讓它最佳化。

其實我原本懷疑 Copilot 能不能單獨辦到這一點, 但實際做下去之後, 發現它完全有這個能力。所以, 即便你只是一個個人團隊, 或者你只是在做你個人的 side project, 那麼你根本不需要安裝 spec kit (和其它工具), 你自己也可以使用 SDD。事實上, 你也可以套用 TDD (同樣也能透過 Copilot 辦到), 兩者互不干擾。

坦白說, 我原本是正在開發一個個人用的 Winform 專案, 由於周邊資訊太多, 寫著寫著, 這個小專案還沒把該做的功能都做好以前, 其規模就已經超出原有的需求範圍了。我等於正在扮演一個 pantser 角色而不是 plotter 角色 (意思是沒有按照原本的規畫去做)。

換句話說, 我是寫到一半才想到可以套用 SDD 來節省開發的工夫。而實際做下來, 發現的確可以節省很多時間和精力。

過程是這樣。我原本在 VS Insiders 2026 寫好了一個 form, 發現在它的 Winform 專案裡, 和過去的 Winform 並沒有什麼兩樣, 有許多事情仍然必須手動調控。當我後來開始覺得有需要再增加一個 form 時, SDD 開始流行起來, 於是我就拿這個概念來應用。不過我實在懶得再去搞 Python 的東西, 不想去裝 Spec Kit (倒不是對它有什麼意見), 所以我就拿現成的 Github Copilot 加減用用, 然後發現它本來就具備這樣的能力。

我第一個對它下的 prompt 是這樣的: "分析 Form1 的組成, 識別並總結其中的核心架構與 UI 設定, 然後將這些分析得出的規則整理成一份標準化的規格文件, 在專案中建立並寫入一個檔案名為 FormSpec.md 的 Markdown 文件。這份文件將作為未來所有新表單的開發指南, 確保專案程式碼的一致性。"。

然後它就自動建立出 FormSpec.md 這個檔案; 我把它列在本文的最後面供大家參考。

不過, 有一點很重要的是: 我有手動去修改它的內容。為什麼要修改? 因為我是希望未來拿這份 Spec 文件去自動產生新的 form (所以才命名為 FormSpec), 並不是要把它拿去做出另一個 Form1。既然要產生新的 form, 當然就不是企圖做出另一個 Form1, 否則我用複製貼上不就好了嗎? 所以我必須手動將 Form1 裡既有的功能刪除掉, 未來的新 form 不必有那些功能。

換句話說, 我等於是把這份規格拿來當作 macro (巨集) / template 或 blueprint 在使用。但是這個巨集裡寫的並不是單純的 script, 而是未來拿去給 Copilot 做 vibe coding 要用的 prompt。

接著, 我把寫好的規格再拿去給 Copilot 產生新的 form。我對它下的 prompt 是這樣的: "依照 FormSpec.md 這份規格, 在專案裡增加一個名為 TestForm 的 form"。

然後它就把新的空白的 form 建立起來了, 完全依照規格中描述的。照這樣建立起來的新的 form 都會有一致的格式和行為。

我必須強調一點: SDD 是 "Spec Driven Development" 而不是 "Spec Description Document"。我們使用 SDD 是為了要做「開發」, 並非只是把規格寫出來就好 (雖然用 AI 幫我們寫規格也蠻方便的, 可以省很多事)。

最後, 我們再來重新複習一下 SDD 的內涵:

  1. "Spec-Driven Development (SDD) is a software development methodology that makes a clear, precise, and executable specification the central focus and driving force of the entire development lifecycle."
  2. "The core idea is to first create an authoritative, detailed, and formal specification, which serves as the single source of truth for all developers, testers, and product managers."
  3. "Ideally, the specification in SDD is machine-readable or executable, allowing for the automatic generation of test cases or the validation of the code against the documented requirements."

大家應該好好留意上面幾句話裡面一再提到的 executable 這個字。

其實 Copilot 完全有能力把這份規格寫成 YAML。但我選擇使用 Markdown 是因為這種格式比較人性化, 各個參與角色 (developers, testers, product managers) 都能輕易看懂 (和修改)。

除了 FormSpec, 所有值得寫成 template 的功能 (例如自訂控制項、pattern、抽象的方法), 尤其是不能寫成公用程式或介面的功能, 都可以用這種方式達成。此外, 如果你偏好採用某一種特別的 implementation (例如禁止使用外部套件), 你也可以寫進規格裡面, 未來產生的程式裡就能遵守這個規則。

FormSpec.md - 

---
Title: WinForms 視窗開發規範
Author: [Johnny Lee]
Date Created: 2025-10-26
Last Modified: 2025-10-28
---

# WinForms 應用程式視窗開發規範

本文檔定義了本專案中所有 Windows Forms 視窗(Form)的基本標準架構、功能和開發規範。所有新建立的表單都應遵循此規格,以確保程式碼的一致性、可維護性和高品質的使用者體驗。如果是既有表單,則應盡可能地進行重構以符合此規範。

## 1. 設計總覽與核心原則

所有視窗的設計都應圍繞以下核心原則:

- **回應迅速 (Responsive)**: UI 執行緒絕不能被長時間執行的任務(如 I/O、複雜計算)所阻塞。
- **使用者友善 (User-Friendly)**: 對於任何耗時超過 500 毫秒的操作,必須提供明確的視覺回饋(如載入指示器、進度條)。
- **狀態持久化 (Stateful)**: 主要視窗應記住其位置和大小,以便在下次啟動時恢復。
- **一致性 (Consistent)**: 所有視窗的佈局、行為和程式碼結構應保持一致。

## 2. 通用功能規格

- Copilot 必須先向開發者確認要建立的新的 form 名稱。以下一律以 `[FormName]` 代表。這個類別名稱也將用於設定檔中的區段名稱。
- 所有新表單都必須或建議實作以下描述的功能。

### 2.1. 設定檔檢查 (建議)
- **目的**: 檢查應用程式的設定檔是否存在,並讀取必要的設定值。
- **實作**:
  - 檢查專案中是否有 `Settings.ini` 這個檔案。如果沒有,請先建立它, 並將它在專案中設定為 '有更新時才複製'。
  - 若該檔案中沒有該表單的對應的設定,包括 `[FormName]` 區段下的 'PositionX', 'PositionY', 'Width', 'Height' 這四個值,則建立它們, 並使用預設值, 分別是 10, 10, 800, 600。然後存檔。
  - 該檔案中的區段名稱應與表單的類別名稱(`[FormName]`)相同,例如 `Form1`、`Form2`。請注意區段名稱的大小寫必須與類別名稱 (`[FormName]`) 完全一致,並不是固定為 `Form1`。
  - 應使用專案共用的 `IniFile.cs` 輔助類別來進行讀寫。

### 2.2. 基本視窗元件以及視窗狀態持久化 (強制)
- **目的**: 提升使用者體驗,記住使用者偏好的視窗佈局。
- **實作**:
  -  每個表單至少應包含以下元件:
    - `ToolStrip`:顯示應用程式的功能選單,固定在視窗的頂部。在這個 `ToolStrip` 中,將 BackColor 設定為'InactiveCaption',其它應包含至少一個 `ToolStripMenuItem`,其 Text 屬性為 'File'。
    - `StatusStrip`:顯示當前狀態資訊,固定在視窗的底部,將 BackColor 設定為 'ScrollBar'。在這個 `StatusStrip` 中,應包含一個 `ToolStripStatusLabel` 用於顯示狀態文字,以及一個 `ToolStripProgressBar` 用於顯示進度。
    - `Panel`: 作為主要內容區域,佔據視窗的中間部分,並提供自動滾動功能,同時將 BorderStyle 設定為 'FixedSingle'。如果使用者改變視窗大小,內容區域應自動調整,並維持與視窗的邊界距離 4 pixel (上、下、左、右)。
  - 在表單的 `Load` 事件中,從 `Settings.ini` 讀取並設定該表單的 `PositionX`, `PositionY`, `Width`, `Height`,以恢復上次使用者關閉視窗時的初始位置和大小。
  - 在表單的 `FormClosing` 事件中,將目前的位置和大小寫回對應的 `ini` 區段。

### 2.3. 非同步操作與 UI 回應 (強制)
- **目的**: 避免 UI 凍結,並告知使用者系統正在處理中。
- **實作**:
  - 任何可能耗時的操作(檔案讀寫、網路請求、大量資料處理)都必須使用 `async`/`await` 在背景執行緒中完成。
  - 在非同步任務開始前,應顯示載入指示器(例如,一個簡單的 `Form` 或面板)。
  - 若操作可回報進度,應使用 `IProgress<T>` 將進度從背景執行緒傳遞至 UI 執行緒,並更新 `ToolStripProgressBar` 等控制項。
  - 所有從背景執行緒更新 UI 的操作,都必須使用 `Control.Invoke` 或 `Control.BeginInvoke` 來確保執行緒安全。

### 2.4. 動態 UI 佈局 (建議)
- **目的**: 確保視窗在不同解析度和縮放比例下都能正確顯示。
- **實作**:
  - 建議透過覆寫 `OnLayout` 方法和處理 `Resize` 事件來程式化地計算和設定控制項的位置與大小。
  - 應定義一個 `LayoutControls()` 方法來集中處理所有佈局邏輯。
  - 避免過度依賴設計工具的 `Anchor` 和 `Dock` 屬性,除非佈局非常簡單。

### 2.5. 資料顯示效能 (適用時強制)
- **目的**: 在顯示大量資料時維持應用程式的效能。
- **實作**:
  - 當將大量項目加入 `ListView` 或類似控制項時,必須使用 `BeginUpdate()` 和 `EndUpdate()` 方法包圍新增操作。
  - 如果資料量極大(超過數千筆),應考慮實作資料分頁或虛擬化 (`Virtual Mode`)。
  - 可設定一個最大顯示資料筆數的常數(如 `MAXINUM_DATA_ROWS`),超出部分不予顯示並提示使用者。

## 3. 專案共用元件

新表單應優先使用以下已建立的共用元件:

- **`IniFile.cs`**: 用於所有 `.ini` 檔案的讀寫操作。
- **`ExcelInterops.cs`**: 專門用於讀寫 Excel 檔案,已包含非同步方法。
- **`Settings.ini`**: 應用程式的中央設定檔,用於儲存視窗狀態和全域設定。

## 4. 新表單開發指南 (給開發者與 Copilot)

當需要建立一個新的 Form 時,請遵循以下步驟:

1.  **建立新表單**: 在 Visual Studio 中加入新的 `Windows Form`。
2.  **實作狀態持久化**:
    - 訂閱 `Load` 和 `FormClosing` 事件。
    - 在 `Load` 事件中,使用 `IniFile` 讀取 `[FormName]` 區段的視窗位置和大小。請注意,區段名稱必須與表單類別名稱完全一致。
    - 在 `FormClosing` 事件中,使用 `IniFile` 將當前視窗位置和大小寫回 `[FormName]` 區段。請注意,區段名稱必須與表單類別名稱完全一致。
3.  **設計 UI 佈局**:
    - 建立 `LayoutControls()` 方法。
    - 覆寫 `OnLayout` 方法並在其中呼叫 `LayoutControls()`。
    - 訂閱 `Resize` 事件並在其中呼叫 `LayoutControls()`。
4.  **處理非同步任務**:
    - 將觸發長時間任務的事件處理常式(如按鈕點擊)宣告為 `async void`。
    - 在 `try...finally` 區塊中執行非同步方法。
    - 在 `try` 區塊開始時顯示載入指示器,在 `finally` 區塊中隱藏/關閉它。
    - 使用 `await` 呼叫非同步方法(如 `ExcelInterops.ReadExcelAsync`)。
    - 使用 `this.Invoke` 將資料綁定和 UI 更新的程式碼封裝起來,確保在 UI 執行緒上執行。
5.  **程式碼結構**:
    - 將常數(如 `MARGIN`)定義在類別頂部。
    - 將事件處理常式、私有方法和公有方法分區管理,可使用 `#region` 進行摺疊。

以下是另一個版本:

---
title: WinForms 視窗開發規範
author: [Johnny]
date: 2025-10-26
---

# WinForms 應用程式視窗開發規範

本文檔定義所有 Windows Forms 視窗(Form)基本架構、功能和開發流程標準,適用於新建表單及既有表單重構。所有開發工作必須遵循此規範,以確保程式碼一致性、易維護性及高品質使用者體驗。

---

## 1. 設計原則總覽

- **回應迅速 (Responsive):** UI 執行緒不可被長時間任務阻塞。
- **使用者友善 (User-Friendly):** 超過 500 毫秒的操作,必須有視覺回饋(載入指示器、進度條)。
- **狀態持久化 (Stateful):** 主要視窗需記住其位置與大小,下次啟動自動恢復。
- **一致性 (Consistent):** 佈局、行為、程式碼結構須標準化。
- **命名規則 (Naming Convention):** 控制項以功能前綴命名,如 `btnOK`、`lblStatus`、`pnlMain`,類別名稱需「首字大寫駝峰」法。
- **錯誤處理 (Exception Handling):** 非同步方法必須捕捉例外,將錯誤顯示於 `StatusStrip` 或統一日誌系統。
- **日誌記錄 (Logging):** 有日誌系統時,長耗時任務均需記錄開始及結束時點。

---

## 2. 通用功能規格

- Copilot 必須先向開發者確認要建立的新的 form 名稱。以下一律以 `[FormName]` 代表。這個類別名稱也將用於設定檔中的區段名稱。
- 所有新表單都必須或建議實作以下描述的功能。

### 2.1 設定檔檢查(強制)

- 檢查專案和執行目錄中是否有 `Settings.ini` 這個檔案。如果沒有,請先建立它, 並將它在專案中設定為 '有更新時才複製'。
- 若該檔案中沒有該表單的對應的設定,包括 `[FormName]` 區段下的 'PositionX', 'PositionY', 'Width', 'Height' 這四個值,則建立它們, 並使用預設值, 分別是 10, 10, 800, 600。然後存檔。
- 該檔案中的區段名稱應與表單的類別名稱(`[FormName]`)相同,例如 `Form1`、`Form2`。請注意區段名稱的大小寫必須與類別名稱 (`[FormName]`) 完全一致,並不是固定為 `Form1`。
- 請注意區段名稱大小寫必須完全對應這個新的 form 的 class 名稱。
- 應透過 `IniFile.cs` 類別讀寫。
- Copilot 需確認設定檔案的名稱是否正確為 `Settings.ini`。如果發現名稱是 `ini Settings.ini`, 應立即改回 `Settings.ini`。

### 2.2. 基本視窗元件與狀態持久化(強制)

- 這個 form 必須包含以下控制項:
  - `ToolStrip`:固定於頂部,並設定其 `BackColor = SystemColors.InactiveCaption`,其下至少必須包括一個 `ToolStripMenuItem`,其 `Text` 屬性必須為 `File`。
  - `StatusStrip`:固定於底部,並設定其 `BackColor = SystemColors.ScrollBar`,其下至少必須包括一個 `ToolStripStatusLabel`(顯示狀態)以及 `ToolStripProgressBar`(顯示進度)。
  - `Panel`:中間內容區,設定其 `BorderStyle = FixedSingle`、`AutoScroll = True`、與視窗四邊維持 `MARGIN = 4`。
- 在 `Load` 事件自動從 `Settings.ini` 讀取視窗位置和大小,在 `FormClosing` 事件寫回這些值。
- 建議於類別頂端設常數:
private const int MARGIN = 4;

### 2.3. 非同步操作與 UI 回應(強制)

- 長任務必須採用 `async/await` 指示詞以執行於背景執行緒。
- 任務前顯示標準載入指示器(如 `LoadingForm` 或 `ProcessingPanel`)。
- 有進度則使用 `IProgress<T>` 搭配前述建立的 `ToolStripProgressBar` 以顯示進度。
- UI 操作須 `Invoke`/判斷 `InvokeRequired` 確保執行緒安全。
- 範例:
```
private async void btnLoad_Click(object sender, EventArgs e)
{
    ShowLoadingIndicator();
    try
    {
        await LoadDataAsync();
    }
    catch (Exception ex)
    {
        statusLabel.Text = ex.Message;
    }
    finally
    {
        HideLoadingIndicator();
    }
}
```

### 2.4. 動態 UI 佈局(建議)

- 覆寫 `OnLayout`,集中在 `LayoutControls()` 方法計算/設定各控件大小位置。
- `Resize` 事件也呼叫 `LayoutControls()`,保證彈性恆定。
- 避免僅依賴設計器的 Anchor/Dock,除非佈局十分簡單。

### 2.5. 資料顯示效能(適用時強制)

- 大量資料加入 `ListView` 時,必用 `BeginUpdate()`/`EndUpdate()`。
- 超過千筆資料時優先考慮分頁(Paging)或虛擬模式(Virtual Mode)。
- 使用虛擬模式時必實作 `RetrieveVirtualItem` 事件,並設上限常數 `MAXIMUM_DATA_ROWS`,多於部分提示不顯示。

### 2.6. UI 無障礙支援(建議)

- 所有主要控件應設 `TabIndex`、`AccessibleName` 與 `ToolTip`。
- 建議自動設計 `ToolTip` 資源集中管理。

---

## 3. 專案共用元件

- **IniFile.cs**:UTF-8 支援,統一 ini 檔操作介面。
- **ExcelInterops.cs**:封裝 Excel 讀寫,支援 `async` 操作。
- **Settings.ini**:共用設定檔,所有 window 狀態與配置用。
- **Logger.cs**(如有):統一日誌記錄。

---

## 4. 新表單開發步驟

1. **新建表單**:在 Visual Studio 中加入新 Windows Form,類別名稱即開發者指定的名稱 (`[FormName]`)。同時加入相關檔案, 包括設計檔 (`.Designer.cs`)、程式碼檔 (`.cs`) 以及資源檔 (`.resx`)。
2. **實作狀態持久化**:
  - 註冊 `Load` 和 `FormClosing`。
  - 於 `Load` 用 `IniFile` 讀取及設定窗體初始定位與大小。
  - 於 `FormClosing` 寫回。
3. **設計 UI 佈局**:
  - 建立 `LayoutControls()` 用於全體控制項的位置調整。
  - 覆寫 `OnLayout`,並同步處理 `Resize`。
4. **非同步任務處理**:
  - 長任務事件處理器宣告為 `async void`。
  - `try...finally` 包裹,載入指示器於任務前顯示、結束後隱藏。
  - 用 `await` 呼叫共用元件如 `ExcelInterops.ReadExcelAsync`。
  - 使用 `Invoke` 將 UI 邏輯封裝於主執行緒執行。
5. **程式碼架構管理**:
  - 類別頂部定義常數。
  - 事件處理、私有、公有方法以 `#region` 分區管理。
6. **錯誤處理範例**:
  ```
  try
  {
      await SomeAsyncOperation();
  }
  catch (Exception ex)
  {
      statusLabel.Text = ex.Message;
      Logger.LogError(ex);
  }
  ```
7. **日誌記錄(如支援)**:
  - 長任務執行前、後均呼叫 `Logger.LogInfo(...)`。

---

## 5. Copilot 指令提示範例 (Copilot 請忽略此段)

- 依照 FormSpec.md 這份規格, 在專案裡增加一個名為 TestForm 的 form  
Implement a new Form named `TestForm` into the project, strictly adhering to all requirements detailed within `FormSpec.md`.
- 修改目前的 Form 以確實符合 FormSpec.md 文件中描述的規格  
Apply all requirements from FormSpec.md to the current Form implementation.

---

## 6. 程式碼範本(簡化)

```
public partial class MyForm : Form
{
    private const int MARGIN = 4;


    public MyForm()
    {
        InitializeComponent();
        this.Load += MyForm_Load;
        this.FormClosing += MyForm_FormClosing;
        this.Resize += (s, e) => LayoutControls();
    }

    private void MyForm_Load(object sender, EventArgs e)
    {
        var ini = new IniFile("Settings.ini");
        int x = ini.ReadInt("MyForm", "PositionX", 10);
        int y = ini.ReadInt("MyForm", "PositionY", 10);
        int w = ini.ReadInt("MyForm", "Width", 800);
        int h = ini.ReadInt("MyForm", "Height", 600);
        this.SetBounds(x, y, w, h);
    }

    private void MyForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        var ini = new IniFile("Settings.ini");
        ini.WriteInt("MyForm", "PositionX", this.Left);
        ini.WriteInt("MyForm", "PositionY", this.Top);
        ini.WriteInt("MyForm", "Width", this.Width);
        ini.WriteInt("MyForm", "Height", this.Height);
    }

    protected override void OnLayout(LayoutEventArgs e)
    {
        base.OnLayout(e);
        LayoutControls();
    }

    private void LayoutControls()
    {
        // 控件位置調整邏輯
    }
}
```

 


Dev 2Share @ 點部落