08-模块二-架构基础功 第08讲-SOLID 原则的 AI 时代新解读 - 为什么 AI 生成的代码总是违反开闭原则

0 阅读19分钟

模块二-架构基础功 | 第08讲:SOLID 原则的 AI 时代新解读 - 为什么 AI 生成的代码总是违反开闭原则

开场:当「能跑」不再是标准,SOLID 变成你与 AI 的「共同语言」

如果你让大模型「帮我写一个审核服务」,它往往会在几十秒内给你一份看起来完整的实现:类名工整、注释礼貌、甚至附带 pytest。但当你把这份代码放进 CodeSentinel 的真实演进里,你会很快遇到一种熟悉的痛:每新增一种审核策略,就要改同一个文件;每接入一个新规则源,就要在核心类里再加一串 if/else;每换一个 LLM 提供商,业务层突然开始 import openai

这不是你「不会提示工程」,而是生成式模型在统计上偏好最短路径补丁:它会把当下上下文里所有条件揉进一个函数,把具体实现直接写在调用方旁边,把「先交付」置于「可扩展」之上。于是 SOLID 不再是教科书里的五个缩写,而变成你与团队、与 AI、与门禁工具之间的硬约束语言S 让你拒绝上帝类;O 让你用扩展点替代改源码;L 让你警惕继承陷阱;I 让你拆分臃肿协议;D 让你把具体技术细节赶到外层。

本讲我们做三件事:用 AI 时代的工程风险重新解释 SOLID;用 Before/After 对照展示「模型常犯错」与「架构师应如何纠偏」;把 CodeSentinel 的 ReviewPolicy 当作贯穿案例——用策略对象组织不同审核策略,证明 SOLID 不是口号,而是可运行的设计。最后你会拿到一份可粘贴进仓库的 AGENTS.md 规则片段,让 AI 在生成代码时默认走向正确结构

把话说得更直白一些:AI 并不是故意与你作对,它在最大化「当下可运行」与「最小改动幅度」。而架构师的工作,是在团队里建立一套让「最小改动」与「长期可演进」同向的约束系统。SOLID 恰好提供了五个可教、可检、可门禁的词汇——它们比「写干净点」更容易被工具化,也比「要有架构感」更容易被新人理解。你在 CodeSentinel 里每一次把硬编码分支改成策略注册,都是在降低未来六个月里 LLM 继续往同一个函数里塞逻辑的概率。

本讲还会反复提醒一个事实:开闭原则不是「永远不改旧代码」,而是「把易变点推离稳定核心」。在审核领域,易变点通常是策略集合、规则版本、LLM 提示模板与组织政策;稳定核心通常是审核生命周期、聚合不变式、证据链模型与跨上下文集成契约。抓住这对矛盾,你在评审 AI 生成代码时会非常快:一眼就能看出改动落在错误层级。把「易变/稳定」画成表格贴在评审模板里,团队对齐成本会显著下降。


全局视角:SOLID 与 CodeSentinel 治理链路的映射

CodeSentinel 采用 Clean Architecture + DDD,边界上下文包含 Review、Rule、Repository、Report。SOLID 主要落在 ReviewContext 的应用层与领域层契约上:审核策略如何扩展、规则评估如何替换、报告如何订阅事实而不反向依赖实现。

flowchart TB
  subgraph Violation["AI 常见坏味道(统计偏好)"]
    V1[上帝类 ReviewService]
    V2[巨型 if/else 策略分支]
    V3[继承层次随意覆写]
    V4[胖接口 AllInOnePort]
    V5[内层 import 具体 SDK]
  end

  subgraph Solid["SOLID 纠偏方向"]
    S1[拆分职责:Policy / Runner / Port]
    O1[扩展:新增策略类而非改核心]
    L1[契约:子类型可替换且不变式成立]
    I1[接口隔离:小而稳定的 Protocol]
    D1[依赖倒置:内层只认抽象]
  end

  Violation --> Solid

第二张图给出 ReviewPolicy 策略族 在类结构上的「正确形状」:核心只依赖抽象,具体策略可插拔,契合开闭原则。

classDiagram
  class ReviewPolicy {
    <<Protocol>>
    +name() str
    +evaluate(ctx: ReviewContextDTO) PolicyResult
  }

  class SecurityReviewPolicy {
    +name() str
    +evaluate(ctx) PolicyResult
  }

  class ArchitectureReviewPolicy {
    +name() str
    +evaluate(ctx) PolicyResult
  }

  class PerformanceReviewPolicy {
    +name() str
    +evaluate(ctx) PolicyResult
  }

  class ReviewPolicyRegistry {
    -_policies: list~ReviewPolicy~
    +register(p: ReviewPolicy)
    +all() list~ReviewPolicy~
  }

  class ReviewOrchestrator {
    -_registry: ReviewPolicyRegistry
    +run_all(ctx) list~PolicyResult~
  }

  ReviewPolicy <|.. SecurityReviewPolicy
  ReviewPolicy <|.. ArchitectureReviewPolicy
  ReviewPolicy <|.. PerformanceReviewPolicy
  ReviewPolicyRegistry o-- ReviewPolicy : aggregates
  ReviewOrchestrator --> ReviewPolicyRegistry

第三张图用 依赖方向 强调 DIP:应用服务依赖端口,LLM/HTTP/文件系统实现落在外层。

flowchart TB
  subgraph Inner["内层:领域 + 应用"]
    UC[ReviewOrchestrator / UseCase]
    P1[LLMPort Protocol]
    P2[RuleEvaluatorPort Protocol]
  end

  subgraph Outer["外层:基础设施"]
    LC[LangChainLLMAdapter]
    RE[SemgrepRuleAdapter]
  end

  UC --> P1
  UC --> P2
  LC -.实现.-> P1
  RE -.实现.-> P2

核心原理:五条原则在 AI 时代的「新解读」

S — 单一职责:AI 为什么爱写「上帝类」?

模型在单次对话里「看到」的是你的即时需求:创建审核、调用规则、写数据库、发通知。它的最优解往往是一个类全包,因为那样最少跨文件跳转。工程上这会导致:变更原因爆炸——任何一条需求抖动都会触动同一坨代码,评审冲突率上升,测试组合爆炸。

AI 时代新解读:单一职责不是「一个类只做一件事」的机械切割,而是把“业务决策点”与“技术细节”分离,把「编排」与「执行」分离。对 CodeSentinel 而言,ReviewOrchestrator 只负责按策略集合驱动流程;具体安全规则扫描、架构层违规检测、性能热点分析应落在不同策略或不同适配器。

O — 开闭:为什么 AI 总用硬编码条件?

开闭原则要求:对扩展开放,对修改关闭。模型却偏爱 if policy == "security": ...,因为显式分支对人类读者最直观。代价是:每加一种策略都要改核心函数,违反 OCP,也让合并冲突成为日常。

AI 时代新解读:你要把「策略选择」从代码分支迁移到注册表 + 多态。新增策略时只新增类并在 Composition Root 注册,核心循环保持稳定。对审核平台这几乎是刚需:企业客户的策略组合千差万别,你不能每次定制都改主干。

L — 里氏替换:继承是模型的「舒适区」,却是系统的雷区

AI 生成继承体系很快,但常常破坏可替换性:子类强化前置条件、弱化后置条件、在不该抛异常的地方抛异常,或在覆写中悄悄引入副作用。对 CodeSentinel 来说,任何 ReviewPolicy 子类若偷偷访问全局单例或写数据库,就会让「替换策略」失去意义。

AI 时代新解读:优先 组合优于继承;若用继承,必须显式写出契约测试(contract tests)保证子类行为一致。对 Python,可用 Protocol 结构化子typing,让静态检查与运行时行为对齐。

I — 接口隔离:胖接口是「一次性满足你所有 import」的陷阱

模型常见写法是定义一个 MegaPort,里面同时包含 scan_repocall_llmsend_slackcharge_billing……调用方只用到两个方法,却被迫依赖整个协议,测试也必须 stub 一堆方法。

AI 时代新解读:把端口按变更频率与调用方拆分:LLMCompletionPortNotificationPortMetricsPort 各自独立。这样 AI 生成代码时也不容易「顺手调用」不该调用的能力。

D — 依赖倒置:具体 import 出现在内层,是架构溃败的起点

AI 极易写出 from openai import OpenAI 出现在 service 文件顶部。短期能跑,长期让内层绑定供应商,切换模型、做离线测试、做合规审计都痛苦。

AI 时代新解读:内层只认识 Protocol/ABC,外层提供适配器。Composition Root 组装具体实现。对 FastAPI,路由可以薄,真正的用例在 application 包,依赖通过构造函数注入。

把五条原则合成一句工程格言

让“变化频繁的部分”可插拔,让“稳定的部分”不被迫反复修改——这就是你与 AI 协作时的护城河。接下来用代码把话说死。


代码实战:Before/After 与 CodeSentinel ReviewPolicy 完整示例

下面示例可在单文件中运行(Python 3.10+),演示 AI 风格坏味道SOLID 重构 对照,并给出 ReviewPolicy 策略注册表。

Before:典型的「AI 一站式」实现(违反 S/O/D)

# bad_ai_style_review.py
# 刻意模拟:上帝类 + 巨型分支 + 具体依赖混在内层

from __future__ import annotations

from dataclasses import dataclass
from typing import Any


@dataclass
class ReviewJob:
    repo: str
    pr_number: int
    policy_name: str


class ReviewService:  # 上帝类:什么都能干
    def run(self, job: ReviewJob) -> dict[str, Any]:
        # 具体“供应商”写死在内层(违反 DIP)
        api_key = "sk-demo-not-real"

        result: dict[str, Any] = {"findings": []}

        # 巨型条件链(违反 OCP)
        if job.policy_name == "security":
            result["findings"].append({"level": "high", "msg": "疑似密钥硬编码"})
            _ = api_key  # 假装调用外部扫描
        elif job.policy_name == "architecture":
            result["findings"].append({"level": "med", "msg": "检测到跨层依赖风险"})
        elif job.policy_name == "performance":
            result["findings"].append({"level": "low", "msg": "建议为热点路径加缓存"})
        else:
            result["findings"].append({"level": "info", "msg": f"未知策略 {job.policy_name}"})

        # 顺手把通知也写了(职责污染,违反 SRP)
        self._notify_slack(f"review done for {job.repo}#{job.pr_number}")
        return result

    def _notify_slack(self, text: str) -> None:
        print(f"[slack] {text}")


if __name__ == "__main__":
    svc = ReviewService()
    print(svc.run(ReviewJob("codesentinel/core", 42, "security")))

问题清单(评审话术可直接用)

  1. ReviewService 同时承担策略选择、(伪)扫描、通知,SRP 崩塌
  2. 新增策略必须改 runOCP 崩塌
  3. api_key 与 Slack 通知是具体技术细节,DIP 崩塌
  4. 难以对单策略做单测,必须打整个服务。

After:策略 + 注册表 + 依赖注入(贴合 CodeSentinel)

# solid_review_policy.py
#  runnable: python solid_review_policy.py

from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol, runtime_checkable, Iterable


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


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


@dataclass(frozen=True)
class PolicyResult:
    policy_name: str
    findings: tuple[Finding, ...]


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

    def evaluate(self, ctx: ReviewContextDTO) -> PolicyResult: ...


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

    def evaluate(self, ctx: ReviewContextDTO) -> PolicyResult:
        # 真实系统里这里调用静态扫描端口;教学示例聚焦结构
        return PolicyResult(
            policy_name=self.name,
            findings=(
                Finding("high", "SEC-001", f"{ctx.repo}#{ctx.pr_number}: 检查密钥与敏感信息"),
            ),
        )


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

    def evaluate(self, ctx: ReviewContextDTO) -> PolicyResult:
        return PolicyResult(
            policy_name=self.name,
            findings=(Finding("med", "ARC-010", "边界上下文依赖方向是否违背 Clean Architecture"),),
        )


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

    def evaluate(self, ctx: ReviewContextDTO) -> PolicyResult:
        return PolicyResult(
            policy_name=self.name,
            findings=(Finding("low", "PERF-020", "热点路径是否缺少批量化与缓存策略"),),
        )


class ReviewPolicyRegistry:
    def __init__(self) -> None:
        self._policies: list[ReviewPolicy] = []

    def register(self, policy: ReviewPolicy) -> None:
        self._policies.append(policy)

    def all(self) -> tuple[ReviewPolicy, ...]:
        return tuple(self._policies)


@runtime_checkable
class NotifierPort(Protocol):
    def send(self, text: str) -> None: ...


class StdoutNotifier:
    def send(self, text: str) -> None:
        print(f"[notify] {text}")


class ReviewOrchestrator:
    """应用服务:编排策略,依赖抽象(DIP)。"""

    def __init__(self, registry: ReviewPolicyRegistry, notifier: NotifierPort) -> None:
        self._registry = registry
        self._notifier = notifier

    def run_policies(self, ctx: ReviewContextDTO, policies: Iterable[str] | None = None) -> list[PolicyResult]:
        wanted = set(policies) if policies is not None else None
        results: list[PolicyResult] = []
        for p in self._registry.all():
            if wanted is not None and p.name not in wanted:
                continue
            results.append(p.evaluate(ctx))
        self._notifier.send(f"review finished for {ctx.repo}#{ctx.pr_number}, policies={len(results)}")
        return results


def build_default_registry() -> ReviewPolicyRegistry:
    reg = ReviewPolicyRegistry()
    reg.register(SecurityReviewPolicy())
    reg.register(ArchitectureReviewPolicy())
    reg.register(PerformanceReviewPolicy())
    return reg


if __name__ == "__main__":
    orchestrator = ReviewOrchestrator(build_default_registry(), StdoutNotifier())
    ctx = ReviewContextDTO("codesentinel/review-context", 108)
    for r in orchestrator.run_policies(ctx, policies=("security", "architecture")):
        print(r.policy_name, r.findings)

对照说明

  • SRPReviewOrchestrator 只做编排;策略类各自评估;通知由 NotifierPort 承担。
  • OCP:新增 ComplianceReviewPolicy 只需新建类并 register,不改 run_policies
  • LSP:所有策略满足同一 ReviewPolicy 契约,可互相替换(注意保持无副作用)。
  • ISPNotifierPort 独立于评估逻辑,调用方不被迫依赖巨型接口。
  • DIP:编排依赖 Protocol,具体 StdoutNotifier 可换为 Slack/Webhook 适配器。

里氏替换:一个「坏实现」反例(评审时用来抓 AI)

下面片段不要独立运行(需与上文 ReviewContextDTO 等同处一模块);它展示「同名策略」如何在特殊输入下抛出非预期异常,从而破坏调用方的统一处理逻辑。

class BrokenSecurityPolicy:
    """假装是安全策略,但抛出意外异常,破坏可替换性。"""

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

    def evaluate(self, ctx: ReviewContextDTO) -> PolicyResult:
        if "legacy" in ctx.repo:
            raise RuntimeError("legacy 仓库直接失败")  # 调用方无法统一处理
        return PolicyResult(policy_name=self.name, findings=())

治理建议:为 ReviewPolicy 增加 契约测试:所有实现必须在相同输入集上返回 PolicyResult,禁止除「领域定义的业务异常」外的裸异常泄漏;若必须失败,应返回带 Finding 的结构化结果或抛出受控的领域异常类型,而不是裸 RuntimeError

再谈接口隔离:把「扫描端口」拆成三条稳定边界

在 CodeSentinel 的演进里,最常见的胖接口长这样:ScannerPort 同时包含 scan_secretsscan_depsscan_licensescan_binary。结果是:安全策略只想查密钥,却被迫依赖整条工具链;CI 时间上升;本地开发还要 stub 一堆方法。

更合理的 ISP 切分是:

  • SecretScannerPort:只关心敏感信息模式;
  • DependencyGraphPort:只关心依赖与版本解析;
  • StaticAnalysisPort:只关心通用规则引擎输入输出。

这样 AI 生成「性能策略」时,不容易误调用密钥扫描(因为类型系统层面就不可用),评审者也能用 import 规则强制分层。

再谈依赖倒置:Composition Root 在 FastAPI 里放哪里?

实践上推荐 main.pycontainer.py 只做装配:创建 ReviewPolicyRegistry,注册策略实例,把 ReviewOrchestrator 注入路由依赖。路由函数保持薄:解析 DTO、调用用例、映射 HTTP 状态码。任何 Depends 返回的具体类都应是外层适配器,而不是领域服务里 new 出来的具体实现。

这套边界对 AI 特别关键:模型如果习惯「一个文件搞定」,你要用脚手架约束它:application/ 不允许出现 FastAPI 的 Request 对象;api/ 不允许出现业务规则判断。把约束写进 AGENTS.md 与 linter,比事后 CR 成本低一个数量级。

与 DDD 协同:SOLID 不是替代聚合设计,而是保护聚合不被污染

在 ReviewContext 内,ReviewRequest 聚合负责状态与不变式;ReviewPolicy 更像是领域服务或策略对象(取决于你是否把它建模为无状态服务)。注意:不要让聚合根直接依赖外层端口,否则你会在领域层看到 Protocol 满天飞,聚合变得难以测试。常见做法是:应用服务从仓储取出聚合,调用策略对象得到 PolicyResult,再把结果以领域命令写回聚合或记录为领域事件——SOLID 与 DDD 的分工是:聚合守护业务真相,策略提供可插拔判断,应用服务负责正确顺序

评审清单(打印贴显示器版)

当你审核 AI 生成的 CodeSentinel 模块时,按下列问题逐条过:

  1. 这个类是否同时修改了「流程状态」与「外部系统副作用」?
  2. 新增一种策略是否需要修改 if/elifmatch 的主干?
  3. 子类是否在父类未声明的情况下改变异常类型或返回值语义?
  4. 是否存在调用方只用到 1~2 个方法却要依赖 10+ 方法的接口?
  5. domain 包是否出现第三方 SDK 的 import?

只要任意一条为「是」,就把重构任务记为 P0,否则技术债会在 LLM 加速下指数膨胀。


生产环境实战:把 SOLID 写进 AGENTS.md,让 AI「默认不跑偏」

在真实团队里,单靠人类评审拦不住 AI 的统计偏好,你需要把架构规则变成模型可见的硬约束。下面是一段可直接放入 CodeSentinel 仓库 AGENTS.md 的片段(你可按团队语气微调):

## 架构生成约束(SOLID / Clean Architecture)

1. 禁止在 `domain/``application/` 引入具体供应商 SDK(OpenAI、Slack、数据库驱动)。
   - LLM、消息、存储必须通过 `ports/` 下的 Protocol 注入。

2. 禁止在应用服务中使用长链 `if/elif` 选择审核策略。
   - 必须使用可注册的 `ReviewPolicy` 实现 + `ReviewPolicyRegistry`3. 新增能力优先使用组合与新类型,避免 3 层以上继承;若使用继承,必须附带契约测试说明。

4. 接口拆分原则:不要把扫描、通知、计费混在同一 Port;按变更频率拆分为独立 Protocol。

5. 任何跨上下文调用必须通过 ACL 或应用服务编排,不允许领域对象直接依赖外部上下文的具体类。

落地建议

  • AGENTS.mdCI 静态规则(如 import-linter)联动:内层 import 外层直接失败。
  • 在 CodeSentinel 的 ReviewPolicy JSON/YAML 配置 中保存策略启用列表,编排层只读配置,不编译策略名进代码,进一步巩固 OCP。
  • 对 AI 生成 PR 启用 结构化评审模板:必须勾选「是否引入新的具体依赖到内层」。

与 CodeSentinel 门禁联动:把 SOLID 变成可度量指标

在生产环境,建议把 SOLID 相关风险映射成可观察信号,而不是停留在口头原则:

  • OCP 指标:统计核心编排文件(如 orchestrator.py)在过去 30 天的变更次数与新增分支数;异常升高通常意味着扩展点设计失败。
  • DIP 指标:对 application/domain/ 运行 import 规则扫描,统计违规 import 数量;把它作为合并门槛。
  • SRP 指标:类级别的 git blame 熵增(多人频繁修改同一类)可作为上帝类预警。

多团队协作下的「策略配置」与「策略代码」边界

大型企业里,审核策略往往一部分来自安全部门的配置(规则包版本、阈值),一部分来自平台团队的代码(对接内部制品库)。此时要清晰划分:

  • 配置层:启用哪些策略、阈值、白名单;
  • 代码层:策略如何解释 diff、如何调用端口。

如果把配置硬编码进 Python,AI 会不断生成「临时开关」;如果把复杂逻辑塞进 YAML,人类又难以维护。正确折中是:YAML 只描述策略参数,策略实现仍是类 + 注册表,符合 OCP。

故障复盘:当 AI 生成代码绕过端口直连 SDK

线上典型事故模式是:某位同学习惯在 Jupyter 里验证 OpenAI 调用,随后让模型把同样代码粘贴进服务层,绕过了你们的限流、审计与密钥轮换。DIP 的价值在事故后才会被高估:把供应商调用锁在适配器,你可以在适配器统一加日志、重试、预算控制与合规脱敏。没有这一层,治理平台会沦为「另一个随意调用大模型的脚本集合」。

安全与合规视角:为什么「开闭」与「审计」是同一件事

审核系统天然需要解释「为什么当时给出这条结论」。如果每次扩展都修改核心文件,你会很难把策略版本与代码版本对齐到一次审核结果上。策略注册表 + 独立策略类让你可以把 policy_name、版本号、配置哈希写进报告证据链——这也是架构师在 AI 时代必须强调的:可扩展不仅是开发效率,更是合规与可追溯


本讲小结

mindmap
  root((SOLID x AI))
    S单一职责
      拆分编排与执行
      拒绝上帝类
    O开闭原则
      注册表多态
      拒绝巨型分支
    L里氏替换
      组合优先
      契约测试
    I接口隔离
      小端口
      降低耦合面
    D依赖倒置
      Protocol在内
      适配器在外
    治理
      AGENTS.md
      import门禁
      CodeSentinel ReviewPolicy

延伸阅读:把 SOLID 映射到 CodeSentinel 的四个限界上下文

当你把视角从「单个服务类」拉到 Review / Rule / Repository / Report 四个上下文时,SOLID 的落点会更清晰:

  • ReviewContext:最需要 OCP 与 SRP。审核主流程稳定,策略与阶段化校验多变;编排类应薄,策略与责任链应厚。
  • RuleContext:最需要 ISP 与 DIP。规则引擎可能是 Semgrep、自研 DSL、或未来接 LLM;端口应小、替换成本应低。
  • RepositoryContext:最需要 DIP。Git/代码索引/二进制存储差异极大,领域层不能知道你是 GitHub 还是 GitLab。
  • ReportContext:最需要 SRP 与事件驱动边界。报告生成与通知发送不要与审核评估搅在一起,订阅事实而不是回调地狱。

这张映射表的价值在于:你能用同一套语言向业务经理解释为什么要拆服务、拆包、拆端口——不是为了炫技,而是为了让 AI 生成功能时「没有捷径可走」,只能走扩展点。

与 Code Review 文化的耦合:SOLID 是评论模板,不是吵架武器

在团队评审里,直接说「你违反 SOLID」往往引发防御。更有效的方式是把原则翻译成可执行改动

  • 「这段逻辑能否抽成 ReviewPolicy 实现并在注册表挂载?」
  • 「能否把 OpenAI SDK 移到 infrastructure/llm_adapter.py 并通过端口注入?」
  • 「这个接口能否拆成两个 Protocol,避免调用方依赖不相关方法?」

当评论可执行,AI 辅助修复也会更稳定:你把评论粘贴回模型,它能产生结构正确的补丁。

落地节奏:从「先能跑」到「能演进」的两阶段策略

很多团队会争论:一上来就 SOLID 是否过度设计?对 CodeSentinel 这种明确要长期演进的平台,建议是 两阶段

  1. 第一阶段(探索期):允许在 api 层快速验证价值,但必须明确哪些文件是「临时代码」,并在 issue 里登记偿还计划。
  2. 第二阶段(产品化):把稳定用例下沉到 application/domain,引入端口与策略注册表,把第一阶段的分支逻辑迁移为策略对象。

关键在于:探索期不能无限续期。你可以接受短期丑陋,但必须给丑陋设定到期日;否则 LLM 会在丑陋之上继续堆叠,形成不可逆的耦合块。架构师的职责之一,就是把到期日写成里程碑,并在 CI 上加上对应的门禁开关。

小结补遗:SOLID 与性能、成本并不天然对立

有人会把原则与性能对立起来,认为多态与注入会增加调用开销。对 CodeSentinel 这类 IO 密集(Git 拉取、规则扫描、LLM 调用)的系统,真正的成本几乎总在外部网络与模型推理,而不是 Python 多态的分发。相反,良好的 DIP 让你能对昂贵资源做统一缓存、批量请求与熔断,从整体上降低成本。架构师在讨论性能时,别被微优化带偏:先治理依赖方向,再谈热点优化。把这句话写进团队的性能评审 checklist,能避免很多无意义的争论。


思考题

  1. 你的项目中最近一次「只为加一个小功能却改了核心类」发生在什么场景?如果用 ReviewPolicy 模式,边界应画在哪里?
  2. 当 LLM 需要作为「策略的一部分」时,如何避免 DIP 被破坏?端口方法应如何设计才能便于替换供应商?
  3. 你会为 ReviewPolicy 写哪些契约测试,用例数据从哪里来(真实 PR diff 片段还是合成)?

下一讲预告

下一讲进入 设计模式实战:我们会把本讲的策略思想升级为完整的 策略模式、观察者模式、责任链模式 三角组合,展示 CodeSentinel 中 EventBus 如何订阅 ReviewCompleted,以及 ReviewPipeline 如何把多阶段校验拆成可插拔链条——让你既能把 AI 生成代码「接进来」,也能在不变更核心编排的前提下扩展到企业级流程。建议你先复习本讲注册表与端口注入,再看下一讲会更顺滑一些。