iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

Codetopia 新手日記:設計模式與原則的 30 天學習之旅系列 第 25

Day 25:城市憲法 I (S/O):一刀劃清權責,一扇敞開未來!

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (25)|城市憲法 I (S/O):一刀劃清權責,一扇敞開未來!

1) 今日熱點 (故事開場 & 痛點) ⚡️

時間:清晨 05:30。地點:Codetopia 控制中心「灰度沙箱」。

窗外的暴雨剛歇,控制中心裡卻瀰漫著比濕氣更凝重的低氣壓。昨晚靠著 Ada 與 Hugo 的直譯器(Day 24),城市算是從淹水的邊緣被拉了回來,暫時止血。但黎明前最深的黑暗,卻在用來模擬演練的「灰度沙箱」中炸開了。

砰! 一聲刺耳的警報,劃破了所有值班工程師的睡意。

一支名為 OrdinanceService 的核心服務,在壓力測試下轟然崩潰。這傢伙簡直是市府裡的「萬能超人」,一手包辦了「載入防災條例」、「彙整審計紀錄」、「向市民廣播」、「預檢版本回滾」……幾乎所有跟決策下達有關的雜事。結果就是,四個不同局處的團隊,在暴雨夜為了救災同時修改了它,然後……大家就在彼此的程式碼上華麗地踩了過去。

值班官員臉色慘白,對著監控螢幕喃喃自語:「沒道理啊……為什麼只是改個廣播通知的文字模板,會把『時光局』的回滾預檢系統也一起弄掛了?這兩件事有什麼關係?」

就在這時,控制室的門被推開。走進來的是憲章起草官 Soren,他面無表情,手上拿著一張紅色便利貼,啪地一聲,貼在了 OrdinanceService 的監控螢幕上。

上面只有一行字:「一個模組,只能有一個改變它的理由。

緊接著,擴展審核官 Octavia 拿著白板筆,在旁邊的白板上迅速畫出一個核心穩固、周邊滿是插槽的架構圖。「各位,別再往房子內牆上鑽洞了。」她說道,聲音清脆而堅定,「我們應該把所有可能變動的東西,全部拉到邊界去。讓城市的核心『對修改關閉』,但永遠『對擴展開放』。

今天的任務很明確:在不推翻前兩天英雄們(Ada, Hugo, Yuki, Atlas)心血的前提下,為 Codetopia 立下憲法第一、二條。用 S (SRP) 與 O (OCP) 原則,讓這座城市的程式碼,學會自己守法。

2) 術語卡 🧭

  • SRP|Single Responsibility Principle(單一職責原則):一個類別或模組,應該只有一個導致其變更的理由(也就是只有一個「變動軸心」)。

  • OCP|Open-Closed Principle(開放封閉原則):軟體實體(類別、模組、函數等)應該對擴展開放,但對修改封閉。白話文:歡迎你來加 new feature,但不准你動我的 source code。

  • Change Axis(變動軸心):驅使某個軟體單元發生變更的單一來源。可能是來自需求、政策、資料來源,甚至是輸出介面的改變。

  • Extension Point(擴展點):在不修改核心程式碼的前提下,允許外部透過介面、事件、組態、或模板鉤子來擴充系統行為的邊界。

3) 笑中帶淚 (反例/壞味道) 😭

讓我們回到 OrdinanceService 崩潰的瞬間,近距離聞聞那幾股讓值班官員快要窒息的「程式碼壞味道」。

  1. 神物件 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 一起開協調會,然後所有人一起等著重新部署這整坨龐然大物。太慘了。

  2. 擴展全靠 if/elif:每次市府推行新版條例,工程師就得在核心邏輯裡多加一條 elif,像是在房子的主樑上多釘一根釘子。

    if provider == "city.law.v1":
        # ... v1 版本的邏輯
    elif provider == "city.law.v2":
        # ... v2 版本的邏輯
    # 下個月還有 v3,再下個月還有 v2.1-hotfix... 這棟樓遲早要塌。
    
  3. 資料模型夾帶副作用:用來求值的規則物件,內部竟然硬塞了廣播訊息的模板、審計紀錄的格式。這導致任何輸出層的視覺調整,都可能污染到最核心的決策引擎。

  4. 回滾鍵直通內臟:某些緊急修復,為了求快,竟然直接呼叫 OrchestraService 的內部私有欄位來覆寫狀態,完全繞過了我們在 Day 20/21 辛苦建立的狀態機和 SOP 流程。這是越權,更是災難的根源。

4) 王牌出手 (核心觀念/落地藍圖) 👑

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 身上,她徹底劃清了**「渲染格式」「傳輸通路」**的職責,設計了 BroadcastTemplateChannelAdapter 兩個介面。要改廣播格式?實作一個新的 Template。要新增推播通路?寫個新的 Adapter。核心發送邏輯,一字不動。

  • PolicyEval 身上,她甚至設計了 ActionPlugin 擴展點。未來如果需要新增一種從未有過的防災動作(例如 ThrottleCrowd 人流管制),只需要寫一個新的 Plugin,就能無痛地接入決策引擎。

護欄:契約測試官 Pax 的進場

最後,契約測試官 Pax 登場,他為 Octavia 設計的每一個擴展點都撰寫了嚴格的契約測試。確保所有插件都必須遵守規範,並用金樣比對 (Golden Master Testing) 鎖定核心行為。他還額外頒布了安全與資源護欄

「所有第三方插件,未來都必須在受限制的沙箱中執行,並設定時間與記憶體上限。只有經過官方審核簽章的 Plugin/Adapter 才能被載入系統。」

這確保了城市在不斷擴展的過程中,穩定性與安全性都不會受到任何影響。

5) 導播切景 (三層並置圖) 🎥

導播,鏡頭拉一下!讓我們從三個不同尺度,看看這場「城市憲法」改革是如何重塑 Codetopia 的架構的。

視角 觀念/模式 在 Codetopia 的說法
微觀(原則) SRP / OCP 類別/模組以單一變動軸心切分;擴展功能透過介面/事件/鉤子等標準插槽進入,不碰核心。
中觀(EIP/EDA) Plugin/Strategy + Template Hooks 規則求值核心管線(Pipeline)固定不變;所有變化,都以外掛(Provider/Plugin/Hook)的形式從外部注入。
宏觀(MAS) Capability Registry + Governance 以「能力登錄中心+擴展審核流程」來治理所有擴展;任何破壞性變更,都必須通過正式的治理流程。

Mermaid|微觀:能力切分與擴展點

https://ithelp.ithome.com.tw/upload/images/20251009/20178500yl701qjm5e.png

Mermaid|中觀:不改核心,增一種動作/來源/通路

https://ithelp.ithome.com.tw/upload/images/20251009/20178500LsXbVAqgzb.png

6) 最小實作 (準生產級 pseudo) 💻

這就是 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

這套架構充分體現了職責分離:渲染與通路各司其職;外掛系統不僅有明確的執行順序與衝突裁決,更有執行預算來確保效能;傳遞機制也加入了重試與幂等性保證。這已然是一套可運維的工業級秩序。

7) 何時用 / 何時不要用

何時該為你的城市立憲法 (When to Use S/O)

  • 當你的某個模組開始被來自四面八方的不同需求拉扯時(一下要改報表格式、一下要換資料庫、一下要接新的 API),這就是最強烈的 SRP 信號。

  • 當需求常常以**「我們需要再新增一種...」**的形式出現時(例如:新增一種支付方式、新增一種通知通路、新增一種報表格式),OCP 就是你的救星。

  • 當你需要一條穩定到可以當傳家寶的核心流程(如:訂單處理流程、規則求值器),然後讓所有易變的細節,都以外掛的形式接入時。

何時該緩一緩,先別急著立法 (When NOT to Use)

  • 當你的專案還在需求探索期,核心抽象邊界還非常模糊、天天都在變。這時候強行抽象只會綁手綁腳。應該先用特性開關 (Feature Flag) 搭配快速原型來探索,等到模式穩定沉澱後,再進行重構。

  • 當過多的擴展點導致治理成本失控時。如果任何人都可以隨意添加插件,系統很快會變得混亂。這時需要引入配套的治理流程,例如:擴展審核會議、廢棄插件的計畫、版本相容性矩陣等。

8) 反模式紅旗 🚩

如果你在 Codetopia 的程式碼庫裡看到以下景象,請立刻升起紅旗,並通知 Soren 和 Octavia!

  • 🚩 神物件 / 多軸混搭:一個類別的程式碼超過五百行,而且你無法用一句話說完它的職責。它又連資料庫、又算商業邏輯、又發通知、還寫日誌。

  • 🚩 擴展等於修改主幹:每次要加一個新功能,你都要去動那個最核心、最不想碰的檔案。這導致每次的 PR 都像在走鋼索,難以審查,更難回滾。

  • 🚩 跨越邊界的髒手:明明 Octavia 已經設計了漂亮的擴展介面,但新的插件卻為了求方便,直接去觸碰底層的資料庫、或操作硬體,完全繞過了城市既有的 SOP 和狀態機。

9) 城市望遠鏡 (升維) 🔭

Soren 和 Octavia 的工作,不僅僅是重構了幾個類別,更是為 Codetopia 的未來發展奠定了基石:

  • 與 Interpreter (Day 24) 對齊:我們的核心引擎現在只負責 AST 求值這件「純粹」的事。未来所有的新規則、新動作,都是透過新增 Provider 和 Plugin 來實現,再也不用去碰那個精密的求值器了。

  • 與 Memento (Day 23) 對齊OrchestraService 不再自己管回滾了!所有快照和回滾的職責都交還給專業的「時光局」。職責分離,天下太平。

  • 與 Template Method (Day 21) 對齊:所有擴展點的設計,都與城市既有的 SOP 流程骨架上的「鉤子」相對應,確保了所有新功能都能被稽核、被監控,維持了流程的完整性。

10) 回到現場 (同一組驗收通過) ✅

OrchestraService 被光榮地重構成幾個職責單一的模組後,控制中心決定用同樣的暴雨事件,再做一次灰度演練。

  • 需求一:條例要從 v1 切換到 v2。操作:注入 V2FileProvider核心程式碼修改:0 行。

  • 需求二:增加人流密度過高時的自動管制。操作:註冊 ThrottleCrowd 插件。核心程式碼修改:0 行。

  • 需求三:廣播通路從簡訊改成手機 App 推播,並加上斷路器保護。操作:注入 CircuitBreakingAdapter(PushAdapter(...))核心程式碼修改:0 行。

結果:核心零修改,灰度演練全線綠燈! 控制台的監控面板顯示:PolicyEval 的 P95 延遲穩定在 50ms 以下,Broadcast 命令的送達率高於 99.9%,且失效率 SLO 低於 0.1%。儀表板上,「被跳過的插件數」掛零,「斷路器狀態」為關閉,「死信佇列長度」為 0,顯示系統不僅高效,而且極具韌性。

Yuki(時光局紀錄官)在控制台確認,新的架構下,回滾點的生成正確無誤。Atlas(回滾工程師)也回報,現在模擬恢復流程時,只會影響到對應的模組,再也不會發生「改東壞西」的慘劇。現場響起了一片如釋重負的掌聲。

11) 測試指北 🧪

契約測試官 Pax 隨後頒布了新的測試指導原則,比之前更加嚴謹:

  • 官方測試套件 (Official Test Kit):針對 OrdinanceProviderActionPluginChannelAdapter 這三大擴展點,提供官方測試包 (包含 fixtures、持久化金樣、與 Property-based 測試腳本)。任何第三方插件,都必須先通過此套件的認證。

  • 破壞性變更流程 (Breaking Change SOP):若 CommandTemplate 的 schema 需要修改,必須遵循「廢棄公告 → 雙寫過渡期 → 正式切換」的標準作業流程,並更新版本相容矩陣。

  • 混沌工程演練 (Chaos Engineering):除了退場測試,還要定期注入故障(如模擬 PushAdapter 網路延遲、ActionPlugin 執行超時),驗證系統的熔斷、回退與死信佇列機制是否如預期般健壯。

  • 靜態守法檢查 (Static Compliance Check):在 CI 流程中加入「單句使命宣言對齊」的靜態分析檢查,利用模組邊界檢測工具,防止職責洩漏與神物件的壞味道悄悄回歸。

12) 鄉民出題 (作業題) 📚

好了,總設計師,現在輪到你了。作為 Codetopia 的一員,請動手鞏固今天的學習成果:

  1. 你的第一個都市更新計畫:找出你目前專案中最大、最臃腫的那個「神物件」。為它畫一張變動軸心圖,然後試著為拆分後的小模組各寫下一句「單句使命宣言」。

  2. 設計你的第一個擴展點:為你專案中一個經常變更的需求(例如:報表格式、通知方式),設計一個擴展點(介面、事件或鉤子都可以)。並為這個擴展點寫一個契約測試,來確保未來的實作不會「亂來」。

  3. 如果你是當時的英雄:回到 OrchestraService 的反例,用 10 行左右的 pseudo code 展示,當需求是「新增一種『簡訊』廣播通路」時,在新架構下,你將如何只透過新增一個 SmsAdapter 類別,而不修改任何核心程式碼來完成任務?

13) 二十字摘要 & 明日預告

摘要:斬斷變動軸心、把變化推向邊界;讓核心在「不改也會長」中演進。

預告(Day 26):城市憲法的下半場即將開議!L/I/D——里氏替換、介面隔離、依賴反轉,我們將徹底搞定物件導向的契約精神!


附錄:ASCII 版圖示

為了確保在不支援 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 │   │
    │                   └──────────┘                     └────────────────────┘   │
    │                                                                             │
    │  ✓ 官方認證        ✓ 契約測試        ✓ 沙箱檢測        ✓ 版本相容        │
    └─────────────────────────────────────────────────────────────────────────────┘

上一篇
Day 24:Interpreter(直譯器):將「城市法規」編成一門語言,讓機器讀懂人話!
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言