你的问题真的是“架构”吗?
很多人跟我说:“我需要学架构。”
我通常会问:你遇到的具体问题是什么?
答案往往是这样的:改一个需求要动很多地方,上线怕出问题,回归测试跟不上,新人看不懂代码。
这些问题听起来像“架构问题”,但我想请你停下来想一想:你现在的痛点,真的需要“更多的架构”来解决吗?还是说,你需要的其实是“更好的设计”?
架构和设计是两件不同的事情。架构是关于系统级别的大决策——技术选型、部署形态、模块划分。设计是关于每一天、每一行代码的小决策——职责怎么分配、接口怎么定义、依赖怎么管理。
很多团队的问题不在于缺少架构,而在于日常的设计太粗糙。代码没有清晰的职责划分,模块之间随意调用,接口定义含糊不清。这些问题积累起来,系统就变成了一团纠缠不清的线球。
而AI编程工具的到来,让这个问题变得更加紧迫了。因为AI产出的速度很快,如果你的设计不能给AI一个清晰的框架,它只会以更快的速度向这团线球里缠入更多的线。
所以在聊“架构”之前,我想先聊一个更基本的问题:分离关注点。
分离关注点:一个被说烂了但很少被做好的事情
“分离关注点”这几个字,每个程序员都听过。但我的观察是,真正在日常工作中贯彻它的团队并不多。
什么叫分离关注点?我举一个金融系统的例子。
一个支付场景里,至少有三个不同的关注点:
业务订单:用户发起了什么操作?状态是什么?这是业务维度的事情。
资金流水:钱从哪里到哪里?渠道返回了什么?请求是否成功?这是资金维度的事情。
账务分录:记账的借方是多少?贷方是多少?余额变化了吗?这是会计维度的事情。
很多系统把这三样东西混在一起。订单表里存着渠道状态,流水和订单共享同一个状态机,账务余额直接在订单表上计算。
一开始,这样做看起来很“高效”——少建几张表,少写几个模块,代码量也少。但随着业务复杂度增加,问题会越来越明显:改订单逻辑怕影响账务,做对账发现数据对不上,退款的时候状态机打架。
这就是没有分离关注点的代价。三个不同的关注点被绑在一起,一个变了其他两个都受影响。
正确的做法是:订单是订单,流水是流水,账本是账本。 三个独立的模块,各自有自己的数据表和状态机。它们之间通过明确的接口通信,而不是共享数据库表。
这样做一开始会多写一些代码,但你获得的是:每个模块可以独立修改、独立测试、独立理解。AI在一个模块里写代码时,不需要理解其他模块的内部逻辑。
小步走:把大问题变成小问题
分离了关注点之后,下一个问题是:怎么改进?
很多人的思路是“大重构”——画一个理想的架构图,然后花几周时间把系统从旧结构迁移到新结构。
我不建议这样做。
大重构的问题在于:它的反馈周期太长了。你花了两周写代码,上线的时候才发现一堆问题。而且在这两周里,业务需求还在持续进来,你的重构分支和主干的差异越来越大,合并的时候冲突满天飞。
更好的方式是小步走。
每次只做一件事:提取一个接口,移动一段代码,消除一个循环依赖。每一步都可以独立提交、独立测试、独立上线。如果出了问题,回滚的范围也很小。
小步走在AI时代特别好用。你可以把每一步交给AI来做:
“帮我把Controller里的权限检查逻辑提取到Application层。”
“帮我给这个支付状态机写一组单元测试。”
“帮我把这个模块对外的接口定义成一个独立的Interface。”
每一步都很小,AI很容易做对。做完之后你可以快速验证。这比让AI做一个大重构要安全得多。
你真的需要微服务吗?
我经常听到这样的话:“我们的系统越来越复杂了,应该拆微服务了。”
每次听到这句话,我都想问一个问题:你的系统复杂在哪里?
如果复杂性来自于模块之间没有清晰的边界——代码互相调用、数据互相访问、改一个地方到处出问题——那拆微服务只是把一团乱麻变成了分布在不同机器上的一团乱麻,外加网络延迟和分布式事务的复杂度。
你的问题不在于代码跑在几个进程里,而在于代码之间的依赖关系没有管好。
模块化单体是一种更务实的选择。它的意思是:在一个代码仓里,把系统按业务领域分成独立的模块。每个模块有自己的代码包、自己的数据表、自己的接口。模块之间不能互相访问数据库表,只能通过接口调用。
这听起来很像微服务。区别在于:它们运行在同一个进程里,所以没有网络延迟、没有分布式事务、没有服务发现。
关键的设计约束是:依赖方向向内。 最内层是领域逻辑(Domain),它不依赖任何外部技术——不依赖数据库、不依赖消息队列、不依赖HTTP框架。技术细节在外层,领域逻辑在内层。这样你的核心业务规则可以独立测试,不需要启动任何中间件。
如果将来你确实需要把某个模块独立部署,边界已经在那里了,拆分是自然而然的事情。但在大多数情况下,你不会需要。
护栏:不是防别人犯错,是防自己犯错
做金融系统的人都知道“正确性”很重要。但“知道”和“做到”之间有巨大的鸿沟。
我见过太多系统,大家都知道幂等很重要,但真正落地的时候只是在接口上加了个唯一索引。当遇到超时重试、消息重复投递、回调乱序这些真实场景时,系统的行为是不可预测的。
问题出在哪里?出在我们把“护栏”当成了“事后补救”,而不是“设计的一部分”。
我认为,在金融系统里,幂等、对账、审计这些东西不是“非功能性需求”,而是一等公民。它们应该在设计阶段就被考虑进去,而不是上线之后才补。
幂等的设计方式:定义清楚幂等的粒度和幂等key的来源。维护一张去重表,每个请求先查表再处理。重复请求返回第一次的结果,而不是报错。这个逻辑要抽象成通用组件,不要每个接口自己实现一遍。
对账的设计方式:保留所有的原始回执和原始数据。每天自动运行对账,比较你的记录和对方的记录。有差异就进入处理流程,不允许手工改账本——只能通过冲正和补录来修正。
审计的设计方式:把审计事件设计成不可变的事件流。每条记录包含操作人、操作时间、操作对象、操作前后的关键字段快照。不要把审计逻辑散落在业务代码里,把它集中到一个独立模块。
这些护栏建好之后,你会发现一个有趣的现象:你对AI产出的代码更有信心了。因为就算AI写的代码有bug,幂等机制可以防止重复操作,对账可以在第二天发现异常,审计可以帮你追溯问题。AI不需要是完美的,它只需要在护栏的范围内工作。
可测试性:比什么架构模式都重要
如果让我只选一个标准来判断一个系统的设计好不好,我会选可测试性。
可测试性好意味着什么?意味着你的模块职责清晰(否则你不知道测什么),接口定义明确(否则你不知道怎么mock),副作用被隔离(否则测试要启动一堆中间件)。
可测试性差意味着什么?意味着代码的职责、接口、副作用都是模糊的、纠缠的、不可控的。
换句话说,可测试性是设计质量的一面镜子。如果你的代码难以测试,那一定是设计出了问题,而不是测试出了问题。
在AI编程时代,可测试性还多了一层意义:它是你验证AI产出的最高效手段。
你可以让AI先写测试,再写实现。或者你写测试,让AI写实现。不管哪种方式,测试都是你和AI之间的“契约”——它定义了正确的行为是什么,AI的代码要通过这个契约才能被接受。
金融系统里最需要测试的几个方面:
状态机的完整性:每个状态转换都有对应的测试?不允许的转换有没有被拒绝?
权限的边界:同角色不同租户能访问吗?不同组织的数据能看到吗?越权操作会被拦截吗?
幂等的正确性:同一个请求发两次,结果一样吗?并发发送呢?
金额计算的准确性:边界值对不对?精度有没有丢失?冲正之后余额对不对?
这些测试写好之后,你让AI改代码就心里有底了。因为测试会告诉你AI有没有破坏已有的行为。
最小的下一步
如果你看完这篇文章,觉得有道理但不知道从哪里开始,我给你一个建议:找到你系统里最痛的那个点,用最小的步骤去改善它。
不要试图一次解决所有问题。不要画一个宏大的架构蓝图。不要花两周时间做一个大重构。
找到那个“每次改需求都要碰、每次碰了都怕出事”的模块。看看它为什么痛——是职责不清?是接口模糊?是缺少测试?还是和其他模块纠缠在一起?
然后做一件最小的事情来改善它。也许是给它的核心逻辑写几个测试。也许是把它和另一个模块之间的直接数据库访问改成接口调用。也许是把散落在各处的权限检查集中到一个地方。
每件事都很小。每件事做完之后,系统都比之前好一点。
这就是我理解的好设计:不是一步到位的完美方案,而是持续改善的能力。
在AI编程时代,这种“小步改善”的能力比以往任何时候都重要。因为AI给了你前所未有的改代码的速度,而你需要的是让每一次改动都是向更好的方向走,而不是更乱的方向走。
分离关注点,小步前进,建好护栏,保持可测试。
做到这四件事,你的系统就能在AI的加持下越来越好,而不是越来越乱。
注:本文风格参考郑晔(开源项目moco作者,《软件设计之美》作者)的技术写作风格。