软件灵活性设计:如何避免陷入编程困境
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第1章 自然和设计中的灵活性

我们很难设计出一种能很好地完成任何特定工作的通用机制,因此大多数工程系统都是为完成一项特定任务而设计的。像螺钉扣这样的通用发明是罕见且具有重大意义的。数字计算机是这类发明的一个突破,因为它是一种可以模拟任何其他信息处理机器的通用机器[1]。我们编写软件来配置计算机,使其在我们需要完成的具体工作中实现这种模拟。

作为过去工程实践的延伸,我们一直在设计软件来很好地完成特定的工作。每个软件都被设计用于完成一项相对小型的工作。当需要解决的问题发生变化时,必须对软件进行相应的修改。但是,问题的微小变化通常不等价于软件的微小修改。软件如果被设计得过于周密以致没有太大的灵活性,系统就不能优雅地演化,因此十分脆弱,必须在问题领域发生变化时用全新的设计来替代[2]。但这是很缓慢并且昂贵的。

我们的工程系统不一定非得是脆弱的。互联网已经从一个小系统扩展到具有全球规模的系统。我们的城市正在有机发展,以适应新的商业模式、生活方式以及交通和通信手段。事实上,从对生物系统的观察中,我们可以看到,构建适应环境变化的系统是可能的,无论是作为个体还是作为进化的整体。为什么这不能作为我们设计和构建大多数软件的方式呢?虽说可能存在历史原因,但最主要的原因是我们一般都不知道怎么做。在这种背景下,如果一个系统在面对需求变化时仍保持其稳健性,那纯属是一个意外。

增量式编程

本书的目标是研究如何构建计算系统,使其能够轻松适应不断变化的需求。人们不用去修改工作程序,而是对它进行补充,以实现新的功能,或者针对新的需求来调整旧的功能。我们将这种方式称为增量式编程(additive programming)。我们探索在不破坏现有程序的情况下为其增添功能的技术,该技术并不保证增添的功能是正确的:增添的内容本身必须经过调试,但它们不应该意外破坏现有功能。

本书中探讨的许多技术并不新颖,其中一些可以追溯到计算机的早期阶段。本书也不是一个全面的集合,而只是一些我们认为有用的东西。本书的目的不是推广这些技术的使用,而是鼓励一种注重灵活性的思维风格。

为了使增量式编程成为可能,有必要减少关于程序如何工作以及如何使用程序的假设。在设计和构建程序时做出的假设可能会减小程序未来扩展的可能性。本书不进行这样的假设,而是在构建程序时,根据程序运行的环境及时做出决定。本书将探讨几种支持这种设计的技术。

我们通过组合程序来整合每个程序支持的行为。但是,我们希望整体大于部分的总和,合并后系统的各个部分能够合作,使系统具有任何一个部分都无法单独提供的功能。但是,这里有一些权衡:组合成一个系统的各个部分必须有鲜明的独立特点。如果一个组件能很好地完成一件事,那么它就更容易被重复使用,同时也比结合了几种不同特点的组件更容易调试。如果我们进行增量式构建,那么重要的是各个部分的组合要有最小的意外交互。

为了方便增量式编程,有必要使所构建的组件尽可能简单和通用。例如,如果一个组件可接受的输入范围比当前问题的严格输入范围更广,那么这样的组件将比输入范围严格受限的组件具有更广泛的适用性。围绕一个标准化的接口规范建立起来的一系列组件可以组合与匹配,从而形成各种各样的系统。重要的是通过确定这一系列组件的领域语言,然后为这一系列语言建立相关系列的方式,来为我们的组件选择正确的抽象层次。我们将在第2章开始考虑这些需求。

为了获得最大的灵活性,一个组件(代码段)的输出范围应该相当小,且定义明确——比任何可能接受该输出组件的可接受输入范围小得多。类似在计算机系统入门科目中的数字抽象法的静态法则(见[126])。数字抽象的本质是输出总是优于下一阶段可接受的输入,这样就可以抑制噪声。

在软件工程中,这一原则被称为“Postel定律”,以纪念互联网先驱Jon Postel。在描述互联网协议的RFC760中(见[97]),他写道:“一个协议的实现必须是鲁棒的。每一个实现都必须要求与由不同个体创建的其他实现进行互相操作。虽然本规范的目标是明确协议要点,但仍有可能出现不同的解释。一般来说,一个实现在其发送行为中应该是保守的,在其接收行为中应该是开放的。”这通常被总结为“在你所做的事情上要持保守的态度,并以开明的态度接纳他人”。

使用比看起来更多的通用组件为系统的整个结构构建了一定程度的灵活性。对需求的小幅变动是可以容忍的,因为每一个组件构建都是为了接受扰动(噪声)的输入。

一组用于特定领域的组合与搭配组件是领域专用语言(domain-specific language)的基础。通常,解决一组困难问题的最好方法是创造一种语言——一套原语、组合手段和抽象方式——使得问题的解决方案更加易于表达。因此,本书希望能够依据需求创建适当的领域专用语言,并灵活地组合这些语言。本书将从第2章开始讨论领域专用语言。更强大的是,可以通过直接评估法来实现这种语言。本书将在第5章对这个想法进行扩展。

提高灵活性的一个策略是通用调度(generic dispatch),这对许多程序员来说应该很熟悉。本书将在第3章广泛探讨这个问题。通用调度根据传递给程序的参数细节,通过添加额外的处理程序(方法)扩展程序适用性。通过要求处理程序对不相干的参数集做出响应,可以避免在添加新的处理程序时破坏现有的程序。然而,与典型的面向对象编程环境中的通用调度不同,本书的通用调度并不涉及类、实例和继承等概念。这些概念都通过引进虚假的本体论承诺,削弱了关注点的分离。

第6章将探讨的是一种完全不同的策略——将数据和程序分层(layer)。这利用了数据通常包含相关元数据,这些元数据可以与数据一起被处理的想法。例如,数值型数据通常有相关单元。第6章将展示在无须对原始程序进行任何更改的情况下,如何在事后提供添加层的灵活性,以增强程序的新功能。

本书还可以建立将多个部分信息(partial information)来源结合起来的系统,以获得更完整的答案。当这些来源来自独立的信息源时,系统是最强大的。在第4章我们将看到类型推理实际上是一个结合多个部分信息来源的问题。关于一个值类型的局部可推导线索可以与其他局部类型约束相结合,产生非局部类型约束。例如,数字类型的比较需要数字输入并产生布尔输出。

在第7章中我们将看到结合部分信息的一种不同方式。与附近恒星的距离可以通过视差这种几何方式来估计:测量地球围绕太阳旋转时,恒星图像在参照系的天空中移动的角度。运用我们对恒星结构和演变的理解,与恒星的距离也可以通过考虑其亮度和光谱的形式来估计。这些估计方式可以结合起来,得到比单个方式更准确的估计。

另一个想法是简并性(degeneracy)的运用:有多种方法可计算某些东西,这些方法可以被组合或调制以供我们所需。简并性有许多有价值的运用,包括错误检测、性能管理和入侵检测。重要的是,简并性也是增量式的:每个贡献部分都是独立的,可以自行产生结果。简并性的一个有趣的用途是可根据上下文动态地选择算法的不同实现方式,避免做出关于如何使用该实现的假设。

灵活性设计和构建均有明确的成本。对于绝对必要的程序部分来说,一个可以接受比解决当前问题所需的更多种类输入的程序,会包含更多的代码,并且会让程序员花费更多的精力。通用调度、分层和简并性也是如此,每一种策略都涉及内存空间、计算时间和/或程序员花费时间的持续开销。但是,软件的主要成本是程序员在产品生命周期内花费的时间,包括维护和适应不断变化的需求。尽量减少重写和重构的设计可以将总体成本降低为用于添加额外的增量而不是完全重写。换句话说,长期成本呈叠加态势,而不是累乘。