09-模块二-架构基础功 第09讲-设计模式实战 - 策略模式 观察者模式 责任链模式在 CodeSentinel 中的应用

3 阅读19分钟

模块二-架构基础功 | 第09讲:设计模式实战 - 策略模式、观察者模式、责任链模式在 CodeSentinel 中的应用

开场:模式不是背书记号,而是「把变化关进笼子里」

很多工程师对设计模式有两极反应:要么觉得「过时」,要么觉得「炫技」。在 CodeSentinel 这种 AI 驱动代码审核与架构治理 的平台里,模式的价值非常具体:它们把高频变化(策略集合、通知链路、校验阶段)从稳定编排里剥离出来,让你在面对 AI 生成代码时,能用可复制的结构约束模型的输出形状。

如果你把「模式」理解成一套 可重复的组织方式,它就和过不过时无关了——就像数据结构不会过时一样,关键是你是否在正确的问题规模使用。对审核平台而言,问题规模来自三方面:策略会持续增长、订阅者会持续增长、校验阶段会随合规要求重排。没有模式,你会用无穷无尽的 if 与回调参数把系统磨成泥浆;有了模式,你把泥浆固化成插槽,让 AI 与人类都在插槽上工作。

本讲聚焦三个在审核域几乎必用的模式:

  1. 策略模式ReviewStrategy 族(安全、架构、性能)——同一输入,多种可替换算法;
  2. 观察者模式EventBus 派发领域事件——ReviewCompleted 触发报告、看板、CI;
  3. 责任链模式ReviewPipeline——SecurityChecker → ArchitectureChecker → StyleChecker → PerformanceChecker 的顺序与可短路扩展。

你会看到 Protocol/ABC 的 Python 写法、完整的 线程安全教学版 EventBus、以及 可组合的责任链基类。最后我们会讨论:为什么这三者组合能让 CodeSentinel 在不改核心编排文件的前提下持续扩展——这正是 AI 时代平台型系统对抗「补丁式生成」的关键。

把「模式」放回现实场景:当一个团队在两周内接入三家企业的代码托管与两套 CI 体系时,你最怕的不是写不出代码,而是 每一次集成都在 orchestrator 里长出新的分支。分支一多,AI 生成的补丁就越容易「顺手改核心」,评审者也越难判断回归面。策略、观察者、责任链的共同目标是:把扩展动作限制为新增类型与注册,而不是修改稳定核心。对 CodeSentinel 来说,稳定核心通常是「审核生命周期 + 证据链模型」,变化部分则是「具体检查器、具体报告渠道、具体策略实现」。

本讲还会强调一个实践观点:模式不是越多越好。如果你用观察者派发 30 种事件,但团队没有成熟的可观测与回溯机制,系统会变得更难调试。因此我们会在生产章节讨论「同步 vs 异步」「异常隔离」「幂等」——它们决定模式能否从演示走向运维。


全局视角:三种模式在审核主链中的位置

第一张图从 用例编排 视角给出三模式的协同:策略负责「怎么评」,责任链负责「按阶段过滤」,观察者负责「事实向外广播」。

flowchart LR
  subgraph Pipeline["责任链 ReviewPipeline"]
    H1[SecurityChecker]
    H2[ArchitectureChecker]
    H3[StyleChecker]
    H4[PerformanceChecker]
    H1 --> H2 --> H3 --> H4
  end

  subgraph Strategy["策略模式 ReviewStrategy"]
    S1[SecurityReviewStrategy]
    S2[ArchitectureReviewStrategy]
    S3[PerformanceReviewStrategy]
  end

  subgraph Bus["观察者 EventBus"]
    E1[ReviewCompleted]
    L1[NotifyReporter]
    L2[UpdateDashboard]
    L3[TriggerCI]
  end

  Pipeline --> Strategy
  Strategy --> Bus

第二张图用 类图 强调责任链的「合成复用」:每个 Handler 只关心下一环。

classDiagram
  class ReviewCheckContext {
    +repo: str
    +pr_number: int
    +diff_text: str
  }

  class ReviewCheckResult {
    +passed: bool
    +messages: list
  }

  class BaseReviewHandler {
    #_next: BaseReviewHandler
    +set_next(h) BaseReviewHandler
    +handle(ctx) ReviewCheckResult*
  }

  class SecurityChecker {
    +handle(ctx) ReviewCheckResult
  }

  class ArchitectureChecker {
    +handle(ctx) ReviewCheckResult
  }

  class StyleChecker {
    +handle(ctx) ReviewCheckResult
  }

  class PerformanceChecker {
    +handle(ctx) ReviewCheckResult
  }

  BaseReviewHandler <|-- SecurityChecker
  BaseReviewHandler <|-- ArchitectureChecker
  BaseReviewHandler <|-- StyleChecker
  BaseReviewHandler <|-- PerformanceChecker
  BaseReviewHandler o-- BaseReviewHandler : next

第三张图用 时序图 展示 ReviewCompleted 后观察者链路的异步观感(教学版同步调用,生产可换任务队列)。

sequenceDiagram
  participant OR as ReviewOrchestrator
  participant ST as ReviewStrategy
  participant PL as ReviewPipeline
  participant EB as EventBus
  participant RP as NotifyReporter
  participant DB as UpdateDashboard
  participant CI as TriggerCI

  OR->>PL: handle(diff)
  PL-->>OR: pipeline_result
  OR->>ST: run_strategies(ctx)
  ST-->>OR: strategy_results
  OR->>EB: publish(ReviewCompleted)
  EB->>RP: on_event
  EB->>DB: on_event
  EB->>CI: on_event

核心原理:为什么审核系统天然吃这三种模式

策略模式:把「评什么」从「怎么跑流程」里拆出去

审核策略的变化来源包括:组织政策、合规条款、不同语言栈、不同风险容忍度。策略模式的核心不是类名,而是 可替换族 + 统一上下文 + 统一结果模型。在 Python 里用 Protocol 可以获得结构化子类型提示,测试时可用 FakeStrategy

AI 生成代码时的典型失败是把策略写进 if,本讲用注册表避免该问题。

观察者模式:审核结果是「事实」,事实需要多方消费

报告、看板、CI、审计日志、Webhook……这些订阅者的变化频率远高于核心审核状态机。观察者模式(或领域事件 + 分发器)让核心 不知道 具体订阅者是谁,只发布事件。对 CodeSentinel,这直接对齐 DDD 的 领域事件 思路。

注意:教学版 EventBus 用同步 list[Callable];生产环境应讨论 至少一次投递重试死信队列,这些在下一讲架构与事件溯源里会继续加深。

责任链模式:阶段化校验与「可短路失败」

责任链适合 管道式校验:安全不过就不必跑性能(可短路);或每阶段都跑完汇总所有问题(不短路)。实现关键是 handle 的契约:是否传递、何时停止、如何聚合消息。

与策略模式的区别:策略往往「并列可选」;责任链往往「串行阶段」。在 CodeSentinel 中,推荐 链负责硬门禁策略负责深度分析(二者可组合)。

何时在 AI 生成代码评审语境使用哪种模式

  • 若 PR 需要 多种评分维度且可独立演进:策略。
  • 若需要 把副作用从核心解耦:观察者。
  • 若需要 阶段性 gate + 清晰扩展点:责任链。

可扩展性:模式如何让 CodeSentinel 抵抗「补丁式 AI 输出」

当 AI 倾向于在单文件堆逻辑时,模式提供「插入点」:register_handlerregister_strategysubscribe。代码审查可以要求:新增能力必须新增类文件并注册,禁止修改 orchestrator 主体。这样 diff 更小、回归面更可控。

进一步说,模式的「社会功能」是让团队在代码评审里有共同词汇:看到 set_next 就知道阶段扩展点;看到 subscribe 就知道副作用边界;看到 register 就知道策略族在演进。没有这些词汇,评审很容易沦为品味之争,AI 也会无所适从——它不知道你更偏好哪种结构。

与 Clean Architecture 的边界

  • 领域层:事件类型、策略结果值对象、链上传递的上下文 DTO(保持无框架)。
  • 应用层:编排策略、构建链、发布事件。
  • 基础设施层:真正的 Slack、CI API、LLM 调用适配器(本讲用打印模拟)。

当你把 FastAPI 路由、SQLAlchemy Session、Redis 客户端混进领域层时,三种模式都会同时失效:责任链会变成「带着数据库游标链条」、策略会变成「带着 HTTP 客户端的策略」、事件订阅者会在意外时刻触发 IO。架构师要在 CI 里用 import 规则守住这条红线。

测试策略:每个模式一条「黄金路径」+ 一条「失败路径」

  • 策略:假策略返回固定 Finding
  • 观察者:spy 记录调用次数;
  • 责任链:中间环失败验证是否短路、消息是否聚合。

与模块一 DDD 工件对齐:事件名不是随便起的

在模块一你已经见过 ReviewCompletedFindingRecorded 等事件命名。本讲的 EventBus 用字符串 "ReviewCompleted" 作为教学最小实现;在真实项目建议升级为 强类型领域事件类(dataclass + 版本字段),并在序列化层统一映射。这样 AI 生成订阅者时不至于拼错事件名,静态检查也能帮上忙。

策略 vs 规则引擎:不要二极管思维

很多同学习惯问:「有了规则引擎还要策略模式吗?」答案是:要,因为它们解决不同层次的问题。规则引擎擅长表达「可配置规则集合与快速迭代」;策略模式擅长表达「完全不同的评阅路径可能需要不同依赖组合」。例如安全策略可能需要二进制扫描端口,性能策略可能需要调用 profiler 适配器——它们未必适合塞进同一份 DSL。

责任链与 AOP/装饰器:Python 里的风格选择

在 Python 生态也有人喜欢用装饰器链或中间件模式实现类似责任链。团队可以选择其一,但要统一:阶段顺序必须可声明、可测试、可观测。不要出现「隐式注册」导致顺序靠 import 副作用决定——AI 特别擅长制造这种隐式魔法。

观察者风暴:订阅者过多时如何分层

ReviewCompleted 的订阅者超过十个,建议拆分为二级事件:例如 ReviewCompletedInternal 只在服务内聚合指标,再派生 ReviewCompletedExternal 给 Webhook。否则你会遇到「一个慢订阅拖死主链路」的经典故障。教学版为了可读性保持同步调用,生产章节会再次强调异步化。

安全与权限:事件 payload 里该放什么

不要在事件里传播完整 diff 或密钥材料。推荐传 review_idrepopr摘要哈希可下载证据的定位符。订阅者需要细节时,再通过受控端口拉取。这样即使某个订阅者日志打印过度,也不会把敏感数据扩散到更多系统。AI 生成事件时常常「图方便」把大文本塞进 payload,评审时要重点拦截。

案例走读:一次「高危 diff」如何穿过链、策略与事件

假设某 PR 在 Python 服务里新增了硬编码密钥,并使用了无超时的 HTTP 调用。责任链的 SecurityChecker 可能因规则不同而未必覆盖「密钥」这一类(教学示例用 API_KEY 字符串触发策略),但 策略层SecurityReviewStrategy 更适合做语义级扫描;PerformanceReviewStrategy 则提示 timeout 风险。链路在 pipeline.passed 为真时发布 ReviewCompleted,三个订阅者分别更新报告、看板与 CI。

这个走读想说明:链与策略是互补的,不要试图把所有智能塞进链条的每一环,否则链条会变成另一个上帝流程。你可以把链条定位为「快速、确定性、低成本」的门禁,把策略定位为「更慢、更贵、更可配置」的分析器。对引入 LLM 的策略尤其如此:千万不要让 LLM 参与每一环硬门禁,否则延迟与成本会拖垮体验。

版本演进:当事件字段需要增加时怎么办

真实系统里事件 payload 会演进。建议从第一天就加 schema_version 字段,并在订阅者做向后兼容解析。否则你会遇到「新服务发新字段,老订阅者反序列化失败」的经典分布式问题。对 CodeSentinel 这种强审计系统,兼容性与可追溯同样重要:你不能因为一次部署就让历史报告无法关联事件。

与团队协作:如何让初级工程师也能按图扩展

把本讲的三个类骨架固化成仓库模板(cookiecutter 或内部 cli):handler new security-extstrategy new compliancesubscriber new webhook。当扩展变成脚手架命令,AI 生成代码也会更稳定——因为上下文里始终有正确样板。人类团队则减少「我忘了注册」的低级错误。

与 CodeSentinel 指标:模式化之后的度量机会

一旦责任链与策略结构化,你可以自然导出指标:每阶段通过率、每策略平均 findings 数、事件投递耗时分布。没有结构时,你只能统计整体耗时,无法定位瓶颈。对架构师来说,可度量性是模式带来的隐藏收益,别浪费。


代码实战:单文件可运行示例(策略 + 观察者 + 责任链)

将以下内容保存为 codesentinel_patterns_lab.py,执行 python codesentinel_patterns_lab.py。该示例刻意保持 无第三方依赖(标准库),便于在任何环境验证结构正确性;你在真实项目可把 print 替换为 httpx、任务队列等。

# codesentinel_patterns_lab.py
from __future__ import annotations

import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Protocol, runtime_checkable


# -----------------------------
# 共享 DTO / 事件 / 结果模型
# -----------------------------


@dataclass(frozen=True)
class ReviewContextDTO:
    repo: str
    pr_number: int
    diff_text: str


@dataclass(frozen=True)
class Finding:
    code: str
    message: str
    severity: str


@dataclass
class StrategyResult:
    strategy_name: str
    findings: List[Finding] = field(default_factory=list)


@dataclass
class PipelineResult:
    passed: bool
    stage: str
    messages: List[str] = field(default_factory=list)


@dataclass(frozen=True)
class DomainEvent:
    name: str
    payload: Dict[str, Any]


# -----------------------------
# 观察者模式:EventBus(教学版,线程安全)
# -----------------------------


class EventBus:
    def __init__(self) -> None:
        self._lock = threading.RLock()
        self._subs: Dict[str, List[Callable[[DomainEvent], None]]] = {}

    def subscribe(self, event_name: str, handler: Callable[[DomainEvent], None]) -> None:
        with self._lock:
            self._subs.setdefault(event_name, []).append(handler)

    def publish(self, event: DomainEvent) -> None:
        with self._lock:
            handlers = list(self._subs.get(event.name, []))
        for h in handlers:
            h(event)


# -----------------------------
# 策略模式:ReviewStrategy(Protocol + 具体策略)
# -----------------------------


@runtime_checkable
class ReviewStrategy(Protocol):
    @property
    def name(self) -> str: ...

    def analyze(self, ctx: ReviewContextDTO) -> StrategyResult: ...


class SecurityReviewStrategy:
    @property
    def name(self) -> str:
        return "security"

    def analyze(self, ctx: ReviewContextDTO) -> StrategyResult:
        findings: List[Finding] = []
        if "API_KEY" in ctx.diff_text or "BEGIN PRIVATE KEY" in ctx.diff_text:
            findings.append(Finding("SEC-001", "疑似敏感信息进入 diff", "high"))
        return StrategyResult(self.name, findings)


class ArchitectureReviewStrategy:
    @property
    def name(self) -> str:
        return "architecture"

    def analyze(self, ctx: ReviewContextDTO) -> StrategyResult:
        findings: List[Finding] = []
        if "import domain" in ctx.diff_text and "infrastructure" in ctx.diff_text:
            findings.append(Finding("ARC-010", "检测到可疑的跨层依赖方向", "med"))
        return StrategyResult(self.name, findings)


class PerformanceReviewStrategy:
    @property
    def name(self) -> str:
        return "performance"

    def analyze(self, ctx: ReviewContextDTO) -> StrategyResult:
        findings: List[Finding] = []
        if "time.sleep(" in ctx.diff_text:
            findings.append(Finding("PERF-020", "diff 中出现阻塞式 sleep,关注吞吐", "low"))
        return StrategyResult(self.name, findings)


class ReviewStrategyRegistry:
    def __init__(self) -> None:
        self._items: List[ReviewStrategy] = []

    def register(self, s: ReviewStrategy) -> None:
        self._items.append(s)

    def all(self) -> List[ReviewStrategy]:
        return list(self._items)


# -----------------------------
# 责任链模式:ReviewPipeline
# -----------------------------


class BaseReviewHandler(ABC):
    def __init__(self) -> None:
        self._next: BaseReviewHandler | None = None

    def set_next(self, n: BaseReviewHandler) -> BaseReviewHandler:
        self._next = n
        return n

    @abstractmethod
    def handle(self, ctx: ReviewContextDTO) -> PipelineResult:
        raise NotImplementedError

    def _forward(self, ctx: ReviewContextDTO) -> PipelineResult:
        if self._next is None:
            return PipelineResult(True, "end", ["pipeline finished"])
        return self._next.handle(ctx)


class SecurityChecker(BaseReviewHandler):
    def handle(self, ctx: ReviewContextDTO) -> PipelineResult:
        if "rm -rf /" in ctx.diff_text:
            return PipelineResult(False, "security", ["阻断:危险命令模式"])
        return self._forward(ctx)


class ArchitectureChecker(BaseReviewHandler):
    def handle(self, ctx: ReviewContextDTO) -> PipelineResult:
        msgs: List[str] = []
        if "from infrastructure" in ctx.diff_text and "domain" in ctx.diff_text:
            msgs.append("提示:可能存在依赖方向风险(教学规则)")
        r = self._forward(ctx)
        return PipelineResult(r.passed and True, r.stage, msgs + r.messages)


class StyleChecker(BaseReviewHandler):
    def handle(self, ctx: ReviewContextDTO) -> PipelineResult:
        msgs: List[str] = []
        if "\t" in ctx.diff_text:
            msgs.append("风格:检测到 tab 字符")
        r = self._forward(ctx)
        return PipelineResult(r.passed, r.stage, msgs + r.messages)


class PerformanceChecker(BaseReviewHandler):
    def handle(self, ctx: ReviewContextDTO) -> PipelineResult:
        msgs: List[str] = []
        if "requests.get(" in ctx.diff_text and "timeout=" not in ctx.diff_text:
            msgs.append("性能:HTTP 调用未设置 timeout")
        r = self._forward(ctx)
        return PipelineResult(r.passed, r.stage, msgs + r.messages)


def build_default_pipeline() -> BaseReviewHandler:
    head = SecurityChecker()
    head.set_next(ArchitectureChecker()).set_next(StyleChecker()).set_next(PerformanceChecker())
    return head


# -----------------------------
# 订阅者:模拟 Report / Dashboard / CI
# -----------------------------


def notify_reporter(event: DomainEvent) -> None:
    print(f"[reporter] 生成报告草稿: {event.payload}")


def update_dashboard(event: DomainEvent) -> None:
    print(f"[dashboard] 更新 KPI: {event.payload['repo']} PR#{event.payload['pr']}")


def trigger_ci(event: DomainEvent) -> None:
    print(f"[ci] 触发后置检查任务: {event.payload['repo']} PR#{event.payload['pr']}")


# -----------------------------
# 编排器:组合三种模式
# -----------------------------


class ReviewOrchestrator:
    def __init__(self, pipeline_head: BaseReviewHandler, registry: ReviewStrategyRegistry, bus: EventBus) -> None:
        self._pipeline_head = pipeline_head
        self._registry = registry
        self._bus = bus

    def execute(self, ctx: ReviewContextDTO) -> Dict[str, Any]:
        pipe = self._pipeline_head.handle(ctx)
        strategies = [s.analyze(ctx) for s in self._registry.all()]
        if pipe.passed:
            self._bus.publish(
                DomainEvent(
                    "ReviewCompleted",
                    {"repo": ctx.repo, "pr": ctx.pr_number, "pipeline": pipe.messages, "strategies": len(strategies)},
                )
            )
        return {"pipeline": pipe, "strategies": strategies}


def main() -> None:
    bus = EventBus()
    bus.subscribe("ReviewCompleted", notify_reporter)
    bus.subscribe("ReviewCompleted", update_dashboard)
    bus.subscribe("ReviewCompleted", trigger_ci)

    registry = ReviewStrategyRegistry()
    registry.register(SecurityReviewStrategy())
    registry.register(ArchitectureReviewStrategy())
    registry.register(PerformanceReviewStrategy())

    orchestrator = ReviewOrchestrator(build_default_pipeline(), registry, bus)

    ctx = ReviewContextDTO(
        repo="codesentinel/review-context",
        pr_number=209,
        diff_text="""+++ b/app/service.py
+API_KEY = "sk-xxxx"
+import requests
+requests.get("https://example.com")
""",
    )
    out = orchestrator.execute(ctx)
    print("pipeline.passed:", out["pipeline"].passed)
    for s in out["strategies"]:
        print("strategy:", s.strategy_name, "findings:", [f.code for f in s.findings])


if __name__ == "__main__":
    main()

代码导读:三个「挂钩点」分别防什么

  1. ReviewStrategyRegistry.register:强制策略 可插拔,避免 AI 在 orchestrator 写 if
  2. EventBus.subscribe:强制副作用 外置,新增订阅者不修改核心。
  3. BaseReviewHandler.set_next:强制阶段 可重排(测试可替换链条)。

你可以把这三个挂钩点理解为 CodeSentinel 的扩展三角:没有注册表,策略扩展会污染编排;没有总线,报告与 CI 会反向耦合审核核心;没有责任链组装点,阶段顺序会散落在多个函数里。评审 AI 生成补丁时,只要检查这三个点是否被绕过,就能快速判断架构是否正在劣化。

责任链的两种变体(团队选型)

  • 短路型:任一阶段失败立即返回(适合硬门禁)。
  • 汇总型:每阶段记录问题,最后统一 passed = not any(blockers)(适合「警告 vs 错误」分级)。

教学示例的 SecurityChecker 是短路型;ArchitectureChecker 更像汇总型的混合演示。真实 CodeSentinel 应把 错误级别 显式建模为枚举,避免 boolean 表达力不足。


生产环境实战:从教学版到可运维系统

EventBus 的生产升级路径

教学版 EventBus 在同进程内同步调用处理器,优点是简单、可测试;缺点是:一个订阅者异常会让发布失败阻塞主链路。生产建议:

  • 先把 publish 改为「写入 outbox 表 + 事务」;
  • 再用 worker 异步投递到 Redis Streams / RabbitMQ;
  • 订阅端实现 幂等键event_id)与 重试退避

ReviewPipeline 与 CI 门禁对齐

责任链的每一环应对应一个可独立运行的检查器镜像或 CLI。这样你可以:

  • 在本地只跑前两环;
  • 在 CI 跑全链;
  • 在 CodeSentinel 服务端跑全链 + 策略深度分析。

策略模式与 LLM 的边界

若某策略内部调用 LLM,务必把调用封装在 infrastructure 适配器,策略只依赖端口。否则你会看到 领域层 import langchain 的灾难 diff——AI 特别爱这么写。

可观测性:给链路与策略加统一结构化日志

建议统一日志字段:review_idrepoprhandlerstrategyduration_msoutcome。这样你可以在 Grafana 上回答:哪一阶段最慢、哪一策略误报最多

代码审查模板(贴到 PR 描述)

  • 新增校验是否通过 新 Handler 类 接入链条?
  • 新增分析维度是否通过 新 Strategy 类 注册?
  • 新增副作用是否通过 事件订阅 而非在 orchestrator 直写?

多租户场景:策略与链条如何按租户裁剪

SaaS 版 CodeSentinel 常见需求是:A 客户启用性能策略,B 客户只启用安全链前两环。实现上有两条路:

  1. 配置驱动链条:用配置描述 handler 序列,工厂按配置组装(注意验证无环与必需阶段);
  2. 策略包版本:把链条与策略绑定在 rule_pack_version 上,避免运行时随意拼装导致不可解释。

无论哪条路,都要避免在 orchestrator 写 if tenant == ...。那是 AI 最容易生成的补丁形态,也是后期最难测试的形态。

与 FastAPI 集成:依赖注入容器里装配链条

在 Web 框架层,推荐把 build_default_pipeline() 的产出挂在 app state 或由依赖注入工厂提供。请求处理函数只做:鉴权 → 构造 DTO → 调用 orchestrator → 映射响应。链条与策略的装配发生在进程启动阶段,避免每次请求重复构建(除非你需要热更新,那就要配套刷新机制与并发安全)。

失败演练:某个订阅者抛异常该怎么办

教学版 EventBus.publish 没有捕获异常。生产建议:

  • 默认 隔离:每个 handler try/except,错误进入 error_counter
  • 关键订阅者失败是否阻断主流程,应显式配置(critical vs best-effort)。

这属于可靠性设计,但它会反向影响你对观察者模式的使用方式:不是所有订阅者都平等

与 CodeSentinel 产品路线图对齐

短期内你可以用本讲结构交付 MVP;中期引入异步总线与 outbox;长期把事件升级为事件溯源(后续讲次会深入)。模式是「骨架」,消息基础设施是「血液循环」——别在没血液时硬上复杂模式,但也别在血液来了才发现骨架撑不住。

性能剖析:责任链中的热点如何定位

当你发现审核耗时异常,第一刀通常是 分阶段计时。给每个 handler 与 strategy 包裹计时器,输出 p95。不要凭感觉把性能问题归咎于 LLM——有时是 Git 拉取、有时是规则引擎冷启动、有时是某个订阅者同步调用外部系统。可观测性不是锦上添花,它是你决定是否异步化、是否拆服务的依据。

与 AI 协作的提示词约束(可直接贴进 AGENTS.md)

  • 新增审核能力必须新增 HandlerStrategy 类文件,并注册;禁止在 ReviewOrchestrator 增加 elif
  • EventBus 订阅函数必须是纯副作用或明确失败策略,不得访问数据库全局会话。
  • 责任链必须可通过测试单独构造,不允许依赖全局单例。

小结式提醒:先跑通结构,再追求「聪明」

很多团队一上来就想把 LLM 嵌进责任链的每一环,结果延迟与成本失控。更稳的路径是:先用确定性链条与策略把 数据流、事件流、扩展点 跑通,再把最需要的环节替换为「模型增强」。模式保证你不会因为引入模型而把系统结构打烂。


本讲小结

mindmap
  root((CodeSentinel模式))
    策略模式
      ReviewStrategy
      注册表扩展
    观察者模式
      EventBus
      ReviewCompleted
      解耦副作用
    责任链模式
      ReviewPipeline
      短路与汇总
    生产化
      Outbox
      幂等
      可观测性

思考题

  1. 如果 PerformanceChecker 很慢,你会把它移出同步链还是改为异步事件?各自的失败模式是什么?
  2. 策略结果与责任链结果在 UI 上应如何合并展示,才能不让用户困惑?
  3. 你会如何为 EventBus 写测试,验证订阅顺序与异常隔离?

如果你愿意动手,可把教学示例中的 ArchitectureChecker 改成严格的汇总型实现,并比较与短路型在误报率与用户体验上的差异。把对比结论写进团队 ADR,后续扩展会更省力,也能让新同学快速理解历史权衡与边界条件,减少重复踩坑与无效争论。

扩展思考(供架构师工作坊)

  1. 当责任链需要并行执行多个无依赖阶段时,你会引入 DAG 调度还是继续线性链?对 CodeSentinel 的审计追溯有什么影响?
  2. 如果某个策略需要读取链上前置阶段的结果,如何避免「链与策略」互相引用形成循环依赖?
  3. 你会如何把本讲结构映射到 OpenTelemetry:一个 span 对应一个 handler 还是对应整个 review?

反模式警示:三种模式在 AI 生成代码中的「常见翻车」

第一种翻车是 假责任链:每个 handler 内部仍然写 if stage == 3,链条只是表象,本质还是上帝函数。评审时要看 handle 是否真正只处理本阶段关注点,是否通过 _forward 明确移交。

第二种翻车是 事件地狱:任何小动作都发事件,调试时无法追踪因果。要建立事件目录(catalog),明确哪些事件属于领域事实,哪些只是技术日志。

第三种翻车是 策略对象读写全局配置:策略看似可替换,实际上通过全局变量耦合租户配置,测试不可控。应通过构造函数注入只读配置或端口。

把这些反模式写进团队的 PR 拒绝理由库,比单纯强调「要用心」有效得多。


下一讲预告

下一讲我们上升到 微服务架构模式:讨论 CodeSentinel 如何按业务能力拆分 ReviewServiceRuleServiceIndexServiceNotificationService,以及 FastAPI 作为 API 网关 的路由、同步 REST/gRPC 与异步消息、熔断与重试。你会看到与本期模式的自然衔接:服务边界越清晰,事件与责任链越不容易「跨进程 spaghetti」。你也可以提前思考:哪些订阅者适合留在单体内同步调用,哪些必须跨进程解耦,并为跨进程链路预留超时、重试与幂等等治理手段。