跳到主內容

Theme / v5.3.3

Ponytail

讓 AI 編碼代理像最懶的資深工程師

實戰範例

實戰範例 007:狀態回歸:以簡單 Enum 取代繁瑣的狀態模式

探討為何在簡單場景下,狀態模式 (State Pattern) 是一種過度設計,並示範如何用列舉 (Enum) 與 Switch 敘述寫出更易懂的狀態機。

實戰範例 007:狀態回歸:以簡單 Enum 取代繁瑣的狀態模式

簡介與核心精神

在物件導向設計 (Object-Oriented Design) 中,狀態模式 (State Pattern) 是大名鼎鼎的 GoF 23 種設計模式之一。它的初衷是為了解決當一個物件的行為取決於其狀態,且狀態切換邏輯過於複雜時,透過將每一種狀態封裝成獨立的類別 (Class) 來消除龐大的條件分支 (if-else 或 switch)。

然而,如同許多經典設計模式一樣,狀態模式在現代開發中經常被不加思索地濫用。Ponytail 的極簡主義哲學強調:「不要為了模式而模式。過早引入結構複雜的設計模式,只會讓簡單的問題變得難以理解。」

對於一個只有 3 到 4 種狀態,且狀態轉換規則固定的簡單業務邏輯(例如:訂單狀態、播放器狀態、文件審核狀態),強行套用狀態模式會導致「類別爆炸 (Class Explosion)」。開發者為了追蹤一個簡單的動作,不得不在十幾個檔案之間來回跳轉。本篇實戰範例將示範如何將這種過度設計的狀態模式,還原成清晰、易懂且集中管理的 Enumeration (列舉) 搭配 Switch 敘述。

實際場景描述

我們正在開發一個簡單的內容管理系統 (CMS)。其中的核心實體是一份「文章 (Document)」。文章的生命週期非常單純,只有三種狀態:

  1. Draft (草稿)
  2. Reviewing (審核中)
  3. Published (已發佈)

可以執行的動作也只有三個:submit()、approve() 與 reject()。

一位剛讀完《設計模式》的熱血工程師,認為這是一個應用「狀態模式」的完美時機。他建立了一個抽象的 State 介面,然後為每個狀態實作了獨立的類別。結果,原本只需要幾行程式碼的邏輯,被拆分成了 5 個不同的檔案,充滿了樣板代碼 (Boilerplate) 與循環依賴。

原始的過度設計代碼

讓我們欣賞這段極度「優雅」但也極度難懂的狀態模式實作。

// 檔案:src/models/document/DocumentState.ts
export interface DocumentState {
  submit(document: DocumentContext): void;
  approve(document: DocumentContext): void;
  reject(document: DocumentContext): void;
}

// 檔案:src/models/document/states/DraftState.ts
import { DocumentState } from '../DocumentState';
import { ReviewingState } from './ReviewingState';
import { DocumentContext } from '../DocumentContext';

export class DraftState implements DocumentState {
  submit(document: DocumentContext): void {
    console.log("Submitting document for review...");
    document.setState(new ReviewingState());
  }

  approve(document: DocumentContext): void {
    throw new Error("Cannot approve a draft document.");
  }

  reject(document: DocumentContext): void {
    throw new Error("Cannot reject a draft document.");
  }
}

// 檔案:src/models/document/states/ReviewingState.ts
import { DocumentState } from '../DocumentState';
import { PublishedState } from './PublishedState';
import { DraftState } from './DraftState';
import { DocumentContext } from '../DocumentContext';

export class ReviewingState implements DocumentState {
  submit(document: DocumentContext): void {
    throw new Error("Document is already under review.");
  }

  approve(document: DocumentContext): void {
    console.log("Document approved. Publishing...");
    document.setState(new PublishedState());
  }

  reject(document: DocumentContext): void {
    console.log("Document rejected. Returning to draft.");
    document.setState(new DraftState());
  }
}

// 檔案:src/models/document/states/PublishedState.ts
import { DocumentState } from '../DocumentState';
import { DocumentContext } from '../DocumentContext';

export class PublishedState implements DocumentState {
  submit(document: DocumentContext): void {
    throw new Error("Document is already published.");
  }

  approve(document: DocumentContext): void {
    throw new Error("Document is already published.");
  }

  reject(document: DocumentContext): void {
    throw new Error("Cannot reject a published document.");
  }
}

// 檔案:src/models/document/DocumentContext.ts
import { DocumentState } from './DocumentState';
import { DraftState } from './states/DraftState';

export class DocumentContext {
  private state: DocumentState;

  constructor(public id: string, public content: string) {
    this.state = new DraftState(); // 初始狀態
  }

  setState(state: DocumentState): void {
    this.state = state;
  }

  // 將動作委託給內部狀態物件
  submit(): void {
    this.state.submit(this);
  }

  approve(): void {
    this.state.approve(this);
  }

  reject(): void {
    this.state.reject(this);
  }
}

這段程式碼雖然完全符合 GoF 狀態模式的定義,但它帶來了巨大的維護成本。如果我們只想知道「在 Reviewing 狀態下呼叫 approve 會發生什麼事」,我們必須打開三個以上的檔案追蹤邏輯。更糟糕的是,這種設計引入了嚴重的類別間循環依賴 (Circular Dependency) —— 狀態類別互相知道彼此的存在,破壞了模組的獨立性。

開發者與 AI 的對話記錄

當團隊試圖為這個文件系統增加一個新的「封存 (Archived)」狀態時,開發者崩潰了,轉而向 AI 求助。

開發者:

「AI,我們需要為 Document 加上一個『Archived』狀態。我現在必須去修改 DocumentState 介面,然後在 DraftState、ReviewingState 和 PublishedState 裡面全部加上新的方法實作,即便它們大部分都會拋出 Error。這改動範圍也太大了吧!狀態模式不是應該『符合開閉原則 (OCP)』嗎?」

AI (Ponytail 模式):

「這就是為什麼在簡單的狀態流轉中,狀態模式往往是一種反模式 (Anti-pattern)。

狀態模式在狀態極多、且每個狀態的行為邏輯異常複雜(例如:包含各種不同的演算法或策略)時才有用。但你們的文章狀態只有三四種,動作也只有三個。你為了封裝幾行簡單的條件判斷,製造了五個類別和一堆重複的例外拋出。這完全違背了極簡主義。」

開發者:

「可是大學教授和設計模式的書上都說,用 Switch case 來寫狀態機是不優雅的,違反了物件導向的精神啊!」

AI (Ponytail 模式):

「這是一種教條主義。對於一個狀態轉換明確的有限狀態機 (FSM),一個集中管理的 Enum 加上 Switch 結構,反而能提供最完美的『全局視角 (Global View)』。

在單一檔案中,你可以一眼看完所有狀態的流轉規則。沒有類別爆炸,沒有循環依賴,增加新狀態只需要加一個 Enum 和幾個 case。讓我們把這些分散的碎片拼回去,回歸最簡單直白的設計。」

重構與優化:集中管理的簡單狀態機

我們將捨棄所有多餘的狀態類別與介面,將邏輯全部收攏回 Document 實體本身。利用 TypeScript 的 Enum 與 Switch,我們打造出一個防呆、清晰且集中管理的微型狀態機。

// 檔案:src/models/document/Document.ts

// 1. 定義簡單明確的列舉
export enum DocumentStatus {
  DRAFT = 'DRAFT',
  REVIEWING = 'REVIEWING',
  PUBLISHED = 'PUBLISHED',
  ARCHIVED = 'ARCHIVED' // 輕鬆新增的狀態
}

export class Document {
  // 將狀態以單一變數儲存
  public status: DocumentStatus;

  constructor(public id: string, public content: string) {
    this.status = DocumentStatus.DRAFT;
  }

  // 2. 集中管理動作與狀態轉換規則
  public submitForReview(): void {
    // 邊界與安全檢查 (Agent Reach 防呆精神)
    if (!this.content || this.content.trim() === '') {
      throw new Error("Cannot submit an empty document.");
    }

    switch (this.status) {
      case DocumentStatus.DRAFT:
        console.log("Submitting document for review...");
        this.status = DocumentStatus.REVIEWING;
        break;
      default:
        throw new Error(`Invalid action: Cannot submit document from status ${this.status}.`);
    }
  }

  public approve(): void {
    switch (this.status) {
      case DocumentStatus.REVIEWING:
        console.log("Document approved. Publishing...");
        this.status = DocumentStatus.PUBLISHED;
        break;
      default:
        throw new Error(`Invalid action: Cannot approve document from status ${this.status}.`);
    }
  }

  public reject(): void {
    switch (this.status) {
      case DocumentStatus.REVIEWING:
        console.log("Document rejected. Returning to draft.");
        this.status = DocumentStatus.DRAFT;
        break;
      default:
        throw new Error(`Invalid action: Cannot reject document from status ${this.status}.`);
    }
  }

  public archive(): void {
    // 新增動作變得極為簡單,邏輯集中
    switch (this.status) {
      case DocumentStatus.PUBLISHED:
      case DocumentStatus.DRAFT:
        console.log("Archiving document...");
        this.status = DocumentStatus.ARCHIVED;
        break;
      default:
        throw new Error(`Invalid action: Cannot archive document from status ${this.status}.`);
    }
  }
}

重構亮點分析

  1. 消滅類別爆炸:檔案數量從 5 個縮減為 1 個。沒有多餘的 State 介面,也沒有分散在各處的實作類別。
  2. 全局視角的掌控感:當開發者想了解「approve 這個動作在哪些狀態下合法」,他們只需要看 approve() 方法裡面的 5 行程式碼。在原來的設計中,他們必須打開每一個狀態類別去檢查是否拋出例外。
  3. 無痛擴充新狀態:加入 ARCHIVED 狀態變得極其輕鬆,只需要在 Enum 增加一個值,並在對應允許該動作的方法中加上一行 case。不需要去每個舊有的狀態類別中補寫無意義的樣板代碼。
  4. 完全消除循環依賴:原先各個狀態物件必須不斷 new 其他狀態物件來進行切換,產生了複雜的記憶體參照與耦合。現在狀態只是一個輕量的字串列舉。

效益分析表格與解讀

透過「反模式化」的重構,我們以極簡的思維大幅提升了程式碼的工程品質。

評估維度 過度設計 (State Pattern) 極簡設計 (Enum + Switch) 改善程度 / 解讀
檔案數量 5+ 個檔案 1 個檔案 降低了尋找邏輯時的目錄跳轉負擔。
程式碼行數 (LOC) 80+ 行 (充滿樣板代碼) 40+ 行 (純粹核心邏輯) 減少 50%。移除了無數的 implements DocumentState 與重複的 Error 拋出。
狀態流轉可視性 碎片化、難以追蹤全局 集中化、一目了然 將行為集中在對應的方法中,符合人類的直覺思維模型。
模組間依賴 嚴重的循環依賴 (Circular) 零依賴 Document 類別完全獨立自主,不依賴外部的狀態管理類別。
擴充成本 高 (需修改所有既有類別) 極低 (僅增加 Enum 項目與 Case) 實踐了真正的敏捷開發,能快速反應業務需求的變化。

結論總結

設計模式是前人智慧的結晶,但絕非放諸四海皆準的萬靈丹。Ponytail 哲學的核心在於「適才適所」。對於一個簡單的有限狀態機,最古老、最基礎的 Switch/Case 往往才是最合適的工具。它提供了無可比擬的直觀性與集中性。

下次當你想在簡單的 CRUD 應用中套用各種高大上的設計模式時,不妨停下來問問自己:「我真的需要這麼複雜嗎?還是我只是想展現我懂這個模式?」 回歸簡單,才是最高級的工程智慧。