Skip to content

高质量的系统演进:让变化始终处于可验证、可控制的范围内 ​

一、从手工发布开始:第一次意识到"不能一直靠人记" ​

2015年时有一个Spring Struc MyBatis项目,我们的发布流程里还没有 Maven 这样的工具。一次上线,意味着要手工把改动过的 .class 文件从本地一个个复制出来,再按照目录结构放到服务器对应的位置上。这个过程看起来很简单,做起来却处处是坑:漏拷贝一个文件,线上就会报错;文件放错了目录,功能表现得莫名其妙;更麻烦的是,谁也说不清这次到底改了哪些东西——全靠开发者自己回忆"我这次改过哪些文件",靠一份手写的清单,靠上线前反复确认"是不是漏了什么"。

这套流程的问题不在于慢,而在于它把可靠性完全押注在了人的注意力上。人会累,会分心,会记错,尤其是在一次改动涉及多个模块、跨越好几天开发的时候,"这次到底改了什么"这个问题本身就已经很难回答准确。

于是我写了第一个小工具:读取 Git 的变更记录,自动生成本次需要上线的文件清单。这在今天看来是再普通不过的自动化脚本,但对当时的我来说,它解决的不只是"发布时漏文件"这一个具体问题,更留下了一个后来反复出现的思路——容易遗漏、容易出错、需要反复确认的事情,尽量不要长期依赖人的记忆和小心。这是我第一次把"人要记住的事情"变成"程序替我记住的事情"。后来随着 Maven、标准化构建和 CI 等工具逐渐普及,这类发布问题已经有了更成熟的解决方式。但当年留下来的那个思路,我一直保留了下来。

二、系统只要还在维护,就一定会持续演进 ​

一个还在被使用、还在被维护的系统,几乎不可能停止变化。这种演进不只是表现为不断上线的新功能——它还包括对已有功能的调整、对历史遗留问题的修复,以及不时发生的重构和迁移。只要系统还活着,这些变化就会一直发生。

问题在于,每一次变化都不是孤立的。哪怕是一个看起来很小的修改,也可能牵动到原本运行得好好的其他部分——一个共用的方法被改了返回值的含义,一个查询条件被"顺手优化"掉了,都可能在完全不相关的功能里埋下问题。而这些问题往往不会立刻暴露,可能要等到某个特定场景被触发,才会被人发现。

所以,真正需要面对的问题,从来不是"如何避免变化"——这既不现实,也不是目标本身。系统要长期有价值,就必须能够被修改、被扩展、被修复。真正重要的问题是:如何避免变化失控。也就是说,高质量的系统演进,核心并不是要求系统永远不出错,而是要求:变化发生之后,系统仍然符合预期。

三、我曾经期待一个"各司其职"的团队 ​

在意识到"变化需要被约束"之后,我最初设想的答案,是一个分工清晰、各司其职的团队。理想状态下,开发不需要包办一切,而是各自在自己的领域里承担相应的职责:开发关注实现本身和代码质量,测试负责系统性的验证、回归测试和接口自动化,产品把关业务规则和需求边界,UI 或交互则专注于用户实际的使用路径是否顺畅。当这些角色能够各司其职并形成有效协作,系统的质量就能够从多个不同角度得到保障,没有哪一个人需要独自扛下所有的责任。

但现实往往和这个理想有距离。团队的能力、时间、人力和资源,永远是有限的。在需求排得很满、迭代节奏很快的情况下,很多重要但不紧急的质量工作——比如补充边界场景的测试、梳理系统性的回归用例——很容易被一次又一次地往后推,久而久之就长期缺位。

这个现实也让我逐渐意识到一件事:一些真正重要的保障,不能永远依赖"某个人记得去做"。哪怕这个人是很负责的测试同学,哪怕大家都清楚这件事很重要,只要它的落实依赖于某个具体的人在某个具体的时间点上"想起来去做",它就始终是脆弱的。

四、把系统的"预期"变成可以持续验证的约束 ​

如果重要的保障不能依赖人记得去做,那答案就只能是:把这些"应该如此"的预期,变成系统本身可以持续验证的东西。

第一步是想清楚,在系统不断演进的过程中,究竟有哪些东西是不能被悄悄改变的。核心功能必须始终可以正常工作,已经确定下来的业务规则不能被无意破坏,用户与用户之间的数据边界必须一直成立,一些关键的状态流转也必须始终符合原来设定的约束。我把这类在演进过程中希望持续成立的约束,暂且称为系统的"不变量"。

要让这些不变量真正被守住,就需要把它们落到不同层次的自动化检查里。单元测试负责验证局部的业务规则是否依然成立,比如某个计算逻辑在各种输入下是否还符合预期。SmokeIT 负责验证核心功能是否仍然可以正常运行,确保系统整体的可用性没有被破坏。而 AuthorizationIT 负责验证用户和数据之间的边界是否依然成立,这恰恰是我过去没有系统验证过的一层。

这里有一个我自己遇到过的具体案例:系统在设计上是支持多用户的,但很长一段时间里,实际使用的只有我自己一个人。在这种情况下,一些查询语句很容易在写的时候漏掉"只查当前用户的数据"这个条件——因为反正长期以来查出来的都是自己的数据,这个遗漏根本不会被察觉。列表查询、详情查询、甚至一些关联操作,都可能存在类似的隐患。

为了把这个隐患找出来,我准备了 A、B 两个完全独立的用户,各自拥有自己的一份数据。验证的逻辑很直接:A 应该只能看到 A 自己的数据,B 应该只能看到 B 自己的数据;更关键的是,哪怕 B 明确知道 A 某条数据的 ID,也不应该能够通过接口直接访问到它。如果一个用户仅仅因为知道另一条记录的 ID,就能直接读取本不属于自己的数据,这属于典型的对象级授权问题,也就是常说的 IDOR;从用户权限关系上看,它又属于水平越权的一种表现。通过让 A、B 两个用户持续地交叉验证彼此的边界,我把"用户只能访问自己的数据"这个原本默认成立的前提,变成了一条可以反复执行、反复验证的规则。

这一步真正带来的改变是:这些重要的约束,不再只是依赖开发者在写每一段代码时都记得加上正确的条件,而是拥有了被系统自动、反复验证的能力。哪怕以后有新人接手这部分代码,哪怕原来的开发者已经忘记了这里曾经有过这样一个坑,这条检查依然会在每一次改动之后重新跑一遍。

后来我了解到,这类思路并不是我一个人凭空想出来的。演进式架构里,本身就有持续验证系统重要特性的理念;而在安全领域,授权测试和授权回归测试也是相对成熟的实践,专门用来防止新功能或者重构在无意中破坏掉已有的授权边界。这些理论体系我并不打算在这里展开,只是想说明一点:从实际问题里一步步推演出来的做法,往往并不是孤立的偶然,而是很多人在不同场景下都会走到的同一个方向。

五、自动化真正带来的,是让"偏离"尽早变得可见 ​

把重要预期变成自动化检查之后,真正的价值并不只是"多了一层保护网",而是它彻底改变了问题被发现的时机。

有些错误会直接报错、直接崩溃,这类问题反而是幸运的,因为它们会立刻被发现。真正危险的,是那些不会报错、甚至看起来一切正常的错误——接口返回了 200,页面渲染得好好的,但背后的逻辑其实已经悄悄跑偏了。前面提到的用户数据越权,就是这类问题的典型代表:从表面上看,系统运行得完全正常,唯一的问题只是它做错了一件本不该做错的事。

自动化验证的意义,就在于把"问题被引入"和"问题被发现"之间的距离尽可能缩短。过去它可能要等到真实用户碰到特定场景以后才被发现;现在,只要把这些检查纳入修改后的固定验证流程,很多偏离就能在进入真实使用场景之前暴露出来。

但要说清楚的是,"提前暴露错误"本身并不是最终目的,它只是达成目的的一种手段。真正的目标,始终是系统在持续变化之后,那些重要的能力、规则和边界依然成立。

也正因为如此,这些检查的价值不应该停留在"写完一次就够了"。不管是一次小的修改,一次较大的重构,还是一个全新功能的加入,都应该重新经过同一套验证机制,而不是每次都重新依赖人工去回忆"这次改动应该重点检查哪里"。从一次性的检查走向持续的反馈,才是这套机制真正应该达到的状态。

六、AI 让过去难以落地的质量保障开始真正落地 ​

前面提到过,我最初期待的是一个各司其职的团队,来共同分担这些质量工作。但现实中,很多事情最终还是只能自己去补——不是因为团队里没有合适的人,而是资源和时间始终有限,一些工作只能在有限的资源中被逐步补齐。

而对我个人来说,真正的限制往往不是"完全不会做",而是工程容量有限。日常的功能开发本身就占据了大量时间,除此之外还要处理业务理解、文档、重构、迁移、线上问题排查这些同样重要的事情。而像接口自动化、系统性的回归测试这类工作,本身又是一整块需要持续投入精力的事情——知道它重要,和真的能挤出时间把它做扎实,是两回事。

这也是为什么 AI 对我来说是一个实实在在的改变,但它改变的地方,可能和很多人想的不太一样。AI 并没有告诉我什么新的、原本不知道的目标——那些值得做的事情,此前就已经知道值得做。AI 真正带来的,是同时降低了认知成本和执行成本:它可以帮助我快速理解一些原本不太熟悉的领域,一起推演容易被忽略的边界场景;也可以直接阅读已有的工程代码,理解现有的接口结构和测试写法,然后完成大量原本需要我自己一行行敲、一步步调的重复性工作。

回到前面提到的 AuthorizationIT。第四章解决的是"应该怎样设计这套用户隔离的验证",而这里要说的,是这套设计为什么在过去很长时间里只停留在想法层面。写一套完整的 AuthorizationIT,意味着要处理两个测试账号的机器认证和 Token 获取、按照项目现有的测试框架把用例组织起来,还要考虑各种接口的调用方式——这些工作本身并不难,但足够琐碎和耗时,在功能开发排得很满的时候,很容易被一拖再拖。而 AI 可以直接基于项目里已经存在的认证方式、已有的 API 封装和测试结构,快速补出大量的实现细节,让这套原本停留在"我知道应该这么做"阶段的验证机制,直到现在才真正开始持续运行。

所以更准确的说法是:AI 并没有改变系统演进本身要追求的目标,改变的是实现这些目标所需要花费的时间、精力和成本。一部分过去因为工程容量不够而长期被搁置的事情,现在真正有机会进入到系统里,成为可以被持续验证的一部分——它扩大的,是一个人能够真正覆盖到的工程边界。

七、我现在理解的"高质量系统演进" ​

回头看这一路走过来的过程,我现在对"高质量的系统演进"有了一个更清晰的理解。

系统一定会变化,这是长期维护中的常态,而不是需要被消灭的异常。人也不可能完全避免犯错——高质量从来不意味着要求每一次修改都绝对正确,那既不现实,也不是问题真正的关键。真正值得投入精力去建设的,是一套能够约束变化、验证预期、并在偏离发生时及时给出反馈的机制。

这套机制在做的一件事,其实可以概括为:让系统逐渐拥有自己的"工程记忆"。最开始,这只是一件很小的事——让程序替我记住"这次到底改了哪些文件"。但后来,需要被系统记住的东西越来越多:哪些业务规则不能被破坏,哪些核心功能必须始终可用,哪些数据边界必须持续成立。这些内容,逐渐不再只存在于某一个维护者的脑子里,也不再依赖某个人某一天恰好想起来去检查——系统本身开始保留一部分对"自己应该怎样运行"的记忆。

最终想要达到的,从来不是一个永远不会出错的系统,那样的系统并不存在。真正想要的,是一个能够长期、稳定、可控地持续演进的系统——它会不断变化,也无法完全避免错误,但那些真正重要的能力、规则和边界,尽可能处于持续验证的范围之内,一旦发生偏离,也能够尽快被发现。