實戰範例 007:狀態回歸:以簡單 Enum 取代繁瑣的狀態模式
簡介與核心精神
在物件導向設計 (Object-Oriented Design) 中,狀態模式 (State Pattern) 是大名鼎鼎的 GoF 23 種設計模式之一。它的初衷是為了解決當一個物件的行為取決於其狀態,且狀態切換邏輯過於複雜時,透過將每一種狀態封裝成獨立的類別 (Class) 來消除龐大的條件分支 (if-else 或 switch)。
然而,如同許多經典設計模式一樣,狀態模式在現代開發中經常被不加思索地濫用。Ponytail 的極簡主義哲學強調:「不要為了模式而模式。過早引入結構複雜的設計模式,只會讓簡單的問題變得難以理解。」
對於一個只有 3 到 4 種狀態,且狀態轉換規則固定的簡單業務邏輯(例如:訂單狀態、播放器狀態、文件審核狀態),強行套用狀態模式會導致「類別爆炸 (Class Explosion)」。開發者為了追蹤一個簡單的動作,不得不在十幾個檔案之間來回跳轉。本篇實戰範例將示範如何將這種過度設計的狀態模式,還原成清晰、易懂且集中管理的 Enumeration (列舉) 搭配 Switch 敘述。
實際場景描述
我們正在開發一個簡單的內容管理系統 (CMS)。其中的核心實體是一份「文章 (Document)」。文章的生命週期非常單純,只有三種狀態:
Draft(草稿)Reviewing(審核中)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}.`);
}
}
}
重構亮點分析
- 消滅類別爆炸:檔案數量從 5 個縮減為 1 個。沒有多餘的
State介面,也沒有分散在各處的實作類別。 - 全局視角的掌控感:當開發者想了解「
approve這個動作在哪些狀態下合法」,他們只需要看approve()方法裡面的 5 行程式碼。在原來的設計中,他們必須打開每一個狀態類別去檢查是否拋出例外。 - 無痛擴充新狀態:加入
ARCHIVED狀態變得極其輕鬆,只需要在 Enum 增加一個值,並在對應允許該動作的方法中加上一行 case。不需要去每個舊有的狀態類別中補寫無意義的樣板代碼。 - 完全消除循環依賴:原先各個狀態物件必須不斷
new其他狀態物件來進行切換,產生了複雜的記憶體參照與耦合。現在狀態只是一個輕量的字串列舉。
效益分析表格與解讀
透過「反模式化」的重構,我們以極簡的思維大幅提升了程式碼的工程品質。
| 評估維度 | 過度設計 (State Pattern) | 極簡設計 (Enum + Switch) | 改善程度 / 解讀 |
|---|---|---|---|
| 檔案數量 | 5+ 個檔案 | 1 個檔案 | 降低了尋找邏輯時的目錄跳轉負擔。 |
| 程式碼行數 (LOC) | 80+ 行 (充滿樣板代碼) | 40+ 行 (純粹核心邏輯) | 減少 50%。移除了無數的 implements DocumentState 與重複的 Error 拋出。 |
| 狀態流轉可視性 | 碎片化、難以追蹤全局 | 集中化、一目了然 | 將行為集中在對應的方法中,符合人類的直覺思維模型。 |
| 模組間依賴 | 嚴重的循環依賴 (Circular) | 零依賴 | Document 類別完全獨立自主,不依賴外部的狀態管理類別。 |
| 擴充成本 | 高 (需修改所有既有類別) | 極低 (僅增加 Enum 項目與 Case) | 實踐了真正的敏捷開發,能快速反應業務需求的變化。 |
結論總結
設計模式是前人智慧的結晶,但絕非放諸四海皆準的萬靈丹。Ponytail 哲學的核心在於「適才適所」。對於一個簡單的有限狀態機,最古老、最基礎的 Switch/Case 往往才是最合適的工具。它提供了無可比擬的直觀性與集中性。
下次當你想在簡單的 CRUD 應用中套用各種高大上的設計模式時,不妨停下來問問自己:「我真的需要這麼複雜嗎?還是我只是想展現我懂這個模式?」 回歸簡單,才是最高級的工程智慧。