Read in English

软件由修改构成

为什么把软件开发理解为持续不断的修改,比理解为一次性的创造更准确。

Lianghui Zhang (Leric Zhang)·v0.1·更新于

本文为中文版,根据英文原版改写,而非逐句直译。

随便挑一个软件团队,随便挑一个普通的工作日,看看他们实际在做什么。

Alice 在加一个“导出为 CSV”的按钮。她打开了控制器、服务层、Excel 生成器、权限检查器,还有三个 i18n 文件。这些文件都已经存在,她的工作是在它们之间穿出一条新路径,同时不弄坏已有的路径。

Bob 在修一个线上 bug:某个地区的客户折扣算错了。他顺着逻辑往回追,经过一个 PricingService、一个 DiscountPolicy 接口、一个策略工厂和一个特性开关检查,最后找到一段三年前的临时绕过方案,没有人记得是谁写的。他改了一个条件,跑了测试套件,祈祷没有别的东西依赖旧的行为。

Claire 在评审一个 pull request。diff 涉及十四个文件,但她发现每一处改动其实都是同一件事:把字段 clientId 改名为 customerId。作者用的是查找替换。她批准了,但在心里记下一笔:这个字段如今散落在整个代码库里,没有任何一个抽象真正拥有它。

Dave 在重构支付模块。他抽出了一个共享的校验器,内联了一个只有一个调用方的抽象,又把一个 400 行的服务拆成三个职责集中的单元。每次提交都必须保持行为不变,所以每走一步他都跑一遍同样的测试套件,盯着有没有断言失败。

不同的人,不同的任务,底层却是同一种活动:修改(modification)。


“开发”与“维护”之分是一种虚构

传统上把软件分为“开发”和“维护”,这是项目管理层面的一种虚构。它或许有助于做预算,却掩盖了编程工作的本质:代码库一旦存在,此后几乎每一个工程动作都是一次修改。

这种虚构带来两种具体的危害。

第一是资源错配。当“开发”被当作一次性的资本支出时,团队会倾向于尽快交付,把结构问题往后推。设计债的积累,并不是因为工程师粗心,而是因为这套核算框架告诉他们:“维护”是未来的一个阶段,有未来的预算。在“开发”阶段交付的代码,正是“维护”阶段要修改的代码;在赶工压力下做出的设计决策,会变成每一位未来修改者都要缴纳的上下文税。

第二是认知割裂。行业训练工程师把功能开发看作创造,把修 bug 看作修理,把重构看作清理,仿佛这是需要三种不同思维方式的三种活动。但三者底层的结构完全相同。功能通过修改现有代码来增加行为,bug 修复通过修改现有代码来纠正行为,重构通过修改现有代码来保持行为。表面差异是真实的,比如意图、紧急程度、范围,但引擎是同一台:找到要改的地方,弄清要保持什么,决定改什么,确认改对了。一旦把它们当作彼此独立的学科,我们就看不见它们共同的成本结构。

在实践中,首次公开发布通常被视为开发与维护的分界线,这并非毫无道理。发布是一个真实的事件:用户开始依赖系统,数据开始积累,集成围绕现有行为形成,运维保障开始变得重要。但这些变化并没有把创造变成另一种叫作“维护”的活动。它们只是给同一条修改流增加了约束:用户数据、向后兼容、数据迁移、弃用周期、运维风险。每一项这样的约束,都是修改者为了确信改动安全而必须获取的额外上下文。发布并没有改变修改的本质,它只是提高了正确修改的上下文成本。


扩展仍然是修改

说到这里,很可能会有人提出异议。开闭原则(Open/Closed Principle)是软件设计中最经久不衰的思想之一,它明确告诉我们:模块应当对扩展开放,对修改关闭。既然行业自己的设计经典都把扩展和修改区分开来,那么“一切皆修改”的说法,岂不是抹平了一个重要的区别?

并没有。OCP 区分的是两种实施改动的策略,而不是两种活动。当你通过实现 PaymentStrategy 接口并注册它来新增一种支付方式时,你并没有避免修改系统。改动前后,系统的行为是不同的:一张原本会以某种方式支付的发票,现在以另一种方式支付。你确实修改了系统,只不过是通过在新位置添加代码,而不是重写已有位置的代码。

这不是文字游戏,这正是 OCP 的全部要义。这条原则并不声称扩展不是修改。它声称的是:对于某些类别的改动,让修改者能够添加而非重写的设计,会让修改更便宜。修改者不必理解现有支付流水线的内部细节就能扩展它。现有代码没有被触碰,修改者也就不必验证现有行为是否得到保持,因为扩展机制在构造上就保证了这一点。这次改动所需的上下文更小了。

这样读来,OCP 并不能证明扩展与修改是两回事。它证明的是:好的设计会降低修改的成本,而最有效的办法之一,是让某些修改可以表达为添加,而不是重写。 目标仍然是修改,只是策略得到了优化。


软件在一连串修改中演化

软件并不是在一次不间断的创造中产出的。它是在对一个尚不完整的系统做一连串改动的过程中逐渐累积起来的。而这些改动要满足的期望,本身也在不断变化。

原因大家都很熟悉:需求在变,领域在演进,集成在变化,实际使用又会暴露出那些原本就错了的假设。

一个活着的系统,从来不是为某一个孤立的未来而设计的。它必须经受住一连串修改,而这些修改的确切方向、频率和形态都是不确定的。这不是需求收集的失败,而是仍然有用的软件的常态:它必须不断适应变化。


设计的真正目的

既然变化是有用软件的决定性条件,可修改性就不只是众多质量属性中的一项,而是软件设计之所以存在所要解决的问题。

设计作为一门不同于“让代码跑起来”的学科,其实践目的是让未来的修改更简单、更安全、更可靠。那些历经数十年实践仍然存活下来的设计原则——信息隐藏、DRY、SOLID、测试——都是凭这一条标准赢得了自己的地位。好设计的回报并不会推迟到某个神话般的维护阶段才兑现;它会加速紧接着的下一次修改,因为它确保修改者恰好拥有安全完成改动所需的上下文。