重读整洁架构:从额外负担到杠杆
整洁架构一直是出色的上下文控制设计,拖累它的是繁文缛节的成本。本章用 CMP 重新解读整洁架构——策略与机制、放置方式与依赖规则——并解释为什么智能体让它的 ROI 发生翻转。
本文为中文版,根据英文原版改写,而非逐句直译。
我一直很喜欢整洁架构(Clean Architecture,下文简称 CA)。在 AI 编程时代,我认为它的优势已经很难被忽视:用 CMP 自己的标尺来衡量——一个设计能否让现实中各种修改所需的充分上下文更便宜地获取?——整洁架构几乎满足了这条原则提出的所有要求。它一直都是好的上下文控制设计。拖累它的是价格:太高了,大多数团队拿不到它的回报。而智能体恰恰砍掉了这个价格——这正是 CA 的结构所奖励的东西。
所以本章只做一件事:用 CMP 来解读 CA,把那张同心圆图拆解成一个单一的机制——**策略(policy)与机制(mechanism)**的分离。这种分离由一种设计纪律制造出来,由布局负责放置,并由一条不变式加以保护。这样看,整洁架构是 CMP 所推荐做法的一个久经考验的化身:它降低了获取系统真实面对的那些修改所需充分上下文的成本。而让 CA 背上坏名声的那部分工作——繁文缛节——恰恰是廉价、可检查的劳动,如今智能体可以免费承担。
1. CA 真正关心的唯一边界:做什么与怎么做

图 1. 整洁架构的同心圆
整洁架构存在的目的,是把策略——系统应该做什么——与机制——事情如何完成——分开。这一条”做什么 / 怎么做”的界线,是整章的主角。分层、依赖规则、端口、适配器、可测试性——每一样的存在,要么是为了制造这条线,要么是为了把东西放在线的正确一侧,要么是为了让它保持可信。请记住这一点。
用例:把一个流程切分成相互独立的子问题
CA 用来画这条线的主要工具是用例(use case),它所做的动作值得放慢来看。一个用例拿来一个业务流程——“下单”、“退款”——把它拆成两部分,这两部分因不同的原因而变化。第一部分是大纲:用领域语言写成的、有先后顺序的流程叙述——校验购物车、预留库存、向客户扣款、记录销售、发送通知。第二部分是一组端口(port):大纲调用的每一项具体能力,都对应一个有名字的洞——InventoryReservation、PaymentGateway、SalesLedger。大纲是策略:它掌管做什么和顺序。每个端口都是一个接缝,它背后的怎么做——Stripe、Postgres、某个消息队列——在你阅读流程时都不在视野之内。
回报是分解。一个纠缠在一起的流程,变成了一段简短可读的策略叙述,加上少数几个相互独立的子问题,每一个都被封在自己的端口后面,或者封在另一个更小的用例里。只看用例本身,你就能把握全部的做什么,而无需为怎么做的细节付出任何代价;你也可以单独重做任何一个怎么做——换支付服务商、重新调优持久化——而不触动大纲或其他端口。一个复杂的业务流程,不再是一个你必须一次性装进脑子里的庞然大物,而变成了一组小问题,你可以逐个路由过去、逐个解决。
实体:为什么静态对象能承载业务规则
策略核心的另一半是实体(entity),而几乎每个刚接触 CA 的人都会在这里绊一跤。实体承载的是企业业务规则(Enterprise Business Rules)——关于业务本身、独立于任何具体应用而成立的规则。可是规则给人的感觉是行为,而实体看起来却是一个静态对象:一堆字段加上 getter。一个名词怎么能装下一条规则?
只要不再把业务规则读作一段过程,而是读作对哪些状态允许存在的约束,这个困惑就消失了。这一重新理解要归功于《Domain Modeling Made Functional》:把领域建模得足够精确,让非法状态无法被表示。相当一部分”规则”其实是关于合法值的陈述——
- “订单至少有一个订单项” → 一个无法构造出空值的类型;
- “只能向已验证的邮箱发货” → 把已验证和未验证做成两个不同的类型,而不是一个布尔标志,这样只有
ship(VerifiedEmail)能通过类型检查; - “订单总额永远不为负” → 一个拒绝非法数值的构造函数。
这样理解,实体就作为不变式的守护者赢得了它的位置:它的静态形状就是规则,因为规则恰恰描述了哪些值算作合法实例——而类型恰恰是对一组合法值的描述。至于那些”动态”规则——提交一个草稿订单、应用一个折扣——则是把一个合法状态带到另一个合法状态的函数(Draft → Placed),每个函数只被允许执行业务所允许的那些状态转换。行为,就是在”构造即合法”的状态之间那些合法移动的集合。(《Domain Modeling Made Functional》用函数式编程的语言把这一点讲得很生动,但这个原则并不只属于函数式:一个校验不变式的私有构造函数,加上值对象,在面向对象的代码库里也能完成同样的工作。)
这就是企业规则位于最内层圆环的原因。由实体自己强制执行的不变式,下游的任何用例、控制器或 ORM 映射都无法打破——没有哪条代码路径能制造出一个非法订单。把规则放进类型,意味着每个外层都免费继承了它:这是一个系统所拥有的最深层、复用最多、变化最慢的策略。
用例和实体界定了策略这一侧;它们放在端口后面的东西,就是机制。用这种方式给核心命名之后,日常的问题就变成了分类:对于一段现有代码,*它属于哪一侧?*判断信号足够稳定,可以写下来。
| 对代码提出的问题 | 策略(向内路由) | 机制(向外路由) |
|---|---|---|
| 它为什么会变化? | 某个业务决策变了 | 某个工具、供应商或交付细节变了 |
| 能向领域专家解释它吗? | 能,用他们的语言 | 不能,这是实现层面的关注点 |
| 换掉框架后它还在吗? | 在,原封不动 | 不在,它就是框架本身 |
| 不论数据如何存储或展示,它都成立吗? | 是 | 否 |
当答案混杂不清——“这个校验既是业务规则,又是数据库约束”——你就找到了一个目前被涂抹在边界两侧的决策。这不是麻烦;这正是 CA 发挥价值的地方。
2. CA 如何制造边界:由内向外的顺序 → 由客户端编写的端口
《边界原则:隐藏上下文,而非代码》已经讲清了一个好边界需要什么:由客户端编写的契约,命名的是用例需要什么,而不是供应商恰好提供了什么。当契约是按客户端的形状塑造的,LSP、ISP 和 OCP 就会作为结果自然出现。那一章留下的是一个程序性的问题:当 SDK 就摆在手边时,究竟是什么让你从客户端一侧去编写契约?
CA 的回答是它的实体 → 用例 → 适配器顺序。你先用策略核心自己的语言设计它,视野中没有数据库,也没有框架。等到你需要持久化或支付服务商时,用例已经写下了它想要什么——于是端口由它的客户端编写,适配器必须去满足它。这个顺序就是迫使客户端编写契约的那条纪律。把顺序反过来,从供应商的 SDK 开始,你得到的就是按服务商形状塑造、却起了个干净名字的包装层:名义上的边界,隐藏了代码,却没有隐藏上下文。
整洁架构为那条原则提供了一个具体的执行机制。依赖倒置把编写权交给客户端;由内向外的顺序则确保你每次都把它指向正确的方向,而不是只在有人记得时才这样做。这就是为什么在 CA 代码库中,可替换性和接口隔离读起来像是结果:顺序制造出了它们所依赖的、按客户端形状塑造的契约。
3. CA 如何放置一次改动:路由上下文的两条坐标轴
放置是局部性的问题:当一次改动到来时,你能否从任何一个自然的入口——一个端点、一个失败的测试、一条领域规则——触及它的整个修改闭包,而不必在整棵目录树里搜索?CA 的回答是两条坐标轴,它们合起来构成一套路由坐标系。
分层是横轴。它按与策略的距离给每一个构件排序——领域规则、仓储端口、适配器、传输层——所以一旦你拿到某个部件,就知道它是哪一类东西、位于线的哪一侧。这个索引是真实的、廉价的、可以用 lint 检查的。单靠分层无法告诉你的,是一次改动归哪个功能所有,因为一次功能改动——“增加促销码”、“支持部分退款”——是一个垂直切片,会同时横穿每一层。
第 1 节中的用例就是纵轴。它是一段连贯的应用行为,所以形状与这段行为一致的修改,会落在单一的所有者身上:策略在用例里,最多在某个端口契约上改一行,端口后面的机制则完全不动。用例是一个功能路由到的地址;分层是进入之后的二级排序。两者交叉,一次改动就有了坐标——这个所有者,那一层——而不是散落在控制器、ORM 映射和序列化器各处。这就是局部性的回报,也是智能体可以停止搜索的原因:它只需进入网格中的一个单元格,而不是整个仓库。
CA 搭建了网格,却不替你在纵轴上画线。哪些行为归入哪个用例,一个功能在哪里结束、下一个从哪里开始——这种切分是领域建模的核心工作:限界上下文、业务的形状,与第 1 节中”做什么 / 怎么做的线落在哪里”属于同一种昂贵的、没有判定器的判断。它恰恰是智能体无法替你做出的决定。CA 给所有权提供了一个归宿——一个可以挂上垂直切片的一等用例单元——然后用分层给它建立索引,用依赖规则来保护它。它提供的是容器,而不是判断。
4. CA 如何保护边界:作为路由判定器的依赖规则
一个边界只有在被信任时,才值它的成本。不可信的边界会退化为名义上的间接层——一层你为了确认还得穿过去看看的东西。依赖规则(源码依赖只能向内指)是支撑这份信任的那一条全局不变式:策略核心从不导入框架、数据库客户端或传输层类型。它的覆盖范围是刻意收窄的:它检查的是依赖的方向,而不是边界是否画在了正确的位置,也不是端口是否按客户端的形状塑造——一张依赖完全向内的图,照样可以包裹着一个画得很糟的边界。它保证的东西更窄,但却是真实的:一个你确实画好了的边界,不会悄无声息地腐烂。
《架构即上下文路由》给这种失败模式起了它真正的名字:架构腐化就是路由失败。一次向内的泄漏——一条停在控制器里的领域规则,一个被导入到实体里的 ORM 类型——既造成局部损害,又让以后的每一个修改者都学到:这个边界会撒谎。一旦边界会撒谎,人们就会绕过它:在官方路径的内外都去搜索,而架构此时收取了全部的承载深度成本,却没有兑现它所承诺的任何搜索缩减。
让这道护栏真正立得住的,是它可以被机械地检查。依赖方向是结构,而结构可以被 lint:一个检查会在泄漏本身发生时触发,就在某个源文件指错方向的那一刻。在 CA 要求的所有东西里,这是唯一一条不需要人的判断就能强制执行的规则——它从不等待审查者去注意,也不会随着熟悉而被侵蚀。在每次改动上运行,依赖规则就成为一个可执行的路由判定器,于是边界的可信是由构造保证的,而不是靠警惕维持的。
5. 它为什么被冷落,以及智能体改变了什么
第 2 到第 4 节里的一切,一直都是真实的价值。CA 被冷落从来不是因为设计不好,而是因为价格。CA 的成本是手工的繁文缛节——映射器、DTO、端口、装配代码、平行的模型——由团队最稀缺的资源(人的注意力)来支付,换来的收益却是延后的、概率性的(大多数应用永远不会更换框架或数据库)。对大多数团队、在大多数时候,诚实的结论就是 YAGNI。
智能体改变了这笔押注中的两项,而且朝着相反的方向:
- **廉价的一半:繁文缛节坍缩了。**映射器、适配器、装配代码和脚手架,正是上一章里那块可由判定器检查的区域——有明确目标可以对齐的工作,而这正是智能体不知疲倦、也做得很好的事。让 CA 显得”太麻烦”的那些劳动,现在是你可以委派出去的部分。
- 昂贵的一半:CA 瞄准的正是没有变便宜的东西。上一章里那个没有判定器、仍然昂贵的判断的头号例子,恰恰就是画错了位置的边界——支付接缝是围绕”更换服务商”搭建的,而真正到来的变化却是支付语义(先买后付、订阅、区域合规),结果每一次真实的改动都不得不刺穿它或绕过它。画好这条线,就是 CA 唯一的工作。CA 并不能让这个判断变便宜。它让这个判断变得可复用、集中且清晰:
- **判断付一次,然后摊销。**什么是策略、端口承诺什么、线落在哪里——CA 把这些判断前置到稳定的结构中,于是之后的每一次改动都继承了答案,而不必重新推导。
- **隔离不可约的部分。**业务规则的正确性没有自动判定器。CA 把这些无法自动检查的判断集中到一个小而纯粹、不依赖框架的策略核心里,而不是任由它散落在控制器、ORM 和序列化代码之中。
- **路由稀缺的注意力。**做什么与怎么做的划分告诉你注意力该花在哪里。做什么——实体和用例——是业务正确性所在之处,是任何测试都无法完全确定的部分。这些代码完全可以由智能体来写,但值得逐行审查:diff 很小,而且去掉了机制层面的噪声之后,它读起来就是纯粹的领域语言。每个端口背后的怎么做只需对其契约负责,所以你可以把它交给智能体自动构建。审查集中在决定系统做什么的那少数几行上;怎么做则在 TDD 的约束下整体委派给智能体。
这就是转折所在。边界、由内向外的顺序和放置规则,恰恰是把昂贵的、无法外包的判断与廉价的、可以完全委派的工作分离开来的那套机制——而智能体终于让廉价的那一半变成了免费。
6. CA 的适用位置:更宽的范围,而不是普遍的默认选项
这一切并不意味着 CA 在任何地方都是正确的默认选择。只有当确实存在一条可以画出的做什么 / 怎么做边界时,它才有回报。把分层强加给一个没有这种边界的薄 CRUD 应用,CA 只会把一个单字段的改动分散到多个文件里,而垂直切片本可以把它们放在一起——智能体让每一次跳转变得更便宜,也修正不了形状上的错误。
智能体改变的是盈亏平衡点,而不是判断 CA 是否适用的标准。它们让繁文缛节可以廉价地产出,并让 CA 的自动化检查成为保证生成代码可信的东西——于是 CA 开始在比过去更低的领域复杂度上就有回报,值得引入这套结构的系统范围扩大了。判断标准本身没有变:这个架构是否沿着系统实际遇到的改动来路由上下文?先回答这个问题——通过观察真实的修改流——然后再去画边界。
7. 一般性的动作:为那些被称作过度设计的做法重新定价
整洁架构只是一个更大模式中的一个实例:一种真正好的做法,因为执行成本太高,大多数团队拿不到它的回报;一旦智能体吸收了这部分成本,它就变得值得。同样的视角还可以重新发掘软件工程中其他被悄悄搁置的经验。拿出任何一项曾被归入好主意,但太贵的纪律,用 CMP 的语言说清它到底买到了什么——为系统真正面对的改动提供更廉价的上下文——然后检查智能体是否改变了它的价格。基于属性的测试、形式化规格、变异测试、与代码保持同步的文档——每一项都是候选:一笔合理的押注,输在了执行成本上,而不是输在它前置的那份判断上;一旦由智能体承担这份成本,它们就值得重新审视。
而整洁架构大概不会是唯一的例子。几十年的软件工程技艺,都是在如今已不再成立的成本假设下被评判的。所以,我们当初以”太贵”为由搁置的一些东西,或许值得在新的经济条件下再看一眼——看看智能体现在让什么变便宜了,又有什么是它依然做不到的。