架构假设如何落地为可执行代码?从ADR到自动化守护
我入职第三年的时候带我的老领导说过一句话我记到现在架构评审通过的那一刻是整个项目最危险的时刻。当时我还不理解后来亲自经历过几次从设计到落地的完整闭环才明白他说的其实是架构评审通过只证明团队在会议室里达成了共识而共识这个东西出了会议室就会开始衰减。真正让架构活下来的不是那张画满方框和箭头的图而是能不能把图里的每一个假设翻译成可运行的、可测试的、可演进的代码。这个标题——将架构假设转化为可执行的代码——其实就是我这些年一直在做的事。架构设计里到处都是假设我们假设流量会涨十倍所以要用消息队列削峰假设订单服务会被拆出去所以先定义好接口假设缓存一定比数据库快所以查缓存失败要回源假设某个模块未来会替换所以要加一层防腐。这些假设分散在设计文档里、评审纪要把里的各个角落如果不主动把它们显性化并落到代码层它们就会变成听说过没验证过的空头支票等到真正出问题的时候谁也说不清当初到底是怎么约定的。这篇文章我想把我自己的一套方法论完整地梳理一遍适合正在从写代码的人往做架构的人转型的同学也适合那些已经在带项目、但总感觉架构设计和团队落地之间隔着一条沟的技术负责人。我的核心主张很简单架构假设必须变成三类东西才能算数——架构决策记录、代码结构约束、自动化守护测试。三者缺一不可。1. 先搞清楚架构假设到底藏在哪儿很多人以为架构假设就是那种写在PPT边角的小字或者在评审时口头说一句这个后面再说的东西。但实际工作中架构假设无处不在而且往往藏在看起来最不起眼的地方。1.1 不是所有设计决策都叫架构假设先给架构假设划个界。我习惯把这个词限定为那些如果错了会导致系统级返工的决策前提。比如我们选的数据库能扛住这波写入量订单模块和服务发现模块之间不存在循环依赖消息至少会被消费一次但不是恰好一次第三方接口的响应延迟P99在200ms以内。这些前提一旦不成立不是改几个函数就能补救的而是要改模块边界、改交互协议、改数据模型也就是架构层面的调整。对比一下方法内部用HashMap还是TreeMap、某个字段要不要加索引、某个接口要不要加一个可选参数这些虽然也是决策但影响范围在局部错了改起来也就一两个工作日的事不值得上升到架构假设的层级来管理。我见过很多团队的问题是反过来的该用架构假设级别来管理的决策被当成普通实现细节处理了。最典型的例子就是缓存策略。设计文档里写着用Redis做二级缓存失效时间五分钟但没人记录为什么是五分钟而不是五秒、为什么要有二级缓存而不是只有Redis。等到线上出现数据不一致排查到缓存这一层根本没人能说清楚五分钟这个数字当初是怎么定的最后只能拍脑袋改配置改完再出问题再改。这种没人负责的假设本质上就是一颗埋在系统里的雷。1.2 一张假设清单应该记录什么我在项目启动的时候会让参与架构评审的人每人至少提一条这个设计能成立的前提条件汇总到一张表格里。表格的列大概是这样的字段说明示例假设编号唯一标识便于引用ASM-001假设内容一句话说清楚前提推送服务的QPS峰值不超过2000来源谁提出的出自哪次讨论张三2025年Q1架构评审影响面如果假设不成立哪些模块/系统会受影响消息推送、用户通知、移动端呈现验证方式用什么手段确认这个假设成立压测脚本、线上监控、AB实验验证状态已验证 / 待验证 / 已推翻待验证关联代码在代码里哪里体现了这个假设PushService.java、RateLimiterConfig.java这张表一开始可能只有七八条但随着设计细化会不断扩充。我见过最多的项目光性能容量类假设就列了三四十条。表格本身不贵贵的是它逼着每个人把自己脑子里默认的前提说出来而很多架构失败恰恰就是因为某个关键前提一直没人说。1.3 谁负责维护这些假设这个问题比看起来重要得多。我见过一些文档化做得不错的团队,架构假设列表漂漂亮亮地挂在Wiki上,但三个月后回到代码仓库里去看,没有一条假设跟现状对得上——因为没有人指定一个假设监护人。我的建议是,每一条架构假设都要指定一个负责人,通常这条假设影响哪个模块就由那个模块的技术负责人来认领。负责人的职责不是保证假设一定正确,而是保证假设被跟踪、被验证、被更新。假设过期了要么推翻要么重写,不能让它躺在文档里装死。另外一个细节假设清单不是一次性的交付物而是要跟着迭代持续更新的。我们每次迭代规划会上都会审核一遍清单里待验证的项目把验证任务安排进迭代这比年终总结时再补救要省事得多。你会发现当假设被当成一等公民对待以后架构评审的质量也会变大家不再泛泛地讨论高可用怎么做微服务怎么拆分而是精确地讨论哪个假设不成立我们就得回退方案D。2. 把假设写下来架构决策记录的正确打开方式光有清单还不够。清单解决的是记录有哪些假设的问题但没解决为什么要做这个决策的问题。后者需要另一种载体——架构决策记录行业内一般叫ADRArchitecture Decision Record。2.1 ADR的轻量模板别让文档变成负担我很早以前用过那种特别正式的架构设计文档模板章节十几个写完没人看。后来换成ADR的思路要求每个影响架构的决策写一张卡片保持在半页到一页的篇幅。我用过比较顺手的结构是背景这个决策是在什么约束下做出的不超过五行。决策我们最终选择了什么一句话说清楚。被否方案列一到两个备选方案以及为什么没用。关键假设这个决策成立依赖的前提条件对应假设清单里的编号。后果这个决策带来什么正面影响同时引入什么代价。不要小看被否方案这一节它往往比决策本身更有信息量。三个月后新人问为什么不用Kafka Streams而用了Flink你直接把ADR抛过去他看一遍就明白了不需要再拉着你讲半小时当时的上下文。而被否方案正是在回答这种问题让为什么是A而不是B有了长久的答案。2.2 从决策记录到代码注释和提交信息ADR写完之后最大的坑就是它跟代码脱节——文档归文档代码归代码。我自己经验是关键假设必须在代码里能反查到。怎么做两个小技巧第一在关键代码位置留下ADR编号引用。比如在配置类、核心接口、边界模块的注释里写一行// 参见 ADR-012订单状态机采用事件溯源而非CDC捕获。这样后面维护代码的同事一眼就知道这段代码背后有设计约束不是随手写的。这个做法成本极低但收益非常大。第二涉及架构变动的提交信息里带上ADR编号。比如feat: 添加订单超时处理, 实现 ADR-014 中的补偿事务方案 (ASM-003验证通过)。这样将来用git log排查问题的时候可以顺着提交记录回溯到设计上下文不用去翻PM系统里的故事卡。很多团队用Conventional Commits的时候只写type和scope其实在footer里加一条ADR: 014再配一个Assumption: ASM-003成本几乎为零但能让架构决策和代码演进的历史对齐。2.3 假设变更时怎么办假设不是一成不变的。流量涨了十倍数据库连接池的容量假设可能就失效了第三方接口升级了协议兼容性假设就要重写。问题的关键是当假设被推翻的时候团队能不能快速感知并做出反应。我推荐建立两个机制一是假设变更事件。任何假设的验证状态发生变化都要在周会上同步并在ADR里打上修订标签。修订不用改变ADR编号但要追加一条变更说明写清楚原假设是什么、实际是什么、我们做了什么调整。这比新建一份ADR更能保持上下文连续。二是假设失效的自动化提醒。这是说给有点代码能力的团队的把一部分假设做成配置或者注解配合编译期或者CI阶段的检查。比如我们团队曾有一个假设对外部支付网关的调用必须通过PaymentGatewayPort接口不允许直接引用第三方SDK类这个约束如果靠人肉review早晚会漏。后来我们把它写成了ArchUnit测试直接在CI里跑违反就构建失败。这种把假设变成可执行代码的做法是我这篇文章标题最直白的体现。3. 将假设落到代码从依赖方向到目录结构把假设写下来只是第一步真正让假设可执行的是把它翻译成代码结构上的约束。这一节我结合几个常见的架构场景讲讲怎么把抽象的概念映射到具体的、可以review的代码组织方式。3.1 依赖倒置不是口号是代码结构架构设计里经常说高层模块不应该依赖低层模块,两者都应该依赖抽象。这句话在图纸上也就是个箭头方向的事很多人画图的时候都会画对但代码写起来就全走样了。我见过最常见的走样方式Service层直接new了一个RepositoryImpl或者Controller里直接调了第三方SDK。从运行角度讲没错也能跑但从架构角度讲这等于把我可以替换这个组件的假设给丢了。如果我们假设缓存中间件是可替换的那么代码里就不应该出现具体Redis客户端的引用散落在业务代码里的情况而应该收敛到一个独立的适配层后面。实操层面我通常会检查这几个点:模块之间只能通过接口交互接口的定义放在消费侧而不是提供侧。比如订单服务需要调用库存服务那库存服务对外接口这个概念从订单服务的视角定义放在订单服务这边库存服务去实现它。这在分布式系统里叫消费方驱动的契约在DDD里叫防腐层的思路本质是一样的。目录结构要能反映架构边界而不是反映技术分层。很多早期项目喜欢用controller/service/mapper这种纯技术分层当系统小的时候没问题但一旦业务复杂了这种结构会让订单相关的代码散落在十几个包里。我更倾向按业务模块划目录然后在模块内部再分层。一个六边形架构风格的包结构长这样com.example.order ├── application │ ├── in // 入站端口接口定义应用服务能力 │ ├── out // 出站端口接口定义对外的依赖能力 │ └── service // 应用服务实现 ├── domain // 领域模型与领域服务 ├── adapter │ ├── in │ │ ├── rest // HTTP入站适配器Controller │ │ └── mq // 消息队列消费者适配器 │ └── out │ ├── redis // Redis出站适配器实现 │ ├── db // 数据库出站适配器实现 │ └── feign // 外部HTTP服务适配器实现 └── bootstrap // 启动装配这种结构的好处是从目录就能看出架构假设依赖方向永远从adapter指向applicationdomain不依赖任何技术框架。如果你在code review的时候发现domain里import了Spring的注解或者application里出现了某个具体中间件的类那不用讨论这就是架构越界了。3.2 端口与适配器六边形架构在真实业务里的落点热词里经常出现六边形架构很多文章讲理论讲得头头是道但落到代码就不知道怎么分。我自己用下来的心得是不需要追求教科书式的纯粹六边形重点抓住三件事就够了第一核心业务逻辑不依赖任何技术设施。什么意思订单领域模型不感知它存到MySQL还是MongoDB不感知它是通过HTTP还是gRPC被调用的甚至不感知自己是在多线程还是协程环境里运行。技术细节全部往适配器里推。第二所有对外部世界的访问都通过端口。端口就是接口。查询库存是InventoryQueryPort发送通知是NotificationPort读取配置是ConfigPort。业务代码里只见到这些端口永远见不到具体的RedissonClient或者RestTemplate。第三用依赖注入/组合根把一切装配起来。在启动阶段把具体的适配器实现绑到端口上去。框架选型、连接池配置、超时时间都集中在装配层。这样切换实现的时候就只改装配配置不动业务逻辑。我经常用一个生活化的类比跟团队解释这个场景你的手机不需要关心充电头是原装的还是第三方的只要接口是Type-C插上就能充。端口就是那个Type-C标准适配器就是各个牌子的充电头核心业务就是手机本体。换了充电头,手机不用拆开重改电路。3.3 分布式场景下的一个真实案例Spring Cloud中的分布式定时任务热词里有springcloud架构中关于分布式定时任务的解决方案这个场景我正好做过拿来当案例讲讲架构假设到代码落地的全过程。当时的情况是订单系统需要每隔5分钟扫一次超时未支付的订单发起自动关闭。单机版很简单那就是Scheduled一个方法搞定的事。但服务部署了三台实例如果每个实例都跑这个定时任务就会重复处理同一批订单导致重复发送超时通知。这个问题的架构假设很明确在分布式部署下同一时刻只能有一个实例执行这个定时任务。但这个假设谁来保证靠每个实例自觉不重复跑是不可行的必须落到代码层。我们最终用的是基于数据库行锁/Redis分布式锁的方案。流程是这样的Component public class OrderTimeoutScanJob { private final OrderTimeoutScanService scanService; private final DistLock lock; public OrderTimeoutScanJob(OrderTimeoutScanService scanService, DistLock lock) { this.scanService scanService; this.lock lock; } Scheduled(fixedDelay 60_000) public void execute() { boolean acquired lock.tryLock(job:order-timeout-scan, 30, 30); if (!acquired) { // 说明其他实例已持有锁本次跳过 return; } try { scanService.scanAndCloseTimeoutOrders(); } finally { lock.unlock(job:order-timeout-scan); } } }注意这里面有几个容易踩坑的细节。一个是tryLock的超时时间和锁的自动过期时间不能拍脑袋要估算任务最长执行时间。我们当时压测下来扫一遍超时订单最坏情况要20秒所以锁的过期时间设了30秒留了50%的余量如果任务真的异常卡死锁30秒后自动释放不至于整个系统死锁。另一个是拿到锁的实例必须注意幂等——即便加了锁代码在极端情况下还是可能重复执行扫描逻辑本身要支持重复执行无副作用。当时我们的scanService.scanAndCloseTimeoutOrders()在更新订单状态时用了UPDATE ... WHERE statusPENDING这样的CAS式更新保证哪怕两个实例都执行了也不会把同一个订单处理两次。这个案例想说明的是架构评审时说的用分布式锁保证互斥跟代码里真的实现一个健壮的分布式锁逻辑中间隔着十万八千里。架构假设转化为可执行代码不是把一个名词翻译成一个类而是要把这个名词背后的约束、边界、失败模式都想清楚才能写出经得起推敲的代码。3.4 嵌入式场景用状态机收敛复杂度说完分布式再聊点底层的。热词里有一条嵌入式软件架构第一课用状态机收敛复杂度从状态建模开始这其实是嵌入式领域架构假设转代码的一个绝佳模板但同样的思路在后端业务代码里也完全适用。嵌入式系统里设备的运行状态是有限的、离散的——开机、待机、运行、故障、停机。如果把这些状态的管理散落在各个业务逻辑里最常见的情况就是一个全局变量currentState然后到处都是if (currentState STATE_RUNNING) { ... }这种写法一开始还能应付等到状态多了、状态之间的迁移条件复杂了代码就会变成一团乱麻。你根本说不清从运行到故障需要满足什么条件故障状态下哪些操作是被禁止的。这套规则混乱的代价在嵌入式这种对安全要求极高的场景里可能是致命的。状态机的价值在于把状态迁移规则这个架构假设集中化。你显式地定义有哪些状态、哪些事件会触发迁移、每个迁移有什么动作和守卫条件。代码结构变成一张表或一个集中式的转移函数而不是散落在各处的if-else。typedef enum { STATE_POWER_OFF, STATE_STANDBY, STATE_RUNNING, STATE_FAULT, STATE_MAX } device_state_t; typedef struct { device_state_t current; device_state_t next; int event; int (*guard)(void *ctx); int (*action)(void *ctx); } state_transition_t; static state_transition_t transitions[] { { STATE_POWER_OFF, STATE_STANDBY, EVT_POWER_ON, NULL, action_power_on }, { STATE_STANDBY, STATE_RUNNING, EVT_START, guard_battery_ok, action_start }, { STATE_RUNNING, STATE_FAULT, EVT_FAULT, NULL, action_enter_fault }, { STATE_FAULT, STATE_STANDBY, EVT_RESET, guard_fault_clear, action_reset }, };这个写法的好处是状态机表本身就构成了可执行的架构文档。新来的工程师想加一个新的状态必须先改这张迁移表这一下就把状态迁移是集中管理的这个架构假设给强制固化了下来——你几乎不可能绕过这张表去直接用if乱跳因为代码结构根本就没给那个空间。我在做后端业务时也会用同样的思路处理一些状态复杂的流程比如订单的状态流转待支付、已支付、已发货、已完成、已关闭。与其在各种方法里写如果订单是已支付状态就不能再取消这种散装逻辑不如把状态迁移规则收敛到一个领域服务里用一张迁移表或状态模式来管理。收敛之后测试也会好写很多——原来要从待支付到已发货要一路调用好几个方法现在一个事件形式的方法就能测试。4. 让假设变得可执行测试与架构守护如果说前面的工作是在把假设翻译成代码结构那这一节要解决的是怎么确保这个翻译不跑偏。单纯靠代码结构约束是不够的人总有偷懒的时候、新人总有不知道的时候所以要以自动化手段把架构假设变成构建即失败的东西。4.1 架构测试把约束变成测试用例后端Java团队我建议直接用ArchUnit这也算是我最顺手的工具之一。它允许你写单元测试来验证架构规则比如AnalyzeClasses(packages com.example.order) public class ArchitectureRuleTest { Test void domain_layer_should_not_depend_on_spring() { JavaClasses classes new ClassFileImporter().importPackages(com.example.order); ArchRule rule noClasses() .that().resideInAPackage(..domain..) .should().dependOnClassesThat().resideInAnyPackage(org.springframework..); rule.check(classes); } Test void application_layer_should_only_see_ports_not_adapters() { JavaClasses classes new ClassFileImporter().importPackages(com.example.order); ArchRule rule noClasses() .that().resideInAPackage(..application..) .should().dependOnClassesThat().resideInAPackage(..adapter..); rule.check(classes); } }这类测试从效果上看相当于把架构评审时画的那张依赖方向图变成了一段可执行代码——任何越过边界的依赖在CI阶段就会让构建变红根本走不到Code Review的环节。用ArchUnit还有一个好处它的规则语言可读性很强写完测试之后其实也是团队的一份活的架构规范。Go语言社区里对应的是go-arch-lint这类工具设定好允许依赖和被依赖的关系违反规则就报错。Python也能用import-linter做类似的检查。规则本质是一样的让架构约束在机器层面被执行起来而不是依赖人的自觉。4.2 契约测试保护服务边界上的假设分布式架构里服务之间的接口约定是另一种极其重要的架构假设。你说订单服务调用库存服务HTTP接口路径是/api/inventory/check请求是OrderCheckRequest响应是InventoryCheckResult。这个约定如果没人守护某天库存服务把字段名改了一下、或者加了必填参数订单服务的调用就会爆炸而且往往是在生产环境爆炸。所以微服务团队一定要引入契约测试。最常用的方法是Spring Cloud Contract和Pact核心思想是把服务之间如何交互这个共识变成一份显式的契约文件双方各自用契约生成测试。提供方根据契约写桩测试保证自己的实现符合契约消费方根据契约做mock测试保证自己的调用方式符合契约。任何一方违反契约各自测试就会失败相当于在集成之前就把接口不兼容的问题拦截下来。这套机制走通之后订单服务与库存服务能顺利集成就从每个人的口头假设变成了一条持续受到验证的自动化测试。很多团队天天喊微服务落地难其实难的不是把服务拆开而是拆开之后这些服务之间隐藏的交互约定没有人管理。契约测试实际上就是在给服务边界上的架构假设上保险。4.3 代码评审清单人工守护与自动守护的互补自动化守护很强大但它覆盖不了所有内容。比如这个新功能是不是应该放到订单模块而不是用户模块这类职责划分的问题自动化基本管不了只能靠人review。那怎么让人在review的时候有章法可循我的建议是给团队整理一份架构评审检查清单从架构假设的角度出发大致含这几个条目这段代码触达了哪些已有的架构边界是否存在绕过端口的访问路径比如领域层直接用了基础设施组件是否引入了新的外部依赖如果引入对应的ADR和假设清单更新了吗异常路径是否符合既有的架构约定比如超时、降级、重试策略新代码是否让某个已有的架构假设变得无效清单不需要很长五到八条就够关键是要落在本次变更是否冲击既有架构假设这个焦点上。把它贴在项目仓库的CONTRIBUTING.md里提交MR的时候自动展示在描述模板里提醒每个参与评审的人按这个思路看代码。这样人力和自动化分工明确架构规则交给机器业务边界和取舍交给活人。5. 一个完整案例复盘把假设翻译成代码的全过程复盘前面理论讲了不少这一节我想用一个完整的实际项目片段把从架构假设到可执行代码的整个链路串起来。案例规模有意控制在一个中等复杂度的范围内但流程和真实项目完全一致。5.1 业务背景与架构假设的挖掘假设我们做一个内容平台包含用户、文章、评论三个核心域。产品提出一个需求用户可以对文章点赞同时要把点赞数实时展示在文章列表和详情页上。点赞操作的高频压力很大热点文章可能一秒内有几千次点赞。我们不想给数据库造成太大压力于是架构上决定引入Redis缓存来扛点赞这个高频操作。在架构评审会上我们形成了这样几条架构假设ASM-001点赞操作集中在Redis上数据异步刷回数据库允许最多30秒的延迟。ASM-002Redis只能作为加速层数据库仍是最终数据源Redis数据可重建。ASM-003点赞操作必须幂等同一用户对同一篇文章的重复点赞需在接口层被识别并拒绝。ASM-004Redis不可用时点赞接口可以降级为拒绝新点赞但不能拖垮其他依赖Redis的功能。这些假设你看下来每一条都直接影响后续代码长什么样。如果团队没有在白板上把这些写下来大概率会出现的情况是有人因为担心Redis挂掉给点赞服务加了本地内存缓存有人顺手把文章的阅读数也塞进了Redis有人把点赞次数在Redis里加到一半就同步到数据库导致丢数据——这些问题的根子都是架构假设没有同步清楚。5.2 把假设映射到接口、实现和配置有了假设清单接下来就是一步步落到代码。首先围绕ASM-002我们定了接口划分业务代码只依赖LikeCountPort一个提供查询点赞数和增加点赞数能力的端口接口。实现上我们做一个RedisLikeCountAdapter同时数据库有自己的LikeRecordRepository做落地。public interface LikeCountPort { long getLikeCount(Long articleId); void incrementLikeCount(Long articleId); }点赞服务里处理点赞逻辑时只跟LikeCountPort打交道Service public class LikeApplicationService { private final LikeCountPort likeCountPort; private final LikeRecordRepository recordRepository; Transactional public void like(Long userId, Long articleId) { // ASM-003幂等检查通过唯一索引保证 if (!recordRepository.tryInsert(userId, articleId)) { throw new BizException(REPEATED_LIKE, 不能重复点赞); } // ASM-001先更缓存异步刷盘交给采集任务 likeCountPort.incrementLikeCount(articleId); } }这里有个关键取舍Transactional只保护了recordRepository.tryInsert这一侧没有保护likeCountPort.incrementLikeCount因为Redis的操作不能放在数据库事务里——这一条也是架构假设我们的ADR里写了Redis操作不参与本地数据库事务以最终一致性为目标。如果不写清楚将来被坑的同事指不定会试图把Redis的写操作包进事务里结果发现回滚根本管不到Redis还白白占用事务时间。其次为了照顾ASM-004的降级要求我们在适配器层做了个简单的开关。配置中心有一个配置项feature.like.cache.enabled默认是true。如果Redis健康检查连续失败动态开关直接置为falseRedisLikeCountAdapter变为不可用LikeCountPort的注入点换成DisabledLikeCountAdapter直接抛当前点赞功能暂不可用请稍后再试的异常。这个降级逻辑不在业务代码里而只在端口装配层处理——这就是当时Redis挂掉不能拖垮其他功能这个假设在代码里的落实。5.3 验证假设的自动化机制最后一步我们把三条关键假设分别映射到不同的验证手段上ASM-001异步延迟最多30秒用一个后台任务周期性把Redis里的增量同步到MySQL并同时把Redis的value和DB的count做对比。每次同步如果差异超过阈值就告警。这个逻辑其实就是一个job但它的报警规则就是架构假设的实时守护者。ASM-003幂等不靠代码review而是数据库订单表建了userIdarticleId的唯一索引再配合应用层的tryInsert保证重复请求不会造成双写。这算是把幂等这个假设直接烙进了数据模型里比任何代码层面的拦截都硬。ASM-004降级隔离用了一个简单的探活定时器定时往Redis写探针key连续三次失败就把缓存开关置为false并告警。这个探活器很粗糙但效果是当Redis真的出问题时系统不是拖着全线功能一起死而是只让点赞功能短暂不可用。整个过程走下来你会发现架构假设不是被某个天纵奇才一口气全解决掉的而是分散到接口设计、装配逻辑、数据模型、后台任务、配置开关里每一个环节都承担了一部分验证假设的责任。当这些机制都转起来以后架构评审中那些抽象的描述——高性能高可用幂等——才开始真正变成系统行为的一部分而不仅仅是写在文档里的漂亮词。6. 避坑清单我踩过的架构落地之坑这一节谈几个我真正踩过的坑基本每次都会在团队里复现整理出来给你做个提前预警。6.1 过度抽象为了架构完整性而加出来的抽象层依赖倒置、六边形架构、端口适配器听上去都很好但很容易走火入魔给一个只有CRUD的小服务硬生生加出五六层抽象。我见过最夸张的案例是一个简单的用户查询功能从Controller到Mapper中间隔了接口、抽象类、泛型基类、装饰器四层封装改一个字段要联动七八个文件。我的底线原则是抽象不是免费的每一层抽象都增加了理解成本和维护成本。只有当某个组件确实存在被替换的可能性或者确实有多个实现需要并存时才值得为它引入端口和适配器。架构假设清单正好可以帮上忙如果这条假设的影响面只限于一个类内部那就不值得上升到接口如果影响面跨了服务边界或者明确有替换预期那才值得抽象。不要为了把代码做得很架构而凭空制造复杂度。6.2 架构假设与实际需求脱节另一种反面情况是架构假设停留在评审会议的想象里跟真实业务需求完全脱节。比如评审时笃定这个模块的用户量会很大必须做缓存、做分片,结果上线半年每天调用量不到几百次。这种情况下团队花了大力气引入的Redis、消息队列、复杂的异步流程全都是在给一个没有出现的未来付利息。我的建议是架构假设至少要和技术债一样被当成需要持续审视的东西。每个迭代都问一句这个假设现在还成立吗如果业务量并没有如期增长那就应该果断砍掉为假设付出的复杂度而不是让精美的架构图纸成为自我安慰。一个设计得很爽但根本用不上那么多能力的系统和设计得平庸但把该解决的问题解决了的系统后者往往活得更健康。6.3 测试写了但没跑守护形同虚设架构测试、契约测试这些自动化守护手段写的时候大家都很热情但过一阵子就变成CI里的黄灯没人管了。我们曾经有一段时间ArchUnit测试开始因为各种无关原因失败大家为了不阻塞发布直接在pom里把相关测试给skip了。直到有一天我偶然看CI日志发现架构守护测试已经三个月没真正跑过——规则还是那些规则但它已经退化成了一段不会咬人的纸老虎。所以我强烈建议架构守护测试和单元测试一样失败必须阻塞合并。如果你发现它经常误报那就去调整规则的粒度而不是直接禁用它。一个偶尔发出噪音但真实有效的护栏远比一个从不报警但也没有约束力的装饰品有价值。6.4 团队没有共识架构只是技术负责人的独角戏最后这个坑其实是最大的坑。架构如果只是技术负责人一个人在文档里画图、定规则团队成员并没有从心底认同这些规则背后的理由那么再完美的设计也挡不住不理解它的人逐渐把它侵蚀掉。ADR和假设清单的价值不在于文档本身而在于它们成了团队交流的公共语言。让每个人都参与假设的提出和验证这不是民主化做形式而是让规则从源头就被理解、被接受。我后来带项目时即使时间很紧也一定会把每周的架构假设与ADR回顾塞进迭代里十五分钟就够哪怕结论是本周没有变化。不要小看这十五分钟它保证了团队始终处在一个共享的架构心智模型里而不是等到系统出问题才想起来当初那套设计到底是怎么定的。结尾一个建议从下一个迭代开始这篇文章写到这里我其实并没有给出什么惊天动地的技巧里面的每一招都很朴素列假设清单、写ADR、用代码结构约束依赖方向、用自动化测试守护规则、定期回顾假设是否过期。这些东西任何一个认真做事的团队都能做到难的地方在于坚持。我个人在实际操作中的体会是转化架构假设这件事最难的时刻不是第一次搭建这些机制的时候而是项目冲刺压力最大的那几周——所有人都觉得先上线再说后面再补文档。我的建议很简单不要等到后面就从下一个迭代开始挑一个最容易被验证的假设把它翻译成代码再配一条自动化测试看着它。尝到第一次甜头之后你会自己上瘾的。顺便说一个藏在实操里的小心得假设清单里最好每一条都能对应到一个可以在十分钟内说清楚的验证动作。我们假设这个服务要支持十万QPS这种话没有意义但用压测脚本在staging环境打满十分钟观察P99是否低于200ms这句话就有意义。架构假设的价值不在于它听起来有多宏伟而在于它能不能被某个明确的手段证伪。只有能被证伪的假设才值得被写成代码。