本文讨论了将模块化作为一种机制来提高系统的灵活性和可理解性,同时允许缩短系统的开发时间。"模块化(modularization)"的有效性取决于将系统划分为模块时使用的标准(criteria)。本文提出了系统设计问题,描述了常规分解和非常规分解(conventional and unconventional decomposition)。结果表明,非常规分解方法具有明显的优势。讨论了用于得到分解的标准。如果采用传统的假设,即一个模块由一个或多个子程序(subroutines)构成,那么在大多数情况下,非常规分解的效率会比较低。本文还简述了一种不具有这种效果的替代实现方法。
关键词:软件,模块,模块化,软件工程,KWIC指标,软件设计
CR类别:4.0
Introduction
模块化编程哲学的清晰陈述可以在 1970 年 Gouthier 和 Pont 关于系统程序设计的教科书中找到,我们引用如下:[1]
定义良好的项目工作分割可以确保系统模块化。每个任务构成一个独立的、不同的程序模块。在实现时,每个模块及其输入和输出都定义良好,在与其的接口中不会与其他系统模块混淆。在检验时,独立测试模块的完整性;在签出(checkout)开始之前,在同步几个任务的完成方面存在一些调度问题。最后,系统以模块化的方式进行维护;系统错误和缺陷可以追踪到特定的系统模块,从而限制了详细错误搜索的范围。
通常没有提到用户将系统划分为模块的标准。本文将讨论这个问题,并通过实例,提出一些可以用于将系统分解为模块的标准。
A Brief Status Report
模块化编程领域的主要进步是编码技术和汇编程序的发展,它们 (1) 允许在不了解另一个模块代码的情况下编写一个模块,(2) 允许在不重新组装整个系统的情况下重新组装和替换模块。这个工具对于生成大块的代码非常有价值,但是最常被用作问题系统示例的系统是高度模块化的程序,并且利用了上面提到的技术。
Expected Benefits of Modular Programming
模块化编程的预期好处是:(1) 管理——开发实践应该缩短,因为单独的小组将在每个模块上工作,几乎不需要交流;(2) 产品灵活性——应该有可能对一个模块进行重大更改,而不需要更改其他模块;(3) 可理解性——应该有可能一次只研究一个模块。因此,整个系统可以更好地设计,因为它可以更好地理解。
What Is Modularization
下面是几个称为 模块化(modularizations) 的部分系统描述。在这种情况下,“模块(module)” 被认为是职责分配,而不是子程序。模块化包括在独立模块的工作开始之前必须做出的设计决策。每个备选方案都包含了不同的决策,但在所有情况下,目的都是描述所有“系统级(system level)”决策(即影响多个模块的决策)。
Example System 1 : A KWIC Index Production System
以下对 KWIC 索引的描述将满足本文的要求。KWIC 索引系统接收一个有序的行集合(set of lines),每一行是一个有序的单词集合,而每个词是一个有序的字符集。任何行都可以通过重复删除第一个单词并将其附加到行尾来进行 "循环移位(circularly shifted)"。KWIC 索引系统按字母顺序输出所有行的所有循环移位列表。
这是一个小系统。除了在极端情况下(庞大的数据库,没有配套软件),这样一个系统可以由一个好的程序员在一两周内制作出来。因此,激发模块化编程的困难对于这个系统来说都不重要。因为彻底地对待一个大系统是不切实际的,所以我们必须把这个问题当做一个大项目来处理。我们给出了一种典型的模块化方法,以及另一种已成功应用于本科课程项目的模块化方法。
Modularization I
我们看到以下模块:
Module 1: Input。该模块从输入介质中读取数据行,并将其存储在内核中,以供其他模块处理。字符被打包成一个单词,使用一个其他未使用的字符来表示单词的结尾。保留索引以显示每行的起始地址。
Module 2: Cricular Shift。这个模块在输入模块完成它的工作后被调用。它准备了一个索引,该索引给出了每个循环移位的第一个字符的地址,以及模块 1 组成的数组中该行的原始索引。它将输出保留在内核中,并使用成对的单词(原始行号、起始地址)。
Module 3: Alphabetizing。该模块将模块 1 和模块 2 产生的数组作为输入。它生成的数组格式与模块 2 生成的数组格式相同。然而,在本例中,循环移位是按另一个顺序(字母顺序)列出的。
Module 4: Output。使用模块 3 和模块 1 生成的数组,该模块生成了一个格式良好的输出,列出了所有的循环移位。在一个复杂系统中,每一行的实际开始将被标记,指向进一步信息的指针可能被插入,循环移位的开始实际上可能不是行中的第一个单词,等等。
Module 5: Master Control。这个模块除了控制其他四个模块之间的顺序外,没有什么别的功能。它还可以处理错误信息、空间分配等。
应该清楚的是,上述内容并不构成一份确定的文档。在工作开始之前,还需要提供更多的信息。定义文档将包含许多显示核心格式、指针约定、调用约定等的图片。在工作开始之前,必须指定四个模块之间的所有接口。
从模块化编程的支持者的意义上来说,这是一种模块化。该系统被划分为若干具有定义良好接口的模块;每一个都足够小,足够简单,足以被彻底理解和良好的编程。小规模的实现证明,这大约是大多数程序员为指定的任务提出的分解。
Modularization II
我们看到以下模块:
Module 1: Line Storage。这个模块由许多函数或子程序(subroutines)组成,这些函数或子程序提供了模块化用户可以调用它的方法。函数调用 CHAR(r, w, c)
的值将是一个整数,表示第 r 行中的第 c 个字符,第 w 个单词。一个调用例如 SETCHAR(r, w, c, d)
将返回第 r 行 的第 w 个单词的第 c 个字符是由字符 d 表示(即CHAR(r, w, c) = d
)。WORDS(r)
返回第 r 行单词的个数。调用这些例程(routines)的方式有一定的限制;如果违反了这些限制,例程就"陷入"由例程用户提供的错误处理子例程。附加的例程可以向调用者显示任何一行中的单词数、当前存储的行数以及任何一个单词中的字符数。提供函数 DELINE
和 DELWRD
来删除已经存储的部分行。类似的模块的精确说明已经在 [3] 和 [8] 中给出,我们在此不再重复。
Module 2: INPUT。该模块从输入媒体中读取原始行,并调用行存储模块将它们存储在内部。
Module 3: Cricular Shifter。该模块提供的主要函数与模块 I 通的函数类似。该模块给人的印象是,我们创建了一个 line holder,该 holder 包含的不是所有的 line,而是所有 line 的循环移位。因此,函数调用 CSCHAR(l, w, c)
提供了表示第 l 个循环移位的 w 个单词的第 c 个字符的值。(1) 如果 i < j
,则第 i 行移位在第 j 行移位之前;(2) 对于每一行,第一次移位的是原行(original line),第二次移位通过一个单词旋转到第一次移位得到,以此类推。提供了一个函数 CSSETUP
,必须在其他函数有指定值之前调用它。有关此类模块的更精确规范,请参见 [8]。
Module 4: Alphabetizer。该模块主要由两个功能组成。其中一个,ALPH
,必须在另一个具有定义值之前被调用。第二个,ITH
,将作为一个索引。ITH(i)
将给出以字符顺序排列的循环移位的索引。[8] 给出了这些函数的正式定义。
Module 5: Ouput。这个模块将打印所需的行的集合或循环移位。
Module 6: Master Control。功能类似于上面的模块化。
Comparison of the Two Modularizations
General。这两种方案都将奏效。前者相当传统;第二个已经成功地在一个 class project [7] 中使用。两者都将编程减少到相对独立的一些小的、可管理的程序的编程。
首先请注意,这两个分解可能共享所有的数据表示和访问方法。我们的讨论是关于两种不同的方法来分割可能是同一个对象。根据分解 1 构建的系统在组装后可以与根据分解 2 构建的系统完全相同。这两种选择的区别在于它们划分工作任务的方式和模块之间的接口。两种情况下使用的算法可能是相同的。即使在可运行表示中(runnable representation)相同,这些系统也有本质上的不同。这是可能的,因为可运行表示只需要用于运行;其他表示用于更改、记录、理解等。这两种系统在其他表现形式中并不相同。
Changeability。有许多设计决策是有问题的,并且可能在许多情况下发生改变。这是一个不完整的列表。
- 输入格式。
- 决策将所有行存储在核心中。对于大型工作,在任何时候将所有的行都放在核心上可能是不方便或不切实际的。
- 决策将四个字符压缩成一个单词。在我们处理少量数据的情况下,打包字符可能是不可取的;单词的每个字符的布局都可以节省时间。在其他情况下,我们可以打包,但格式会不同。
- 决策为循环移位创建索引,而不是实际存储它们。同样,对于小型索引或大型核心,将它们写出来可能是更可取的方法。或者,我们可以选择在 CSSETUP 期间不准备任何东西。所有计算都可以在调用其他函数(如 CSCHAR)期间完成。
- 决策按名字福顺序排列列表一次,而不是(a)在需要的时候搜索每个项目,或(b)部分按字母顺序排列,就像在 Hoare 的 FIND [2] 中做的那样。在许多情况下,将字母排序所涉及的计算分配到生成索引所需的时间是有利的。
通过观察这些变化,我们可以看到这两种模块之间的差异。第一个变化局限于两个分解中的一个模块。对于第一次分解,第二次更改将导致每个模块的更改!第三个变化也是如此。在第一次分解中,所有程序都必须使用核心中的行存储格式。在第二种分解中,情况完全不同。除了模块 1 之外,所有人都不知道这些行存储的确切方式。任何存储方式的改变都只能局限于该模块!
在这个系统的某些版本中,分解中有一个额外的模块。在杭存储模块中使用了符号表(symbol table)模块(如 [3] 中指定的)。这个事实对系统的其他部分是完全看不见的。
第四个变化局限于第二个分解中的循环移位模块,但是在第一个分解中,字母排序器和输出例程也知道这个变化。
在第一次分解中,第五次变化也将被证明是最困难的。输出模块期望索引在开始之前已经完成。第二次分解中的字母排序模块被设计成这样,用户无法检测到何时实际完成了字母排序。不需要更改其他模块。
Independent development。在第一个模块化中,模块之间的接口是上面描述的相当复杂的格式和表组织(table organizations)。这些代表了不能掉以轻心的设计决策。表的结构和组织对各个模块的效率至关重要,必须仔细设计。这些格式的开发将是模块化开发的主要部分,这一部分必须由几个开发小组共同努力。在第二种模块化中,接口更加抽象;它们主要由函数名、形参的数量和类型组成。这些都是相对简单的决策,模块的独立开发应该更早开始。
Comprehensibility。为了理解第一个模块化中的输出模块,需要理解字符排序器(alphabetizer)、循环移位器(circular shifter)和输入模块(input module)的一些内容。输出所使用的表的某些方面只会因为其他模块的工作方式而更有意义。由于其他模块中使用的算法,表的结构将受到限制。这个系统只有在整体上才能被理解。我的主观判断是,在第二次模块化中不是这样的。
The Criteria
许多读者现在将看到在每个分解中使用了哪些标准。在第一个分解中,使用的准则是使处理中的每个主要步骤成为一个模块。有人可能会说,为了得到第一个分解,我们需要做一个流程图。这是分解或模块化最常见的方法。它是所有程序员培训的结果,它告诉我们,我们应该从一个粗略的流程开始,然后从那里走向一个详细的实现。流程图对于有 5000 - 10000 条指令的系统来说是一种有用的抽象,但当我们超越它时,它似乎是不够的需要一些额外的东西。
使用 "信息隐藏(information hiding)"[4] 作为标准进行第二次分解。模块不再对应于处理中的步骤。例如,行存储模块在系统的几乎所有操作都使用。根据所使用的方法,字母排列可以,也可以不对应于处理中的一个节点。类似的,在某些情况下,循环移位可能根本不生成任何表,而是根据要求计算每个字符。第二次分解中的每个模块都具有其设计决策知识的特征,这些知识对所有其他模块都是隐藏的。他的接口或定义被选择来尽可能少地揭示它内部工作。
Improvement in Circular Shift Module
为了说明这样一个准则的影响,让我们从第二次分解来仔细看看循环移位模块的设计。现在,事后看来,这个定义揭示了的信息比必要的还要多久。虽然,我们小心地隐藏了存储或计算循环移位列表的方法,但我们指定了该列表的顺序。程序可以有效地编写,如果我们只指定(1) 行表示(lines indicated)在循环移位的当前定义将全部在表中存在,(2) 其中一个将包括两次,和(3) 一个额外的功能存在这将使我们能够确定原始的行的改变。通过规定转换的顺序,我们提供了比必要的更多的信息,因此不必要地限制了我们可以在不改变定义的情况下构建系统类别。例如,我们不允许在这一的系统中按字母顺序产生循环移位,ALPH 为空,而 ITH 只是将其参数作为值返回。我们在第二次分解构造系统时未能做到这点,必须清楚地将其归类为设计错误。
除了每个模块对系统的其他部分隐藏一些设计决策的一般标准之外,我们还可以提到一些具体的分解示例,它们似乎是明智的。
- 一个数据结构(data structure),它的内部链接,访问过程(accessing procedures) 和 修改过程(modifying procedures) 都是单个模块的一部分。它们不像传统的那样被许多模块共享。这个概念可能只是 Balzer [9] 和 Mealy [10] 论文背后假设的详细阐述、考虑到这一点的设计显然是 BLISS [11] 的设计背后。
- 调用给定例程所需的指令序列和例程本身是同一个模块的一部分(The sequence of instructions necessary to call a given routine and the routine itself are part of the same module)。这条规则在用于实验的 Fortran 系统中是不相关的,但是对于用汇编语言构建的系统来说确实必不可少的。真正的机器不存在完美的通用调用序列(general calling sequences),因此,随着我们继续寻找理想序列,它们往往会发生变化。通过将生成调用的责任分配给负责例程的人,我们是这种改进变得容易,也使在相同的软件结构中有几个不同的序列更加可行。
- 操作系统和类似程序中队列中使用的控制快的格式必须隐藏在"控制模块"中(The formats of control blocks used in queues in operating systems and similar programs must be hidden within a "control block module")。 传统的做法是对各个模块之间的接口进行格式化。因为设计的发展迫使控制模块格式频繁的改变,这样的决定通常被证明是非常昂贵的。
- 字符代码、字母顺序和类似的数据应该隐藏在一个模块中,以获得最大的灵活性(Character codes, alphabetic orderings, and similar data should be hidden in a module for greatest flexibility)。
- 处理某些项的顺序应该(尽可能)隐藏在单个模块中。从设备的添加到操作系统中某些资源的不可用等各种变化是的排序非常不稳定。
Efficiency and Implementation
如果我们不小心的话,第二种分解会比第一种分解效率低得多。如果每个函数实际上是作为一个具有复杂调用序列的过程来实现的,那么由于模块之间的重复切换,将会有大量这样的调用。第一个分解不会遇到这个问题,因为模块之间的控制转移相较少。
为了节省过程调用开销,同时获得我们在上面看到的优势,我们必须以一种不同寻常的方式实现这些模块。在许多情况下,程序最好由汇编程序(assembler)插入到代码中;在其他情况下,将插入高度专业化和高效的转移。为了成功和有效地利用第二种类型的分解,需要一种工具,这种工具可以将程序编写为子例程,但是通过任何合适的任何适当的实现来组装。如果使用这种技术,模块之间的分离在最终代码中可能不清楚。因此,额外的程序修改功能也是有用的。换句话说,程序的几种表示(前面提到过)必须在机器中与执行它们之间映射的程序一起维护。
A Decomposition Common to a Compiler and Interpretor for the Same Language
在早期尝试将这些分解规则应用于设计项目时,我们为用 [6] 中扫描的符号表示的 Markov 算法构建了一个转换器。尽管我们无意研究一种语言的编译和解释性翻译程序之间的关系,但我们发现,我们的分解对于这种语言的纯编译器和多种解释器都是有效的。尽管在每种编译器类型的最终运行表示中存在深刻和实质性的差异,但我们发现,早起分解中隐含的决定适用于所有类型。
如果我们按照编译器(compiler)或解释器(interpretor)的经典路线划分职责(例如,语法识别器(syntax recognizer)、代码生成器(code generator)、编译器的运行时例程(run time routines for a compiler)),这就不会是真的。相反,分解是基于各种决策的隐藏,如上面的示例所示。因此,寄存器表示(register representation)、搜索算法(search algorithm)、规则解释(rule interpretation)等都是模块,这些问题都存在于编译和解释转换器(compiling and interpretive translators)中。不仅分解在所有情况下都是有效的,而且许多例程只需要在任何类型的翻译器进行微小的更改就可以使用。
此示例为该语言提供了额外的支持,即在进行模块分解时,不应该使用预期发生处理的时间顺序。他进一步证明了,仔细的分解工作可以导致大量的工作从一个项目转移到另一个项目。
对这个示例的更详细的讨论包含在 [8] 中。
Hierarchical Structure
我们可以在根据分解 2 定义的系统中找到 Dijkstra [5] 所示的意义上的程序层次结构。如果一个符号表存在,他在没有任何其他模块的情况下运行,因此它是在 1 级。如果没有使用符号表,则行存储是在第 1 级,否则行存储在第二层。输入和循环移位器的功能需要行存储。输出和字母排序器将需要循环移位器,但是由于循环移位器和行持有器(line holder)在某种意义上是兼容的,所以很容易构建这些例程的参数化版本,这些例程可以用于按字母顺序排列或打印出原始行或循环移位。在第一次使用中,他们不需要循环移位器;第二次他们会使用。换句话说,我们的设计允许我们有一个单一的程序表示,它可以在两个层次中的任何一个层次上运行。
在讨论系统结构时,很容易混淆良好的分解和层次结构的好处。如果模块或程序之间可以定义某种关系,并且这种关系是部分排序的,那我们就有了层次结构。我们关心的关系是 "使用" 或 "依赖"。最好使用程序直接的关系,因为在很多情况下,一个模块只依赖于另一个模块的一部分(例如,虚幻移位器只依赖于 line holder 的输出部分,而不依赖与 SETWORD 的正确工作)。可以想象,如果没有这种部分排序,我们可以获得我们已经讨论过的好处,例如,如果所有模块都在同一水平上。部分排序给我们带来了两个额外的好处。首先,系统的某些部分受益(简化),因为它们使用 lower level 服务。其次,我们能够切断上层,仍然有一个可用的和有用的产品。例如,符号表可用于其他应用程序;line holder 可以作为问答系统的基础。层次结构的存在保证了我们可以 “修剪(prune)” 树的上层,并在旧的树干上开始一颗新树。如果我们设计的系统中 "low level" 模块使用了 "high level" 模块,我们就不会有层次结构,我们会发现很难删除系统的某些部分,"level" 在系统中也没有多少意义。(这里的 "lower" 以为 "lower numbered")。
我们可以想象拥有一个版本 1 中所显示的分解类型的系统(接口中的重要设计决策),但是保留层次结构,因此我们必须得出这样的结论:层次结构和 "干净的" 分解是系统结构的两个可取但独立的属性。
Conclusion
我们已经试图通过这些示例说明,在流程图的基础上开始将系统分解为模块几乎总是不正确的。我们建议从一组困难的设计决策或可能发生变化的决策开始。然后,每个模块都被设计为对其他模块隐藏这样的决定。因为,在大多数情况下,设计决策超越了执行时间,模块将不对应与处理中的步骤。为了更加高效的实现,我们必须放弃模块是一个或多个子例程的假设,而是允许子例程和程序被来自不同模块的代码集合组装起来。
References
- Gauthier, Richard, and Pont, Stephen. Designing Systems Programs, (C), Prentice-Hall, Englewood Cliffs, N.J., 1970.
- Hoare, C. A. R. Proof of a program, FIND. Comm. ACM 14, 1 (Jan. 1971), 39-45.
- Parnas, D. L. A technique for software module specification with examples. Comm. ACM 15, 5 (May, 1972), 330-336.
- Parnas, D. L. Information distribution aspects of design methodology. Tech. Rept., Depart. Computer Science, CarnegieMellon U., Pittsburgh, Pa., 1971. Also presented at the IFIP Congress 1971, Ljubljana, Yugoslavia.
- Dijkstra, E. W. The structure of "THE"-multiprogramming system. Comm. ACM 11, 5 (May 1968), 341-346.
- Galler, B., and Perlis, A. J. A View of Programming Languages, Addison-Wesley, Reading, Mass., 1970.
- Parnas, D. L. A course on software engineering. Proc. SIGCSE Technical Symposium, Mar. 1972.
- Parnas, D. L. On the criteria to be used in decomposing systems into modules. Tech. Rept., Depart. Computer Science, Carnegie-Mellon U., Pittsburgh, Pa., 1971.
- Balzer, R. M. Dataless programming. Proc. AFIPS 1967 FJCC, Vol. 31, AFIPS Press, Montvale, N.J., pp. 535-544.
- Mealy, G. H. Another look at data. Proc. AFIPS 1967 FJCC, Vol. 31, AFIPS Press, Montvale, N.J., pp. 525-534.
- Wulf, W. A., Russell, D. B., and Habermann, A. N. BLISS, A language for systems programming. Comm. ACM 14, 12 (Dec. 1971), 780-790.