時間:清晨 05:30。地點:Codetopia 控制中心「灰度沙箱」。
窗外的暴雨剛歇,控制中心裡卻瀰漫著比濕氣更凝重的低氣壓。昨晚靠著 Ada 與 Hugo 的直譯器(Day 24),城市算是從淹水的邊緣被拉了回來,暫時止血。但黎明前最深的黑暗,卻在用來模擬演練的「灰度沙箱」中炸開了。
砰! 一聲刺耳的警報,劃破了所有值班工程師的睡意。
一支名為 OrdinanceService
的核心服務,在壓力測試下轟然崩潰。這傢伙簡直是市府裡的「萬能超人」,一手包辦了「載入防災條例」、「彙整審計紀錄」、「向市民廣播」、「預檢版本回滾」……幾乎所有跟決策下達有關的雜事。結果就是,四個不同局處的團隊,在暴雨夜為了救災同時修改了它,然後……大家就在彼此的程式碼上華麗地踩了過去。
值班官員臉色慘白,對著監控螢幕喃喃自語:「沒道理啊……為什麼只是改個廣播通知的文字模板,會把『時光局』的回滾預檢系統也一起弄掛了?這兩件事有什麼關係?」
就在這時,控制室的門被推開。走進來的是憲章起草官 Soren,他面無表情,手上拿著一張紅色便利貼,啪地一聲,貼在了 OrdinanceService
的監控螢幕上。
上面只有一行字:「一個模組,只能有一個改變它的理由。」
緊接著,擴展審核官 Octavia 拿著白板筆,在旁邊的白板上迅速畫出一個核心穩固、周邊滿是插槽的架構圖。「各位,別再往房子內牆上鑽洞了。」她說道,聲音清脆而堅定,「我們應該把所有可能變動的東西,全部拉到邊界去。讓城市的核心『對修改關閉』,但永遠『對擴展開放』。」
今天的任務很明確:在不推翻前兩天英雄們(Ada, Hugo, Yuki, Atlas)心血的前提下,為 Codetopia 立下憲法第一、二條。用 S (SRP) 與 O (OCP) 原則,讓這座城市的程式碼,學會自己守法。
SRP|Single Responsibility Principle(單一職責原則):一個類別或模組,應該只有一個導致其變更的理由(也就是只有一個「變動軸心」)。
OCP|Open-Closed Principle(開放封閉原則):軟體實體(類別、模組、函數等)應該對擴展開放,但對修改封閉。白話文:歡迎你來加 new feature,但不准你動我的 source code。
Change Axis(變動軸心):驅使某個軟體單元發生變更的單一來源。可能是來自需求、政策、資料來源,甚至是輸出介面的改變。
Extension Point(擴展點):在不修改核心程式碼的前提下,允許外部透過介面、事件、組態、或模板鉤子來擴充系統行為的邊界。
讓我們回到 OrdinanceService
崩潰的瞬間,近距離聞聞那幾股讓值班官員快要窒息的「程式碼壞味道」。
神物件 OrchestraService
(數位八爪章魚):這傢伙的職責多到令人髮指,它的觸手深入城市管理的各個角落,最後把自己打了個死結。
class OrchestraService:
# 一把抓:載入規則、求值、下命令、廣播、審計、回滾預檢...
def handle_event(self, event):
rules = self._load_rules_from_db() # 變動軸心 ①:規則怎麼存?
commands = self._evaluate(rules, event) # 變動軸心 ②:決策邏輯怎麼算?
self._broadcast_to_citizens(commands) # 變動軸心 ③:廣播格式/通路?
self._snapshot_if_risky(commands) # 變動軸心 ④:何時備份?(跟時光局搶飯碗)
self._aggregate_audit_log(commands) # 變動軸心 ⑤:稽核日誌要記啥?
壞味道:五個完全不同的「變動軸心」被強行綑綁。改廣播模板的工程師,得跟調整資料庫連線的 DBA 一起開協調會,然後所有人一起等著重新部署這整坨龐然大物。太慘了。
擴展全靠 if/elif
:每次市府推行新版條例,工程師就得在核心邏輯裡多加一條 elif
,像是在房子的主樑上多釘一根釘子。
if provider == "city.law.v1":
# ... v1 版本的邏輯
elif provider == "city.law.v2":
# ... v2 版本的邏輯
# 下個月還有 v3,再下個月還有 v2.1-hotfix... 這棟樓遲早要塌。
資料模型夾帶副作用:用來求值的規則物件,內部竟然硬塞了廣播訊息的模板、審計紀錄的格式。這導致任何輸出層的視覺調整,都可能污染到最核心的決策引擎。
回滾鍵直通內臟:某些緊急修復,為了求快,竟然直接呼叫 OrchestraService
的內部私有欄位來覆寫狀態,完全繞過了我們在 Day 20/21 辛苦建立的狀態機和 SOP 流程。這是越權,更是災難的根源。
Soren 和 Octavia 的計畫,就像一場外科手術結合都市更新,精準而優雅。
S:先動刀,斬斷變動軸心 (by Soren)
Soren 的手術刀,就是「單一職責原則」。他對著 OrchestraService
這隻數位章魚,精準地切下了四刀,分離出四個權責清晰的獨立部門:
PolicyIO
:專職政策的讀取與版本管理。
PolicyEval
:專職政策的求值與決策。
CommandEmitter
:專職命令的生成與廣播派送。
AuditTrail
:專職稽核紀錄的彙整。
並且,Soren 為每個新部門都寫下了一句不容置喙的單句使命宣言 (Single-Sentence Mission Statement)。例如:
PolicyEval
的使命是:「作為一個無副作用的純函數,它僅負責:『給定一份解析後的條例 (AST) 與當前城市情境 (Context),產出一系列標準化的命令 (Command[])』。」不多也不少,這就是它的全世界。
O:再規劃,把變化推向擴展點 (by Octavia)
如果說 Soren 是外科醫生,那 Octavia 就是建築師。她為這些新部門設計了面向未來的「擴充槽」:
在 PolicyIO
身上,她設計了一個 OrdinanceProvider
介面。未來不管是 v2 條例、還是從雲端來的條例,只需要寫一個新的 Provider 來實現這個介面,PolicyIO
核心完全不用改動。
在 CommandEmitter
身上,她徹底劃清了**「渲染格式」與「傳輸通路」**的職責,設計了 BroadcastTemplate
和 ChannelAdapter
兩個介面。要改廣播格式?實作一個新的 Template。要新增推播通路?寫個新的 Adapter。核心發送邏輯,一字不動。
在 PolicyEval
身上,她甚至設計了 ActionPlugin
擴展點。未來如果需要新增一種從未有過的防災動作(例如 ThrottleCrowd
人流管制),只需要寫一個新的 Plugin,就能無痛地接入決策引擎。
護欄:契約測試官 Pax 的進場
最後,契約測試官 Pax 登場,他為 Octavia 設計的每一個擴展點都撰寫了嚴格的契約測試。確保所有插件都必須遵守規範,並用金樣比對 (Golden Master Testing) 鎖定核心行為。他還額外頒布了安全與資源護欄:
「所有第三方插件,未來都必須在受限制的沙箱中執行,並設定時間與記憶體上限。只有經過官方審核簽章的 Plugin/Adapter 才能被載入系統。」
這確保了城市在不斷擴展的過程中,穩定性與安全性都不會受到任何影響。
導播,鏡頭拉一下!讓我們從三個不同尺度,看看這場「城市憲法」改革是如何重塑 Codetopia 的架構的。
視角 | 觀念/模式 | 在 Codetopia 的說法 |
---|---|---|
微觀(原則) | SRP / OCP | 類別/模組以單一變動軸心切分;擴展功能透過介面/事件/鉤子等標準插槽進入,不碰核心。 |
中觀(EIP/EDA) | Plugin/Strategy + Template Hooks | 規則求值核心管線(Pipeline)固定不變;所有變化,都以外掛(Provider/Plugin/Hook)的形式從外部注入。 |
宏觀(MAS) | Capability Registry + Governance | 以「能力登錄中心+擴展審核流程」來治理所有擴展;任何破壞性變更,都必須通過正式的治理流程。 |
Mermaid|微觀:能力切分與擴展點
Mermaid|中觀:不改核心,增一種動作/來源/通路
這就是 Soren 和 Octavia 聯手起草,並由 Pax 加上層層護欄的城市憲法,轉譯成程式碼後的樣貌:
import time
import random
from dataclasses import dataclass
from typing import List, Dict, TypedDict, Optional, Any, Tuple
from enum import Enum
# --- -1. 可觀測性 & 異常 ---
class TransientError(Exception): pass
def now_ms() -> int: return int(time.time() * 1000)
def metrics(**kwargs: Any) -> None: print(f"[METRICS] {kwargs}") # 模擬指標系統
# --- 0. 契約 (結構化型別與版本治理) ---
@dataclass
class OrdinancePayload:
version: str; text: str; hash: str; source: str; fetched_at: float
class CommandType(str, Enum):
BROADCAST = "Broadcast"
GATE_LIMIT = "GateLimit"
class Command(TypedDict):
type: CommandType
version: int
payload: Dict
def _coerce_command_type(t: Any) -> CommandType:
"""兼容 enum 與 str;若為未知值將丟出 ValueError"""
if isinstance(t, CommandType):
return t
if isinstance(t, str):
return CommandType(t)
raise ValueError(f"Invalid command type: {t!r}")
def validate_command(cmd: Command) -> None:
"""在邊界校驗指令:型別兼容、更清晰的錯誤"""
cmd["type"] = _coerce_command_type(cmd.get("type")) # 允許 str → Enum
assert isinstance(cmd.get("version"), int) and cmd["version"] >= 1, "Invalid command version"
assert isinstance(cmd.get("payload"), dict), "Command payload must be a dict"
# --- 1. 介面 (由 Octavia 設計的穩定邊界) ---
class OrdinanceProvider:
def fetch(self, version: str) -> Optional[OrdinancePayload]: ...
class ActionPlugin:
order: int = 100
def apply(self, context: dict, commands: List[Command]) -> List[Command]: ...
class BroadcastTemplate:
def render(self, command: Command) -> str: ...
class ChannelAdapter:
def send(self, rendered_message: str, *, key: Optional[str] = None) -> None: ...
class PushAdapter(ChannelAdapter):
"""與正文示例相符的最小推播通路;實務中以外部 SDK 實作"""
def __init__(self, client, max_retries: int = 3):
self.client = client
self.max_retries = max_retries
self._sent_keys: set[str] = set()
def send(self, rendered_message: str, *, key: Optional[str] = None) -> None:
# 簡單幂等去重;實務建議落地到外部 KV/訊息中介
if key and key in self._sent_keys:
return
for attempt in range(self.max_retries):
try:
self.client.push(rendered_message) # 可能丟 TransientError
if key:
self._sent_keys.add(key)
return
except TransientError:
time.sleep(0.05 * (2 ** attempt) + random.random() * 0.01)
raise RuntimeError("Push delivery failed after retries")
class AuditTrail:
def record(self, *, context: dict, ast_hash: str, commands: List[Command], notes: str = "") -> None: ...
# --- 2. 核心 (由 Soren 劃清權責,對修改關閉) ---
class PolicyIO:
def __init__(self, provider: OrdinanceProvider, audit: AuditTrail):
self._provider = provider
self._audit = audit
def load(self, version: str, fallback_version: str = "stable"):
payload = self._provider.fetch(version)
note = ""
if not payload:
note = f"fetch failed for {version}, falling back to {fallback_version}"
metrics(component="PolicyIO", event="fetch_failed", version=version, fallback=True)
payload = self._provider.fetch(fallback_version)
if not payload: raise RuntimeError(f"Fallback version {fallback_version} also failed.")
ast = parse_to_ast(payload.text)
# 讓 context 可追蹤實際生效的版本
context = {"effective_policy_version": payload.version}
self._audit.record(context=context, ast_hash=payload.hash, commands=[], notes=note)
return ast, context
class PolicyEval:
def __init__(self, plugins: List[ActionPlugin], audit: Optional[AuditTrail] = None, budget_ms: int = 100):
self._plugins = sorted(plugins, key=lambda p: getattr(p, "order", 100))
self._audit = audit
self._budget_ms = budget_ms
def eval(self, ast: Dict, context: dict) -> List[Command]:
started = now_ms()
cmds = eval_ast(ast, context)
skipped = 0
for p in self._plugins:
if now_ms() - started > self._budget_ms:
skipped += 1
continue
cmds = p.apply(context, cmds)
if skipped > 0:
metrics(component="PolicyEval", skipped_plugins=skipped, latency_ms=now_ms()-started)
final_cmds = self._resolve_conflicts(cmds)
if self._audit:
self._audit.record(context=context, ast_hash=ast.get("hash","?"), commands=final_cmds, notes="evaluation completed")
return final_cmds
def _resolve_conflicts(self, commands: List[Command]) -> List[Command]:
"""解決衝突指令,並確保輸出順序穩定"""
best: Dict[Tuple, Command] = {}
seq = 0
for c in commands:
seq += 1
if c["type"] == CommandType.GATE_LIMIT:
key: Tuple = ("GATE_LIMIT", c["payload"].get("target", "global"))
current_best = best.get(key)
if not current_best or c["payload"]["value"] < current_best["payload"]["value"]:
best[key] = c
else:
best[("OTHER", seq)] = c
# 依 key 排序,確保輸出穩定 (有利於金樣測試)
return [best[k] for k in sorted(best.keys(), key=lambda x: (x[0], str(x[1])))]
class CommandEmitter:
def __init__(self, template: BroadcastTemplate, channel: ChannelAdapter):
self._template = template
self._channel = channel
def emit(self, commands: List[Command]):
for cmd in commands:
validate_command(cmd) # 在邊界校驗
if cmd["type"] == CommandType.BROADCAST:
msg = self._template.render(cmd)
# 若外部未提供幂等鍵,使用訊息摘要做安全回退
key = cmd["payload"].get("idempotency_key") or str(hash(msg))
self._channel.send(msg, key=key)
else:
dispatch_to_fsm(cmd)
# --- 3. 擴展 (可靠傳遞與業務邏輯) ---
class CircuitBreakingAdapter(ChannelAdapter):
def __init__(self, inner: ChannelAdapter, failure_threshold=5, cool_down_ms=5000):
self.inner = inner
self.failure_threshold = failure_threshold
self.cool_down_ms = cool_down_ms
self.failure_count = 0
self.circuit_open_until = 0
# 死信佇列:保留 key 與內容便於追溯(避免純文字丟失對應性)
self.dlq: List[Tuple[Optional[str], str]] = []
def send(self, rendered_message: str, *, key: Optional[str] = None) -> None:
now = now_ms()
if now < self.circuit_open_until: # 斷路器開啟
self.dlq.append((key, rendered_message))
metrics(component="CircuitBreaker", state="open", dlq_size=len(self.dlq))
return
try:
self.inner.send(rendered_message, key=key)
self.failure_count = 0 # 成功後重置
except Exception:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self.circuit_open_until = now + self.cool_down_ms
metrics(component="CircuitBreaker", event="opened")
self.dlq.append((key, rendered_message)) # 失敗訊息進入 DLQ
raise
# 假設 parse_to_ast, eval_ast, dispatch_to_fsm 存在
def parse_to_ast(text: str) -> Dict: return {"ast": text, "hash": str(hash(text))}
def eval_ast(ast: Dict, ctx: Dict) -> List[Command]: return []
def dispatch_to_fsm(cmd: Command): pass
這套架構充分體現了職責分離:渲染與通路各司其職;外掛系統不僅有明確的執行順序與衝突裁決,更有執行預算來確保效能;傳遞機制也加入了重試與幂等性保證。這已然是一套可運維的工業級秩序。
✅ 何時該為你的城市立憲法 (When to Use S/O)
當你的某個模組開始被來自四面八方的不同需求拉扯時(一下要改報表格式、一下要換資料庫、一下要接新的 API),這就是最強烈的 SRP 信號。
當需求常常以**「我們需要再新增一種...」**的形式出現時(例如:新增一種支付方式、新增一種通知通路、新增一種報表格式),OCP 就是你的救星。
當你需要一條穩定到可以當傳家寶的核心流程(如:訂單處理流程、規則求值器),然後讓所有易變的細節,都以外掛的形式接入時。
⛔ 何時該緩一緩,先別急著立法 (When NOT to Use)
當你的專案還在需求探索期,核心抽象邊界還非常模糊、天天都在變。這時候強行抽象只會綁手綁腳。應該先用特性開關 (Feature Flag) 搭配快速原型來探索,等到模式穩定沉澱後,再進行重構。
當過多的擴展點導致治理成本失控時。如果任何人都可以隨意添加插件,系統很快會變得混亂。這時需要引入配套的治理流程,例如:擴展審核會議、廢棄插件的計畫、版本相容性矩陣等。
如果你在 Codetopia 的程式碼庫裡看到以下景象,請立刻升起紅旗,並通知 Soren 和 Octavia!
🚩 神物件 / 多軸混搭:一個類別的程式碼超過五百行,而且你無法用一句話說完它的職責。它又連資料庫、又算商業邏輯、又發通知、還寫日誌。
🚩 擴展等於修改主幹:每次要加一個新功能,你都要去動那個最核心、最不想碰的檔案。這導致每次的 PR 都像在走鋼索,難以審查,更難回滾。
🚩 跨越邊界的髒手:明明 Octavia 已經設計了漂亮的擴展介面,但新的插件卻為了求方便,直接去觸碰底層的資料庫、或操作硬體,完全繞過了城市既有的 SOP 和狀態機。
Soren 和 Octavia 的工作,不僅僅是重構了幾個類別,更是為 Codetopia 的未來發展奠定了基石:
與 Interpreter (Day 24) 對齊:我們的核心引擎現在只負責 AST 求值這件「純粹」的事。未来所有的新規則、新動作,都是透過新增 Provider 和 Plugin 來實現,再也不用去碰那個精密的求值器了。
與 Memento (Day 23) 對齊:OrchestraService
不再自己管回滾了!所有快照和回滾的職責都交還給專業的「時光局」。職責分離,天下太平。
與 Template Method (Day 21) 對齊:所有擴展點的設計,都與城市既有的 SOP 流程骨架上的「鉤子」相對應,確保了所有新功能都能被稽核、被監控,維持了流程的完整性。
在 OrchestraService
被光榮地重構成幾個職責單一的模組後,控制中心決定用同樣的暴雨事件,再做一次灰度演練。
需求一:條例要從 v1 切換到 v2。操作:注入 V2FileProvider
。核心程式碼修改:0 行。
需求二:增加人流密度過高時的自動管制。操作:註冊 ThrottleCrowd
插件。核心程式碼修改:0 行。
需求三:廣播通路從簡訊改成手機 App 推播,並加上斷路器保護。操作:注入 CircuitBreakingAdapter(PushAdapter(...))
。核心程式碼修改:0 行。
結果:核心零修改,灰度演練全線綠燈! 控制台的監控面板顯示:PolicyEval
的 P95 延遲穩定在 50ms 以下,Broadcast
命令的送達率高於 99.9%,且失效率 SLO 低於 0.1%。儀表板上,「被跳過的插件數」掛零,「斷路器狀態」為關閉,「死信佇列長度」為 0,顯示系統不僅高效,而且極具韌性。
Yuki(時光局紀錄官)在控制台確認,新的架構下,回滾點的生成正確無誤。Atlas(回滾工程師)也回報,現在模擬恢復流程時,只會影響到對應的模組,再也不會發生「改東壞西」的慘劇。現場響起了一片如釋重負的掌聲。
契約測試官 Pax 隨後頒布了新的測試指導原則,比之前更加嚴謹:
官方測試套件 (Official Test Kit):針對 OrdinanceProvider
、ActionPlugin
、ChannelAdapter
這三大擴展點,提供官方測試包 (包含 fixtures、持久化金樣、與 Property-based 測試腳本)。任何第三方插件,都必須先通過此套件的認證。
破壞性變更流程 (Breaking Change SOP):若 Command
或 Template
的 schema 需要修改,必須遵循「廢棄公告 → 雙寫過渡期 → 正式切換」的標準作業流程,並更新版本相容矩陣。
混沌工程演練 (Chaos Engineering):除了退場測試,還要定期注入故障(如模擬 PushAdapter
網路延遲、ActionPlugin
執行超時),驗證系統的熔斷、回退與死信佇列機制是否如預期般健壯。
靜態守法檢查 (Static Compliance Check):在 CI 流程中加入「單句使命宣言對齊」的靜態分析檢查,利用模組邊界檢測工具,防止職責洩漏與神物件的壞味道悄悄回歸。
好了,總設計師,現在輪到你了。作為 Codetopia 的一員,請動手鞏固今天的學習成果:
你的第一個都市更新計畫:找出你目前專案中最大、最臃腫的那個「神物件」。為它畫一張變動軸心圖,然後試著為拆分後的小模組各寫下一句「單句使命宣言」。
設計你的第一個擴展點:為你專案中一個經常變更的需求(例如:報表格式、通知方式),設計一個擴展點(介面、事件或鉤子都可以)。並為這個擴展點寫一個契約測試,來確保未來的實作不會「亂來」。
如果你是當時的英雄:回到 OrchestraService
的反例,用 10 行左右的 pseudo code 展示,當需求是「新增一種『簡訊』廣播通路」時,在新架構下,你將如何只透過新增一個 SmsAdapter
類別,而不修改任何核心程式碼來完成任務?
摘要:斬斷變動軸心、把變化推向邊界;讓核心在「不改也會長」中演進。
預告(Day 26):城市憲法的下半場即將開議!L/I/D——里氏替換、介面隔離、依賴反轉,我們將徹底搞定物件導向的契約精神!
為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:
┌─────────────┐ ┌─────────────────────┐ ┌─────────────┐ ┌─────────────┐
│ PolicyIO │ │ OrdinanceProvider │ │ PolicyEval │ │ CommandEmit │
│ (核心) │◆─→ │ (介面) │ │ (核心) │ │ ter(核心) │
│ │ │ ┌─────────────────┐ │ │ │ │ │
└─────────────┘ │ │ - fetch(v) → │ │ └─────────────┘ └─────────────┘
│ │ │ Ordinance │ │ │ │
│ │ └─────────────────┘ │ │ │
v └─────────────────────┘ v v
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 擴展插槽 (Extension Points) │
├─────────────────────┬─────────────────────┬─────────────────────┬─────────────────┤
│ ActionPlugin │ BroadcastTemplate │ ChannelAdapter │ AuditTrail │
│ (介面) │ (介面) │ (介面) │ (工具) │
│ ┌─────────────────┐ │ ┌─────────────────┐ │ ┌─────────────────┐ │ ┌─────────────┐ │
│ │ + order: int │ │ │ + render(cmd) │ │ │ + send(msg,key) │ │ │ + record() │ │
│ │ + apply(ctx, │ │ │ → str │ │ │ → void │ │ │ → void │ │
│ │ cmds) → cmds │ │ └─────────────────┘ │ └─────────────────┘ │ └─────────────┘ │
│ └─────────────────┘ └─────────────────────┘ └─────────────────────┘ └─────────────────┘
└─────────────────────────────────────────────────────────────────────────────────────┘
擴充模組 (對擴展開放) 核心管線 (對修改關閉)
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │
│ Provider: v2 條例 │ │ ┌──────────┐ ┌──────────────┐ │
│ ┌─────────────────────────────┐ │─────→│ │PolicyIO │──→│ PolicyEval │ │
│ │ + fetch_v2_ordinance() │ │ │ └──────────┘ └──────────────┘ │
│ └─────────────────────────────┘ │ │ │ │ │
│ │ │ v v │
│ ActionPlugin: 人流管制 │ │ ┌──────────────────────────────┐ │
│ ┌─────────────────────────────┐ │─────→│ │ CommandEmitter │ │
│ │ + throttle_crowd_density() │ │ │ └──────────────────────────────┘ │
│ └─────────────────────────────┘ │ │ │ │
│ │ └───────────────┼──────────────────┘
│ Template: 手機推播 │ v
│ ┌─────────────────────────────┐ │─────→ ┌─────────────────────┐
│ │ + render_mobile_push() │ │ │ Output Channels │
│ └─────────────────────────────┘ │ │ │
│ │ │ ┌─────────────────┐ │
│ Adapter: 推播通路 │─────────→ │ │ PushAdapter │ │
│ ┌─────────────────────────────┐ │ │ │ SmsAdapter │ │
│ │ + send_via_push_service() │ │ │ │ EmailAdapter │ │
│ └─────────────────────────────┘ │ │ └─────────────────┘ │
└─────────────────────────────────┘ └─────────────────────┘
╔══════════════════════════════════════════════════════════════╗
║ 關鍵原則:核心穩固不變,所有變化透過標準插槽注入 ║
║ • Provider 介面:新增規則來源 ║
║ • Plugin 介面:新增決策邏輯 ║
║ • Template 介面:新增輸出格式 ║
║ • Adapter 介面:新增傳輸通路 ║
╚══════════════════════════════════════════════════════════════╝
舊架構:OrchestraService (神物件)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 🐙 數位八爪章魚 │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │規則 │ │求值 │ │廣播 │ │稽核 │ │回滾 │ │格式 │ │通路 │ │備份 │ │
│ │載入 │ │邏輯 │ │訊息 │ │記錄 │ │預檢 │ │模板 │ │管理 │ │策略 │ │
│ │ ① │ │ ② │ │ ③ │ │ ④ │ │ ⑤ │ │ ⑥ │ │ ⑦ │ │ ⑧ │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ │ │ │ │ │ │ │ │ │
│ └────────┼────────┼────────┼────────┼────────┼────────┼────────┘ │
│ └────────┼────────┼────────┼────────┼────────┘ │
│ └────────┼────────┼────────┘ │
│ └────────┘ │
│ ⚠️ 問題:任何一個變動軸心的改變,都會牽連到整個神物件 │
└─────────────────────────────────────────────────────────────────────────────┘
│
v 🔪 Soren 的手術刀
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ 新架構:職責分離 (SRP) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ PolicyIO │ │ PolicyEval │ │CommandEmit │ │ AuditTrail │ │
│ │ │ │ │ │ ter │ │ │ │
│ │ 專職:規則 │ │ 專職:求值 │ │ 專職:廣播 │ │ 專職:稽核 │ │
│ │ 載入與版本 │ │ 與決策邏輯 │ │ 與命令分派 │ │ 記錄彙整 │ │
│ │ │ │ │ │ │ │ │ │
│ │ 變動軸心: │ │ 變動軸心: │ │ 變動軸心: │ │ 變動軸心: │ │
│ │ • 規則來源 │ │ • 決策邏輯 │ │ • 輸出格式 │ │ • 稽核需求 │ │
│ │ • 版本管理 │ │ • 衝突處理 │ │ • 傳輸通路 │ │ • 歸檔政策 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │ │
│ v v v v │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │擴展插槽 (O) │ │擴展插槽 (O) │ │擴展插槽 (O) │ │擴展插槽 (O) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ✅ 優勢:各模組只有一個改變理由,修改互不影響 │
└─────────────────────────────────────────────────────────────────────────────┘
擴展點治理架構 (by Octavia & Pax)
╔═══════════════════════════════════════════════════════════════════════════╗
║ 中央治理委員會 ║
║ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ ║
║ │ Octavia │ │ Pax │ │ 能力登錄中心 │ ║
║ │ (架構設計官) │ │ (契約測試官) │ │ (Extension Registry) │ ║
║ │ │ │ │ │ │ ║
║ │ • 設計擴展介面 │ │ • 撰寫契約測試 │ │ • 版本相容性矩陣 │ ║
║ │ • 定義插槽規範 │ │ • 金樣比對測試 │ │ • 官方認證流程 │ ║
║ │ • 治理流程制定 │ │ • 安全沙箱檢測 │ │ • 廢棄計畫管理 │ ║
║ └─────────────────┘ └─────────────────┘ └─────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════════╝
│
v
┌─────────────────────────────────────────────────────────────────────────────┐
│ 標準插槽介面 │
│ │
│ OrdinanceProvider ActionPlugin BroadcastTemplate ChannelAdapter │
│ ┌─────────────┐ ┌──────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │<<interface>>│ │<<inter- │ │<<interface>>│ │<<interface>> │ │
│ │ │ │ face>> │ │ │ │ │ │
│ │+ fetch(v) │ │+ apply() │ │+ render() │ │+ send() │ │
│ │ → Payload │ │+ order │ │ → string │ │+ circuit_ │ │
│ └─────────────┘ │ : int │ └─────────────┘ │ breaker() │ │
│ ▲ └──────────┘ ▲ └──────────────┘ │
│ │ ▲ │ ▲ │
│ │ │ │ │ │
└─────────┼─────────────────┼────────────────┼───────────────────┼────────────┘
│ │ │ │
v v v v
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第三方實作 (需通過官方認證) │
│ │
│ V2FileProvider ThrottleCrowd MobilePushTmpl CircuitBreakerAdapter │
│ ┌─────────────┐ ┌──────────┐ ┌─────────────┐ ┌────────────────────┐ │
│ │+ fetch(v) │ │+ apply() │ │+ render() │ │+ send() with retry │ │
│ │ from v2.db │ │ 人流管制 │ │ 手機推播格式│ │+ dlq management │ │
│ └─────────────┘ │+ order:50│ └─────────────┘ │+ failure_threshold │ │
│ └──────────┘ └────────────────────┘ │
│ │
│ ✓ 官方認證 ✓ 契約測試 ✓ 沙箱檢測 ✓ 版本相容 │
└─────────────────────────────────────────────────────────────────────────────┘