0%

系统划分为模块的范式

本文翻译自:https://prl.ccs.neu.edu/img/p-tr-1971.pdf

摘要

本文主要讨论用以提升系统的灵活性及可理解性,同时缩短开发时间的模块化机制。模块化的有效性依赖于模块划分的范式。后文将以两个系统设计为例,对任何一个,都展示了一个常规划分方式和一个非常规划分方式。同时也阐明非常规的划分方式在指定的目标上具备显著优势。这些划分本身遵循的范式也在本文中得到了讨论。非常规的划分方式,常规划分方式是指一个模块🈶一个或多个子程序组成,在大多数情况下都可能不会发挥应有效力。文章也简单描述了另一种没有这样(负面)影响的实现方式。

简介

如果程序员会唱赞美诗,那么大部分流行的赞美诗将献给模块化编程。作者在一本讲述系统设计的书中找到这么一句关于模块化编程哲学的表述:

在明确定义项目如何分段上的努力使得系统具备模块化特性。每个任务形成了一个独立的程序模块。在实现阶段,每个模块的input和output都被很好的定义了,对于系统其他模块来说都不存在着不明确。在验证的时候,模块是否完整的实现了TA的承诺则被分别测试;验证阶段也不需要协调多个任务的完成时间。最后,系统通过模块化的方式维护;系统错误和依赖可以被追踪到具体的模块,因此缩小了缺陷搜查的范围。

首先,我非常赞同这段陈述,但不太赞同引申而来的一些可能的诠释。注意,关于如何将系统划分为多个模块,上述论述什么也没聊。因为将系统划分为大小适中的模块的决定并不能真的导向解耦,本文会讨论这个问题,并且将通过例子的方式,推荐一种应该被用来将系统划分为模块的范式。

回顾下该领域的现状

在模块化编程领域的主要进展是(1)编码技巧的发展以及(2)编译器在“在编写一个模块时可以不关心其他模块”上的发展,其中(2)使得模块可以独立于整个系统来重新编制和替换。这项便利对于生产大规模代码是极有价值的,但这项技术的使用还没带来预期的收益。事实上,那些经常被用来讲述大规模系统的例子本身就是高度模块化的程序,这些程序使用了上述复杂的编码和编译技术。

模块化编程承诺的优势

模块化编程承诺的优势可以分为如下三类:(1)可管理 – 因为不同组织工作在不同的模块,且极少需要沟通(也不必事后后悔当时为什么没做更充分的沟通),因此研发时间被大大缩短了;(2)生产的灵活性 – 希望能够做到一个模块发生剧烈变化是可以不影响其他模块;(3)可读性 – 希望整个系统可以被逐个模块的学习,因为更容易理解系统也就会得到更好的设计。

什么是“模块化”

在这个的上下文中,模块最好被理解为一个独立的工作单元而不是一个子程序。模块化想要描述的是在各个模块的工作开始前必须要做的设计决策。尽管在不同的方案中的决策差异极大,但所有方案的(决策)意图都被描述为系统级的决策(例如,那些会影响超过一个模块的决策)。

示例系统1:关键字索引生成系统

对于不了解什么是关键字索引的人来说,下面这段(关于关键字索引)介绍对于理解本文就足够了。关键字索引系统接收一个顺序的文字行的集合,每一行是一个顺序的词的集合,每个词是一个顺序的字母的集合(对于英文)。每一行都可能是”循环移位的”,即,不断地将第一个单词移掉在拼接到行的末尾。这个关键字索引系统以字母序输出每一行的所有循环移位。这是个很小的系统。除非是极端情况(巨大的数据集,没有支持软件),这样一个系统都可以在1-2周的时间搞定。因此这是个不合适的例子,因为没有任何必要在这样的系统上进行模块化编程。因为细致又完全地分解一个大系统是不实际的,因此我们还是试着把这个问题当做是个大项目吧。我们给出了两种模块化的方式。第一个,我们觉着,是现在比较典型的工程划分办法;另一个是在毕设中成功被应用的划分方法。

模块划分方法一

我们看下面的模块:
Module 1:输入:这个模块包含一个主程序,TA读取输入设备中的数据行,然后存储在core中,以待其他模块的处理。在core中,每四个字符被存储在一个字中,一个未被使用的字符被用来标识一个字的结束。维护了一个索引来展示每行的开始地址。
Module 2:循环移位:这个模块在输入模块结束之后被调用。本模块准备了一个索引,用以给出每个循环移位的开始字符,以及module 1生成的行数组的原始index,而不是将所有循环移位都存在core里,本模块把输出通过一个个的pair存入core中(每个pair由原始行号,开始地址组成)。
Module 3:字母序排序:本模块把module 1和module 2的输出作为输入。本模块产生了一个跟module 2(产生的结果)一样的数组。然后,将其按照字母序排序。
Module 4:输出:使用module 3和module 1输出的数组,本模块输出一个格式化好的输出,该输出列出了所有的循环移位。在一个复杂系统中,每一行的起始点会被标记,指向一行中更多信息的指针会在后续的步骤中被插入进来,每个循环移位的起始可能不是行中的第一个字母,等等。
Module 5:主流程控制:这个模块只关注以一定的顺序把上述四个模块串联起来。TA也可能会处理错误信息,内存分配等等。
很明确,上述的说明并不是一个详尽的文档。在正式工作开始前,还需要提供更多的信息。定义文档应该包含一系列展示核心格式,指针使用规范,调用规范等等。只有当四个模块间的所有接口被确定之后,工作才能真的开始。
这是大部分的模块化编程支持真想要表达的“模块化”。系统被划分成几个相对独立的模块,且模块间接口定义清晰;每一个模块足够小,且足够简单,使得更容易理解以及编码。小范围的实验表明,这几乎是大部分接到这个任务的程序员首选的模块分解方式。图1给出了这个系统的结构图。

图1,模块化方式1

模块化方法2

我们看如下的模块:
Module 1:Line Storage:这个模块包含了几个方法,这些方法在图2中给出了精确的定义。通过调用这些方法,可以在最后一行的最后一个单词增加一个字母,开始一个新的单词,或开始新的一行。也可以通过调用其他方法来查找第j行的的第k个单词的第k个字母。本模块的其他方法,可以用来查找行数,一行中的单词数,一个单词的字母数。这个模块的精确定义在图2中。定义的方法在引文[3]中详细阐述了。
Module 2: Input:这个模块从输入设备中读取原始输入,调用Line Storage模块来存储。
Module 3: 循环移位器:这个模块包含了如下几个方法:CSSTUP使得其他的方法又有初始值。其他的方法与模块1中提供的方法信息类似。使用他们,可以得到第i个循环移位的第j个单词的第k个字母,同时也可以获得所有行、单词的长度,等等。这也在图3中展示出来了。
Modeule 4:字母顺序排序器:这个模块主要包含两个方法。第一个,ALPH,必须在另一个方法(ITH)有了初始值之前调用。第二个,ITH,作为一个索引使用。ITH(i)会给出在字母序排序后的第i行在循环移位中的下标。这些方法的正式定义在图4中描述。
Module 5:输出:这个模块提供了任意循环移位需要的打印方式。该模块需要在循环移位方法之后被调用。
Module 6:主控制逻辑:与模块化方式1中的(主控制模块)类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Figure 2:LineStorage定义
介绍:这份定义描述了一个机制,该机制会容纳p1行,每行包含最多p2个词,每个词包含最多p3个字母。
其中ERLXXX的方法需要由模块的使用者提供。这个调用意味着该用户违背了模块定义的约束;这些方法中需要包含TA自己的恢复逻辑。

Function WORD
possible values: integers
initial values: undefined
parameters: l, w, c all integers
effect:
call ERLWEL if l < 1 or l > p1
call ERLWNL if l > LINES
call ERLWEW if w < 1 or w > p2
call ERLWNE if w > WORDS(l)
call ERLWEC if c < 1 or c > p3
call ERLWNC if c > CHARS(l, w)

Function SETWRD
possible values: non
initial values: not applicable
parameters: l,w,c,d all integers
effect:
call ERLSLE if l < 1 or l > p1
call ERLSBL if l > 'LINES' + 1
call ERLSBL if l < 'LINES'
call ERLSWE if w < 1 or w > p2
call ERLSBW if w > 'WORDS'(l) + 1
call ERLSBW if w < 'WORDS'(l)
call ERLSCE if c < 1 or c > p3
call ERLSBC if c .noteq. 'CHARS'(l,w)+1
if l = 'LINES' + 1 then LINES = LINES + 1
if w = 'WORDS'(l) + 1 then WORDS(l) = w
CHARS(l, w) = c
WORD(l,w,c) = d

Function WORDS
possible values: integers
initial values: 0
parameters: l an integer
effect:
call ERLWSL if l < 1 or l > p1
call ERLWSL if l > LINES
call ERLWSL(MN) if l > LINES

Function LINES
possible values: integers
initial value: 0
parameters: none
effect: non

Function DELWRD
possible values: none
initial value: not applicable
parameters: l,w both integers
effect:
call ERLDLE if l < 1 or l > LINES
call ERLDWE if w < 1 or w > 'WORDS'(1)
call ERLDLD if 'WORDS'(l) = 1
WORDS(l) = 'WORDS'(l) - 1
for all c WORD(l,v,c) = 'WORD'(l, v+1, c) if v >= w
for all v > w or v = w CHARS(l, v) = 'CHARS'(l, v+1)

Function DELLINE
possible values: none
initial values: not applicable
parameters: l an integer
effect:
call ERLDLL if l < 0 or l > 'LINES'
LINES = 'LINES' - 1
if r = 1 or r > 1 then for all w, for all c
(WORDS(r) = 'WORDS'(r+1)
CHARS(r, w) = 'CHARS'(r+1, w)
WORD(r, w ,c) = 'WORD'(r+1, w ,c))

Function CHARS
possible vlaues: integer
initial value: 0
parameters: l,w both integers
effect:
call ERLCNL if l < 1 or l > LINES
call ERLCNW if w < 1 or w > WORDS(l)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Figure 3 循环移位模块定义
在本定义中,我们假定line holder的方法有值,并且定义一个方法(TA允许我们以各种方式处理lineholder,除了把行的所有循环移位都包含在line holder中)。一个附带的功能是可以把指定行标记为"取消",尽管他们可以访问。
Function CSWROD
possible values: integers
initial values: undefined
parameters: l,w,c all integer
effect:
call ERCWNL(MN) if l < 1 or l > p4
call ERCWNL(MN) if l > CSLINES
call ERCWNW(MN) if w < 1 or w > p2
call ERCWNC(MN) if c < 1 or c > p3
call ERCWNC(MN) if c > CSCHARS(l,w)

Function CSWORDS
possible values: integers
initial value: 0
parameters: l an integer
effect:
call ERCWNL(MN) if l < 1 or l > p4
call ERCWNW(MN) if l > CSLINES

Function CSLINES
possible values: integers
initial values: 0
parameters: none
effect: none

Function CSCHRS
possible values: integer
initial value: 0
parameters: l,w both integers
effect:
call ERCCNL(MN) if l < 1 or l > CSLINES
call ERCCNW(MN) if w < 1 or w > CSWORDS(l)

Function CSSTUP
possible values: none
initial value: not applicable
parameters: none
effect:
call ERCNES(MN) if SUM(1,l,'LINES','WORDS'(1)) > p4
CSLINES = SUM(1, l, 'LINES', 'WORDS'(l))
let HIP(l) = minimum k such that SUM(1, l, k, 'WORDS'(l)) .> or =.
let SHI(l) = l - SUM(1, l, HIP(1) - 1, 'WORDS'(l) - 1)
then for all l such that l .< or =. CSLINES
CSWORDS(l) = 'WORDS'(HIP(l))
CSCHARS(l,w) = 'CHARS'(HIP(l), (w+SHI(l))mod 'CHWORDS'(l))
CSWORD(l,w,c) = 'WORD'(HIP(l), (w+SHI(l))mod 'CSWORDS'(l), c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
Figure 4 对于Line Holder做字母序排序
这个模块完成对于上述模块产生的内容进行字母序排序,本模块提供了一个方法ITH,TA给出了第i行在字母序排序后的序列中的下标。

Function ITH
possible values: integers
initial values: undefined
parameters: i an integer
effect:
call ERAIND if value of function undefined for parameter given

Function ALPHC
possible values: integers
initial value: ALPHC(l) = index of l in alphabet used
ALPHC(l) = infinite if character not in alphabet
ALPHC(undefined) = 0
parameter: l an integer
effect:
call ERAABL if l not in alphabet being used, i.e., if ALPHC(1) = infinite

Mapping Function EQW
possible values: true, false
parameters: l1, l2, w1, w2 all integers
values:EQW(l1, l2, w1, w2) = for all c('WORD'(l1, w1, c) = 'WORD'(l2, w2, c))
effect:
call ERAEBL if l1 < 1 or l1 > 'LINES'
call ERAEBL if l2 < 1 or l2 > 'LINES'
call ERAEBW if w1 < 1 or w1 > 'WORDS'(l1)
call ERAEBW if w2 < 1 or w2 > 'WORDS'(l2)

Mapping Function ALPHW
possible values: true, false
parameters:l1, l2, w1, w2 all integers
values: ALPHW(l1, w1, l2, w2) = if !'EQW'(l1, w1, l2, w2) and
k = min c such that ('WORD'(l1, w1, c) != 'WORD'(l2, w2, c))
then 'ALPH'('WORD'(l1, w1, k)) < 'ALPHC'('WORD'(l2, w2, k))
else false;
effect:
call ERAWBL if l1 < 1 or l1 > 'LINES'
call ERAWBL if l2 < 1 or l2 > 'LINES'
call ERAWBW if w1 < 1 or w1 > 'WORDS'(l1)
call ERAWBW if w2 < 1 or w2 > 'WORDS'(l2)

Mapping Function EQL
possible values: true, false
parameters: l1, l2 both integers
values: EQL(l1, l2) = for all k ('EQW'(l1, k, l2, k))
effect:
call ERALEL if l1 < 1 or l1 > 'LINES'
call ERALEL if l2 < 1 or l2 > 'LINES'

Mapping Function ALPHL
possible values: true, false
parameters: l1, l2 both integers
values: ALPHL(l1, l2) = if !'EQL'(l1, l2) then
(let k = min c such that 'EQW'(l1, k, l2, k))
'ALPHW'(l1, k, l2, k) else true
effect:
call ERAALB if l1 < 1 or l1 > 'LINES'
call ERAALB if l2 < 1 or l2 > 'LINES'

Function ALPH
possible values: none
initial values: not applicable
effect:
for all i >=1 and i <= 'LINES' (
ITH(i) is given values such that (
for all j >= 1 and j <= LINES
there exists a k such that ITH(k) = j
for i > -1 and i < 'LINES' (that 'ALPHL'(ITH(i), ITH(i+1)))
)
)

比较这两个模块化方法

两种方式都是可以工作的,第一个是非常常见的做法;第二个被成功的应用在一个课程项目中了。两种方式都将编程工作分解成了多个相对独立的,小的,可控的程序。我们必须看的更加深入,才能看到我们之前承诺的目标达到了什么程度。
我必须强调在两种分解方式中,我没有修改任何的描述或方法。我的目标就是把原本可能是一个客观实体的内容以两种方式切分开。以第一种分解方式划分的系统在经过编译后可以与第二种分解方式下的系统在功能上是完全相同的。两个以不同方式划分出来的系统的主要区别是,这些模块的定义,工作的划分方式,接口等。在两个系统中用的算法可以是相同的。我想强调的是,即使系统的运行时表现相同,他们也是完全不同的。这可能是因为运行时表示是只用来运行的(面向机器的);其他的表示则是用来更改,写文档,理解的。在其他的表示中,这两个系统就完全不同了。
(1) Changeability。一些设计决策可能是有问题的,并且在多数情况下很容易改变。一个不完全列表如下:

  1. 输入格式。
  2. 将所有行存储在内存里的决策。对于大型索引,在同一时间将所有行存储在内存中可能是不方便或完全不可能的。
  3. 将四个字母压缩成一个word存储的决策。在处理较小的索引时,可能不需要压缩这些字母,将存储变为一个字母一个word很可能会降低开发/执行时间。甚至有可能,我们会希望以其他格式来压缩存储。
  4. 对循环移位做index而不是把所有的循环移位都存储起来,这个决策,也是一样的。再一次,对于一个小索引或一个大内存,直接存储所有循环移位可能是最高效的。
  5. 一次性将整个列表排序而不是在需要时查询,或者是类似Hoare’s查找中实现的部分排序,这个决策,也是类似。在一些场景下,将创建索引时排序所需的计算分散开,可能会是比较有利的。

通过观察这些改变,我们可以看到上述两种模块化方式的区别。第一种变化,在两种模块化方式中,都被控制在一个模块中了,但第二种改变会导致第一种模块化方式中所有模块的变更。第三种变化也是同样。在第一种分解方式中,行的存储格式必须被所有模块使用。在第二种分解方式中,故事则完全不同。行存储的确切方式,被完全隐藏起来了,除了module 1没有人需要了解。所有存储方式上的变更,也都只需要改这一个模块。

实际上,在这个系统的多个版本中,分解时还存在着附加的模块。一个符号表被用来实现行的存储模块。这个事实,是真的,也是对系统的其余部分完全不可见。

第四种变化,对于第二种分解方式,只影响循环移位模块;但在第一种划分方式中,排序模块与输出模块也需要了解这个变化。

第五种变化,在第一种分解方式中也是困难的。输出模块会其他索引已经被完全排好序了。第二种分解方式中的排序模块被设计得用户无法发现排序确切是在什么时候进行的。没有其他模块需要变化。
(2) 独立开发。在第一个模块化方式中,各个模块间的接口是比较复杂的,如上面的表格描述的。这些现存的设计决策不容易被轻易的改变。表结构与组织方式关系到多个模块的效率,因此必须被仔细设计。这个部分的开发会成为模块开发的一个重要部分,但该部分还需要多个开发团队的共同协作。在第二种模块化方式中,接口更加抽象,他们只包含方法名和参数的类型和数量。有一些简单的决策和模块的独立开发可以更早开始。

(3) 可读性。为了理解第一种模块化方式中的输出模块,必须要理解排序模块,循环移位模块和输入模块。输出模块使用的表,有许多信息是因为其他模块的工作依赖与此。会因为其他模块使用的算法影响表的结构。整个系统必须要放在一起才能理解。我的主观判断是,在第二种模块化方式中不存在这样的问题。

划分的标准

现在,大多数读者应该明白,每种划分方式使用了怎样的标准了。在第一个划分方式中,标准是将每个“主要步骤”在一个单独模块中处理。也可以说,通过第一种划分方式,得到了一个流程图。图1是一个流程图。这是最常见的划分方式了。这是所有程序员培训的副产品,这些培训教我们先从一个大致的流程图开始,然后再逐步填充实现。当代码量在5000-10000行时,流程图是一种有用的抽象,但一旦超出这个界限,流程图看起来就不够用了;一些额外的标准必须被加进来。
第二种划分方式是以“信息隐藏”为标准划分的。这些模块不再对应于流程中的一个步骤。例如,行存储模块,被系统中的大部分行为使用了。排序可能也可能不对应一流程中的一个阶段,这完全取决于怎么用了。类似的,循环移位在一些场景下,不提前存任何表,完全是在按需计算。第二种分解方式中的每一个模块都是,按照它需要对其他模块隐藏的设计决策,来描述的。TA的接口或是定义会尽量少的展示其内部究竟是如何工作的。

循环移位模块的优化

为了展示划分标准的影响,让我们再仔细看看第二种划分方式中的循环移位模块的定义。后见之明,这个定义中包含了不需要暴露的信息。当我们小心的隐藏存储及计算循环移位列表的具体实现时,我们预示着该列表是有序的。程序可以被高效的写出来,如果我们只说明: (1)循环移位定义中的”lines”都会在”table”中存在,(2)有且只包含一次,(3) 有个方法可以提供这样的能力,即可以根据”shift”得到原始行。通过提前描述循环移位的顺序,我们提供了很多不必要的信息,这些信息反过来制约了我们系统在不改变定义前提下的灵活性。例如,我们没有允许构建这样一个系统,在这个系统中,循环移位以字母序的方式产生,alph是空的,且ITH简单的返回了入参。如果我们使用第二种分解方式构建系统时,这可以非常清晰的被认定为设计失误。

效率和实现

如果我们不小心一点,第二中分解方式将会非常低效。如果每个功能点被实现成了一个需要进行一系列调用的程序,那么将会产生大量跨module频繁切换控制权的调用。第一种分解方式则不受这个问题的影响,因为相对来说模块间控制流的转移不算频繁。
为了在避免程序调用的额外开销的同时还能获得第二种分解方式的好处,我们我们必须采用一种非常规方式来实现这些模块。大多数情况下,常用方法最好是通过汇编器插入到代码中;其他情况下,高度定制且高效的转移需要被单独插入。为了成功且高效的使用第二种解耦方式,需要一个工具,通过这个工具可以实现,方法看起来像实际存在的,但其实现可以在汇编时选择合适的(就是dependency injection)。如果使用这样的技术,模块划分在最终生成的代码中可能是看不出来的。换句话说,程序的另一种表示(前面提到的)必须在”支持映射该展现模式”的机器上运维起来。

第二个例子:一个MARKOV算法翻译器

尽管第一个例子已经展示了本文的大部分观点,再看看其他的例子也会非常有帮助。这是一个要实现Markov算法的翻译器。Markov算法在很多地方都被论述过了;作为编程语言的描述最棒的是Galler and Perlis。对于那些不熟悉的人,Markov算法可以被描述为穷人版SNOBOL。机器上唯一的内存是一个字符串(只要需要就可以随意扩展)。算法通过一系列的规则描述。每个规则包含一个待匹配的规则,和一个用于替换被匹配部分的字符串。顺序性原则标明规则的匹配遵循最左匹配原则(匹配一次则不再匹配)。当替换完成之后,第一个可应用规则仍然有效(例如,对于最后一个规则来说没有内存供匹配了,即到头了)。

常规的模块化方式

对于这种翻译器,有两种常见的模块化方式。他们分别是:

Interpretor

输入模块:读取输入,分析成规则列表,把规则的一种直接表现形式存进主存中。
Intepretor:尝试将每一条规则应用到寄存器上。TA访问存储规则的数据结构,使用对应的pattern来做匹配,如果匹配上了,就替换掉寄存器上对应的部分。
也会有个输出模块,来做对应的打印。

Compiler:

输入模块:读取输入,分析,并将每个句法单元作为参数传给下一个模块,encoder。
Encoder:这个模块包含用于“运行一个规则或部分规则”,并产生对应的机器码程序,例如,TA们对每个pattern分别产生一个机器码程序,这些程序搜索对应pattern出现的位置。这被称为编译代码。
运行时:包含了机器码成需的标准集合,这些程序会被各个算法使用。被编译的程序被链接到这些标准程序上,比如输出方法等等。

一种替代的搞法

我们成功的使用了如下的模块化方式:
规则存储:在主存中存储了规则的一种表示形式。这个模块跟之前说的Line存储有点相近。
规则分析:了解规则的具体含义,例如,知道如何解析存储的规则,以及如何应用这些规则。
寄存器操作:包含了一系列操作寄存器的常用方法。
排序器:选择下一个要被应用的规则。
输入:读取输入,然后调用规则存储及寄存器操作模块,来做内部存储。
输出:对寄存器做必要的打印,以及最好一条规则的应用,等等。

对于第二个例子的讨论

许多关于第一个例子的讨论都可以在这里重复。例如将寄存器操作模块与其他模块分离开,使得修改寄存器的具体展示方式更加容易。规则排序与规则分析的分离,使得更容易尝试其他形式的Markov算法。
我们选择这个例子是为了表明另一个观点。这种模块化方式没有在解释器或编译器上做出决策。我们可以将一个解释型翻译器和一个编译型翻译器相对容易的替换,且我们也可以从二者身上各自选取一些优点。寄存器操作,排序器,输入和输出都可以保持不变。主要的修改都会集中在分析器模块上,假如采用编译器模式那么就将生成的机器码程序存储起来,但如果使用的是解释器模式就在被调用时应用这些规则。在这两种方式中可能存在这大量相同的代码。例如,寄存器操作代码可以在两个版本中都被使用。在编译器方式中,(寄存器操作代码)是运行时程序的一部分;在解释器方式中,会被规则解析模块调用。

层次化结构

我们可以通过Dijkstra提出的分解方式方式找到程序的层次结构。如果一个符号表存在,并且可以不依赖其他模块工作,那么TA在第一层。如果没有用符号表的话,Line存储在第一层;如果用了符号表,则在第二层。输入和循环移位器需要Line存储。输出和排序器需要循环移位器,但由于循环移位器和line holder在某种意义上是兼容的,因此做一个可以用于同时支持排序/输出原始行或者循环移位后的行的程序是比较容易的。在第一种(打印原始行)使用方式中不需要循环移位器;在第二种(打印循环移位后的行)则需要。换句话说,我们的设计允许我们将一种程序运行在两个不同的层次上。
在讨论系统结构的时候,容易混淆好的分解方式的收益和层次化结构的收益。如果模块或程序将可以定义某种关系,且该关系是偏序的,那么我们就拥有了一个层次结构。我们关注的关系主要是”uses”或”depends upon”。如果大多数情况下,一个模块只依赖于有限个其他模块,那么在程序之间有关系是比较好的(例如循环移位器只依赖与line holder的输出部分,与SETWORD则无关)。可以想象,我们可以获得我们讨论过的好处,但不依赖于偏序,比如,所有的模块都在同一层。偏序关系给了我们另外的两个好处。第一,上层系统可以使用下层系统提供的能力。第二,我们可以砍掉上层,仍然可以继续使用下层。例如,符号表可以被用在其他应用中,line holder可以作为一个问答系统的基础部分。层次结构的存在是的我们可以减去树的上层,然后在树干上再开一颗新树。如果我们设计了一个系统,在系统中“底层”模块依赖了上层模块,我们就不具备层次了,我们会发现移除系统中的一部分将非常困难,并且“层次”在系统中也会包含非常杂的信息。
可以想象,如果我们可以通过第一种分解方式来构建系统但保持了层次结构,我们必须得出这样的结论层次化结构和”干净”的分解是两种有意义但相互独立的系统属性。

结论

我们试图通过这些例子说明,通过流程图来做系统分解几乎总是不正确的。我们建议根据一系列困难或易变的设计决策来做分解。每一个模块都被设计以对其他模块隐藏一个决策。因此,在大多数情况下,设计决策是跨越执行时间的,模块可能不会对应到流程上的步骤。为了得到一种高效的实现,我们必须摒弃这种假设(模块是一个或多个子程序),取而代之,应该允许程序通过将多个不同模块汇编到一起得到。