低维护成本的软件开发

目标

  • 没有技术债务。
  • 软件的设计是容易理解的,需要修改软件时,将很容易找到相应的代码。
  • 软件以可维护为最优先目标,开发者可以在忘记它的数年后回来继续改进它。
  • 软件无限接近完成状态,直到因环境变更而落后或新的需求出现为止都令人满意。
  • 可以自信地升级主版本相同的依赖项。
  • 当软件被废弃时,可顺利从现有系统中移除。

原则

谨记“天下没有免费的午餐”,你不可能什么都不做就得到一个低维护成本的软件项目。实现低维护成本的软件开发的关键不在于少犯几个错误,而在于尽可能把每一件小事都做对,这无疑是相当有难度的。
此处总结了一些有助于实现低维护成本的软件开发的一般原则:
  • 摒弃所有非必要功能。
  • 停止使用生态环境不健康的语言和工具链。
  • 使用具有强表达力的语言,避免在编码过程中扭曲和偏离语言的常见模式。
  • 停止将宏用于任何可能产生代码坏味道的目的。
  • 只使用具有类型定义或类型标注的语言。
  • 尽可能使用经过时间检验的技术,因为这些技术有希望存在更长时间。
  • 全局性地思考问题,避免战术编程。
  • 积极重构代码,允许大量的代码行修改以降低软件熵。
  • 通过将复杂性转移给使用者来砍掉不那么必要的设计,因为人性化的设计往往是最难以维护的。现实中类似的例子已经有很多了。让我们考虑一下Markdown编辑器和所见即所得编辑器:前者需要用户付出(极少量的)学习成本,而后者几乎没有学习成本,然而,后者在可维护性上要比前者差得多:
    • Markdown编辑器更容易编写出结构清晰,人类可读的纯文本文件。所见即所得编辑器编写出的要么是难以迁移的内部文档模型,要么是完全陷入混乱的HTML。
    • Markdown文档在2050年还可以使用,Markdown编辑器往往是极简和可替代的。所见即所得编辑器更容易过时,维持向下兼容性更困难,对开发者来说也更难维护,供应商在2050年很可能已经不复存在。
  • 停止使用可能导致供应商锁定的大型软件框架。当一个软件框架正在适配一切环境,或是企图提供一站式解决方案时,它不再是软件框架,而是一颗定时炸弹。
  • 尽可能减少依赖低质量的软件包。这意味着对依赖进行代码质量审查,幸运的是,坏代码风格很容易识别,往往不需要真正理解软件包的工作原理就可以下判断。
  • 不为没有向下兼容性的平台编写软件。
  • 除非软件功能单一,否则应该尽力避免让软件跨平台。
  • 禁止使用云计算平台提供的专有服务。
  • 禁止使用不能锁定子依赖项版本的包管理器,禁止使用Git仓库作为软件包的来源。
  • 如非必要,不应迎合现存的标准和协议:
    • 大多数标准和协议是垃圾,其中大部分甚至没有参考价值,迎合它们只会拖累软件的发展。例如,你可能会不幸地遇到无法用这些标准和协议实现的需求,然后陷入进退两难的窘境。
    • 那些非常流行以至于无法被忽视的标准和协议,其流行度大多来自于它们的先发优势,以及向下兼容的保证。这使得它们在生产中的实现充斥着各种奇怪的边缘案例,收集和应付这些边缘案例会让人精疲力尽。
  • 关注工具链的抽象水平,停止使用难以迁移或升级的工具链。
  • 编写大量自动化集成测试,以提升依赖项升级时的安全感。
  • 软件运行所需的环境应当是专用的,隔离的,而不是与其他项目共享的。
  • 重视技术的自有化,尽可能剥离会受外部因素影响的依赖。
  • 尽可能缩减核心开发者以外的开发者数量,避免因协作上的摩擦损害项目的开发效率。这件事就像堵车一样,最好的解决方案不是拓宽道路,而是减少同时行车的数量。

重新发明轮子

重新发明轮子是一种反模式,因为总是已经有一个或多个已经发明好的轮子等着你复用。然而,对已有轮子的复用经常引发依赖管理问题:事情起初很顺利,但问题很快接踵而至。最简单的道路往往是最大化技术债务的道路。走在这种道路上,经常会感到挫败。久而久之,再怎么有信心和激情的人都会产生习得性无助,认为软件开发是一件永远看不到头的苦差事。
判断依赖项是否值得被引入是一项很容易被忽视的重要能力,背后需要大量难以言传的经验来支持。当你缺乏这项能力时,你会很容易受骗上当,成为未来的受害者乃至加害者的帮凶。由于大多数开发者都缺乏这项能力,行业内劣币驱逐良币的现象因此非常普遍。
达到一定规模的开源软件项目很容易变成屎山:
  • 错误的抽象,糟糕的架构,可怕的API表面积。
  • 缺乏令人信服的测试。
  • 文档没有得到积极维护,与实际不一致,或是掩盖或是忽略了有用的细节。
  • 太多的战术编码,太少的战略编码,新增的代码量总是比减少的代码量要多。
  • 为不存在的需求投入过多的时间成本,导致软件膨胀和腐烂。
  • ……
除非社区里有能以战略视角把关,严格执行代码审查的权威维护者,否则开源软件的参与模式天然会制造这种屎山,无论贡献者的水平有多高。闭源软件有着同样的问题,只是闭源软件的不透明性进一步掩盖了这些问题,问题被扫到地毯下面不代表问题消失了。
为了从技术债务中解脱,有时你只能戴上NIH帽子。通过重新发明轮子,你可能会注意到,除了能让我们摆脱糟糕的第三方依赖以外,重新发明轮子有着那些技术债务缠身的开发者们永远无法知道的好处:
  • 有可能会发明出更好的轮子。
  • 你会越来越擅长发明解决方案。
  • 你会越来越有能力辨别依赖项的好坏。
当你以低维护成本为目标的时候,重新发明轮子的收益可能会在未来进一步放大:丢掉债务后,剩下的都是利息,多么简单的道理。久而久之,这些正反馈的叠加会让重新发明轮子这一选项在你心中占据越来越重要的位置。不过,在重新发明轮子之前,总是应该先审慎地评估其经济性。重新发明一切仍然是不可取的,显然有一些轮子重新发明的性价比很低,并且开发失败的风险很高,那么你就得小心。

构建电池式依赖

一些编程语言和框架会声称自己附带电池(batteries included),这指的是它们内置了可以直接使用的高级功能。这些“电池”的本质只是相对稳定的,极力避免暴露底层细节的高级API,正确的抽象和测试使得它们很少出现破坏性变更。
“电池”的概念完全可以套用到依赖管理上,可以建立这样一种“电池式依赖”:这种依赖是实际使用依赖的项目与依赖项之间的中间层,它基于依赖项提供一套有序整洁的API,从而允许避免让业务代码直接使用那些野蛮人发明出来的带有巨型或巨量参数的函数或类。
由于你只关注这些依赖项中使用到的部分,你可以在中间层编写自己的测试代码,以确保在依赖项版本更新时,其行为仍然符合预期。当未来依赖项失效时,你可以选择用旧版本的依赖项,更换依赖项,或是自己实现,之前编写的测试被用来保证重构时的有效性。这便是自己构建的电池和他人构建的电池之间的关键区别,你总是可以给自己的电池充电或扩容,而对于他人的电池,通常你只能看着它慢慢没电。
由于形式上的相似性,可能会混淆电池式依赖和经过封装的业务逻辑,两者的主要区别在于它们与依赖项之间的距离。电池式依赖通常距离依赖项很近,而业务逻辑通常距离依赖项很远。只有在少数场合,例如业务逻辑直接建立在相关依赖上的情况下,两者才趋于相同。
电池式依赖似乎是解决依赖管理问题的圣杯,但仍然有几个问题:
  • 它毕竟是额外的中间层,当你需要新功能,或是进行破坏性变更时,都要比直接使用依赖项要麻烦些。
  • 建立正确的抽象很难。在摸索正确抽象的道路上,有时不可避免会需要推倒重来。人们对未来的需求也经常缺乏预见性,因此一时正确的抽象也可能在未来显得不正确。人们害怕项目由于错误的抽象陷入反复修改的困境,所以宁可把自己卷进一个更大的漩涡里,至少在出错时怪别人总是更容易,很难反驳这种想法。

使用小型软件包

小型软件包是Unix哲学和语义化版本控制的共同体现。软件包的粒度越小,单个包的功能就越少,功能越少就越稳定,改变代码会打破的东西也就越少。
小型软件包有时会带来一些新的问题,例如软件包数量激增带来的管理问题。这类问题会很棘手,因为它们会随着日积月累变得越来越难以靠蛮力解决。这种趋势最后会逼出一些专门的解决方案,例如Facebook的codemod之类的东西,但使用这些东西仍然很麻烦。

在可读性和性能之间选择可读性

在实际的编码中,会遇到在可读性和性能之间摇摆的情况,这时开发者很容易因为一时的短视选择性能而不是可读性。选择性能通常没有很强烈的理由作为支持,牺牲可读性换取性能的取舍在日后看来往往并不划算。在一些情况下,开发者选择性能甚至只是因为“编写高性能代码可以显得自己很聪明”而产生的幼稚心理收益。
当你在可读性和性能之间摇摆不定时,应该总是选择可读性,这一点再怎么强调都不为过。非常确定这份代码只会被编写一遍并且这辈子都不可能再改动?选择可读性。可读性更好的代码慢1000倍?选择可读性。如果真的产生可观察到的负面性能影响,则再编写一份代码:这份代码是高可读性代码经过转译后的高性能版本,你需要在旁边写上非常详细的文档来解释这份代码是如何产生的。
一种例外情况是你需要编写高性能程序,由于你一开始的目标就是高性能,这时可读性作为一种选项就消失了。不过,即使是高性能程序,也仍然有机会提升代码的可读性,只是这些机会很难被察觉到——往往只有在第二次编写代码的时候,才会注意到。而一旦注意到这种提升可读性的机会,就没有理由继续忽视它们——在维持性能不变的情况下,总是应该尽可能提高可读性。

欣赏无需维护的软件

应当将软件的“无需维护性”视作一种美来欣赏。软件的“无需维护时间”越长,其“无需维护性”经受的考验就越久,积累的美就越多。
最关键的部分在于,你清楚地知道软件不是不能维护,也不是难以维护,而是它不需要维护。软件代码就在那里,但你不需要摆弄它,因为它仍然满足需要,正常运作,并且运作得很好。
即使到了需要维护的时候,软件的维护阻力也应当如同窗户纸那样,轻轻一捅就可消除。因为没有技术债务,所以你不需要支付利息。强迫症发作想要升级依赖项?没问题,因为这项升级可以在十分钟内完成,而你有很多个十分钟可以浪费。但比这更好的是你战胜了自己,及时制止了自己的行为——不仅仅是因为还没有产生新的需求,还因为你不想破坏软件随时间积累出的美。
要培养出这种欣赏能力颇有难度,因为它很反直觉。人们会下意识地将一个事物的新颖程度错误地视作一种衡量事物好坏的要素——赶上流行有时似乎比什么都重要——稍加思考就会发现这并不理性。仅仅在日常生活中,就能发现那些用料扎实,工艺成熟的装置经常耐用得超乎预料——冰箱这种不间断工作的家电就是最典型的例子,没几个人会每年更换新的冰箱。软件作为一种数字产物,没道理会比硬件更快过时,容易更新最不应该成为更新的理由。