程序原本(六十九):应用开发基础——应用开发技术(模块化的精髓不在于外在形式的分离,而在于内在逻辑的延续)

阅读数:43 2019 年 10 月 3 日 14:27

程序原本(六十九):应用开发基础——应用开发技术(模块化的精髓不在于外在形式的分离,而在于内在逻辑的延续)

图 32 展示了在稍早一些的应用开发语言中,从“代码的粒度”出发的抽象概念。

图 32 从“代码的粒度”出发的抽象概念2 3

程序原本(六十九):应用开发基础——应用开发技术(模块化的精髓不在于外在形式的分离,而在于内在逻辑的延续)

2 语句与行的不同,通常也被称为逻辑行与物理行概念上的不同。此外,有些语言是强制要求以物理行来表达“语句”这一概念的,即一行语句必须书写于一行代码中。

3 单元与模块除了称谓的不同,很多时候其抽象概念也并不完全相同或者互相覆盖,例如一个单元可以是(或不是)一个模块。我们这里只取在某些语言中、将模块特指为“一系列函数”的这一概念。

其中,单元或模块用于组织一系列函数,而一个应用4则是由单元或模块构成。在这样的体系中,“化整为零”的问题会变得相对简单,即如何有规则或有逻辑地将一堆函数组织成单元。这里的“规则”与“逻辑”阐述了组织法则的两个方向。

4 早期的应用开发语言也直接将应用称为“程序”(program)。

其一,我们可以设定一个简单的分类依据,使得位于同一个单元中的函数表现出一定的相似性。例如开发一个图形库,我们可以将与图形设备相关的功能放在 device 库中,将绘制功能放在 graph 库中,将渲染功能放在 render 库中,将与图形库无关但又与计算机基础环境相关的功能放在 base 库中,如此等等。最后,我们将一些杂乱无章的功能放在 misc 库中。请注意,这一切的分类依据是“功能的归属与使用者”。类似地,我们也可以依据数据的位置来建立分类依据。例如同样是这个图形库,我们可以将基础数据运算放在 bits 库中,并基于此建立关于图形运算的类型抽象库 types。接下来我们定义在不同设备上适用的数据结构,比如在存储设备中的种种文件格式 fileTypes、在内存中复制和运算的 dibs(设备无关位图,Device-Independent Bitmap)以及在某种具体显示设备中适用的 cudaTypes(CUDA,Compute Unified Device Architecture)等。这样依据数据(所处的)位置以及需要进行的计算进行分类,也便于将数据及其副本放在不同的环境下开发。

这一类的方案或试图交付一个可以被使用甚至被共用的功能集,或通过抽取不同层次(例如面向不同设备或不同场景)的代码,使之可以“或多或少”应用于不同的环境。与这个组织原则密不可分的一个问题是:如何使一个“单元 / 模块”向外公布它所具有的功能集。这形成了著名的“开放细节”与“公开功能但隐藏细节”之争5,如今后者已成为应用接口设计思想的主流,前者则部分地影响并推进了开放源代码这一思想。

5 参见《人月神话》中“关于信息隐藏,Parnas 是正确的,我是错误的”小节,以及 David Parnas 关于信息隐蔽理论的著名论文:《论将系统分解为模块的准则》、《设计易于扩展和收缩的软件》和《复杂系统的模块化架构》。

但总的来说,这个组织法则只解决了一个应用中能被静态规则化的部分。无论如何,它无法满足“让程序运行起来”之后可能带来的种种变化。

其二,我们可以使得一个单元或多个单元中的函数存有某种逻辑关系。著名的“自顶向下程序设计”的思想,就处于这一组织法则所代表的方向上。例如:

设有一个逻辑(X),功能是将 m 变换为 n,如图 33 所示;

图 33 基本功能:将 m 变换为 n

程序原本(六十九):应用开发基础——应用开发技术(模块化的精髓不在于外在形式的分离,而在于内在逻辑的延续)

由于 X 的规模巨大,我们将它分成三个逻辑步骤(顺序逻辑 1→2→3)来实现,如图 34 所示。

图 34 分解:三个步骤

程序原本(六十九):应用开发基础——应用开发技术(模块化的精髓不在于外在形式的分离,而在于内在逻辑的延续)

虽然向下一层的分解并不限定各步骤之间的关系,但我们注意到此前讨论过的一个事实,即所有逻辑都可以被理解为顺序逻辑中的一个步骤。也就是说,步骤 2 依赖于步骤 1,步骤 3 依赖于步骤 2。随后我们继续向下一层分解,如图 35 所示。

图 35 持续分解:更多的步骤、逻辑或子系统

程序原本(六十九):应用开发基础——应用开发技术(模块化的精髓不在于外在形式的分离,而在于内在逻辑的延续)

在图 35 中:

  • 步骤 1 的子步骤 1.1 分成了 1.2 和 1.3 两个分支,最终以 1.3 为出口,传出数据 n’;
  • 步骤 2 则被分解为三个子步骤的循环,并总是以步骤 2.3 为出口,传出数据 n“;
  • 步骤 3 分解为两个顺序的子步骤,得到转换结果 n。

由此无论是针对顶层 X 这个逻辑,还是针对第二、三层各分解的逻辑,整体的逻辑关系都没有变化。所有的逻辑关系在函数与函数之间,以及在“一堆函数”与“另一堆函数”之间都可以被简单地抽象为“顺序依赖”。即使将这些单元 / 模块之间的关系映射到最终子系统的划分之上,这种逻辑关系也不会变化。例如从子系统划分上看:

  • 子系统 1 被设计为“预处理器”(preProcessor);
  • 子系统 2 被设计为“分析器”(analyzer)或“过滤器”(filter);
  • 子系统 3 被设计为(下一阶段的)“数据供应器”(dataProvider)。

这三个子系统以及它们的组织关系就可以构成某种数据处理系统的、整体的、面向运行期的逻辑架构。

进一步地说,通常单元 / 模块之间的逻辑关系只是简单的依赖关系。这一关系足以支撑由结构化程序设计带来的计算需求,包括支持数据流转与逻辑执行。6

6 DFD(DataFlow Diagram,数据流图)通常用于解释在上述执行过程中 m-n 之间的转换关系不变(即数据单一入口与单一出口)。但在本例中,它亦用于解释自顶向下过程中的逻辑关系不变,整体保持着顺序执行关系。这一过程是抽象概念——从程序语言中逻辑的结构化,到应用系统中组织的模块化——的延伸。

评论

发布