下载本文的PDF版本 PDF

你不懂软件维护

Paul Stachour 和 David Collier-Brown

长期以来被认为是事后才考虑的事情,但当软件维护从系统底层构建时,它是最容易和最有效的。

每个人都知道维护是困难且枯燥的,并避免去做它。 此外,他们那些尖酸刻薄的老板会说这样的话:

      “没有人需要做维护——那是浪费时间。”

      “现在就把软件发布出去:我们可以稍后决定它的真正功能是什么。”

      “先做硬件,不用考虑软件。”

      “不要为扩展留出任何空间或设施。 你可以稍后决定如何将更改夹在其中。”

这些说法是对上一次繁荣时期开发的真实写照,而且与我们今天许多人正在做的事情相差不远。 这不是一件好事:当你遇到第一个错误时,所有你通过忽略维护需求而“节省”的时间都将消失殆尽。

在上一次繁荣时期,通用电气设计了一台大型计算机,声称它足以满足波士顿所有计算机用户的需求,并且永远不必为了维修或更改软件而关闭。 它最终制造的机器远没有那么大,但它确实成功地连续运行,不需要为了硬件或软件更改而停止。

现在我们拥有由数千家企业提供的分布式计算机网络,足以满足至少北美乃至全世界所有人的需求,但我们不得不不断关闭各个部分以维修或更改软件。 我们这样做是因为我们已经忘记了如何进行软件维护。

什么是软件维护?

软件维护不像硬件维护,后者是将物品恢复到原始状态。 软件维护涉及使物品脱离其原始状态。 它涵盖与软件更改过程相关的所有活动。 这包括与“错误修复”、功能和性能增强、提供向后兼容性和掩盖硬件错误、创建用户界面访问方法和其他外观更改以及更新以使用新算法相关的所有内容。

在软件中,在铁路桥上增加一条六车道的高速公路被认为是维护——如果能在不停止火车交通的情况下完成,那就特别好了。

是否有可能设计出可以以这种方式维护的软件? 是的,有可能,但我们没有这样做。

天启四骑士

有四种处理维护的方法:传统型、永不型、离散型和连续型——或者,也许是战争、饥荒、瘟疫和死亡。 无论如何,其中 3½ 种都是糟糕的主意。

传统型,或“每个人的第一个项目”。 这种方法很简单:甚至不要考虑维护的可能性。 硬编码常量,避免子程序,使用所有全局变量,使用简短、无意义的变量名,总而言之,使其难以在不更改所有内容的情况下更改任何一项。 每个人都知道这种方法的例子——以及那些不假思索地将你推入其中的 PHB,通常是因为进度压力。

尝试维护这种软件就像打仗。 敌人会反击! 当你必须更改接口时,它尤其会反击,而且你发现你只更改了部分副本。

永不型。 第二种方法是预先决定永远不会进行维护。 你只需从一开始就编写出色的程序。 这在某些嵌入式系统中实际上是可信的,这些系统将被烧录到 ROM 中并且永远不会更改。 烤面包机、视频游戏和巡航导弹就是例子。

你所要做的就是设计完美的规范和接口,并且永远不要更改它们。 只更改实现,并且仅在产品发布之前进行错误修复。 代码质量比传统方法好得多,但永远不够好到完全避免更改。

即使对于非常简单的嵌入式系统,规范和设计也不够好,因此在实践中,规范在仍然存在缺陷时就被冻结了。 这通常是因为它无法验证,因此你无法判断它是否有缺陷,直到为时已晚。 然后,在编写代码时,规范没有得到遵守,因此你无法证明程序遵循规范,更不用说证明它是正确的了。 因此,你要测试直到程序迟到,然后交付。 几个月后,你通过发送新的 ROM 将其作为一个完整的实体进行更换。 这是国防部嵌入式系统、游戏和洗衣机的典型历史。

离散型 离散更改 方法是当前的实践状态:为软件元素定义严格且快速、高度配置控制的接口,并定期执行大规模的全部更改; 然后交付整个程序的新副本,或一个“补丁”,该补丁会静默地替换整个可执行文件和库。 (在我们编写本文时,新版本的 Open Office 正在要求我们下载它。)

从理论上讲,该过程(不情愿地)接受更改的事实,保留每个项目的零件清单和工具清单,允许在严格的配置控制下进行预先授权的更改,以及在一个离散步骤中完成所有服务器/用户的更改。 在实践中,程序在多个地方运行,每个地方都必须踢掉其用户,进行升级,然后再让他们重新上线。 更改发生的频率和地点比预测的更多,没有记录一个项目的所有组件,并且由于授权的时间延迟和系统的重建时间,修补程序仍然存在(并且,不幸的是,正在蓬勃发展)。

此外,虽然官方接口受到控制,但非官方接口却激增; 对于 C 和较旧的语言,数据结构非常可用,以至于即使需要更改,也有太多函数“知道”该结构具有特定的布局。 当你更改数据结构时,一些你甚至不知道存在的程序或库开始崩溃或返回 ENOTSUP。 较旧的 Linux 内核和较新的 glibc 之间的不匹配曾经导致 getuid 返回“Operation not supported”,这让接收者感到非常惊讶。

经验表明,期望所有可以看到接口的用户都能够在同一时间进行更改是完全不现实的。 结果是单步更改无法发生:多个更改相互关系冲突,网络意味着多个版本同时处于最新状态,并且所有者/用户想要控制更改日期。

供应商试图强制进行离散更改,但更改实际上随着时间的推移以波浪形式在计算机人群中传播。 这通常比作瘟疫,并且同样不受欢迎。

客户针对这些瘟疫的供应商使用“永不型”软件维护的变体:他们构建一个已知的工作配置,然后“冻结并忘记”。 当需要更新时,他们从头开始构建一个全新的系统并冻结它。 除非你获得紧急安全补丁,否则这可行,此时你要么忽略它,要么启动一个大型的计划外重建项目。

连续更改。 起初,这种维护方法听起来像是你只是随意运行新代码,然后看看会发生什么。 我们至少知道一家公司就是这样做的:新登录的用户会在不知不觉中运行与其他人不同的代码。 如果它不起作用,用户要么崩溃,要么被系统管理员踢掉,然后必须重新登录并使用以前的版本重复工作。

然而,这并不是连续的真正含义。 真正的连续方法来自 Multics,这台永远不应该关闭并且使用受控、透明更改的机器。 开发人员明白,唯一不变的是变化,并且在系统运行期间硬件、软件和功能的迁移是必要的。 因此,从一开始就设计了更改能力。

特别是软件需要编写成随着更改的发生而演进,使用弱类型的**高级**语言,并且在较旧的程序中,使用良好的宏汇编器。 如果可以避免,则不允许直接引用任何内容。 每个数据结构都设计为可扩展的,并且可以自我识别版本。 每个代码段都通过编译器或其他构造过程进行自我识别。 代码和数据在每个命令/进程/系统的基础上都是可更改的,并且尽可能少地保留任何内容的副本,因此可以根据需要动态更新单个副本。

最重要的是管理接口更改。 即使在 Multics 时代,也容易忘记更改接口的每个实例。 今天,对于分布式程序,一次更改接口的所有可能副本将非常困难,如果不是完全不可能的话。

谁做得对?

BBN Technologies 的人员在 1969 年构建 ARPANET 主干网时,是第一个进行连续受控更改的人。 他们在每个数据包中放置了一个 1 位版本号。 如果它从 0 变为 1,则意味着 IMP(路由器)要切换到其软件的新版本,并在每个传出数据包上将该位设置为 1。 这使得整个 ARPANET 能够轻松切换到软件的新版本,而不会中断其运行。 这对于前 TCP 互联网非常重要,因为它具有很强的实验性并且经历了大量的变化。

对于 Multics,开发人员完成了所有这些好事。 最重要的是数据结构使用的纪律:如果一个接口采用多个参数,则所有参数都通过将它们放在带有版本号的结构中进行版本控制。 调用者设置版本,接收者检查版本。 如果它完全过时,则会被断然拒绝。 如果它只是不太新,则会以不同的方式处理它,通过在输入时升级并在返回时可能降级。

这意味着程序或内核模块的许多不同版本可以同时存在,而升级在用户方便时进行。 这也意味着升级可以自动发生,并且多个站点、多个供应商和网络不会引起问题。

一家美国仓储公司的示例使用了这种结构(从 Multics PL/1 翻译成 C)

struct item_loc_t {
        struct {
                unsigned short major; /* = 1 */
                unsigned short minor; /* = 0 */
        } version;
        unsigned part_no;
        unsigned quantity;
        struct location_t {
                char state[4];
                char city[8];
                unsigned warehouse;
                short area;
                short pigeonhole;
        } location;
        ...

该公司收购了一家加拿大竞争对手,需要增加国家间转运,最初是从其边境城市的三个仓库开始。 这反过来又要求将州字段拆分为两个部分

char country_code[4]
char state_province[4];

为了识别这一点,该公司将版本号从 1.0 增加到 2.0,并安排服务器支持这两种类型。 新客户端使用 2.0 版本结构,并且能够运送到加拿大。 旧客户端继续使用 1.0 版本结构。 当服务器收到类型 1 结构时,它使用一个“updater”子例程,该子例程将数据复制到类型 2 结构中,并将国家代码设置为 US。

在更现代的语言中,你将添加一个新的子类,其构造函数支持国家代码,并更新你的新客户端以使用它。 过程如下

  1. 更新服务器。
  2. 更改在三个边境州仓库中运行的客户端。 现在他们可以将物品从美国运送到加拿大仓库。
  3. 将更新后的客户端部署到那些需要移动库存的加拿大地点。
  4. 在他们方便的时候更新所有美国本土的客户端。

使用这种方法,你永远不会停止整个系统,只会停止各个副本,并且你可以在企业自身的计划允许时这样做。 如果你现在需要更改,你可以拥有它。 如果不需要,你可以等到更合适的时间。

一旦它们全部更新完毕,你就可以丢弃仅支持一个国家的旧代码,并进行检查,以在有人意外使用仅限美国的旧版本客户端时生成服务器错误消息。 此检查有点像 else-if 中的“不可能发生”的情况:它用于识别不可能过时的调用。 它会明显失败,然后系统管理员可以找到并替换该程序的旧版本。 这也阻止了不明智的人永久推迟对其程序的修复,就像当前实践中整个程序的粗略版本号一样。

现代示例

这种细粒度的版本控制有时在最近的程序中可以看到。 链接器就是一个例子,因为它们读取包含编号记录的文件,每个记录都标识一种特定类型的代码或数据。 例如,记录号 7 可能包含链接子程序调用所需的信息,其中包含要调用的函数的名称和地址空间等项。 如果链接器使用记录类型 1 到 34,并且稍后需要为新编译器扩展 7,那么你可以创建类型 35,将其用于新编译器,并在所有其他编译器中安排从类型 7 到类型 35 的更改,通常通过宣布不再接受类型 7 记录的日期。

另一个例子是在网络协议中,例如 IBM SMB(服务器消息块),用于 Windows 网络。 它既有协议版本,也有数据包类型,可以像链接器的记录类型一样使用。

面向对象语言也可以通过创建作为同一父类的子类的新版本来支持受控维护。 这是子类的一种稍微奇怪的用法,因为你创建的变体不一定旨在持久存在,但是你可以在不再使用它们之后返回并清除不需要的变体。

使用 AJAX,每次程序运行时都可以下载一个相当小的客户端,从而允许在没有版本控制的情况下进行更改。 较大的客户端只需要一个简单的版本控制方案,足以允许在它过时时下载它。

关系数据库中存在一种优雅的现代形式的持续维护:人们总是可以向关系中添加列,并且存在一个称为 null 的众所周知的值,它代表“无数据”。 如果使用数据库的程序理解任何与 null 的计算都会产生 null,那么可以添加一个新列,程序在一段时间内更改为使用它,并将旧列填充为 null。 一旦旧列的所有用户都不再存在,如列在一段时间内为 null 所指示的那样,则可以删除旧列。

另一种优雅的机制是标记语言,例如 SGML 或 XML,它可以随意添加或删除类型的属性。 如果你小心地在类型更改时更改属性名称,并且你的 XML 处理器理解将 3 添加到 null 值仍然是 null,那么你就可以轻松地传输和存储变异数据。

维护并不难,它很容易

在上一次繁荣时期,在通常疯狂的时间压力下,初级作者的团队需要为多个后端创建一个单一的前端。 前端将一些参数和一个 C 结构传递给后端,并且随着后端的开发,该结构需要反复更改以适应其中一个或另一个后端。

即使当所有程序都在同一台机器上时,我们也无法同时更改它们,因为这将迫使团队停止他们正在做的一切并应用结构更改。 因此,我们开始使用版本号。 如果后端需要 2.6 版本的结构,它会告诉前端,前端会将其交给新版本。 如果它只能使用 2.5 版本,那就是它要求的。 我们从来没有“标志日”,所有工作都停止以应用接口更改。 我们可以在我们可以安排它们时进行这些更改。

当然,我们最终确实必须进行更改,我们的管理层必须对此进行管理,但是我们能够在不破坏我们的计划的情况下进行更改。 在测试驱动设计的早期先驱中,我们进行了一项回归测试,该测试检查所有版本号是否都是最新的,并在我们需要更新任何内容时警告我们,这样我们就不会错过更改。

第一次我们避免了标志日,我们赚回了我们花费的几个小时来准备更改。 到第 12 次,我们已经大获全胜。

维护确实很容易。 更重要的是,投入时间为维护做准备可以为你和你的管理层在最疯狂的项目中节省时间。

喜欢它,讨厌它? 让我们知道

[email protected]

Paul Stachour 是一位软件工程师,在开发、质量保证和流程方面同样得心应手。 他的重点领域之一是如何以有效和高效的方式,使用多种编程语言创建正确、可靠、功能齐全的软件。 他的大部分工作都与来自明尼苏达州双子城基地的生命、安全和安全关键型应用程序有关。

David Collier-Brown 是一位系统程序员和作家,曾就职于 Sun 公司,主要从他在多伦多的基地从事性能和容量方面的工作。

© 2009 1542-7730/09/1000 $10.00

acmqueue

最初发表于 Queue 第 7 卷,第 9 期——
数字图书馆 中评论这篇文章





更多相关文章

Catherine Hayes, David Malone - 质疑评估非加密哈希函数的标准
尽管加密和非加密哈希函数无处不在,但在它们的设计方式上似乎存在差距。 出于各种安全要求的动机,存在许多加密哈希的标准,但在非加密方面,存在一定程度的民间传说,尽管哈希函数历史悠久,但尚未得到充分探索。 虽然针对真实世界数据集的均匀分布很有意义,但当面对具有特定模式的数据集时,这可能是一个挑战。


Nicole Forsgren, Eirini Kalliamvakou, Abi Noda, Michaela Greiler, Brian Houck, Margaret-Anne Storey - DevEx 行动
DevEx(开发者体验)在许多软件组织中越来越受到关注,因为领导者寻求在财政紧缩和人工智能等变革性技术的背景下优化软件交付。 从直觉上讲,技术领导者普遍认为,良好的开发者体验可以实现更有效的软件交付和开发者幸福感。 然而,在许多组织中,旨在改进 DevEx 的拟议倡议和投资难以获得支持,因为业务利益相关者质疑改进的价值主张。


João Varajão, António Trigo, Miguel Almeida - 低代码开发生产力
本文旨在通过展示使用基于代码、低代码和极限低代码技术进行的实验室实验结果来研究生产力差异,从而提供对该主题的新见解。 低代码技术已清楚地显示出更高的生产力水平,为低代码在短期/中期内主导软件开发主流提供了强有力的论据。 本文报告了程序和协议、结果、局限性和未来研究的机会。


Ivar Jacobson, Alistair Cockburn - 用例至关重要
虽然软件行业是一个快节奏且令人兴奋的世界,在这个世界中,新的工具、技术和技巧不断被开发出来以服务于商业和社会,但它也很健忘。 在其快速前进的匆忙中,它容易受到时尚的反复无常的影响,并且可能会忘记或忽略针对其面临的一些永恒问题的成熟解决方案。 用例,最早于 1986 年引入,后来普及,就是这些成熟的解决方案之一。





© 保留所有权利。

© . All rights reserved.