“无常软件(Impermanent Software)” 是基于软件工程实践提出的理念,其本质是接受软件的 “非永久性”,并围绕 “可删除、可变更” 设计代码与系统,而非追求代码 “永久存续、不可改动”。
无常软件不是 “短命软件”,而是接受 “软件必然会变” 的客观规律,把 “可删除、可拆解” 作为设计核心,让系统始终保持灵活、低改造成本的状态—— 最终反而能让软件 “更长久地存活”,因为它能随需求持续调整,而非因僵化被全盘重构。
我们总执着于让代码长久留存,或许,更该费心的是让它体面退场。
有句话多年来一直萦绕在我们的心头:“写出易删除的代码,而非易扩展的代码。”—— 泰夫(Tef),《编程很糟糕》
初次读到这句话时,很多人或许会像笔者一样反驳:编写代码的核心要义,不就是让它能存续、可扩展、可在此基础上持续构建吗?
但只要试过从一个三年的代码库中移除某个日志库,就会明白这句话的深意 —— 这类代码往往悄无声息地渗透到 40 个文件中,移除它的过程,堪比给一位骨骼长到海绵里的病人做手术。
我们自欺欺人的谎言
编写代码时,我们总会给自己编织一个美好的幻象:这段代码会在这里存在五年,所以要把它写得健壮、可复用、易扩展。
但数据却戳破了这个谎言:大多数功能数月内就会被修改,不少甚至会被彻底砍掉。一个成熟的生产环境代码库中,往往有整目录的代码数年无人触碰 —— 不是因为它们完美无缺,而是所有人都不敢删除。
我们总把代码当成 “承重结构” 来写,但多数时候,它根本不配。
颇具讽刺的是,越是试图让代码 “永久化”—— 用抽象层层包裹、耦合进共享工具、渗透到系统各个角落,代码就越难改动。我们用灵活性,换来了 “耐用” 的假象。
“易删除” 究竟意味着什么
这绝非指编写一次性代码,也不是省略测试、忽视结构,而是:为可逆性而设计。
当你编写一个功能时,不妨问问自己:如果明天就要移除它,过程会是怎样的?
若答案是 “需要提交一个 400 行的 PR,改动 20 个文件”,那问题必然出在设计阶段,而非删除阶段。
易删除的代码,往往具备这些特质:
1. 代码归置一处
代码重复向来名声不佳,DRY(Don't Repeat Yourself,不要重复自己)原则虽是良言,但走向极端,只会催生深度纠缠的代码。当同一个函数在八个不同场景下被复用,你为一个场景修改它时,不得不顾虑其他所有场景。
有时,一点点重复,是换取独立性的必要代价。两个模块各自拥有一个 formatDate 函数,彼此可以独立演进、甚至消失,不会对对方造成任何影响。
2. 边界清晰明确
最难删除的代码,是那些四处渗透的代码:被导入到 UI 组件里的数据库客户端、层层传递十二次的配置对象、沦为核心基础设施的工具函数。
清晰的边界,是安全删除的前提。一个独立的模块、简洁的接口、封装在明确 API 后的服务…… 这些都能让你放心地移除、替换或重写代码,无需提心吊胆。
3. 低耦合、少依赖
易删除的代码,往往带着一种 “恰到好处的无知”:它不了解系统的其他部分,只接收输入、完成自身功能、输出结果;不会擅自获取全局状态,也不会修改非自身创建的内容。
值得一提的是,这种 “无知” 的代码往往也易于测试,这并非巧合。
4. 隐藏在 “衔接层” 之后
功能开关、适配层、接口抽象 —— 这些绝非形式化的工程技巧,而是删除代码的 “抓手”。被开关包裹的功能,几秒内就能关闭;接口背后的代码,替换后调用方完全无感知。
“扼杀者模式”(Strangler Fig Pattern)的存在正是基于此:包裹旧代码,在旁构建新代码,待旧代码被隔离后,再彻底删除。而 “衔接层”,正是这一模式落地的关键。
对抽象的全新思考
我们常为避免重复而使用抽象,但抽象最有价值的作用,是隔离代码、赋予其明确的名称和边界,让你无需改动其他所有代码,就能修改或移除它。
以日志功能为例:你可以在各处零散地写 console.log,写起来轻松,后续修改却寸步难行;但如果所有日志都通过一个统一的日志模块输出,那么无论你想替换日志库、添加上下文,还是彻底关闭日志,都只需改动这一个文件 —— 仅此一个。
抽象的意义,并非因为日志功能复杂,而是因为日志是可能变更或消失的模块,我们希望这个过程毫无痛苦。
抽象应落在 “衔接处”,而非 “核心处”。
以 “可删除性” 审视代码评审
笔者在代码评审时,开始养成一个习惯:除了问 “这段代码能用吗?”,还要问 “移除这段代码需要付出什么代价?”
这个问题能带来全新的视角。
一个新增功能却改动 15 个文件的 PR,本身就是警告信号 —— 未必是代码逻辑错误,而是它预示着未来的变更成本极高;而通过单个、边界清晰的模块实现相同功能的 PR,留下的技术债会少得多。
这一思路也可延伸到架构决策中:引入新依赖前,不妨问自己:“两年后移除这个依赖会有多难?” 有些依赖无伤大雅,因为它们体积小、稳定且独立;而另一些依赖,就像引入入侵物种,会渗透到系统各处,最终无法根除。
接纳无常,并非消极妥协
禅宗中有个概念译作 “无常”,即万物皆有生、住、异、灭的过程。这并非悲观,而是对事物规律的客观描述。
软件亦是如此:功能迭代更新,产品调整方向,需求不断变化。你今天写的代码,终将被部分或全部替换。这不是失败,而是鲜活的软件本该有的样子。
为 “无常” 编写代码,意味着接纳这一现实,并以此为设计准则。我们的目标,不是写出永远无法被移除的代码,而是让代码的移除成本足够低。
那些能稳定运行 30 年的系统,并非因为代码无法被改动,而是因为代码易于理解、易于隔离;当需要迭代时,又能被逐块替换。
实操清单:写代码前先自查
提交代码前,不妨快速自检这几个问题:
能否通过单个 PR 删除这个功能?若不能,原因何在?
这段代码改动了多少个文件?数量多未必错,但需确保每一处改动都有明确目的。
这个模块是否知晓了不该知晓的内容?比如不必要的导入、全局变量、副作用。
若这个依赖明天消失,影响会有多大?能否在一个下午完成替换?
这个抽象是让代码更易变更,还是仅仅为了避免重复?
这并非要让大家畏手畏脚 —— 你不必把每个微服务都设计得仿佛随时会消失。但培养对 “删除成本” 的直觉,就像培养对性能、可读性的敏感度一样,会潜移默化地让代码库更健康。
不必编写的代码,才是最好的代码
最后想提的是:最容易删除的代码,是从未被编写的代码。
每个功能都是一份责任,每个抽象都是一个维护面,每个依赖都是一段需要维系的关系。不存在的代码,没有 bug、没有耦合、没有删除成本。
这并非主张 “零开发”,而是强调 “审慎而为”。当你想新增一层抽象、泛化仅用过一次的功能、为未必出现的场景提前构建代码时,不妨先停下来。
或许正确的选择是等待,是编写最精简的代码,是为 “删除” 留足空间。
因为,能轻松变更的软件,才具备长久存续的生命力。而实现可变更性的秘诀,并非精妙的架构或高超的抽象,而是清醒地知道:你今天构建的一切,明天都能被体面地拆解。
夜雨聆风