摘要
2015 年初,我加入 A 公司不久,看到的交易平台已经处在一种矛盾的状态里:系统被拆得越来越细,平台开放的接口和扩展点越来越多,业务交付却没有因此变得更轻。人手总是不够,项目排期很长,延期并不罕见;每到周四的固定发布日,数十个分支汇在一起,任何一个业务的变化都可能要求其他业务陪着回归。
问题并不只是代码里有太多 if/else,也不只是传统 SPI 不够灵活。更深层的问题是,平台没有清楚回答几个问题:一段定制逻辑属于谁,只应该影响谁;一个业务场景如何跨越多个应用保持完整;多个规则同时生效时怎样组合;业务代码能否脱离平台独立演进。Lattice 就是围绕这些问题形成的开源业务扩展调用框架。它先分开业务与平台、业务与业务、逻辑业务架构与物理应用架构,再用业务身份、Ability、Use Case、垂直与水平组合以及 Reduce,把“到处修改平台”改造成“在明确边界内组装业务”。最后,我会说明自己如何在 Spark 中把这些判断整理成 AI 架构 Skill,使它们在设计、模块划分和代码生成时真正成为约束。
高度可定制的平台,关键不是提供尽可能多的扩展点,而是让每次变化都有明确的归属、作用域、组合规则与发布边界。Lattice 要解决的不是“如何让业务插入一段代码”,而是“如何让业务变化不再污染整个平台”。
第一部分 · 当平台开始追不上业务
012015 年,我看到的不是一个技术问题
2015 年初加入 A 公司后的前半年,我花了很多时间观察交易平台怎样承接业务。那时最直观的感受不是某个接口设计得不好,而是一种持续存在的疲惫:人手不够,加班严重,排期很长,项目经常延期;平台与业务采用共建方式协作,但团队越多,协同成本越高。大家都很努力,系统也拥有大量看起来先进的技术机制,可业务交付仍然越来越重。
这不是因为分布式架构本身做错了。A 公司早期的电商业务相对单一,一个系统可以包含商品发布、购物车、订单创建和履约等完整功能。随着业务量和团队规模增长,把单体拆成用户、商品、交易、店铺等职责更内聚的系统,是一次必要而且成功的演进。每个系统能够独立设计、承接需求和发布,研发效率与稳定性都曾因此提升。
但任何架构的收益都有适用尺度。业务继续扩张,行业越来越多,应用和服务数量进入数千量级之后,原本用于降低复杂度的拆分,开始制造另一种复杂度:一个完整的业务意图被切散在许多应用和团队里。
一个需求从提出到上线,可能要经过多个团队共同分析、设计、开发、联调,并按照特定顺序发布。更麻烦的是,没有人能有把握地说清楚它究竟会触及多少系统。主流程容易看见,异常分支和多年积累的旁路最容易在后期暴露。漏掉一个点,后面可能就是方案重评、返工和延期。
我们在架构层完成了应用解耦,却没有在业务层完成意图的重新聚合。
系统可以被拆成一千个服务,但用户提出的仍然是一个完整需求。技术边界越多,越需要一种机制把业务意图重新组织起来。
02从 API 开放到业务方共建
平台早期最重要的开放方式是 API。平台定义稳定的服务接口,业务系统通过远程调用复用商品、交易、营销等能力。这种方式适合表达相对稳定的功能,却很难穷举不同行业的业务差异。
当差异越来越多,接口参数中开始出现 Map。调用方可以往里面放平台模型没有预先定义的上下文,但一个不认识这些参数的平台,也很难真正理解它们对应的业务语义。再往后,Java SPI 被引入:平台定义接口,业务团队实现接口,把特定业务逻辑放进平台执行路径。这种“业务方共建”一度释放了很大的生产力。
它的问题也逐渐显现。平台没有提供足够强的业务—平台分离机制,业务代码和平台代码仍在同一个代码库里纠缠。业务定制不能独立发布,必须跟随平台排期进入统一窗口。我印象很深的是,每到周四的固定发布日,需要合并四五十个分支。每个分支单独看都有自己的理由,合在一起却形成一次巨大的全网回归:A 业务上线,B 业务也要担心自己被影响。
平台的代码质量和隔离性,主要依赖开发者足够谨慎,而不是一种可以被验证的结构。人数少、业务少时,经验和评审也许能够兜住;规模扩大后,“大家小心一点”从来不是可靠的架构。
03一段减库存代码如何慢慢腐化
减库存策略是一个很小、却足够典型的例子。以下代码只为说明问题,和任何生产代码都没有关系。
最初,平台统一采用“拍下减库存”。后来,大家电商品价格高、库存少,容易遭遇拍下不付款造成的库存占用,于是业务希望改成“付款后减库存”:
public ReduceType getInventoryReducePolicy(OrderLine orderLine) { |
虚拟商品出现后,有些商品根本不需要扣库存,于是前面又多了一个判断。再后来,大家电支持门店自提:交付给用户的是虚拟提货码,背后对应的实物库存却仍然必须扣减。“虚拟商品不减库存”与“大家电付款后减库存”发生了交叉,判断顺序开始影响结果。
接着,大家电内部也不再统一。单价高于某个值采用付款后减库存,低于这个值仍然拍下减库存。一开始清楚的默认规则,逐渐变成由商品类型、行业标签、交付方式与价格共同决定的分支树。
这样的腐化不是某一天由某个糟糕的程序员制造出来的。恰恰相反,每一次改动都解决了一个真实需求,每一次局部判断也可能是当时最经济的选择。几年以后,一个方法可以长到数千行。后来的人面对新需求,只能小心地寻找一个“看起来合适”的位置加入自己的分支,然后祈祷没有改变其他业务。
这段历史让我意识到,所谓遗留代码,很多时候不是缺少重构,而是缺少业务变化的所有权模型。代码不知道哪条规则属于哪个业务,也不知道规则之间应该隔离、覆盖还是合并,只能让执行顺序替组织做决定。
04SPI 清理了代码,却没有消除耦合
很自然地,团队会把这些分支抽成 SPI:每个实现提供 filter 与 execute,平台遍历实现列表,找到第一个满足条件的策略。
public interface InventoryReducePolicySpi { |
当实现类不多时,这种设计很有效。一个几千行的方法被拆成多个职责更清楚的类,局部代码可读性和测试性都提高了。但规模继续增长,新的问题只是换了一种形态:
- 新需求应该修改已有实现,还是注册一个新实现?
- 新实现放在列表第几个,才不会被前面的规则截获?
- 它的
filter是否与已有几十个实现重叠? - 调整顺序后,哪些业务会悄悄改变行为?
更复杂时,filter 里还会调用规则引擎或远程服务。此时即使逐个阅读代码,也很难静态判断某个实现何时生效。在我接触过的复杂系统里,类似 SPI 可以接近千个,每个接口又可能有几十个实现。全局遍历和“第一个命中”使所有业务共享同一棵隐形的决策树。
表面上,平台消灭了 if/else;本质上,条件分支被搬到了注册顺序和过滤逻辑里。业务之间依然耦合,而且耦合变得更难看见。
- 症状 — 核心方法分支膨胀,修改时不敢确定影响范围。
- 第一次修复 — 把分支抽成 SPI 实现,以列表顺序和
filter决定谁生效。 - 新的失败 — 条件与顺序形成全局决策树,业务之间仍然互相竞争。
- 根因 — 扩展机制没有业务身份、作用域、组合语义和独立发布边界。

图 1 · 当扩展机制失去作用域。 服务拆分降低了单个应用的复杂度,却没有自动保留完整业务意图;全局 SPI、共享代码库和统一发布窗口,又把已经分散的变化重新耦合在一起。
05应用视角不是业务视角
另一个更隐蔽的问题,是平台把扩展点按照应用来开放,而业务方是按照场景来思考的。
在 A 公司当时的体量下,各类业务能力已经被拆分到超过 2000 个微服务中。这个数字的意义不在于系统规模有多大,而在于它已经超过任何一个人能够完整记忆、评估和掌控的范围。没有人可以只凭个人经验画出完整的依赖关系,也很难在需求刚提出时就准确回答:它会经过哪些应用,涉及哪些团队,改变哪些扩展点,又有哪些不在主链路上的入口会被遗漏。
这并不是说一个需求会修改 2000 个服务,也不是说微服务拆分本身错了。真正的问题是,当完整的业务意图被切散到如此多的技术边界之后,影响评估仍然依赖人去逐个发现和拼接。评估会变成一轮轮人员召集、链路追踪和经验确认;只要少找到一个系统或一个旁路,分析结论就可能是不完整的。
假设某个业务不允许商品通过购物车购买。从业务语言看,它只有一条规则:“这个业务不支持购物车购买”。但在应用视角里,业务团队可能要完成一串分散的定制:商品详情页隐藏加入购物车按钮;购物车接口拒绝绕过页面直接加入;订单创建服务拒绝伪造购物车渠道下单;其他旁路入口也要做一致校验。
每个应用提供的扩展点单独看都可能是合理的,可完整性责任被推给了业务交付团队。开发者不仅要理解自己的行业,还要从两千多个微服务中识别与当前场景有关的那一小部分,理解它们开放了什么能力、彼此有什么关系,并证明自己没有漏掉关键入口。应用拆得越细,单个系统的职责越清楚,跨系统的业务评估却可能越困难。
所以,当时真正要解决的并不是“再设计一种更强的 SPI”,而是把扩展的组织单位从应用和技术处理点,重新提升到业务与场景。平台需要让开发者先看见一个完整业务要完成什么,再由场景去组织它所依赖的能力,而不是要求每个业务团队重新遍历一次庞大的应用地图。
第二部分 · 先重新定义问题,再设计框架
06我们需要回答的六个问题
在几次设计研讨中,我请团队不要先讨论类图和框架,而是完成一句话:“如果平台能做到……就好了,这样我们就可以……”。这种方式帮助我们从技术偏好回到实际阻力。
大家期待的并不神秘:特定业务的订约与履约方式可以快速构建成业务包;业务包与平台解耦,也与其他业务隔离;业务可以按真实流程选择和组合已有能力;产品与开发能够看见平台提供了什么、当前业务用了什么;业务不必等待平台统一发布窗口;同一套逻辑架构还可以适应不同部署形态。
把这些诉求继续收敛,可以得到六个必须被框架明确回答的问题:
| 平台症状 | 真正需要回答的问题 |
|---|---|
| 业务代码混入平台仓库 | 变化属于谁,代码边界在哪里? |
| A 业务修改可能影响 B 业务 | 一段规则的作用域如何确定? |
| 需求散落在多个应用 | 如何按完整场景组织和复用能力? |
| 扩展点埋在物理应用中 | 如何形成不依赖应用拓扑的统一业务视图? |
| 多个 SPI 同时命中 | 规则如何组合,冲突由谁裁决? |
| 数十个分支统一发布 | 业务能否独立开发、测试和交付? |
这六个问题决定了 Lattice 的形状。框架里的注解、接口和运行时只是实现手段;如果忘了这些问题,Lattice 也很容易被误用成另一套 SPI 工具。
07重读《干净的架构》:核心不是画同心圆
后来再读 Robert C. Martin 的《干净的架构》,我对它最深的体会不是 Entities、Use Cases、Interface Adapters 和 Frameworks 应该分别放在哪个工程目录,而是“变化应该沿什么方向传播”。
书中最著名的是依赖规则:源码依赖只能指向更内层。外层是容易变化的机制,内层是更稳定、更接近业务目的的策略。Web、数据库和框架都应该服务于业务规则,而不是让业务规则反过来适应这些技术细节。一个典型用例会调度实体完成业务目标,再通过边界与外部系统交换数据;跨越边界的依赖仍然朝内。[1][2]
但在大型业务平台里,仅仅让数据库依赖领域接口还不够。业务定制本身也是一种外部变化。 某个行业的库存策略、某个营销玩法的限购规则、某种履约方式的核销要求,都不应该反向侵入所有业务共享的核心。
这使我对依赖规则有了一个更具体的理解:架构的内外,不只按技术层划分,也要按政策的适用范围划分。共享实体规则、场景规则、垂直行业规则和横向能力规则,稳定程度与作用范围不同。我们需要让它们通过明确边界协作,而不是在同一个全局列表里互相猜测。
《干净的架构》还强调独立开发、独立部署、插件边界,以及架构应该直接表达系统用途,而非首先暴露所用框架。[1] 这些观点给了我一套更准确的语言,去解释当年那些从实践中逼出来的判断:
- 业务与平台分离,不只是代码整洁,而是在控制变化方向;
- 逻辑业务视图与物理应用拓扑分离,不只是方便检索,而是在恢复完整业务意图;
- 按用例组织扩展,不只是目录调整,而是在让架构说业务语言;
- 插件独立交付,不只是发布提速,而是在兑现边界;
- 运行时只执行已确定的组合,不只是性能优化,而是在把隐含冲突变成显式政策。
我的读书体会: 干净并不意味着代码少、层次多或接口多。所谓“干净”,是高层业务政策不会被低层机制和局部定制拖着走;当变化发生时,我们知道它应当停在哪条边界上。
第三部分 · Lattice 如何逐一解决这些问题
08为什么叫 Lattice
Lattice 在英文里是格子、网格的意思。我们观察业务时,会看到两个相互交织的维度:一个是彼此隔离的垂直业务,另一个是可以被选择和叠加的水平能力。纵向与横向交织,形状很像格子。当然还有一个不那么严肃的原因:程序员喜欢穿格子衫。
因此,这个业务扩展调用框架被命名为 Lattice。它不是为了给传统 SPI 换一个新名字,而是要把业务身份和能力组合变成框架的一等概念。开源仓库位于 hiforce/lattice,本文最近一次更新时主分支版本为 1.0.21。[3]
09Lattice 的解法,先从三个分离开始
前面的问题看起来很多,根上却是三种边界同时消失了:业务代码进入平台核心,不同业务进入同一个全局判断空间,完整业务流程又被物理应用切碎。Lattice 的第一步不是增加更多扩展接口,而是先恢复三个分离。
| 分离 | 要隔开的对象 | 建立边界的机制 | 直接解决的问题 |
|---|---|---|---|
| 业务与平台分离 | 平台稳定能力与业务定制 | 稳定契约、业务插件、独立代码库 | 业务代码不再污染平台核心 |
| 业务与业务分离 | 不同业务的规则与实现 | 业务身份、定向加载、调用链上下文 | 一个业务的变化默认不会进入另一个业务 |
| 逻辑架构与物理架构分离 | 业务流程与应用、服务部署拓扑 | 注解元数据、统一上报、业务管理视图 | 设计者按场景发现能力,不必遍历应用地图 |
这三个分离不是三项互不相关的功能。第一层回答代码应该放在哪里,第二层回答运行时应该进入哪个规则空间,第三层回答人在设计需求时应该看见什么。缺少任何一层,另外两层都会被削弱:只有插件而没有业务身份,插件仍可能互相误命中;只有身份而没有代码边界,业务仍要跟随平台一起构建发布;只有运行时隔离而没有逻辑视图,需求评估仍会在两千多个服务之间艰难搜索。

图 2 · Lattice 的三个分离。 左侧把业务变化挡在平台核心之外,中间先以业务身份选择唯一规则空间,右侧再把散落在许多应用中的能力还原成一个可供设计和评估的逻辑业务视图。
10第一个分离:业务代码与平台代码
过去的“业务共建”,是平台定义 SPI,业务团队把实现直接提交进平台工程。它在早期很有效,因为业务可以快速进入主流程;但随着参与方增多,平台仓库里逐渐同时存在稳定机制和大量局部政策。平台修改要担心业务实现,业务修改也要跟随平台分支、回归和发布,最终形成“你中有我、我中有你”的代码与交付关系。
Lattice 要求平台仓库只保留稳定的实体、用例、Ability、扩展契约和默认实现,具体业务规则只存在于业务自己的插件包和代码库中。运行容器通过注册与插件加载机制把二者连接起来:平台负责定义“这里允许改变什么”,业务插件负责回答“我的业务在这里如何处理”。依赖关系指向平台提供的稳定契约,而不是让平台核心反向依赖某个具体业务。[3]
于是,三组边界可以尽量对齐:
- 代码边界 — 业务定制不进入平台核心仓库;
- 测试边界 — 业务插件可以围绕自己的规则与所选能力独立验证;
- 发布边界 — 业务变化不再天然绑定平台统一版本和所有业务的回归窗口。
插件化不等于自动实现独立发布。共享数据库、契约兼容、运行容器和组织流程仍然可能形成耦合。但它至少从源代码结构上切断了最危险的反向依赖,使独立构建、验证与交付成为架构真正支持的选项。
11第二个分离:一个业务与另一个业务
传统 SPI 通常问:“当前请求满足哪个实现的过滤条件?”Lattice 先问:“当前请求属于哪个业务?”
这两种问题看起来接近,后果却完全不同。传统 SPI 的每个实现都用 filter 猜测自己是否适用于当前请求,所有业务的实现因而被放进同一个候选集合。一旦某个条件少写一个限制、多覆盖一个渠道,或者几个条件发生重叠,原本属于 A 业务的规则就可能在 B 业务中生效。判断正确与否分散在每个实现里,业务越多、组合越复杂,想证明所有过滤条件互斥就越不现实。
Lattice 把顺序倒过来:每个业务场景进入调用链时,第一件事就是确定业务身份。 身份一旦确定,运行时便获得一个明确坐标,只加载这个业务身份对应的插件包和实现;其他业务的代码根本不进入候选集合。业务规则的隔离因此不再依赖每位开发者都写出脆弱但永远正确的过滤条件,而成为框架执行模型的一部分。
|
@Business 声明业务,@Realization 声明这段实现属于哪个业务。调用链携带统一业务编码,运行时按业务身份定位插件与实现。这里最重要的不是注解语法,而是所有权从隐含条件变成了显式声明:先确定“我是谁”,再寻找“我有哪些规则”。
统一业务身份还必须贯穿调用链。如果入口知道当前业务,进入下游服务后却丢失了身份,隔离就只停留在一个进程里。Lattice 将业务编码作为扩展调用的基本上下文,使同一业务的规则能够在不同能力调用中保持一致。
至此,前两个分离解决了代码归属和运行作用域。要完成第三个分离,还需要先把平台能力从零散接口提升为可发现的业务语义,并把这些能力重新组织进完整 Use Case。下面的 Ability、业务二维模型、Reduce 与 Use Case,正是构建逻辑业务视图所需的语言。
12Ability 让平台能力变得可发现、可调用
业务身份回答“谁的规则”,Ability 回答“平台在这个载体上提供什么能力”。
在 Lattice 中,一个能力围绕特定载体聚合一组可扩展行为。比如订单明细是载体,价格计算是能力,“自定义商品单价”是其中一个扩展点。能力负责加载当前业务的实现,并用明确的归并策略得到结果;业务方只实现自己需要改变的部分。
public interface OrderLinePriceExt extends IBusinessExt { |
这种组织方式带来三个变化。第一,平台能力可以被发现,开发者不必在数千个应用中搜索零散 SPI。第二,默认行为仍然由平台提供,没有定制的业务自然回落到默认值。第三,扩展结果怎样归并不再由实现列表的偶然顺序决定,而是由能力定义的语义决定。
这里需要克制。Ability 不是把每个方法都包装一层的新名词。只有那些具有稳定业务语义、确实允许不同业务做出不同选择的行为,才值得成为能力或扩展点。否则,接口越多,平台并不会越灵活,只会多出另一套需要维护的表面积。
13在讨论两个维度之前,先回答什么是业务
“业务”是一个很容易被泛化的词。有人把自己负责的任何工作都叫业务,也有人把业务直接等同于组织架构中的某个部门。为了让后面的模型可以落到代码上,本文需要一个更可操作的定义:业务是围绕产品或服务完成价值交付,并通过收入增长或成本下降形成可持续经营结果的一组活动、决策与规则。
这个定义同时包含增收与降本。企业在组织上常把部门分为利润中心与成本中心:前者更多承担市场、产品、经营模式和产业链的创新,直接寻找收入与利润;后者更多通过平台复用、流程优化和效率提升降低成本。两者关注点不同,但最终都要回答同一个经营问题:如何让价值持续产生,并让收入与成本之间留下健康空间。
映射到软件平台,经营变化会稳定地形成两类规则。第一类规则通常围绕一个具体行业,描述这个行业如何定价、交易、履约和承担风险;它们共同组成一个完整经营模式。第二类规则来自与行业无关的通用玩法或平台能力,通过改变某一段交易方式,为多个行业增加销量、效率或用户价值。前者需要自治,后者需要复用;前者回答“这是哪个行业的生意”,后者回答“这个行业选择了哪些通用玩法”。
这就是垂直与水平两个维度的来源。它们不是为了画一张整齐的架构图而发明的分类,而是企业经营责任与平台复用方式在规则层面的投影。
14为什么业务会形成垂直与水平两个维度
垂直业务通常就是行业。 它是一个拥有完整经营责任的业务空间,例如汽车金融、即时配送、电影票。它不仅拥有几个差异化配置,还要对这个行业里的客户、收入、履约、风险和最终经营结果负责。因此,垂直业务的规则必须彼此隔离:汽车金融的资格校验、预授权超时和无物流履约,不应该进入即时配送;即时配送的计价、配送范围与 SLA,也不应该成为电影票业务的默认规则。
即使两个垂直业务今天恰好把付款超时都设置为十分钟,也应在各自业务空间里独立声明。可以复用的是“付款超时”这项能力及其实现机制,而不是把一个业务做出的十分钟决策直接借给另一个业务。否则其中一个明天把配置改成十五分钟,另一个会在没有参与决策的情况下被改变。垂直维度解决的是所有权与隔离:一个经营主体的规则,不能因为代码相似就变成另一个经营主体的规则。
水平业务通常是与行业无关的通用玩法或平台。 它也可以称为平台业务或水平能力。它不拥有一套完整的行业经营模型,而是先把某种可以跨行业创造价值的玩法沉淀成可复用规则。例如,电子凭证的核心是发码、核销与退款,它并不关心使用者是电影票、景区门票还是汽车保养;营销活动可以改变不同行业的购买入口、价格与销量统计;货到付款可以改变不同行业的付款时点、收银台行为和现金零头处理。
水平业务的定义不依赖任何一个具体垂直业务,但它可以被许多垂直业务选择。电影票和汽车保养可以共同使用电子凭证,却选择不同的核销次数;一个业务可以同时安装电子凭证与预售,也可以再叠加营销活动。垂直业务愿意接受这些规则变化,是因为水平能力能够扩大可交易范围、促进成交、降低成本或改善体验。水平维度解决的是复用与增量:它不是替代垂直业务,而是对垂直业务基线的一组可选择修改。
两类业务还有一个决定性的差异:垂直行业相对可数、稳定,水平玩法则开放、近乎不可穷举,而且变化频繁。 这里的“可数”与“不可数”不是集合论概念,而是工程上的稳定性判断。一个企业当前经营汽车金融、即时配送还是电影票,通常可以列出清单;新增一个行业意味着明确而低频的经营决策,因此适合获得长期稳定的业务身份。水平玩法却没有一张能够提前写完的清单:新的营销方式、履约方式、支付方式、会员权益与风控规则会持续出现,还会彼此组合。
如果把每一种“行业 × 水平玩法组合”都建模成一个新的业务类型,假设有 |V| 个垂直行业、|H| 个可独立选择的水平玩法,理论组合上限会接近 |V| × 2^|H|。每增加一个水平玩法,组合空间就可能翻倍。把这些组合写成继承层次、业务编码或全局 SPI 条件,只会让类型数量和判断分支一起爆炸。
Lattice 选择了相反的建模方向:把相对可数、稳定的垂直行业作为坐标原点和业务身份,再为这个身份选择 N 个开放变化的水平业务。一次请求先锚定唯一的垂直身份,获得它的默认规则与所有权边界;运行时再沿水平方向加载这个业务已经选择的能力与实现。新增一个水平玩法,相当于增加一列可选能力,不需要重新定义所有垂直行业;某个行业改变自己的选择,也不会改写其他行业的基线。稳定的身份承载开放的组合,这才使高频业务定制保持稳定。
由此还可以推出两个边界。第一,如果一个流程跨越多个垂直行业,就应拆成多个带有明确身份的业务上下文协作,而不是让一条请求同时属于多个垂直空间。第二,在每个垂直基线上,可以选择零个、一个或多个水平能力,N 因而可以为零,也可以随着商业创新持续增长。
因此,一个业务真正生效的完整规则集可以表达为:
完整业务规则集 = 1 个垂直业务基线 + N 个水平业务增量(N ≥ 0)
这里的“1”确立唯一、稳定的经营所有权与默认规则,“N”表达开放、可复用、可叠加的业务增强。它不是类继承关系,也不是把几份配置简单拼接在一起,而是一个有作用域、有顺序、有归并语义的稀疏组合:每个垂直业务只选择自己需要的那些水平能力。传统 SPI 只有一个扁平的全局实现列表;Lattice 则先确定垂直业务空间,再解析这个空间选择了哪些水平能力,最后处理它们在同一扩展点上的组合。

图 3 · Lattice 的两个业务维度。 垂直业务保持经营规则的所有权与隔离,水平能力提供跨行业复用;一个交点不是全局条件偶然命中,而是某个垂直业务对一项水平能力的明确选择。
15Reduce 不替业务决策,却能让冲突显式
一旦完整规则被定义为“一个垂直基线 + N 个水平增量”,冲突就不再是偶发异常,而是组合模型必须正面回答的问题。多个水平能力可能修改同一个业务点,垂直基线也可能对这个点已有自己的判断。框架不仅要找到这些规则,还要说明它们能否共存,以及共存时怎样得到一个最终结果。
有些规则可以归并。例如可购买数量:垂直业务要求不能超过库存;电子凭证一次最多购买 250 份;某个营销活动限制为 10 件。这三条规则表达的是不同来源的上限,没有谁需要覆盖谁。取最小值即可让三者同时成立,因此适合 MIN 一类的 Reduce 策略。
另一些规则具有排他性。例如平台营销活动默认不统计销量,但某个垂直业务经过双方 Owner 协商,要求自己的销量规则优先。两个布尔结论不能通过取最小值或合并集合解决,这时需要显式的冲突优先级,说明究竟由垂直业务还是水平业务做最终决定。
所以,Lattice 对组合冲突的处理包含两个不同问题:Reduce 算法回答“多个结果如何归并”,冲突优先级回答“无法归并时谁拥有裁决权”。 前者可以取第一个非空值、合并集合、求最小值或执行其他有业务含义的算法;后者必须由相关业务 Owner 协商并声明。技术框架不能替业务负责人决定政策,但它可以要求这个决定被明确表达,而不是藏在实现注册顺序里。
最终业务规则 = Reduce(垂直业务基线,水平业务增量 1…N,冲突优先级)
| 维度 | 传统 SPI | Lattice |
|---|---|---|
| 实现归属 | 由 filter 间接推断 |
由业务身份显式声明 |
| 候选范围 | 全局实现列表 | 当前业务及其选定能力 |
| 多实现冲突 | 注册顺序或第一个命中 | Reduce 与优先级策略 |
| 业务复用 | 复制实现或共享隐含条件 | 水平能力按业务组合 |
| 影响评估 | 阅读所有过滤逻辑 | 检查业务空间与组合关系 |
16Use Case 让架构重新说业务语言
能力解决的是可扩展行为怎样组织,但业务人员讨论的通常不是某个实体的某个方法,而是“买家下单”“卖家履约”“买家退款”这样的场景。《干净的架构》把 Use Case 放在实体之外:它引导数据在实体之间流动,编排实体完成一个特定应用目标。[2]
这对交易平台特别重要。一个电子凭证场景会跨越订单、凭证、通知和核销等多个实体与系统;如果只开放实体级 SPI,业务仍然要自己拼出完整流程。Lattice 因此支持在用例层沉淀可复用业务资产,并按业务活动组织场景级 SDK。
以电子凭证为例,平台可以提供生成凭证、发码、核销、退款等完整场景能力,只把有真实差异的政策开放出来:
public interface ETicketTradeSDK extends IBusinessExt { |
电影票业务可以声明单次核销,包含十二次洗车的汽车保养套餐可以声明多次核销。业务开发者面对的是“凭证是否支持多次核销”这句业务语言,不必理解底层订单把它存在哪个 K/V 字段,也不必分别在每个应用里寻找技术扩展点。
场景层负责把有语义的规则映射到底层实体。例如电子凭证用例可以在订单保存时,把“是否允许多次核销”转换为订单属性。实体层只知道如何可靠保存属性,不需要知道电影票、洗车券或未来出现的新行业。依赖关系由场景指向实体,实体不会反向依赖具体场景,这正是依赖规则在业务扩展中的一次具体落地。
更重要的是,场景资产可以组合。一个电影票业务可以同时使用电子凭证与预售:电子凭证决定核销方式,预售决定定金比例。业务不是继承一个越来越庞大的“电影票平台”,而是在自己的业务空间里安装两个相对独立的场景能力。
17第三个分离:逻辑业务架构与物理应用架构
前两个分离解决代码放在哪里、运行时进入哪个规则空间;Ability 与 Use Case 又补齐了描述业务能力和完整场景的语言。以此为基础,第三个分离解决的是设计者从哪里理解平台。传统平台按照物理应用开放扩展点:商品应用提供一组 SPI,购物车、交易、履约应用再各自提供一组。这样的目录忠实反映了系统怎样部署,却无法直接回答一个业务场景怎样完成。面对两千多个微服务,要求任何人先掌握应用拓扑再设计业务,本身就是不可能完成的影响分析。
Lattice 让平台通过 @Ability、@Extension 等注解显式声明能力、扩展点、默认行为和归并语义,也让 @Business、@Realization 声明业务及其实现归属。这些元数据可以由运行容器统一发现并上报到业务管理平台。上报后的组织单位不再是“某应用里的某个 Java 接口”,而是下单、履约、退款、核销等业务流程,以及流程中允许变化的业务政策。
业务人员或设计师由此可以从逻辑视图开始需求分析:沿着完整流程查看平台已经提供哪些能力、当前业务安装了哪些水平玩法、哪些扩展点已有实现、同一点采用什么 Reduce 策略。对本次需求能复用的部分,可以直接选择和组合;对感觉缺失的部分,可以形成一张具体的差距清单,再与平台设计者讨论是否应该增加一个稳定扩展点。沟通从“我猜某个系统可能要改”变成“这个业务步骤缺少哪项可复用政策”,平台能力的增强也就更有方向和节奏。
这套管理视图不能只是一个扁平的扩展点注册表。为了真正支持设计与影响评估,每个扩展点还需要保留业务语义、所属场景、输入输出、默认行为、Reduce 策略、Owner、版本兼容和当前使用方等信息;同时保留从逻辑节点追溯到物理应用、接口和运行实例的映射。逻辑架构与物理架构分离,不是把物理世界藏起来,而是不再让物理拓扑成为业务设计的起点。
注解也不会自动产生正确的业务模型。如果平台把过细的内部方法全部上报,只会把两千多个服务换成另一张庞大的接口清单。平台设计者仍然要识别稳定业务语义、组织 Use Case 与 Ability,并为契约演进负责;Lattice 提供的是一条从代码声明、统一上报、业务设计到运行执行的闭环,而不是替代建模本身。
这也呼应了我对《干净的架构》中“框架是细节”的理解。Lattice 自己应该待在合适的位置:它提供业务身份解析、扩展调用、实现加载、元数据发现和结果归并机制,但真正有价值的业务规则仍然属于业务与用例。

图 4 · Lattice 在干净架构中的位置。 Lattice 负责让外部业务插件越过稳定契约参与扩展,但依赖方向仍指向实体与用例;框架、数据库和插件都是机制,不应成为业务政策的中心。
18能在运行前确定的,不留给运行时猜
传统 SPI 的很多风险来自运行时动态猜测:遍历所有实现、执行任意 filter,直到某个实现命中。结果依赖当时的数据、远程调用乃至实现顺序,影响范围难以提前说明。
Lattice 的设计倾向,是把业务使用哪些能力、规则如何叠加、发生冲突时如何裁决,尽可能在管理和配置阶段确定;运行时严格执行已经形成的业务规则与冲突策略。官方仓库也把“管理域与运行域分离”列为核心设计之一。[3]
这并不意味着业务运行时没有动态条件,而是要区分两类信息:哪些规则属于这个业务 应该尽量静态、可见、可审计;至于规则如何处理本次订单,才根据运行数据计算。前者不应每次请求都重新猜测。
当组合关系能够在运行前呈现,产品和开发才有机会共同做影响分析:当前业务安装了哪些场景,覆盖了哪些扩展点,同一个点有哪些实现,使用什么 Reduce 策略。架构从只存在于代码阅读者脑中,变成可以被管理的业务模型。
第四部分 · 从一个最小样例看完整闭环
19业务 A 定制价格,业务 B 保持默认
Lattice 的最小闭环并不复杂。平台定义“自定义订单明细单价”扩展点与价格能力;业务 A 声明自己的业务身份,并实现该扩展点返回 2000L;业务 B 不做定制。两次调用的订单默认单价都是 1000L。
OrderLine orderLine = new OrderLine(); |
业务 A 的结果是 2000L,业务 B 的结果仍是 1000L。这个例子看起来像普通的策略模式,但关键差异在于:实现由业务身份定位,默认策略由能力管理,调用结果按扩展点契约归并。完整 Quick Start 与更多场景样例可以在 lattice-sample 中查看。[4]
本文最近一次更新时,sample 仓库主分支仍把 Lattice 依赖设为 1.0.19。我将其临时切换到 1.0.21 后,重新编译了 Quick Start 与 Use Case 两个模块;两者均通过,下面电子凭证叠加预售的控制台结果也由 1.0.21 实际运行得到。这个验证很朴素,却能避免长文里的代码只停留在“看起来合理”。
当扩展规模从两个实现增长到成百上千时,这些“多出来的概念”才显示价值。它们为每一段变化补齐了过去缺失的信息:属于哪个业务、扩展哪种能力、以什么方式组合、没有实现时如何回退。
20电子凭证叠加预售
再回到更接近真实业务的组合。电影票业务安装电子凭证场景,声明凭证只允许单次核销;同时安装预售场景,声明定金比例为 40%。下单用例调用两个场景能力,并将需要持久化的业务信息映射到订单实体。
[UseCase]PreSaleTrade load custom down payment ratio: .40 |
这段输出背后有四层清晰分工:业务插件声明电影票的差异;预售和电子凭证用例封装可复用场景;订单实体负责稳定的数据与生命周期;Lattice 运行时根据业务身份连接实现,并归并扩展结果。
如果未来增加汽车保养业务,它可以复用同一个电子凭证场景,只把核销方式改成多次;如果又使用预售,也可以独立配置定金比例。原有电影票规则不需要修改,新业务也不用加入一个全局 SPI 排序队列。
21问题与解法的完整对应
| 当年的挑战 | Lattice 的机制 | 直接改变 |
|---|---|---|
| 业务与平台代码纠缠 | 业务插件、独立代码边界 | 定制逻辑不进入平台核心 |
| 业务之间互相影响 | 统一业务身份 | 实现先按业务空间隔离 |
| 扩展点散落在应用中 | Ability 与场景级 SDK | 从应用语言回到业务语言 |
| 业务流程被物理拓扑切碎 | 注解元数据、统一上报与管理视图 | 按完整场景发现能力并形成差距清单 |
| 水平能力难以复用 | 垂直业务 + 水平能力组合 | 场景资产可选择、可叠加 |
| 多实现顺序决定结果 | Reduce 与冲突优先级 | 组合语义由契约明确表达 |
| 运行时过滤难以评估 | 管理决策与运行执行分离 | 组合关系可提前检查 |
| 统一发布、全网回归 | 插件加载与发布边界 | 为业务独立交付创造条件 |
这张表也是全文最想保留的部分。一个框架是否值得采用,不应先看它有多少注解和扩展策略,而应看每个机制是否对应一个真实且反复出现的问题。
第五部分 · 重读之后,我更在意 Lattice 的边界
22Lattice 没有消灭业务复杂性
《干净的架构》容易被误读成只要分层正确,系统就会自然保持整洁;扩展框架也容易被误读成只要插件化,任何业务都可以零成本定制。我现在更愿意强调相反的一面:Lattice 不会消灭业务复杂性,它只是尝试让复杂性各归其位。
垂直业务之间是否应该隔离,需要业务建模判断;一个水平能力是否值得复用,需要真实场景验证;两个规则冲突时谁优先,仍然需要 Owner 做出决定;一个扩展点设计得太细、太底层,业务团队照样会陷入技术细节。框架能把决定显式化,却不能替人做出正确决定。
有几类误用尤其值得警惕:
- 把所有方法都开放为扩展点。 这会暴露平台内部结构,使契约数量失控。
- 用业务编码替代领域建模。
bizCode解决作用域,不负责解释业务本身。 - 把 Reduce 当成冲突逃生舱。 没有业务语义的归并算法,只会掩盖规则矛盾。
- 只有运行时,没有管理视图。 如果没人能看见当前组合,影响评估仍然困难。
- 插件物理分离,数据仍然强耦合。 代码边界不能自动解决共享数据与接口演进。
因此,使用 Lattice 之前,我会先问三个问题:这是否是一个稳定、可命名的业务差异点?它的所有者和作用范围是否清楚?两个实现同时生效时,是否存在可解释的组合语义?如果三个问题都答不出来,先增加一个扩展点通常只是在推迟建模。
23重读《干净的架构》后,我留下的七条工程判断
多年后再回看这段实践,我不再把读书体会停在“实体、用例、适配器应该放在哪一层”。同心圆、模块名和接口都只是表达,真正要检验的是变化成本:加入一个新业务需要多少不相关团队参与,多少既有业务必须回归,错误能否停在清楚的范围内,未来是否仍保留替换机制和部署方式的选择权。
把这些理解与 Lattice 的实践放在一起,我最终留下七条工程判断。它们不是前文之外突然出现的第二套总结,而是把业务身份、Use Case、Reduce、插件边界和管理视图压缩成一张可以用于设计评审的检查表。
第一条说明架构最终要优化什么,第二到第六条说明变化应该经过哪些边界,第七条提醒我们不要让解决方案本身反过来支配业务。到这里,这些判断仍然主要依赖人去阅读和记忆。进入 AI 编程时代,下一步是让它们成为编码代理每次动手前都会执行的工作流。
24从交易平台到 Spark:在 AI 编程时代把架构做成 Skill
交易平台的经历告诉我边界为何重要,后来在 Spark 的实践则让我面对一个更现实的问题:AI 可以在几分钟内跨模块生成大量代码,但它看到的通常是当前任务和附近文件,不会天然知道一个仓库多年形成的依赖方向、模块责任和失败教训。只在提示词里说一句“请遵循干净架构”,得到的往往仍是局部正确、整体越界的实现。
所以,我开始把架构知识分成四层。docs/principle/01.clean_architect_with_lattice.md 保存完整背景,解释为什么需要这些边界;docs/principle/02.ai-clean-architecture-lattice-guidelines.md 把背景压缩成可以检查的放置规则、Session Scope 规则和 Process / Activity / Result 规则;仓库根目录的 AGENTS.md 负责说明哪些任务必须先读取它们;架构 Skill 则把“读取—审视—设计—编码—复核”变成 AI 必须执行的步骤。
| 层次 | 作用 | 回答的问题 |
|---|---|---|
| 背景原则 | 保存案例、推理与取舍 | 为什么要这样设计? |
| 可执行规则 | 列出允许、禁止与检查项 | 什么代码可以放在哪里? |
| 仓库触发规则 | 根据任务类型要求 AI 先读指定原则 | 什么时候必须进行架构审视? |
| 架构 Skill | 固化审视顺序和输出格式 | AI 在写代码前后必须做什么? |
Skill 不是架构真相的唯一副本。长期规则仍应留在仓库中,让 Codex、Claude 以及人类开发者都能读取;Skill 更像一个执行适配器,负责在合适时机加载这些事实,并阻止 AI 跳过架构设计直接生成代码。根据 Spark 的真实规则裁剪后,它的核心可以写成下面这样:
--- |
真正使用时,我不会直接对 AI 说“帮我把功能写完”,而是先让 Skill 迫使它给出架构答案。例如,要为 Spark 增加 Kubernetes 上的 Python 构建能力,对话可以是这样:
我: 为 Spark Flow 增加 Kubernetes Python 构建能力。先执行架构 Skill,不要写代码;给出业务身份、模块边界、依赖方向、扩展点和验证方案。
AI: 业务身份使用
build.k8s.python。稳定 Context、Result、Extension、Ability 与默认实现进入spark-console-sdk;构建流程编排留在spark-pipeline;Kubernetes Python 的具体实现进入spark-k8s-plugin。插件只接收平台准备的源码、构建参数和凭据引用,通过 Result / Observation 返回镜像、日志与状态,不读取平台业务表。场景入口建立一次 Session Scope,Pipeline 内部不再按PYTHON增加分支。我: 先检查这个划分是否违反依赖规则;通过后再按模块逐步编码,每完成一层都报告对应规则和测试证据。
AI: 我会依次完成 SDK 叶子扩展契约与默认实现、Ability、插件
@Realization、平台调用与测试;提交前再次检查平台是否依赖插件、SDK 是否混入实现对象、内部是否重复识别业务身份。
这段对话最重要的不是 AI 是否准确说出了几个模块名,而是它被要求在写代码前先暴露自己的架构模型。人可以在成本最低的时候纠正业务身份、模块归属和依赖方向,而不是等几千行代码生成后再做结构性返工。代码写完后,同一个 Skill 还要反向审计实现:Controller 是否偷偷承担了编排,SDK 是否混入 DO,插件是否绕过 Context 读库,Session Scope 是否在内部被重复打开,Activity 是否各自写数据库和索引。
对于创建任务、任务流转、批量操作这类复杂场景,Skill 还会要求 AI 先定义统一 Request 与 Result,再由 Process 编排多个 Activity。每个 Activity 只把校验、权限、分配、持久化计划和事件计划累积到同一个 Result,最后在流程末端统一完成数据库、索引和事件副作用。这样,AI 生成的不只是“能运行的若干类”,而是一条仍然保持概念完整性的 Use Case。

图 5 · 当架构原则成为 AI 编程的执行规则。 背景文档保存原因,可执行规则提供约束,Skill 在编码前输出架构方案、在编码后审计越界;身份在入口确定,活动共享统一 Result,插件通过契约交换材料,副作用在流程末端收口。
这改变了我对架构文档的看法。过去,文档常常是代码完成后的解释;AI 编程时代的架构文档既是知识,也是控制输入。它需要同时包含原因、允许项、禁止项、触发条件和验证证据。AI 不会自动带来干净架构,但它给了我们一个机会:把过去依赖资深工程师口口相传的判断,变成每一次设计和编码都可重复执行的架构审视。
从这个角度看,Lattice 的价值也不只是在运行时找到一个扩展实现。业务身份、Context、Result、Ability 与插件契约共同构成了一种人和 AI 都能理解的变化协议:谁可以改变什么,代码应该放在哪里,通过什么材料进入,返回什么证据,影响应该止于哪里。
25结语:让变化停在它应该停下的地方
2015 年我面对的交易平台,技术上并不缺少接口、服务和扩展机制。真正稀缺的是边界:业务与平台的边界,业务与业务的边界,逻辑业务流程与物理应用拓扑的边界,静态决策与运行执行的边界。
没有这些边界,增加 API 会产生更多上下文参数,增加 SPI 会产生更大的全局实现树,拆出更多应用会产生更重的协调。每一种局部正确的技术,都会在规模扩大后把成本转移到别处。
Lattice 的出发点,是把这些被转移、被隐藏的成本重新放到架构里:用业务插件分开业务与平台,用业务身份分开业务与业务,用注解上报和管理视图分开逻辑架构与物理拓扑;再以 Ability 组织可扩展行为,以垂直与水平两个维度表达隔离和复用,用 Reduce 说明规则怎样组合,用 Use Case 恢复完整业务语言。三个分离建立边界,后面的机制则让边界可以被设计、执行和验证。
重读《干净的架构》后,我更确定这件事的价值不在于创造了多少概念。好的架构并不承诺业务从此不再复杂;它只是让一个新变化到来时,我们能说清楚它属于哪里、依赖什么、会影响谁,以及应该在哪里停下来。
在 AI 也参与设计和编码的今天,这些边界不能只存在于少数人的经验里。它们需要同时成为仓库中的知识、Skill 中的执行步骤和交付前的验证证据,才能在代码生成速度不断提高时,仍然守住变化的方向。
这也是我理解的“干净”:不是没有变化,而是变化不再四处流淌。
参考资料与延伸阅读
- Robert C. Martin. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Pearson / Addison-Wesley, 2017.
- Robert C. Martin. “The Clean Architecture Dependency Rule.” InformIT, 2017.
- HiForce. “Lattice Framework.” GitHub;本文最近一次更新时主分支
pom.xml版本为1.0.21。 - HiForce. “Lattice Sample.” GitHub,包含 Quick Start、Use Case 与多能力组合样例。
- HiForce. “Lattice Clean Architecture Practice Sample.” GitHub,展示实体、用例、接口适配器与业务插件的工程组织。