软件大部分成本其实不在最初开发阶段,而是在于整个生命周期内的持续投入,包括维护与bug修复,监控系统来保持正常运行、故障排查、适配新平台、搭配新场景、技术缺陷完善及增加新功能。

可惜许多程序员不喜欢维护这些所谓的遗留系统,例如修复他人埋下的bug或使用过时的开发平台或被迫做不喜欢的工作。每个遗留系统总有过期理由,所以很难给出通用建议该如何对待它们。

但换个角度,可从软件设计时就开始考虑,尽可能减少维护期的麻烦,甚至避免制造出易过期系统。为此,需特别关注软件系统的三个设计原则:

  • 可运维性
    方便运维团队来保持系统平稳运行。
  • 简单性
    简化系统复杂性,使新工程师能够轻松理解系统。注意这与API的简单性不同
  • 可演化性
    后续开发能轻松对改进系统,并根据需求变化将其适配到非典型的场景,也称为可延伸性、易修改性或可塑性

类似可靠性、可扩展性,实现这些目标也没简单解决方案。但我们首先还是要建立对这三个特性的理解。

4.1 可运维性:运维更轻松

“良好的操作性经常可以化解软件局限性,而不规范操作则会轻松击垮软件”。虽然某些操作可以而且应该自动化 ,但最终还是需要人工执行配置并确保正常工作。

运维团队对保持软件系统顺利运行至关重要。 优秀团队通常至少负责:

  • 监视系统健康状况,井在服务出现异常状态时快速恢复服务
  • 追踪问题原因,例如系统故障或性能下降
  • 保持软件和平台至最新状态, 如安全补丁
  • 了解不同系统如何相互影响,避免执行带有破坏性的操作
  • 预测未来可能问题,并在问题发生前解决(例如大促之前就完成机器扩容)
  • 建立用于部署、配置管理等良好的实践规范和工具包
  • 执行复杂的维护任务, 例如将应用程序从1个平台迁移到另1个平台
  • 配置更改时,维护系统的安稳
  • 制定流程,规范操作行为,并保持生产环境稳定
  • 保持相关知识的传承(如对系统理解),及时复盘存档。如发生团队人员离职或新员工加入

良好可操作性意味着日常工作的简单,使运维团队能专注更高附加值的任务。数据系统设计可以在这方面贡献很多, 包括:

  • 提供对系统运行时行为和内部的可观测性,方便监控
  • 支持自动化, 与标准工具集成
  • 避免绑定特定机器,这样在整个系统不间断运行的同时,允许机器停机维护
  • 提供良好的文档和易于理解的操作模式,诸如“如果我做了X,会发生Y”
  • 提供良好的默认配置,且允许管理员在需要时方便修改默认值
  • 尝试自我修复,在需要时让管理员手动控制系统状态
  • 行为可预测,减少意外发生

4.2 简单性:简化复杂度

小软件项目通常能写出简单而漂亮的代码 ,但随项目变大,就越复杂和难理解。这种复杂性拖慢后续的开发效率,增加维护成本。一个过于复杂的软件项目被称为大泥潭。

复杂性有各种表现方式:

  • 状态空间的膨胀
  • 模块紧耦合
  • 令人纠结的相互依赖关系
  • 不一致的命名和术语
  • 为了性能而采取的特殊处理
  • 为解决某特定问题而引人的特殊框架等

复杂性使维护变得越来越困难, 最终导致预算超支和开发进度滞后。对复杂软件系统,变更而引人潜在错误的风险会显著增大,最终开发人员更加难以准确理解、评估或更加容易忽略相关系统行为,包括背后的假设,潜在的后果,设计之外的模块交互等。而降低复杂性就能大大提高软件可维护性,因此简单性应该是构建系统的关键目标之一。

简化系统设计不代表减少系统功能,而意味着消除意外方面的复杂性,有大佬把复杂性定义为一种“意外”,即它并非软件固有、被用户所见或感知,而是实现本身所衍生出的问题。

消除意外复杂性最好的手段之一是抽象。 好的设计抽象:

  • 能隐藏大量实现细节,并对外提供清晰易懂的API
  • 可用于各种不同的应用程序。这样,复用远比多次重复实现更有效率
  • 也带来更高质量的软件,而质量过硬的抽象组件所带来的好处,可以使运行其上的所有应用轻松获益

例如,高级编程语言作为一种抽象,就隐藏了汇编、 CPU寄存器和系统调用的细节和复杂性。 SQL作为一种抽象,隐藏内部复杂的磁盘和内存数据结构及来自多客户端的并发请求,系统崩溃之后的不一致等问题。当然了,使用高级编程语言最终也没有脱离汇编代码,只是井非直接使用,与汇编代码打交道的事情已由编程语言抽象为高效的接口代替我们来完成。

但设计好抽象很有挑战性。在分布式系统领域中,虽然已有许多好算法可参考,但很多时候并不太清楚如何利用他们,封装到抽象接口中,最终帮助将系统的复杂性降低到可靠控的级别。

日常开发时,我们可以广泛考察如何设计好的抽象,这样至少能够将大型系统的一部分抽象为定义明确、可重用的组件,提高自己的年终绩效!