實戰背景
「上帝函式 (God Function)」是 legacy code 最常見的痛點之一:一個函式同時做太多件事,任何修改都會引發連鎖風險。本範例示範如何用 OMO 的多代理協同,把一個 200 行的 processOrder 拆成三個聚焦函式,並由架構代理 Prometheus 在事前與事後把關影響範圍。
痛點與問題描述:
我們的訂單服務有這樣一個函式:
// src/services/orderService.ts(簡化示意)
export async function processOrder(order: Order): Promise<OrderResult> {
// ── 區塊 A:欄位驗證 ──
if (!order.items?.length) throw new ValidationError("empty items");
if (order.totalCents < 0) throw new ValidationError("negative total");
// ... 約 50 行驗證
// ── 區塊 B:資料庫持久化 ──
const conn = await db.connect();
try {
await conn.query("BEGIN");
await conn.query("INSERT INTO orders ...");
// ... 約 80 行持久化
await conn.query("COMMIT");
} catch (e) {
await conn.query("ROLLBACK");
throw e;
}
// ── 區塊 C:通知(Email / Slack / Webhook)──
await email.send(order.customerEmail, "訂單成立");
await slack.notify("#orders", `新訂單 ${order.id}`);
// ... 約 70 行通知
return { ok: true };
}
三個關注點全擠在一起,重構時最大的風險不是「拆不開」,而是「拆完之後沒發現某個呼叫端依賴了原本的副作用」。我們讓 Prometheus 來回答這個問題。
規格定義
本次變更計畫的 ID 為 omo-split-process-order:
# 實戰範例 010:openspec/changes/omo-split-process-order/specs/order-service/spec.md
## MODIFIED Requirements
### Requirement: Separated order processing concerns
`processOrder` 必須(SHALL)僅負責流程編排,實際工作必須(SHALL)委派給三個獨立函式:
`validateOrder`、`persistOrder`、`notifyOrder`。
#### Scenario: Behavior preserved after split
- **WHEN** 執行既有訂單整合測試套件
- **THEN** 所有既有測試必須(SHALL)在不修改呼叫端的前提下通過。
- **AND** 三個新函式必須(SHALL)各自有獨立的回歸測試。
端到端 8 輪 AI 協作紀錄
── 第 1 輪:Prometheus 做 inbound 影響分析 ──
- You:
幫我釐清 processOrder 在 codebase 裡被誰呼叫,以及這些呼叫端有沒有依賴它的副作用。 - AI:
我派出 Prometheus 做架構層級的 inbound 分析,沿著呼叫圖回溯
processOrder的所有呼叫端。Prometheus 找到的 inbound 呼叫路徑:
processOrder ← CheckoutController.placeOrder (src/api/checkout.ts:42) processOrder ← ReplayJob.replayStaleOrder (src/jobs/replay.ts:88) processOrder ← TestHelper.seedOrder (tests/helpers/order.ts:15)Prometheus 的判讀:
CheckoutController與ReplayJob都只消費OrderResult,沒有讀取processOrder內部的通知副作用。TestHelper.seedOrder直接呼叫 email / slack mock,所以通知邏輯抽到獨立函式後,這個 helper 必須一起更新。
結論:這次拆分安全,唯一要連動修改的是
TestHelper.seedOrder。
── 第 2 輪:起草 OpenSpec 提案 ──
- You:
/opsx:propose "將 processOrder 拆成 validateOrder / persistOrder / notifyOrder,並補回歸測試" - AI:
提案
omo-split-process-order已建立。- proposal.md:記錄 Prometheus 的影響分析結論,標明
TestHelper.seedOrder為連動修改點。 - specs/order-service/spec.md:定義三個新函式的職責與驗收 scenario。
- tasks.md:拆成 4 個任務(拆出 3 個函式 + 補 3 套回歸測試 + 更新 helper)。
輸入
/opsx:apply即可讓 OMO 開始實作。 - proposal.md:記錄 Prometheus 的影響分析結論,標明
── 第 3 輪:啟動 Team Mode 三代理 ──
- You:
/opsx:apply - AI:
我以 3 個 worker 啟動 Team Mode,把 4 個任務分配出去:
任務分派:
Worker: impl-1 (Sisyphus)── 抽出validateOrder與persistOrder,掛回processOrder編排。Worker: impl-2 (Sisyphus)── 抽出notifyOrder,更新TestHelper.seedOrder。Worker: tests (Athena)── 為三個新函式各補 3 個回歸測試。
因為三個 worker 都會動到
src/services/orderService.ts,Hash-Anchored Edit 控制器已上線。
── 第 4 輪:觀察 Hash-Anchored Edit 在多方寫入時的行為 ──
- You:
報告 orderService.ts 上目前的鎖定狀況。 - AI:
三個 worker 同時要動同一個檔案,鎖調度特別關鍵。
Hash-Anchored Edit 目前在
src/services/orderService.ts上的鎖定分佈:- 區塊
validateOrder(第 12-58 行)→ 鎖主Worker: impl-1 - 區塊
persistOrder(第 60-140 行)→ 鎖主Worker: impl-1 - 區塊
notifyOrder(第 142-205 行)→ 鎖主Worker: impl-2 - 區塊
processOrder編排(第 207-220 行)→ queue,等 impl-1 完成後交接
Athena 想在檔尾新增
export語句,但發現它依賴 impl-1 還沒寫完的validateOrder宣告,於是自動排在編排區塊之後。零強制覆蓋。 - 區塊
── 第 5 輪:Athena 平行補回歸測試 ──
- You:
Athena 那邊在寫什麼測試? - AI:
Athena 在
tests/services/orderService.test.ts補了三組回歸測試,這裡展示validateOrder的部分:describe("validateOrder", () => { it("rejects empty items", () => { expect(() => validateOrder({ ...baseOrder, items: [] })) .toThrow(ValidationError); }); it("rejects negative total", () => { expect(() => validateOrder({ ...baseOrder, totalCents: -1 })) .toThrow(ValidationError); }); it("passes a well-formed order", () => { expect(() => validateOrder(baseOrder)).not.toThrow(); }); });persistOrder與notifyOrder各有對應的事務回滾與通知失敗重試測試。
── 第 6 輪:合併與全測試 ──
- You:
合併三個 worker 的成果,然後跑全測試。 - AI:
三個 worker 的變更已合併回主分支,因為全程使用 Hash-Anchored Edit,這次合併零衝突。
✔ orderService - validateOrder rejects empty items ✔ orderService - validateOrder rejects negative total ✔ orderService - persistOrder rolls back on db error ✔ orderService - notifyOrder retries on slack failure ... (共 18 tests) Tests: 18 passed, 18 total.包含
TestHelper.seedOrder在內的所有既有測試也全數通過,印證 Prometheus 在第 1 輪的影響分析正確。
── 第 7 輪:Prometheus 跑 outbound 影響驗證 ──
- You:
拆出來的三個新函式,有沒有被意外外露到不該外露的地方? - AI:
再派 Prometheus 做一次 outbound 分析,沿著呼叫圖往下游追
notifyOrder的對外依賴。Prometheus 找到的 outbound 呼叫路徑:
notifyOrder → email.send (vendor) notifyOrder → slack.notify (vendor) notifyOrder → webhook.retry (vendor, with backoff)三個新函式的對外依賴都落在預期的 vendor 邊界,沒有意外外露到其他業務模組。封裝乾淨。
── 第 8 輪:歸檔與重構收益 ──
- You:
收尾,並總結這次重構的收益。 - AI:
Archive Complete
計畫
omo-split-process-order已歸檔。指標 重構前 重構後 processOrder行數200 行 14 行(純編排) 單一函式最大認知複度 高(混三種關注) 低(各自單一職責) 該檔案回歸測試數 4 18 合併衝突 N/A 0(Hash-Anchored Edit 保護)
關鍵學習點
- 重構前先派 Prometheus 做 inbound 分析,可以事前抓出像
TestHelper.seedOrder這類「隱性連動點」,避免事後才發現呼叫端壞掉。 - 上帝函式拆分時,Hash-Anchored Edit 的價值在多 worker 同檔案寫入。即使三個 worker 都改
orderService.ts,鎖調度讓合併依舊零衝突。 - 事後的 outbound 分析跟事前一樣重要。確認新函式的對外依賴落在預期邊界,等於封裝驗收。
- OpenSpec 提案在這種規模才真正發揮作用。範例 009 的小工具函式可以略過,但拆分上帝函式這種牽一髮動全身的變更,先把
proposal.md與tasks.md寫清楚,能讓多代理的分工有明確依據。