使用 Observer Pattern 重構複雜的資料上傳流程 - 以 PrimaryAPI 與 BackupService 整合為例
在後端開發中,我們常遇到需求不斷疊加,導致原本單純的類別(Class)變成了「上帝類別(God Class)」,職責混雜且難以測試。最近我在處理一個將資料上傳到 PrimaryAPI(主要資料平台)與 BackupService(備份服務)的功能時,就遇到了「堆疊式複雜度」的問題。本文將分享如何利用 Observer Pattern(觀察者模式)1 與 Mediator Pattern(中介者模式)2 來優化架構,達成高內聚低耦合的設計。
問題背景
原本的 StreamDataDriver 類別最初只負責「將資料分批上傳到 PrimaryAPI」。隨著業務需求增加,加入了以下邏輯:
- BackupService 資料備份:上傳 PrimaryAPI 的同時,要判斷資料是否符合 BackupService 格式。
- 寫入暫存檔:符合 BackupService 格式的資料要寫入暫存檔。
- 上傳 BackupService:PrimaryAPI 上傳完成後,要把暫存檔上傳到 BackupService 平台。
- 強順序性要求:如果 PrimaryAPI 上傳失敗,BackupService 流程必須全部中止,避免資料不一致。
這導致 StreamDataDriver 裡充滿了與 PrimaryAPI 無關的程式碼(寫檔、判斷 ID 格式、呼叫其他上傳服務),違反了 單一職責原則 (Single Responsibility Principle, SRP)3。
// 重構前的 God Class 示意
class StreamDataDriver {
execute() {
// 1. 讀取資料
stream.on('data', row => {
// 2. 處理 PrimaryAPI 上傳邏輯
buffer.push(row);
if (buffer.full) uploadToPrimary(buffer);
// 3. 處理 BackupService 邏輯 (混雜在一起!)
if (isBackupFormat(row)) {
writeToTempFile(row);
}
});
// 4. 等待所有流程結束,處理 BackupService 上傳
await Promise.all([primaryPromise, backupPromise]);
}
}
這樣的架構有幾個痛點:
- 測試困難:要測試 PrimaryAPI 上傳邏輯,還得 Mock BackupService 的相關依賴。
- 錯誤處理脆弱:並行執行下,很難精準控制「PrimaryAPI 失敗則 BackupService 不上傳」。
- 擴充性差:如果明天要多傳一個 S3,程式碼會變得更亂。
解決方案:Observer + Mediator
我們決定將架構拆解,引入設計模式來分離職責。
1. Observer Pattern (觀察者模式)
首先,我們將 StreamDataDriver 降級為單純的 Stream Driver(資料流驅動者)與 PrimaryAPI Uploader。它不再關心「誰要用這些資料」,只負責廣播「我處理到一筆資料囉」。
我們定義了一個明確的介面:
// 定義監聽器的合約
export interface IDataListener {
onSchemaDetermined(schemaInfo: SchemaInfo): void
onRowProcessed(row: Record<string, any>, transformedItem: IDataItem): void
}
接著實作 BackupService 的專屬邏輯 BackupFileBuilder:
// 專門負責 BackupService 邏輯的觀察者
export class BackupFileBuilder implements IDataListener {
onRowProcessed(row: any, item: any) {
// 判斷是否符合 BackupService 規格
if (this.canWrite(row)) {
this.writeBackupFile(row, item);
}
}
// ... 其他寫檔邏輯
}
在 StreamDataDriver 中,只需要一行程式碼通知大家:
class StreamDataDriver {
private listeners: IDataListener[] = [];
addListener(listener: IDataListener) {
this.listeners.push(listener);
}
private handleRow(row) {
// ... PrimaryAPI 處理邏輯 ...
// 通知所有聽眾,完全不管他們要做什麼
this.listeners.forEach(l => l.onRowProcessed(row, item));
}
}
2. Mediator Pattern (中介者模式)
雖然解耦了,但業務需求中的「強順序性(PrimaryAPI 成功 -> 才做 BackupService)」該由誰負責?這時候就需要一個 Mediator(中介者)。在我們的案例中,由最外層的 Action Class (TaskOrchestrator) 擔任。
class TaskOrchestrator {
async execute() {
// 1. 建立元件
const uploader = new StreamDataDriver();
const backupBuilder = new BackupFileBuilder();
// 2. 組裝 (註冊觀察者)
uploader.addListener(backupBuilder);
try {
// 3. 執行主流程 (PrimaryAPI 上傳)
await uploader.execute();
// 4. 控制流程:只有 PrimaryAPI 成功,才繼續 BackupService
// 從 Builder 取得準備好的暫存檔路徑
const result = await backupBuilder.finish();
if (result.count > 0) {
await backupUploader.execute(result.filePath);
}
} catch (err) {
// 5. 統一錯誤處理與資源清理
backupBuilder.cleanup();
throw err;
}
}
}
新舊架構比較
| 比較項目 | 舊架構 (Coupled / Parallel) | 新架構 (Decoupled / Sequential) |
|---|---|---|
| 責任歸屬 | 混亂。Class 同時負責上傳、邏輯判斷、寫檔。 | 單一職責。Uploader 管上傳;Builder 管資料準備;Action 管流程。 |
| 錯誤處理 | 脆弱。PrimaryAPI 失敗時難以中斷 BackupService 流程。 | 強健。由 Mediator 統一控制,失敗直接跳過後續步驟。 |
| 擴充性 | 低。新增功能需修改核心程式碼。 | 高。只需新增 Listener 即可。 |
| 測試難易度 | 難。依賴過多,Mock 困難。 | 易。各個元件可獨立單元測試。 |
技術反思:前端 vs 後端思維
身為前端開發者轉戰後端,可能會覺得這種設計「參數傳來傳去很麻煩」或是「為什麼不直接寫在裡面就好」。但這正是後端開發與前端元件化思維的異曲同工之妙:
- Listener 其實就像 React 的
props.onEvent或 Vue 的$emit。讓元件只負責發出訊號,而不負責處理後續。 - Dependency Injection (DI) 就像是把
logger或config當作 props 傳進元件,讓元件保持純粹,方便測試。
總結
這次重構展示了即使在業務邏輯強相依(Sequential constraint)的情況下,依然可以透過 Observer Pattern 實現程式碼層級的解耦。我們保留了 StreamDataDriver 作為資料流驅動者,利用 listeners 陣列保留擴充性,最後透過外層的 Mediator 來確保業務邏輯的執行順序。
這樣的架構不僅讓目前的程式碼更乾淨,也為未來可能新增的「S3 備份」、「Log 監控」等功能預留了極佳的擴充空間。
延伸閱讀:
- Design Patterns: Elements of Reusable Object-Oriented Software - Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) 於 1994 年出版的經典著作,定義了 23 種設計模式。
Reference
- Observer Pattern - Game Programming Patterns - Observer Pattern 是 Gang of Four 定義的 23 種設計模式之一,屬於行為型模式,定義物件間的一對多依賴關係。 ↩
- Mediator Pattern - Spring Framework Guru - Mediator Pattern 透過中介者物件封裝物件間的互動,促進鬆耦合設計。 ↩
- Single-responsibility principle - Wikipedia - Robert C. Martin 提出的 SOLID 原則之一,主張一個類別應該只有一個改變的理由。 ↩