代码大全_cc6-10


第六章 模块化设计 75 第六章第六章第六章第六章 模块化设计模块化设计模块化设计模块化设计 目录目录目录目录 6.1 模块化:内聚性与耦合性 6.2 信息隐蔽 6.3 建立模块的理由 6.4 任何语言中实现模块 6.5 小结 相关章节相关章节相关章节相关章节 高质量子程序的特点:见第 5 章 高层次设计:见第 7 章 抽象数据类型:见第 12.3 节 “你已经把你的子程序放入我的模块中” “不,你已经围绕着我的子程序设计好了模块” 人们对于子程序和模块之间的区别往往不很注意,但事实上应该充分了解它们之间的区别, 以便尽可能地利用模块所带来的便利。 “Routine”和“Modu1e”这两个单词的意义是很灵活的,在不同的环境下,它们之间的区 别可能会变化很大。在本书中,子程序是具有一定功能的,可以调用的函数或过程,关于这一 点在第五章已经论述过了。 而模块则是指数据及作用于数据的子程序的集合。模块也可能是指,可以提供一系列互相 联系功能的子程序集合,而这些子程序之间不一定有公共的数据。模块的例子有:C 语言中的 源文件,某些 Pascal 版本中的单元及 Ada 语言中的“包”等等。如果你所使用的语言不直接支 持模块,那么可以通过用分别编程技术来模仿它,这也可以得到许多由模块带来的优点。 6.1 6.1 6.1 6.1 模块化:内聚性与耦合性模块化:内聚性与耦合性模块化:内聚性与耦合性模块化:内聚性与耦合性 “模块化”同时涉及到子程序设计和模块设计。这是一种值得研究的,非常有用的思想方 法。 在1981 年出版的《Software Maintenance Guidebook》一书中,Glass 和 Noiseux 认为模 块化给维护性带来的好处要比给结构带来的好处多得多,它是提高维护性的最重要因素。Lientz 和 Swanson 在《Software Maintenance Management》一书中引用的一项研究表明,89%的代码使 用者认为使用模块化编程改进了维护性(1980)。在一次理解测验中发现,采用模块化设计程序 的可读性要比不采用这种设计的程序可读性高 15%(1979)。 模块化设计的目标是使每个子程序都成为一个“黑盒子”,你 知道进入盒子和从盒子里出来 的是什么,却不知道里边发生什么。它的接口非常简单,功能明确,对任何一个特定的输入, 第六章 模块化设计 76 你都可以精确地预测它相应的输出结果。如果你的子程序像一个黑盒子,那么它将是高度模块 化的,其功能明确,接口简单,使用也灵活。 使用单独一个子程序是很难达到这一目的的,这也正是引入模块的原因。一组子程序常常 要使用一套公用的数据,在这种情况下,由于子程序间要共享数据,因而它们不是高度模块化 的,作为一个单个的子程序,它们的接口也不简单。但是,作为一个整体,这组子程序则完全 有可能为程序的其它部分提供一个简单的接口,也完全有可能达到高度模块化这一目标。 6.1.1 6.1.1 6.1.1 6.1.1 模块内聚性模块内聚性模块内聚性模块内聚性 模块的内聚性准则,与单个子程序的内聚性准则一样,都是十分简单的。一个模块应该提 供一组相互联系的服务。 比如一个进行驾驶控制模拟的模块,其中应含有描述汽车目前的控制设置和目前速度的数 据。它可以提供像设定速度、恢复到刚才的速度、刹车等功能。在其内部,可能还有附加的子 程序和数据来支持这些功能,但是,模块外的子程序则不需对它们有任何了解。如果这样的话, 那么这个模块的内聚性将是非常强的,因为模块中的每个子程序都是为提供驾驶控制模拟服务 的。 再比如一个进行三角函数计算的子程序,模块中可能含有 Sin()、Cos()、Tan()、Arcsin()等 全部密切相关的三角函数子程序。如果这些子程序都是标准的三角函数,那么它们无须共享数 据,但这些子程序间仍然是有联系的,因此这个模块的内聚性仍然是非常强的。 下面是一个内聚性不好的模块例子,设想一个模块中含有几个子程序为实现一个堆栈: init_stack()、push()和 pop();模块中同时还含有格式化报告数据和定义子程序中用到的所有全局 数据的子程序。很难看出堆栈与报告子程序或全局数据部分有什么联系,因此模块的内聚性是 很差的。这些子程序应该按照模块中心的原则进行重新组织。 在上例中,对模块内聚性的估计是以模块数据和功能为基础进行的。它是在把模块作为一 个整体的层次上进行的。因而,模块中的子程序并不会因为模块内聚性好而一定具有良好的内 聚性。所以模块中的每个子程序设计,也要以保证良好内聚性为准则。关于这方面的问题,见 5.3 节“强内聚性”。 6.1.2 6.1.2 6.1.2 6.1.2 模块耦合模块耦合模块耦合模块耦合 模块与程序其它部分间的耦合标准与子程序间的耦合标准也是类似的。模块应被设计成可 以提供一整套功能,以便程序的其它部分与它清楚地相互作用。 在上述的驾驶控制例子中,模块担任了如下功能:SetSpeed()、GetCurrentSettings()、 ResumeFormerSpeed()和 Deactivate()。这是一套完整的功能,因而程序的其它部分与它的相互作 用完全是通过规定的公用接口进行的。 如果模块所提供的功能是不完善的,其它子程序可能被迫对其内部数据进行读写操作。这 就打开了黑盒子盖而使其成为透明的了,这实际上破坏了模块化。结构化设计的先驱 Larry Constantine 指出,模块提供的功能必须是完整的,以便它的调用者们可以各取所需。 为了设计出强内聚而又松散耦合的模块,必须在设计模块和设计单个子程序的标准之间进 行平衡与折衷。降低子程序之间耦合性的重要措施之一,就是尽可能减少使用全局变量。而创 建模块的原因之一则是为了让子程序可以共享数据;你若想使同一模块中的子程序不必通过参 第六章 模块化设计 77 数表进行传递,可以采用对其中所有数据进行直接存取来实现。 从所有模块中的子程序可以对它进行存取的角度来说,模块中数据很像是全局数据。但从 不是程序中所有的子程序都可以对它进行存取的角度来说,它又不像是全局数据,它只对模块 中的子程序来说,才是可以存取的。因此,在模块设计中的最重要决定之一,便是决定哪个子 程序需要对模块中数据进行直接存取。如果某个子程序仅仅是由于可以对模块中数据进行存取 的原因才留在模块中的,那么,它应该被从模块中去掉。 6.2 6.2 6.2 6.2 信息隐蔽信息隐蔽信息隐蔽信息隐蔽 如果你阅读了书中所有推荐参阅文献的注释,你就会发现其中有 400 多个是关于信息隐 蔽的。拥有这么多参考文献的内容一定是非常重要的吧?是的,它的确非常重要。 进行信息隐蔽的设计思想贯穿了软件开发的每一个层次,从使用命名的常量而不是使用自 由常量到子程序设计、模块设计和整个程序设计。由于这一思想往往是在模块这一层次得到最 充分体现的。因此,我们在本章详细讨论它。 信息隐蔽是为数不多的几个在实践中无可辩驳地证明了自己价值的理论之一(Boehm 1987)。研究发现,应用信息隐蔽进行设计的大型程序容易更改指数要比没采用这一技术的高 4 倍。同时,信息隐蔽也是结构化设计和面向对象设什的基础之一。在结构化设计中,黑盒子思 想便来源于信息隐蔽。在面向对象设计中,也是信息隐蔽引发了抽象化和封装化的设计思想。 6.2.1 6.2.1 6.2.1 6.2.1 保密保密保密保密 信息隐蔽中的关键概念是“ 保密”。每一个模块的最大特点都是通过设计和实现,使它对其 它模块保密。这个秘密或许是可能被改动的区域、某个文件的格式化、一个数据结构的实现方 式、或是一个需要与程序其它部分隔离开来,以便其中的错误产生的危害最小的区域。模块的 作用是将自己的信息隐蔽起来以保卫自己的隐私权。信息隐蔽的另一个称谓是“封装”,其意思 是一个外表与内容不一样的盒子。 无论管它叫什么,信息隐蔽都是设计子程序和模块的一种方法,它对模块的意义更重要些。 当你隐藏秘密时,你就设计了一组存取同一套数据的子程序。对一个系统的改动可能涉及到几 个子程序,但是,它只应涉及一个模块。 在设计模块时,一项重要任务就是决定哪些特性应该是对模块外部公开的,哪些应该是作 为秘密隐藏起来的,一个模块中可能使用 25 个子程序而只暴露出其中的 5 个,其余 20 个都只 在内部使用。模块中也可能用到了几个数据结构,但却把它们全部隐藏起来。它可能会也可能 不会提供把数据结构信息通知给程序其余部分的子程序。模块设计的这一方面一般被称作“可 见性”,因为它主要涉及了模块的功能特性是否是对外部分开或暴露的。 模块的接口应该尽可能少地暴露它的内部内容。一个模块应该像是一座冰山,你只看到它 的一角,而它其余 7/8 的部分则藏在水面下。 与设计的其它方面一样,设计模块的接口也是一个逐渐的过程,如果接口在第一次是不正 确的,可以再试几次直到它稳定下来;如果它稳定不下来,那么就需要重新设计它。 可以用各种不同的图形来代表模块。模块表示图的关键是,它应该区分开仅供模块内部使 用的功能和对外开放的功能。这种图形通常称之为“积木图”,是由 Erody Boock 在开发 Ada 第六章 模块化设计 78 语言过程中提出来的。图 6-1表示出了一种模块图。 其中公用部分是矩形块,个别部分如黑盒子那样表示。 信息隐蔽不必暗示出一个系统的形状;系统可能具有分层结构,也可能像图 6-2 中所示那 样具有网状结构。 在网状结构中,你只要规定哪些模块可以与其它模块通信,这种特定的通信是如何进行的, 然后再进行联接的就可以了,如图 6-3 所示,积木图也可以用在网状结构中。 图 6-1 一个模块中公用和个别部分 图 6-2 网状结构系统 图 6-3 用包含信息隐蔽思想的符号表示网状系统 第六章 模块化设计 79 6666....2222....2222 信息隐蔽举例信息隐蔽举例信息隐蔽举例信息隐蔽举例 几年前我曾写了一个中型系统(有 20K 行代码),在其中广泛使用了链表结构。问题域是由 数据结点构成的,每一个结点又与亚坐标、实坐标和等同点相联接。由于我选用了链表结构, 因此在程序中到处都是类似这样的语句: node = node.next and phone = node.data.phone 这些语句直接对链表数据结构进行操作。尽管链表非常自然地将问题进行了模块化,但是 这种方法对内存的使用效率却非常低,于是我想使用整型数组索引来代替内存指针,因为这样 可以提高内存利用率,并且为在其它区域进行性能优化创造机会。但是,由于刚才提到的那种 编码语句充满了程序,因而修改工作非常困难。因为我无法在 20000 多行代码中把它们一一找 出来。如果当初我采用了含有如下存取子程序的模块的话,我只要在一个地方即存取子程序中 改动代码就可能了。 node = NearestNeighbor(node) phone = EmergencyContact(node) 我到底赢得了多少内存?我将会赢得或者失去多少速度,我不知道,但是如果当初我隐含 了数据结构的细节并且使用了存取子程序,我就可以很容易地找到答案。而且,我还可以尝试 一下另外几种方法。我本来可以从许多方案中挑选一个最好的,可是,由于我把数据结构的细 节暴露给了整个程序,我不得不使用我所厌恶的方案。 除了方便修改,隐含复杂数据结构细节的另一个重要原因是:隐含细节可以澄清你编写某 段代码的意图。在上例中,一个富有经验的程序员不难读懂下面这条语句的: node = node.next 显然,这个语句指的是一个链表结构,但除此之外,它什么也不能告诉你。然而,一个像 node = NearestNeighbour(node)这样的存取子程序,则清楚描述了链表所代表的内容,因而这是 很有用的,并且提醒你应该对 node 这个名称进行改进(node 与其邻居有什么关系),node = node.text 这样的语句与实际相脱离,你根本无法想到应该改进它们的名称以说明实际问题。 隐含数据结构的最后一个原因是出于对可靠性的考虑。如果你用一个专门的子程序来存取 数据结构,你只需在其中设置一个安全验证就可以了。否则,你就不得不在所有这个子程序访 问变量的地方设置安全验证。比如,如果你使用了链表,并且想读取链表中的下一个元素,并 且要注意不超过链表的最后一个元素,你可能用如下的代码: if ( node.text<>null ) then node = node.text 如果在某种情形下,为了更谨慎一些,你可以使用如下代码: if ( node<>null ) then if ( node.next<>null ) then 第六章 模块化设计 80 node = node.text 如果你的程序中充斥着 node=node.next 这样的语句,你可能要在其中某些地方进行测试而 跳过其余部分。但是,如果这个操作是独立在一个子程序调用中: node=NearestNeighbor(node) 那么,你只要在子程序中一个地方进行测试,那么这一测试就会在整个程序中都起作用。 如果有时你想在整个程序中都对使用 node 的地方进行测试,也很容易漏掉其中某些地方。然而, 如果你把这一操作独立在一个子程序中,那么是不可能有遗漏的,因为此时这项工作完全是自 动进行的。 隐含数据结构细节的另一个好处是容易调试,比如,你发现 node 值在某处变得有问题了, 但却不知道是在哪里。如 果 存取 node 的代码充斥了整个程序的话,那 么 找到出问题的地方不亚 于大海捞针。但如果它是被孤立在一个子程序中的话,那么你可以在其中加入一段检查 node 的调试码,从而在每次存取它时都进行测试,这样事情就解决了。 应用存取子程序最后一个优点是,可以使所有对数据的存取所遵循的是一种平行的组织形 式;或者通过存取子程序、或者直接对数据进行存取,不会两者兼而有之。当然,在负责数据 的模块内部,对数据的存取都是直接的,在这种情况下失去平行性是不可避免的,这样做的目 的是不在公共场合吹脏肥皂泡。这常常伴随着对在存取程序中进行直接数据操作这一拙劣设计 的隐含。 6666....2222....3333 常见需要隐含的信息常见需要隐含的信息常见需要隐含的信息常见需要隐含的信息 在你所从事的项目中,你可能与不计其数需要隐含的信息打交道,但是,其中只有几种是 你要反复遇到的: · 容易被改动的区域 · 复杂的数据 · 复杂的逻辑 · 在编程语言层次上的操作 以上每一项都将在下面的部分中给予详细论述。 容易被改动的区域容易被改动的区域容易被改动的区域容易被改动的区域 容易改动是好的程序设计中一项最富于挑战性的工作。目的是将不稳定的区域孤立起来, 以便使改动带来的影响仅限于一个模块中。以下是你在为应付改动的工作中要遵循的步骤。 1. 识别出那些可能被改动的地方。如果分析工作做得很好的话,其中应该附有可能改动 的地方和改动内容的明细表。在这种情况下,找出可能的改动是非常容易的。如果需 求分析中没有进行这项工作,可以参阅下面关于在任何项目中都可能被改动的区域的 讨论。 2. 把可能被改动的地方分离出来。把第一步中发现的每一个可能改动的地方分隔到自己 的模块中,或者将其与其它可能一起被改动的要素一起,独立到一个模块中。 3. 独立可能被改动的地方。应把模块间的接口设计成对可能变动不敏感,同时,接口应 该把变动限制在模块内部,外部不会受到内部变动影响。而其它调用这个被改动过模 块的模块,不应感受到这个模块被修改过。模块的接口应该能保护模块的隐私权。 第六章 模块化设计 81 以下是一些可能变动的区域: 对硬件有依赖的地方对硬件有依赖的地方对硬件有依赖的地方对硬件有依赖的地方。对于监视器、打印机、绘图机等,要清楚在尺寸、颜色、控制代码、 图形能力及内存等方面可能的变化。其余对硬件有依赖性的方面包括与磁盘、磁带、通讯口、 声音器件等接口的变化等等。 输入和输出输入和输出输入和输出输入和输出。在比原始的硬件接口稍高一些的设计层次上,输入/输出是另外一个反复无常 的区域。如果某一应用产生它自己的数据文件,那么当这一应用变得复杂起来时,文件的格式 可能也要变化。用户层次上的输入和输出格式也有可能变化,比如,在打印纸上边界的位置、 每页上边界的数量、域的排列顺序等等。总之,检查所有的外部接口以寻找可能的变化是个好 主意。 非标准语言特性非标准语言特性非标准语言特性非标准语言特性。如果在程序中使用了非标准扩展,应该把这些扩展隐含在一个模块中, 以便当运行环境变化时你可以很容易地替换它。同样,如果你使用了不是在所有环境下都存在 的库子程序,应该把实际的库子程序放在另一个环境下也可以使用的接口后面。 难于设计和实现的域难于设计和实现的域难于设计和实现的域难于设计和实现的域。最好把难于设计和实现的域隐含起来,因为此处的工作可能作得很 糟,你可能不得不返工。把它们分隔起来,以便使由于拙劣设计或实现对系统所带来的危害最 小。 状态变量状态变量状态变量状态变量。状态变量指示程序的状态,往往比其它数据更容易被改动。在典型的情形下, 你可能最初把某一错误状态变量定义成逻辑变量。但后来又发现如果把它赋成具有 NoError, WarningError 和 FatalError 三个值的枚举型变量来实现会更好。 你至少可以在使用状态变量时,加上两个层次的灵活性和可读性。 ···· 不要使用逻辑型变量作为状态变量,应使用枚举型变量。赋予状态变量一种新状态是 非常常见的,给枚举型变量赋一个新的类型只需要重新编译一次,而对于逻辑型变量 则需要重新编写每行检查状态变量的代码,谁难谁易是很明显的。 ···· 使用存取子程序检查变量,而不要对其直接检查,通过检查存取子程序而不是状态变 量,可以进行更复杂的状态测试。例如,如果想检查一个错误状态变量和一个当前函 数状态变量,那么把测试隐含在子程序中来进行,要比用充斥着程序的复杂代码进行 测试容易得多。 数据规模限制数据规模限制数据规模限制数据规模限制。如果你说明一个数组中含有 15 个元素,那么你就把系统不需要的信息暴 露给了它。应该保护其隐私权,信息隐蔽并不总是意味着把一系列功能装入模块这类复杂的工 作,有时,它简单到就是用一个像 MAX_EMPLOYEES 之类的常量来代替 15,以便隐含它。 商业规则商业规则商业规则商业规则。商业规则指法律、政策、规定、惯例等编入一个计算机系统中的东西。如果你 在编写一个工资发放系统,你可能把 IRS 关于允许的扣留数和估计税率等规则编入程序。其余 附加的规则是由工会规定的关于加班率、节假日付酬等方面的规定。如果你正在编写一个引用 保险率的软件,其规定来源于州关于信誉、实际保险率等的管理规定。 这些规定往往是数据处理系统中频繁变动的部分。因为国会可能修改法律,保险公司会调 整保险率。如果你遵从信息隐蔽原则,那么当规则变动时,建立在这些规则上的逻辑关系不会 完全垮掉。这些逻辑关系会隐藏在系统中唯一一个阴暗角落里,直到需对其作出改动为止。 预防到改动预防到改动预防到改动预防到改动。当考虑一个系统中潜在的改动时,应该按照使得改动范围或大小与其改动可 能性成反比的原则来设计系统。如果改动很可能发生,要确保系统可以容易地容纳这一特征。 只有极其不可能发生的变动,才应该被允许在变动时,会影响到系统中一个以上的模块。 第六章 模块化设计 82 一个寻找可能发生变动域的技术是,首先分析程序中可能会被用户用到的最小的子单元, 这些子单元组成了程序的核心,而且很可能被改变。其次,规定对系统的最小增值。它们可以 小到看起来完全是琐碎的程度。这些潜在改进域组成了对系统的潜在改进。应使用信息隐蔽原 则对这些域进行设计。首先分析核心,可以发现哪些要素事实上是后加上的,从而从那里推测 并隐含改进。 复杂的数据复杂的数据复杂的数据复杂的数据 所有的复杂数据都很可能被改动;如果它很复杂而对它使用得又很多,那么在实现层次 上与其打过交道后,可能会发现实现它的更好方式。如果应用信息隐蔽来隐含数据实现,就 可以付出较少的努力而获得更好的实现方法。如果不是这样,那么你每次与这些数据打交道 时,你可能都会在后悔,如果当初进行了信息隐蔽,改动实现将会是多么容易啊! 对复杂数据的使用程度,主要取决于程序。如果是一个只有几百行代码的小程序,你想 在其中对变量进行直接操作,那就这样干吧,这样可能影响程序,但也可能不会。在担心由 于对数据直接操作而带来的维护问题之前,应首先考虑这个小程序的特点。如果你正在编写 一个大一些的程序或使用了全局数据,那么就该考虑使用存取子程序。 复杂的逻辑复杂的逻辑复杂的逻辑复杂的逻辑 隐含复杂的逻辑可以改善程序的可读性。复杂的逻辑并不总是程序的最主要方面,把它 隐含起来可以使得子程序的活动更清楚。与复杂数据一样,复杂逻辑也是很可能变动的部分。 所以,把程序的其它部分从这种变动里隔离出去是非常有益的。在某些情况下,你可以将所 使用的逻辑种类隐含起来,例如,你可以通过一个大的 if 语句、case 语句或查表方式来进行 测试。除了这些进行测试的代码外,其余的代码不需要知道这些细节。如果程序中的其余代 码只需要知道结果,那么你就应该仅仅告诉它们结果。 在程序语言层次上的操作在程序语言层次上的操作在程序语言层次上的操作在程序语言层次上的操作 你的程序越是像一个实际问题的解决方案,它就越是不像程序语言结构的组合,那么, 其质量也就越好,应该把过于专业化的信息隐含起来,比如,下面的语句: EmployeeID = EmployeeID+1 CurrentEmployee = EmployeeList [ EmployeeID ] 这是一段很不错的程序,但是它是用过于专业化的语言来表达的,应该用较高程度抽象 的语言来进行这个操作: CurrentEmployee = NextAvailableEmployee() 或者用: CurrentEmployee = NextAvailableEmployee( EmployeeList, EmployeeID ) 通过加入一个隐含了用专业化语言解释正在发生什么的子程序,使得在一个更高的抽象 层次上处理这个问题。这使得你的意图更清楚,而且使得代码更容易理解和改动了。 如果用图表来实现一个排序问题。函数 HighestPriorityEvent(),LowestPriorityrEvent()和 NextEvent()是抽象函数,隐含了实现细节;而 FrontOfQueue(),BackOfQueue()和 NextInQueue() 第六章 模块化设计 83 并没有隐含多少细节,因为它们提到了实现,暴露了它们该隐藏的秘密。 一般来说,在设计一组在程序语言语句层次上操作数据的子程序时,应该把对数据操作 隐含在子程序组中,这样程序的其余部分就可能在比较抽象的层次上处理问题了。 6.2.4 6.2.4 6.2.4 6.2.4 信息隐蔽信息隐蔽信息隐蔽信息隐蔽的障碍的障碍的障碍的障碍 绝大多数信息隐蔽障碍都是心理上的,它主要来自于在使用其它技术时形成的习惯。但 在某些情况下,信息隐蔽也的确是不可能的,而一些看起来像是隐蔽障碍的东西,但仅仅是 借口而已。 信息过度分散信息过度分散信息过度分散信息过度分散 信息隐蔽的一个常见障碍是系统中信息过于分散。比如在一个系统中到处分布着常数 100。把 100 当作一个常数,降低了引用它的集中程度。如果把信息隐蔽在一个地方会更好, 因为这样它的值将只在一个地方改变。 另一个信息过于分散的例子是程序中分布着与用户交互的接口。如果需要改变交互方式, 比如,从命令行方式改为格式驱动方式,那么所有的代码事实上都要被改动。因此,最好把用 户交互接口放入一个单独的模块中,这样,你不必影响到整个系统就可以对交互方式进行改动。 而还有一个例子则是全局数据结构,比如,一个在整个系统中四处被存取的拥有多达 1000 个元素的雇员数据数组。如果程序直接使用这个全局数据,那么这个数据结构的实现信息—— 它是一个数组且拥有最多 1000 个元素——将充斥着整个程序。如果这个程序只通过存取子程 序来使用这个数据结构,那么就只有这个存取子程序才知道这些细节。 交叉依赖交叉依赖交叉依赖交叉依赖 一个不易察觉的信息隐蔽障碍是交叉依赖。比如模块 A 中的某一部分调用了模块 B 中的 一个子程序,而模块 B 中又有一部分调用了模块 A 中的子程序。应避免这种交叉依赖现象。 因为只有在两者都已准备好的情况下,你才能测试其中的一个。当程序被覆盖时,必须使 A 和 B 同时驻留在内存中,才能避免系统失败。通过寻找两个模块中被其它模块使用的部分, 把这些部分放入新的模块 A’和 B’中,用模块 A 和 B 中的其余部分来调用 A’和 B’,基本上可 以消除这一问题。 误把模块数据当成全局数据误把模块数据当成全局数据误把模块数据当成全局数据误把模块数据当成全局数据 如果你是个谨慎的程序员,那么信息隐蔽的障碍之一便是误把模块数据当作全局数据而 避免使用它,因为要避免由于使用全局数据而带来的麻烦。但是,如同在 6.1 节“模块化: 内聚性与耦合性”中所说的那样,这两种数据是不同的。由于只有在模块中的子程序才可以 对其进行存取,因而由模块数据带来的麻烦要比全局数据小得多。 如果不使用模块数据,就不会知道了解由模块所带来的巨大收益。如果一个子程序向模 块传递了只有它才能处理的数据的话,那么就不该由模块来承担拥有数据集合并对其进行操 作的罪责。比如,在前面列举的建议利用如下语句来提高抽象程度的例子中: CurrentEmployee = NextAvaliableEmployee() 或使用: 第六章 模块化设计 84 CurrentEmployee=NextAvailableEmployee(EmployeeList, EmployeeID) 这两个赋值语句间的区别是:在第一种情形下,NextAvailableEmployee()拥有关于雇员表 和目前表中的入口是哪一个入口的信息,而在第二种情况下,NextAvailableEmployee (EmployeeList, EmployeeID)只是从向它传递数据的子程序中借用这些信息。当你使用 NextAvailableEmployee()时,为了提供全部的抽象能力,不必担心它所需要的数据,只要记住 它负责自己的问题就可以了。 全局数据主要会产生两个问题:(1)一个子程序在对其进行操作时并不知道其它子程序也 在对它进行操作;(2)这个子程序知道其它子程序也在对其进行操作,但不知道它们对它干了 什么。而模块数据则不会产生这些问题,因为只有被放在一个单独模块中的有限几个子程序 才被允许对模块数据进行直接存取操作,当一个子程序进行这种操作时,它知道别的子程序 也在进行同样操作,并确切知道这些是哪几个子程序。如果你还不相信的话,试一下,结果 会令你满意的。 误认为会损失性能误认为会损失性能误认为会损失性能误认为会损失性能 信息隐蔽的最后一个障碍是在结构设计和编码两个层次上,都试图避免性能损失。事实 上,在两个层次上你都不必担心这一点。在结构设计层次上,这种担心之所以不必要是因为, 以信息隐蔽为目标进行结构设计,与以性能为目标进行结构设计是不矛盾的,只要你同时考 虑到这两点,那么就可以同时达到这两个目标。更常见的担心是在编码层次上,这种担心主 要是认为间接而不是直接地存取数据结构会带来运行时间上的损失,因为这样做附加了调用 层次。当测试了系统的性能并在瓶颈问题上有所突破时,这种担心是不成熟的。为提高软件 性能做准备的最好手段之一就是模块化设计,这样,在发现了更好的方案之后,不必改变系 统其余部分,就可以对个别子程序进行优化。 6.3 6.3 6.3 6.3 建立模块的理由建立模块的理由建立模块的理由建立模块的理由 即使不经常使用模块,凭直觉也很可能会对可以放入模块的数据和子程序种类有所了解。 从某种意义来说,模块并不是人们的目标,它只是数据及对数据所进行的操作的集合,并且支 持面向对象的概念——抽象和封装。模块不支持继承性,因而它也并不完全支持面向对象编程, 描述它的这种有限的面向对象特性的词汇是 Booch 1991 年提出来的“基于对象”编程。 以下是一些适合使用模块的域: 用户接口用户接口用户接口用户接口。可以建立一个模块来把用户接口要素独立起来。这样,不 会 影响程序其它部分, 你就可以进行改进。在许多情况下,用户接口模块中往往包含有几个模块来进行诸如菜单操作、 窗口管理、系统帮助等。 对硬件有依赖的区域对硬件有依赖的区域对硬件有依赖的区域对硬件有依赖的区域。把 对 硬件有依赖的区域放入一个或几个模块中。这些区域常见的有: 与屏幕、打印机、绘图机、磁盘驱动器、鼠标等的接口。把这些对硬件有依赖的区域独立起来 可能帮助把程序移植到新环境下运行。设计一个硬件经常变动的程序时,这也是很有帮助的, 可以编写软件表模拟与特定硬件的交互作用,硬件不存在或不稳定时,让接口子程序与这些模 拟软件打交道。然后在硬件稳定时,再让接口子程序与硬件打交道。 输入与输出输入与输出输入与输出输入与输出。把输入/输出封装起来,可以使程序其余部分免受经常变动的文件和报告 格式的影响。把输入/输出放入模块,也使得程序很容易适应输入/输出设备的变动。 操作系统依赖部分操作系统依赖部分操作系统依赖部分操作系统依赖部分。把对操作系统有依赖的部分放入模块的原因与把对硬件有依赖部分放 入模块的原因是相同的。如果你正在编写一个在 Microsoft Windows 下运行的软件,为什么要把 它局限于 Windows 环境下呢?你完全可以把对 Windows 的调用放在一个 Windows 接口模块中。 如果以后想把程序移植到 Macintosh 或者 OS/2 环境下,你所要做的只是改动一下接口模块而已。 第六章 模块化设计 85 数据管理数据管理数据管理数据管理。应把数据管理部分放入模块中,让其中的子程序去与那些杂乱的实现细节打交 道。而 让模块外的子程序用抽象的方式与数据打交道,这 种 方 式应该尽可能避免实际处理问题, 如果你认为将数据管理模块化是将其放入一个单独模块中,那你就错了。通常,每一种主要的 抽象数据类型,都需要一个单独的模块来管理。 真实目标与抽象数据类型真实目标与抽象数据类型真实目标与抽象数据类型真实目标与抽象数据类型。在程序中,需要为每个真实目标创立一个模块。把这一目标所 需要的数据放入模块中,然后再在其中建立对目标进行模块化的子程序。这就是所谓抽象数据 类型。 可再使用的代码可再使用的代码可再使用的代码可再使用的代码。应把计划在其它程序中再用的程序部分进行模块化。建立模块的一个优 点是,重新使用模块要比重新使用面向功能的程序实用得多。在面向对象设计和面向功能设计 方法中,刚开始的项目都不能从以前的项目中借用许多代码,因为以前项目还不够多,无法提 供足够的代码基础。使用面向功能设计方法开发的程序,大约可以从以前的项目中借用 35%的 代码:而在使用面向对象设计方法开发的项目中,则大约可以从以前的项目中借用 70%的代码。 如果可以通过深思远虑而在以后的项目中避免重写 70%的代码,那为什么不这样做呢? 可能发生变动的相互联系的操作可能发生变动的相互联系的操作可能发生变动的相互联系的操作可能发生变动的相互联系的操作。应该在那些可能发生变动的操作周围修建一道隔墙。这 事实上是容错原则的一种,因为这样可以避免局部的变动影响到程序的其余部分。在 6.2 节中, 给出了一些经常发生变动的区域。 互相联系的操作互相联系的操作互相联系的操作互相联系的操作。最后,应把互相联系的操作放到一起。在绝大多数情况下,都可以发现 把看起来互相联系的子程序和数据放在一起的更强的组织原则。在无法隐蔽信息的情况下,比 如共享数据或计划增强灵活性时,仍然可以把成组操作放在一起,比如,三角函数、统计函数、 字符串操作子程序、图像子程序等。通过精心地成组放置相关操作,还可以在下一个项目中重 新使用它。 6.4 6.4 6.4 6.4 任何语言中实现模块任何语言中实现模块任何语言中实现模块任何语言中实现模块 有些语言直接支持模块化,但有些语言则需要补充一些编程标准才可以。 6.4.1 6.4.1 6.4.1 6.4.1 模块化所需的语言支持模块化所需的语言支持模块化所需的语言支持模块化所需的语言支持 模块包括数据、数据类型、数据操作以及公共和局部操作的区分等。为了支持模块化,一 种语言必须支持多种模块。如果没有多模块,其它任何要求都是空谈。 数据需要在三个层次上可以被存取和隐含,在局部,在模块中及在全局中,绝大多数语言 都支持局部数据和全局数据。如果想要使某些数据仅对模块中的子程序才是可以存取的,那么 就要求语言支持模块数据,即只有某些而不是全部子程序都可以存取的数据。 第六章 模块化设计 86 对于数据类型的可存取性和可隐含性的要求,与对数据的要求是类似的。某些类型应该隐 含在某一特定模块中,而另一些类型应该是对其它模块开放的。模块需要能够对那些可以知道 其它类型的模块进行控制。 对模块层次上的子程序的要求也与上述相类似。有些子程序应该只有在模块内部才能调 用,而且模块应该对某一子程序是专用的还是公用的可以进行控制。在模块之外,不应该有其 它模块或子程序知道这个模块中存在专用子程序。如果模块设计得很好,那么其它模块或子程 序不应该有任何理由来关心专用子程序的存在。 6.4.2 6.4.2 6.4.2 6.4.2 语言支持慨述语言支持慨述语言支持慨述语言支持慨述 在下表中,对几种语言支持信息隐蔽的必要结构进行了总结: 通用 Basic 和通用 Pascal 不支持多模块,所以被排在支持模块化的前列,Fortran 和 QuickBasic 不能控制支持模块化的数据类型。只有 Ada、C++和 C 以及 Turbo Pascal 才允许模块 限制对某一子程序的调用,从而使这个子程序真正是专用的。 简而言之,除非使用 Ada、C、C++或 Turbo Pascal,否则,就不得不通过命名或其它约 定来扩充所使用语言的能力,以便模拟使用模块。以下部分简单论述了直接支持模块化的语 言,并且将告诉你在其它语言中怎样模拟使用模块。 Ada 与与与与 Modula-2 支持支持支持支持 Ada 通过“包”的概念来支持模块化。如果用 Ada 编过程序,那么就已经知道如何建立包 了。Modula-2 通过模块的概念来支持模块化。虽然 Modula-2 并不是本书的特性,它对模块化 的支持仍然是非常直接的,以下给出了一个用 Modula-2 进行排序的例子: definition module Events; export EVENT, EventAvailable, HighestPriorityEvent, LowestPriorityEvent; 数据 数据类型 源程序 语言 多模块 局部 模块 全局 局部 模块 全局 专用 模块/全局 Ada · · · · · · · · · C · · · · · ++++ · · · C++ · · · · · · · · · Fortran77 ++++ · ++++ · ---- ---- ---- ---- · 通用 Basic ---- ---- ---- · ---- ---- ---- ---- · 通用 Pascal · · · · · · · · · Turbo Pascal ---- · ---- · · ---- · ---- · QuickBasic · · · ---- · ---- ---- ---- · 这些是公共的 第六章 模块化设计 87 type EVENT = integer; var EventAvailable:boolean; { true if an event is available } function HighestPriorityEvent:Event; function LowestPriorityEvent:Event; end Events; 面向对象的语言支持面向对象的语言支持面向对象的语言支持面向对象的语言支持 面向对象的语言,如 C++,对模块化的支持是直接的,模块化是面向对象编程的核心。以 下是一个 C++来实现排序的模块的例子: class Buffer { public; typedef intEVENT; BOOL Eventavailable; /*true if an event is available */ EVENT HighestPriorityEvent(void); EVENT LowestPriorityEvent(void); Private; ... }; Pascal 的支持的支持的支持的支持 某些版本的 Pascal,即 4.0 版和随后的 Turbo Pascal,利用单元的概念来支持模块化。“单 元”是一个可以包括数据、数据类型和子程序的数据文件。单元中有一个说明了可供模块外部 使用的子程序和数据接口。数据也可以被说明为在这个文件内部的函数和过程使用,而且是仅 在其内部使用。这为 Turbo Pascal 提供了可以在局部、模块和全局层次上的数据可存取性。以 下是在 Turbo Pascal 5.0 版中的排序模块形式: unit Events; INTERFACE type EVENT=integer; var EventAvailable:boolean;{ true if an event is available } 第六章 模块化设计 88 function HighestPriorityEvent: Event; function LowestPriorityEvnet:Event; IMPLEMENTATION ... ——文件中这部分的子程序数据,如果没有在上面 INTERFACE 中说明 的话,则对其它文件来说是隐蔽的 end.{unit Events} 属于 Generic Pascal 标准的 Pascal 实现并不直接支持模块化,不过你可以通过扩展它们来达 到模块化,这将在后面讨论。 CCCC 的支持的支持的支持的支持 虽然用 C 语言编程的程序员们并不习惯在 C 中使用模块,但事实上 C 也直接支持模块化。 每一个 C 源文件都可以同时含有数据和函数,可以把这些数据和函数说明为 Static,这将使它 们只在源文件内部才能使用。也可以不把它们说明为 Static,此时它们在源文件外也可以使用。 当每一个源文件都被当作模块时,C 就完全支持模块化了。 由于源文件和模块并不完全相同,你需要为每一个源文件创建两个标题文件——一个作为 公用、模块标题文件,另一个作为专用的、源文件标题文件。在源文件的公用标题文件中,只 放入公用数据和函数说明,下面是一个例子: /* File:Event.h 本文件仅包含公共的可用类型、数据和函数说明 Contains public declarations for the "Event" module. */ typedef int EVENT; extern BOOL EventAvailable; /* true if an event is available */ EVENT HighestPriorityEvent(void); EVENT LowestPriorityEvent(void); 而在源文件专用标题文件,只放入供内部使用的数据和函数说明。用#include 命令把标题 文件只放入组成模块的源文件中,不要允许其它文件含有它。以下是一个例子: /* File:_Event.h Contains private declarations for the "Event" module. */ /* private declarations */ ——这里是专用类型、数据和函数 ... C 源文件中,要使用#include 来把两个标题文件都包含在其中。在其它使用了 Event 模块 中的公用子程序模块中,用#include 命令来只把公用的模块标题包含进去。 如果在单独一个模块中,需要使用一个以上源文件,可能要为每一个源文件都加一个专用 标题文件;但对组成模块的子程序组,应该只加一个公共的模块标题文件。对于模块中的源 第六章 模块化设计 89 文件来说,利用#include 来包含同一个模块中其它源文件的标题文件是可以的。 Fortran Fortran Fortran Fortran 的支持的支持的支持的支持 Fortran90 为模块提供了全部支持。而 Fortran77 如果在规定范围内使用,则只对模块提供 了有限的支持。它拥有创建一组对数据具有独占存取权的机制,这个机制就是多重入口点, 对这种机制的随便使用,往往会产生问题。而如果小心使用的话,它则提供了一种不必使数 据成为公用的,就可以使一组子程序对同数据进行存取的途径。 可以说明数据为局部的、全局的或者是 COMMON 的。这个特性又提供了使子程序对数 据进行受限制存取的方法,定义一组组成一个模块的子程序。Fortran 编译程序无论如何也不 会认为它们组成了一个模块,但是从逻辑上来说,它们的确组成了一个模块。对模块中每一 个允许对数据进行直接操作的子程序,都应该用 COMMON 作为其前缀来命名。不要让模块 以外的子程序使用 COMMON 作为前缀。利用编程标准来弥补程序语言不足这一办法,将在 后面讨论。 6.4.3 6.4.3 6.4.3 6.4.3 伪模块化伪模块化伪模块化伪模块化 在像通用 Basic,通 用 Pascal 和其它既不直接也不间接支持模块化的语言中,该如何进行模 块化呢?答案在前面已经提到过了,利用编程标准来代替语言的直接支持。即使你的编译程序 并不强制你采用好的编程应用,还可以采用能够达到这一目的的编码标准。以下的讨论涉及了 模块化所要求的每一方面。 把数据和子程序装入模块 这个过程的难易程度取决于编程语言,即它是允许使用各种源文件的还是要求所有的代码 必须是在一个源文件中的?如果你需要用 10 个模块,那就创建10 个源文件。如果环境要求所 有代码都要在一个源文件中,那就按模块把代码划分为几部分,同时用注释来区分每一个模块 的开头和结尾。 保证模块的内部子程序是专用的 如果所采用的程序语言不支持限制内部子程序的可见性,使得所有的子程序对其它子程序 来说都是公用的,可以利用编码规定来限定只有标明公用的子程序才能被模块外的子程序使用。 不管是什么样的编译程序,下面都是你可以做的: 通过在说明的地方加注释,来明确区分公用和专用子程序。明确区分与每个子程序相关的 模块。如果别人不得不通过查询子程序说明来使用子程序,要确保在说明中注释了它是公用的 还是专用的。 不允许子程序调用其它模块的内部子程序。使用注释把属于同一个模块的子程序联系到一 起。 采用命名约定来表明一个子程序是内部的还是外部的。可以让所有的内部子程序名称前面 都带有下划线(_)。虽然这种方法无法区分不同模块的内部子程序,但是绝大多数人还是可以 区分一个子程序是否是属于它们自己的模块。如果它不是自己模块中的子程序,那么显然它是 其余模块中的子程序。 第六章 模块化设计 90 采用表明它是内部的还是外部的命名规定采用表明它是内部的还是外部的命名规定采用表明它是内部的还是外部的命名规定采用表明它是内部的还是外部的命名规定。所采用约定的细节,往往取决于编程语言所赋 予你对子程序命名的灵活性。例如,一个 DataRetrieval 模块的内部子程序名称可能会以 dr_作 为前缀。而 UserInterface 模块内部子程序的名称前缀则可以是 ui_。而 属于同一模块的外部子程 序则分别会以 DR_和 UI_作为前缀,如果对名称的字节长度是有限制的(如,ANSI FORTRAN77 中的 6 个字母),那 么命名的约定就会用掉其中相当一部分,这时,在制定命名约定时就要注意 这一长度限制。 保证子程序的内部数据是专用的保证子程序的内部数据是专用的保证子程序的内部数据是专用的保证子程序的内部数据是专用的。 保证模块层次上的内部数据的专用性与保证模块层次上子程序的专用性是相似的。通常, 要采用清楚表明只有特定数据才能在文件外部使用的编码标准。不管编译程序允许你做的是什 么,以下是一些可以采用的步骤: 首先,利用注释来说明数据是公用的还是专用的。明确区分模块所涉及到的每一个数据的 可存取性。 其次,不允许任何子程序使用其它模块中的专用数据,即使编译程序把此数据作为全局变 量也不可以。 第三,采用可以使你注意一个文件是专用还是公用的命名约定。为了保持连贯性,应该使 这一命名原则与子程序的命名原则类似。 第四,采用表明数据属于哪一个模块,即它是外部还是内部的命名约定。 6666....4444....4 4 4 4 检查表检查表检查表检查表 模块的质量模块的质量模块的质量模块的质量 · 模块是否有一个中心目的? · 模块是否是围绕着一组公用数据进行组织的? · 模块是否提供了一套相互联系的功能? · 模块功能是否足够完备,从而使得其它模块不必干预其内部数据? · 一个模块相对其它模块是否是独立的?它们之间是松散耦合的吗? · 一个模块的实现细节,对其它模块来说,是隐含的吗? · 模块的接口是否抽象到了不必关心其功能实现方式的地步?它是作为一个黑盒子来设 计的吗? · 是否考虑过把模块再划分为单元模块?是否对其进行了充分的再划分工作? · 如果用不完全支持模块的语言编程,你是否制定了编程约定以使这种语言支持模块? 6666....5 5 5 5 小小小小 结结结结 · 不管调用哪一个,子程序与模块的不同是很重要的,要认真考虑子程序与模块的设计。 · 从模块数据是被几个子程序使用的这一角度来说,它与全局数据是相同的,但从可以 使用它的子程序是有限的,而且清楚地知道是哪些子程序可以使用它这一角度来说, 模块数据与全局数据又是不同的。因此,可以使用模块数据而没有全局数据的危险。 第六章 模块化设计 91 · 信息隐蔽总是有益的。其结果是可以产生可靠的易于改进的系统,它也是目前流行的 设计方法的核心。 · 创建模块的原因有许多是与创建子程序相同的。但模块概念的意义要比子程序深远得 多,因为它可以提供一整套而不是单独一个功能,因此,它是比子程序更高层次的设 计工具。 · 可以在任何语言中进行模块设计。如果所采用的语言不直接支持模块,可以用编程约 定对其加以扩展,以达到某种程度的模块化。 第七章 高级结构设计 92 第七章第七章第七章第七章 高级结构设计高级结构设计高级结构设计高级结构设计 目录目录目录目录 7.1 软件设计引论 7.2 结构化设计 7.3 面向对象 7.4 对目前流行设计方法的评论 7.5 往返设计 7.6 小结 相关章节相关章节相关章节相关章节 高质量子程序的特点:见第 5 章 高质量模块的特点:见第 6 章 软件结构设计:见 3.4 节 有些人可能认为高层次设计不是真正的实现活动,但是在小规模项目中,许多活动都被认 为是实现活动,其中包括设计。在一些较大的项目中,一个正式的结构设计可能只是分解系统, 而其余大量的设计工作往往留在创建过程中进行。在其它的大型项目中,设计工作可能是非常 详细的,以致于编码不过是一项机械的工作而已,但是设计很少有这样详尽的。编码人员常常 要进行一些设计工作。 高层次设计是一个很大的主题,同时也由于它只部分地与本书主题有关系,因此,我们将 只论述其中的几个方面。模块设计和子程序设计的好坏在很大程度上取决于系统的结构设计好 不好,因此,要确保结构设计先决条件(如 3.4 节所述)已经被满足了。也有许多设计工作是 在单个子程序和模块层次上进行的,这已在四、五、六章中论述过了。 如果已经对结构化设计和面向用户设计非常熟悉了,你可能想阅读下一部分的介绍,再跳 到 7.4 节关于两种技术的比较,最后阅读 7.5 节。 7.1 7.1 7.1 7.1 软件设计引论软件设计引论软件设计引论软件设计引论 “软件设计”一词的意思是指,把一个计算机程序的定义转变成可以运行程序的设计方法。 设计是联系要求定义和编码与调试的活动的桥梁。 它是一个启发的过程而不是一个确定的过 程,需要创造性和深刻的理解力。设计活动的绝大部分活动都是针对当前某特定项目进行的。 7.1.7.1.7.1.7.1.1111 大型和小型项目设计大型和小型项目设计大型和小型项目设计大型和小型项目设计 在大型的、正规的项目中,设计通常是与需求分析、编码等活动分立的,它们甚至可能是 由不同人分别完成的。一个大型项目可能有几个级别的设计工作——软件结构设计、高层次模 块设计和实现细节设计。结构设计具有指导的意义,在小规模编码阶段往往也是很有帮助的。如 第七章 高级结构设计 93 果进行了通用或高层次设计,那么其指导方针则在大规模编码阶段是非常有意义的。不管是出 于什么原因,即使名义上设计工作已经结束了,但事实上它远没有停止。 在小型的、非正式的项目中,大量的设计工作都是由程序员们坐在键盘前面完成的。“设计” 可能只是用程序语言编程,用 PDL 来编写子程序。也可能是在编写子程序之前画一下流程图。 不管是怎样进行的,小型项目与大型项目一样,都会从良好的设计工作中受到大量好处。而使 得设计活动明显化,则会极大地扩展这一好处。 7777.1..1..1..1.2 2 2 2 设计的层次设计的层次设计的层次设计的层次 软件系统中,设计是在几个不同层次的细节上进行的。有些技术适用于所有的层次,而另 外一些,则往往只会适合其中的一或两个层次,下面是这些层次: 层次层次层次层次 1111:划分成子系统:划分成子系统:划分成子系统:划分成子系统 在这一层次上,设计的主要成果是划分系统的主要子系统。这些子系统可能是很大的—— 数据库接口、用户接口、命令解释程序、报告格式程序等等。这一层次的主要设计活动是如何 将系统划分成几个主要要素,并且定义这些要素间的接口。在这一层次上的划分工作主要是针 对那些耗时在几天以上的项目。在图 7-1 中,设计是由三个主要元素和它们的交互作用组成的。 其中程序划分为系统(l),子系统更进一步划分为模块(2),一些模块划分为程序(3), (4)为每个子程序内部设计。 “子程序”和“模块”的特定意义已经在前几章中引入了。“Subprogram”一词将在本章中 使用,我们可以称它为“亚程序”,它是指小于整个程序的任何程序,其中也包括子程序和模块。 层次层次层次层次 2222:划分成模块:划分成模块:划分成模块:划分成模块 这一层次的设计包括识别系统中的所有模块。在大型系统中,在程序分区层次上划分出来 第七章 高级结构设计 94 的子系统往往太大,难以直接翻译成代码。例如,一个数据库接口子系统可能非常复杂,需要 有十几个子程序来实现。如果出现这种情况,那么还需要将这个子系统再划分为模块:如数据 存储、数据恢复、问题解释等模块。如果分解出来的模块还是太复杂,那么将对它再次划分。 在许多程序的设计中,在层次 1 中分解出来的子系统将直接转变成层次 2 中的模块,而不再区 分这两个阶段。 在定义模块时,同时也要定义程序中每个模块之间的相互作用方式。例如,将定义属于某 一模块的数据存取函数。总之,这一层次的主要设计活动是确保所有的子系统都被分解成为可 以用单个模块来实现它的每一部分。 与把一个系统分解成子系统一样,把子系统分解成模块也是主要针对耗时在几天以上的项 目的。如果项目很大,这一层次的分解活动是与上一层次严格区分的。如果项目较小,层次 1 与层次 2 可能是同时进行的。在图 7-1 中,分解为模块的活动已经包含于每个元素中了。正如 图中所示的那样,对于系统不同部分的设计方法是也不同的。对某些模块之间关系的设计可能 是以网络思想为基础的,也可能是以面向对象思想为基础的。如同图中左面两个子系统那样。 而其它模块的设计则可能是分级的。如图中右侧的子系统那样。 层次层次层次层次 3333:划分成子程序:划分成子程序:划分成子程序:划分成子程序 这个层次的设计包括把每个模块划分成各种功能,一旦一个子程序被识别出来,那么就同 时规定它的功能。由于模块与系统其它部分是相互作用的,而这一作用又是通过功能子程序进 行的,所以,模块与系统其余部分的作用细节是在这一部分规定的。例如,将严格定义如何调 用解释程序。 这一层次的分解和设计工作对于任何耗时超过几个小时的项目都是需要的,它并不一定需 要被正式地进行,但起码是要在心中进行。在图 7-1中,在左上角的一组中的一个模块中,给 出了划分成子程序的工作活动。当你揭开黑盒子的盖子时,如同图 7-l 中标有 3 的模块,你可 以发现由模块提供的功能是由层次组织的子程序组成的。这并不是意味着每个黑盒子中都含有 层次结构,事实上只有某些黑盒子中才有。其余组织形式的子程序可能没有或很少有层次结构。 层次层次层次层次 4444:子程序内部的设计:子程序内部的设计:子程序内部的设计:子程序内部的设计 在子程序层次上的设计,包括设计单个子程序中的详细功能等。子程序内部设计往往是由 程序员进行的。这一设计包括编写 PDL,在参考书中寻找算法,在子程序中组织代码段落,编 写编程语言代码等活动。这种层次的工作在任何一个项目中都是要进行的,无论是有意识的还 是无意识的,是作得好还是作得坏。如果缺少了这一层次的工作,任何程序都不可能产生。在 图 7-1 中,在标有(4)的一组中,表现了这个层次的工作。 7.1.3 7.1.3 7.1.3 7.1.3 创建中的设计工作创建中的设计工作创建中的设计工作创建中的设计工作 对于设计层次的讨论,是论述本章其余部分内容的前提,当人们在提到“设计”一词时, 他们事实指的可能是许多不同的活动。这是一个非常重大的主题,也是非常重要的。以下是关 于这一活动,但这次是按照从细节到总体的顺序进行的。 第七章 高级结构设计 95 内部子程序设计内部子程序设计内部子程序设计内部子程序设计 在第四章中间明确讨论过内部子程序设计问题,在第五章“高质量子程序的特点”中,对 这一问题又作了进一步的讨论。在第五章关于数据和数据控制部分中,从个别程序语句和子程 序中模块的层次,对这一问题进行了讨论。在本章中,这一问题的讨论主要分布在各个部分中。 划分成子程序划分成子程序划分成子程序划分成子程序 在结构化设计方法中,“设计”往往是指设计程序的结构,而不指单个子程序内部的设计。 关于程序结构问题,完全可以写一本专著,本章中论述的只是构造简单程序集合的技术总结, 这些子程序集合将是你在设计中经常要实现的。 划分成模块划分成模块划分成模块划分成模块 在面向对象设计中,“设计”指的是设计一个系统中的不同模块。模块定义也是一个很大的 足以写成一部书的主题,在第六章中已经论述过了。 划分成子系统划分成子系统划分成子系统划分成子系统 对于中小型程序来说(一般是10000 条语句左右),定义模块和子程序的技术往往隐含在整 个程序的设计中,在更大一些程序中,往往需要特殊的设计方法,这对于本书来说(本书重点 是实现)是难以详尽论述的。在许多情况下,特别是在一些小项目中,设计工作是键盘上完成 的,因此事实上是一种实现活动,虽然它应该在早期就完成了。本书之所以涉及了模块和子程 序设计,是因为它们处在实现的边缘上。而关于程序划分的其它讨论,则不在本书之列。 7.2 7.2 7.2 7.2 结构化设计结构化设计结构化设计结构化设计 结构化设计这一概念是1974年在《IBM 系统日报》(IBM System Journal)一篇论文中出现 的。在后来由 Ed Yourdon 和 Larry Constantine 写进《Structured Design:Fundamentals of a Discipline of Computer Program and Systems Design》书 中( 1 971),对 其作了全面补充与扩展。 Constantine 是最初那篇论文的作者之一,而“自顶向下设计”一词则是指一种非正式的结构化设计,类似 的词还有“逐步求精”和“分解”等,指的基本都是同一意思。结构化设计是与其它结构化设 计方法一道使用的。 结构化设计是由以下部分组成的: • 系统组织,系 统将被设计成几个黑盒子,明确定义的子程序和模块、接口的实现细节对 其它子程序来说都是隐含的。 • 开发设计的策略。 • 评估设计准则。 • 关于问题的明确说明,这是解决问题的指导原则。 • 表达设计的图形和语言工具,包括 PDL 和结构图。 在下面的内容中,将对这些内容作比较详细的论述。 第七章 高级结构设计 96 7777.2.1.2.1.2.1.2.1 选择需进行模块化的要素选择需进行模块化的要素选择需进行模块化的要素选择需进行模块化的要素 在前面几章论述了程序和模块相关好坏的标准,并提供了确定子程序和模块质量的检查表, 但并没有给出识别子程序和模块的方法,在本节中,将论述这一问题的指导原则。 自顶向下分解自顶向下分解自顶向下分解自顶向下分解 把程序分解为子程序的一种流行方法是自顶向下分解,也称为自顶向下设计或逐步求精。 其特点是从关于程序功能的粗略说明出发,逐步推进到程序要做的每一项特定的工作。从粗略 的层次出发往往是指从程序中的“主要”子程序出发,通常,把这个子程序画在结构图的顶部。 以下是几项在进行自顶向下分解时要牢记的原则: • 设计高层次。 • 避免特定语言细节。从设计中,不应该看出打算在程序中使用什么语言,或者说当在设 计中更换要用的语言时,不会产生任何麻烦。 • 暂时不指出下一层次的设计细节(与信息隐含类似)。 • 正规化每个层次。 • 检验每个层次。 • 转移到下一个层次,进行新的求精工作。 自顶向下设计指导原则的依据是:人脑一次只能考虑有限数量的细节。如果你从一个较简 略的子程序开始,逐步把它分解成更加详细的子程序,就不必每次考虑过多的细节。这种方法 也常称之为“分而治之”战术。它对于分层结构往往是最有效的。如图 7-2 所示。 从两个方面来看,这种分而治之战术是一个逐次迭代逼近的过程,首先,这是由于往往在 一次分解之后你并不会马上停止,还要进行下几个层次的分解。其次,是由于分解并不是一蹴 而就的,采用某种方法分解一个程序,再看一下效果,然后,又用另外一种方法来分解这个程 序,看效果是否会好些,在几次尝试之后,就有了一个很好的办法,同时也知道为什么这样做。 需要把一个程序分解到什么程度呢?要持续不断地分解,直到看起来下一步进行编码要比 再分解要容易为止,或者到你认为设计已经非常明了详细,对再分解已经感到不耐烦为止,到 这时,可以认为分解已经完成了。由于你比任何人都熟悉这个问题,而且也比任何人都清楚, 因此,你要确保其解决方案是很容易理解的,如果连你都对解决方案有些困惑的话,那么,试 想一下,又有谁会理解它呢? 自底向上合成自底向上合成自底向上合成自底向上合成 有时候,自顶向下方法过于抽象以至于让人不知从何下手。如果想要进行一些具体的工作, 那么可以试一下自底向上的设计方法,如图 7-2 所示。你可以问自己,“这个系统需要做什么?” 毫无疑问,你能够回答这个问题。你可以识别出系统需要具备的较低层次的功能,例如,你可 能知道这个系统需要进行报告格式化、计算报告总数、用不同颜色在屏幕上显示字母等等。当 你识别出某些低层次功能后,再从事较高层设计可能会有把握些。 以下是一些在进行自底向上合成时要牢记的原则: • 问自己,关于系统要做什么你都知道哪些? • 利用这一问题识别出某些低层次功能。 第七章 高级结构设计 97 • 识别出这些低层次功能共同的方面,将其组合到一起。 • 向上一个层次,进行同样的工作,或回到顶端开始自顶向下。 其中自顶向下是从一般到特珠,自底向上是从特殊到一般。 自顶向下与自底向上自顶向下与自底向上自顶向下与自底向上自顶向下与自底向上 自底向上与自顶向下策略的首要区别是前者是合成,而后者则是分解。一个是从可控制的 零散问题出发,把它们合成到一起从而获得一个总体解决方案,而另一个则从总体问题出发, 将其分解成可控制的零散问题。两种方法各有优缺点。在实际使用时要详加比较。 自顶向下设计方法的特点是比较简单,人们往往擅长把大问题分解成小问题,而程序员们 则更是擅长这点。当一个问题是用层次结构模型化的时候,自上而下的分解方法恰好与其相符。 自顶向下设计的另一个优点是你可以避开实现细节。由于系统常常受到实现细节变动的干 扰(比如文件结构或报告格式的变化),因 此把这些细节隐含在层级结构的底部,而 不 是 让它在 顶部起支配作用,是非常有益的。 这种设计方法也有它的缺点。其中之一是系统的总体功能可能是很难识别的。关于系统所 作的最重要决定之一就是如何进行第一步分解工作,而在自上向下设计中,刚开始接触系统, 对其了解还很少时,便不得不做出这一决定,这是很危险的。它的另一个缺点是:由于许多系 统本身并不是层级结构的,因此是很难清晰地分解。或许这种设计方法的最大缺点就是它要求 系统在顶层要有一个单一而又清楚的功能,而对于现代事件驱动系统来说,这是很难想象的。 自底向上设计方法的优点是它在早期就可以识别出有用的功能子程序,结果是坚实可靠的 设计。如果已经开发了相似的系统,那么可以参阅一下旧系统,看看有什么可以借用的。 这种文件的弱点是很难单独地使用它。因为大多数人都不善于从小的概念出发形成综合 的设想。这就像一个自己组装的玩具,我想我已经组装完了,怎么盒子中还有零件呢?好在, 你不必单独使用它。 它的另一个弱点是,有时从你识别出的细节出发,无法建造出整个程序,就像你无法用砖头 造出一架飞机一样。而 且当你知道在底部需要什么功能时,你可能已经不得不进行顶层设计了。 第七章 高级结构设计 98 不过,这两种方法并不是互相矛盾的。设计是一个启发的过程,就是说没有一种百试不爽 的设计方法,它总是一个尝试的过程。因此,在找到好方法之前,尽可以大胆尝试,可以用自 顶向下工作一会儿,再用自底向上工作一会儿。 设计也是一个逐次迭代逼近的过程。因此,你在第 n 次用自底向上方法学到的东西,将在 第 n+l 次用自顶向下方法设计时起到很大帮助作用。 7.37.37.37.3 面向对象面向对象面向对象面向对象 面向对象设计方法的特点是通过对实际问题的分析,从中抽象出目标,然后再用程序语言 来表现它,其过程主要是:识别目标中的分目标并识别出对于分目标的操作,然后再根据分目 标的操作开发出一个系统。面向对象设计是在程序中设计目标或模块的一种方法。在较低的程 度上说,它也是设计单个子程序的一种方法。 虽然有些鼓吹者把计算机历史划分为面向对象设计出现前阶段和面向对象设计出现后阶 段,但事实上面向对象设计与其它设计方法并不冲突。特别地,面向对象设计与结构化编程所 提供的低层次结构化并不是不兼容的,但它与高层次结构化的确不兼容。在更高的层次上,面 向对象设计方法在简单的功能性层次结构上,添加了类、群和非层次结构等新的概念。对这些 高层次的组合思想进行研究和标准化工作,将会使编程技术再向前产生一次飞跃。 在本书,对于面向对象设计的讨论是非常浅显的。与结构化设计方法相比,面向对象设计 的抽象化程度更高。本节着重论述的只是在较低层次上起作用的抽象方法,其中主要是在个别 语句、子程序和有限数量的子程序这个层次上的。这种设计方法相对来说也是一种新的设计理 论。它还没有完全成熟,关于这方面积累的设计经验也还不够丰富,但是,它是很有前途的。 7.3.17.3.17.3.17.3.1 关键思想关键思想关键思想关键思想 面向对象设计是建立在如下主张之上的,即:一个程序模型越是真实地反映了实际问题, 那么,由此产生出的程序质量越好,在多数情况下,关于项目的数据定义要比功能稳定得多, 因此应象面向对象设计一样,根据数据来进行设计,这可以使设计更稳定。对于现代编程来说, 面向对象设计中有许多思想是很重要的。 抽象抽象抽象抽象 抽象所带来的主要好处是可以忽略掉无关紧要的细枝末节问题,而专注于重要的特性。绝 大多数现实世界中的目标都是抽象的,房屋是木材、钉子、玻璃、砖和水泥等的抽象,是把它 们组织起来的一种特殊形式。而木材本身,则又是纤维、细胞及某些矿物质的抽象,而细胞呢, 则又是各种各样的物质分子的抽象。 在建造房屋时,如果你从分子层次上与木材、钉子等打交道,是永远不可能建成房屋的。 同样,在建造一个软件系统时,如果总是在相当于分子的层次上工作,那是不可能建成软件系 统的。在面向对象设计中,你尽力创造出与解决真实问题某一部分抽象程度相同的编程抽象, 以便解决编程问题中的类似部分,而不是用编程语言实体来解决问题。 面向对象设计擅长使用抽象,但因为它所使用的“智力砖块”,要比结构化设计中功能方 法所使用的“智力砖块”大得多。在结构化设计中,抽象的单位是函数;而在面向对象设计中, 第七章 高级结构设计 99 抽象的单位是目标。由于目标包括函数及受函数影响的数据,从而使得在比函数更高层次上对 问题进行处理成为可能。这种抽象能力使你可以在更高层次上对问题进行考虑,而且,不必把 神经绷得太紧,就可以一次考虑很多问题。 封装封装封装封装 封装是对抽象不存在地方的补充。如果抽象对你说“你应该在较高层次上看一个目标”,而 封装则会说“你只能在这个层次上看一个目标”。这事实上就是 6.2 节所述的信息隐蔽的重复。 你对于一个模块所知道的只是它让你知道的那些,别的什么也没有。 我们继续用房屋比拟来说明问题:封装是一个使你可以看到房屋的外表但不能走进去的办 法,当然,或许你可以透过窗户看到一小部分内部情况。在较为过时的语言中,信息隐蔽完全 是自愿的行为,因为大门上没有“禁止入内”的标志,房门没有上锁,窗户也是敞开的,而对 于 Ada 语言来说,信息隐蔽则是强制性的:门被牢牢地锁上了,窗户紧闭,而且警报系统也在 工作,你所看到的就是你所得到的,而且是你得到的一切。 模块化模块化模块化模块化 面向对象设计中的模块与结构化设计中模块的含义是一致的。相联系的数据和功能被放 入模块,在理想情况下,模块是高度内聚而又松散耦合的。同信息隐蔽一样,当模块内部发生 变化时,其接口保持不变。 层次结构和继承性(层次结构和继承性(层次结构和继承性(层次结构和继承性(inheritance)))) 在设计软件系统时,你经常可以发现两个之间非常相似,其差别非常小的目标。例如,在 一个会计系统中,你可能既要考虑正式雇员,也要考虑兼职雇员,与两种雇员相联系的绝大多 数数据都是相同的,但也有一小部分是不同的。在面向目标编程中,你可以定义一种广义雇员, 把正式雇员当作一种广义雇员,但二者之间稍有差别。把兼职雇员也看作一种与种类无关的广 义雇员,那么这种操作就按广义雇员模式进行。如果某种操作是与雇员种类有关的,那么就按 雇员种类不同,分别进行操作。 定义这种目标间的共同和不同点称为“ 继承性”,因 为 兼职和正式雇员都从广义雇员那里继 承了许多特点。 继承策略的好处是它与抽象的概念是一致的,抽象在不同层次的细节上与目标打交道。在 某一层次上调用某种分子;在下一个层次上调用纤维与植物细胞的组合,在最高层次上调用一 片木头。木头有某种特性(如你可以用锯子锯它或用斧头劈它),不管是松木还是加州红杉木, 它们有许多共同的特性,如同它们有不同特性一样。 在面向对象编程中,继承性简化了编程,因为你只要写一个通用子程序来处理目标间的共 同特性,再编写几个专用子程序去处理它们间的不同特性就可以了。有些操作,例如 Getsize(), 可能在任何抽象层次上都适用。程序语言支持 Getsize()这种直到运行时才需要知道操作对象的 子程序特性称为“ 多形性”。像 C++等面向对象的语言,自动支持继承性和多形性。而以目标为 基础的语言,如 Ada 和过程性语言如 C 和 Pascal 则不支持这种特性。 第七章 高级结构设计 100 目标与类目标与类目标与类目标与类 在面向对象设计中,最后一个关键概念是目标与类。目标是程序在运行时其中的任何一个 实体,而类则是当你看程序清单时存在的一个静态实体。目标是在程序运行时具有特定值和属 性的动态实体。例如,你可以定义一个具有名字、年龄、性别等属性的人,而在运行时则会遇 到 Nancy,Tom 等人,也就是说,目标是类的特例,如果你对数据库术语很熟悉的话,它们的 区别与其中“模式”与“事例”的区别是类似的,在本节其余部分将不严格区分这些词,常会 把两种实体都称为“目标”。 7.3.2 7.3.2 7.3.2 7.3.2 面向对象设计的步骤面向对象设计的步骤面向对象设计的步骤面向对象设计的步骤 面向对象设计的步骤是: • 识别目标及其属性,它往往是数据。 • 确定对每个目标可以做些什么。 • 确定每一个目标可以对其它目标做些什么。 • 确定每个目标对其它目标来说是可见的部分——哪一部分是开放的,哪一部分是专用 的。 • 确定每个目标的公共接口。 这些步骤下一定非要按某一特定顺序来进行,但是却需要重复。逐次迭代逼近对面向对象 设计是与其它设计方法同样重要的,下面将分别论述这些步骤。 识别目标及其属性识别目标及其属性识别目标及其属性识别目标及其属性。计算机程序通常是以客观世界实体为基础的,例如,你可以用客观世 界中的雇员,时间卡及帐单为基础来建造一个时间——记帐系统。图 7-3 中表示了从面向对象 观点来看,这一系统的表现形式。 在图形系统中的目标将包括:窗口、对话框、按钮、字体、绘图工具等。对问题域进行进 一步研究,可能会发现比这种一对一方式更好的软件目标识别方法,但是,从客观世界中的真 实目标出发总是一个比较好的方法。 识别目标属性并不比识别目标困难。每一个目标都有与计算机程序相关联的特性。例如, 在时间——记账系统中,一雇员目标将具有姓名、标题和记账率;顾客目标将有姓名、支票地 址、收支状况及存款余额等;账单目标具有欠账数量、顾客姓名、日期等等。 图中目标的图形符号与第六章讲述的模块符号类似。 确定可以对一个目标做些什么确定可以对一个目标做些什么确定可以对一个目标做些什么确定可以对一个目标做些什么。对每一个目标都可以进行一系列操作,在时间——记账系 统中,雇员的记账率等可以变动,可以要求提供雇员的奖励情况等,顾客目标可以更改其存款 额或地址等。 确定目标之间可以互相做些什么确定目标之间可以互相做些什么确定目标之间可以互相做些什么确定目标之间可以互相做些什么。这一步骤与其字面意义是相似的。在时间——记账系统 中,雇员可以对其它目标做些什么呢?做不了什么。雇员所能做的一切便是输入时间卡信息。 而账单则可以接受时间卡信息,在更复杂的系统中,其它相互作用更为明显。 确定每一个目标中对其它目标来说是可见的部分确定每一个目标中对其它目标来说是可见的部分确定每一个目标中对其它目标来说是可见的部分确定每一个目标中对其它目标来说是可见的部分。在设计中的一个关键问题就是决定目标 的哪些部分应该是可见的,哪些部分应该是隐蔽的。对于数据和功能来说,都要做出这种确定。 在表示时间——记账系统的图 7-2 中只表示出了可见的部分,隐蔽的部分则被藏在黑 第七章 高级结构设计 101 图 7-3 四个主要目标组成的记帐系统 盒子中。顾客与雇员目标看起来是非常复杂的,因为它们每一个都具有七八个可见的特性。这 种复杂的表现形式是图示法的一个弱点,这种情况会随着可见特性的增加而恶化。而一个精心 设计好的目标往往有许多附加的可见特性。 定义每一个目标的接口定义每一个目标的接口定义每一个目标的接口定义每一个目标的接口。在设计目标中的最后一个步骤是为每个目标定义一个正式的、语 法的、在程序层次上的接口。这包括由目标提供的功能和目标与类之间的继承关系。特别地, 这一步将包括函数和子程序说明。例如:时间卡的接口(用 C++编写)可能是这样的: class TimeCard { public: int Enter ( EMPLOYEE_ID Employee, DATE Date, CLIENT_ID Client, PROJECT ProjectCode, int Hours ); 第七章 高级结构设计 102 int Retrieve ( int& Hours, DATE& Date, CLIENT_ID& Client, PROJECT ProjectCode, EMPLOYEE_ID Employee ); protected: … }; 当你进行完这一步的工作,到达高层次的面向对象系统组织时,可以用两种方法进行迭代, 以便得到更好的目标——类组织。你也可以对定义的每一个目标进行迭代,把设计推向更详细 的层次。 7.3.3 7.3.3 7.3.3 7.3.3 典型面向对象设计的评论典型面向对象设计的评论典型面向对象设计的评论典型面向对象设计的评论 一个面向对象系统通常有至少四类目标。如果你不知道这其中每类目标的一些情况,你很 可能会漏掉某类目标。 • 问题域要素。这个要素直接指向问题域,在前述的记账系统中,问题域包括客观世界中 的目标,如:雇员、顾客,时间卡和账单等。 • 用户接口要素。这个要素指的是系统中负责人机交互的部分。它包括数据入口类型、窗 口、对话框、按扭等等。正如 6.2 节中提到的那样,最好把系统的用户接口部分隐蔽 起来以适应修改。 • 任务管理要素。这类要素指的是计算机本身的目标。包括实时任务管理程序、硬件接口、 通讯协议等。 • 数据管理要素。这部分要素包括保持一致的数据。它包括数据库以及其相联系的所有存 储、维护和检索等功能。 7.4 7.4 7.4 7.4 对目前流行设计方法的评论对目前流行设计方法的评论对目前流行设计方法的评论对目前流行设计方法的评论 如果你仔细观察日前流行的设计方法——包括结构化设计和面向对象设计——你会发现每 种方法都包括两个主要部分: • 把一个系统分解成子系统的准则 • 解释分解的图形式语言符号 • 有些方法还包括第三个要素 • 防止你使用其它方法的规定 把“设计”的意义限制在前两个要素上说明设计的核心是把系统分解成亚程序 ( Subprogram ) ,同时也说明亚程序的设计并不具备足够的挑战性,不值得讨论。 第七章 高级结构设计 103 一个好的系统分解的确是很有价值的,但并不是说一旦确立了好的结构,设计就可以停止 了。在确认出子程序的模块之后,还有许多设计工作要做。 伴随着某些设计方法的第三个要素,即强调应该只使用一种方法的思想,是非常有害的。 没有一种方法囊括了设计系统所需的全部创造力和洞察力。强调使用一种方法将破坏设计中的 思维过程。 但是,设计方法的选择往往会成为一种宗教似的问题——你去参加了一个宗教复兴会议, 听一些结构化设计的先知讲道,然后你回到自己的圣地在石碑上写下一些神圣的程序,从此以 后,你不再允许自己进入非基督教的领域。你应该知道,软件工程不是宗教,不应该引人宗教 的狂想性。 如果你是个建筑工人,你不会用同样的方法建造每一幢建筑物。在周一你在浇注水泥,而 到了周末你则在修建一幢木屋。你不会用水泥来修木屋,也不会在一幢新建好的摩天大楼门口 挂上“成人禁止入内”的牌子。你会根据不同的建筑物而采取不同的施工方法,从建造房子中, 你应该得到关于编程方法选择的启示,应该选择与问题相适应的方法,这种世俗方法的合理性 已经被许多例子和研究所证明。每种方法都有其优点,但同时也有其弱点,应分析使用(Peter 和 Tripp 1977,Mannino 1987,Kelly 1987,Loy 1990)。 但是,有些方法的障碍是由它们自己的复杂的术语产生的。比如,你想学习结构化设计, 你必须懂得如下词汇:输入流与模块、输出流与模块、正规、控制、公用、全局和内容耦合、 功能的、顺序的、通讯的、过程的、临时的、逻辑的和偶然性内聚。输入、输出、事务处理中 心、事物处理分析和事物处理模块,甚至 Benuzerfreundlichkeit(多可怕!)一词也出现了。字 典也无法给出这些词的解释。 结构化设计,以隐蔽信息为目标的设计和面向对象设计等方法提供了看问题的不同角度。 图 7-4 给出了使用它们的典型方法。 从事结构化设计的人与从事面向对象设计的人会发现他们进行交流非常困难,原因是他们 没有意识到是在不同层次上讨论设计的,因此主题也是不同的。从事结构化设计的 Tom 说:“我 想这个系统应该分解成 50 个子程序。”面向对象设计的 Joh 则说:“我认为系统应划分成 7 个目 标”。如果你仔细观察,可能会发现这 7 个目标中可能共含有 50 个子程序,而 Tom 的子程序或 许可以分成 7 组。 7.4.1 7.4.1 7.4.1 7.4.1 何时使用结构化设计何时使用结构化设计何时使用结构化设计何时使用结构化设计 结构化设计主要是一种把程序分解成子程序的方法。它强调功能但不强调数据。一个面向 功能的问题域的例子是一个以批处理方式读入数据,按照可以预计的顺序对数据进行可以预计 的处理并且写数据。 结构化设计并没有把子程序组成一个协同工作子程序组的概念,也没有子程序内部设计的 概念,除非这个子程序的内部会影响到整个系统。因此,结构化设计非常适用于具有许多互不 作用的独立功能的系统。同时,它也适于那些只有几百行代码的小型程序,因为这些程序过于 简单,没有建立类、目标和属性的必要。 结构化设计的最先提出者 Larry Constantine,曾经发表过一篇“目标、函数和程序可扩展性” 的文章,论述了把结构化设计和面向对象设计组合到一起的设计方法。如果数据变动可能性很 大,那么采用面向对象设计比较合适,因为它将变动可能性较大的数据独立在目标(模块) 第七章 高级结构设计 104 图 7-4 对于一个问题的不同设计(不同设计导致不同的解决方法,每一种都是正确的) 中。如果功能变动的可能性较大,采用面向对象设计就不太适合了。因为功能散布在许多目标 (模块)中。如果功能变化的可能性比数据要大,那最好采用分解功能的结构化设计。 7.4.2 7.4.2 7.4.2 7.4.2 何时采用信息隐蔽何时采用信息隐蔽何时采用信息隐蔽何时采用信息隐蔽 无论什么问题领域,都应该尽量采用信息隐蔽。使用它没有任何危险。到目前为止,联邦 卫生委员会还没有发现它会发生危险,不论是设计子程序、模块,还是目标程序,它都是很有 效的,因此你尽可以放心使用它。 7.4.3 7.4.3 7.4.3 7.4.3 何时采用面向对象设计何时采用面向对象设计何时采用面向对象设计何时采用面向对象设计 面向对象设计与结构化设计的主要区别是:面向对象设计在较高抽象层次上要比结构化设 第七章 高级结构设计 105 计有效。这是具有重大历史意义的。因为结构化最初开发起来时程序员们正在建立定义大而复 杂的系统,到现在已经远不及如今的大规模系统那样复杂了。 面向对象设计主要是设计模块数据和对数据操作的集合。它非常适于从最顶层分解系统。 而当你识别出目标的接口并开始编码时,你往往会转向结构化设计。如果你用面向对象的语言 编程,那么很难认为你没有在面向对象设计,因为你用面向对象方法、信息或其它进行结构设 计工作。如果你是在用比较传统的过程性语言进行设计,则很容易认为你是在用较旧的结构化 设计方法,在这时,使用这一方法是很合适的。 面向对象设计适合于任何客观世界中的目标。这类系统的例子包括高度交互化的窗口、对 话框、按钮等程序;面向对象的数据库;需要对随机事件做出反应的事件驱动系统等等。 关于面向对象设计技术的研究工作主要是针对代码在 105~1005 行以上的系统的。对于这 种规模的系统,结构化设计基本上是无能为力的,而面向对象设计则往往比较有效。然而,除 了这些超大型项目之外,稍显陈旧的结构化设计还是比较有效的,而面向对象设计对于较小型 项目的有效性,却还有等待证明。 7777....5555 往返设计往返设计往返设计往返设计 通过组合使用主要设计方法来扬长避短是完全可能的。每种设计方法都只是程序员工具箱 中的一件工具,不同的工具适合不同的工作,你将从研究所有方法的启发中获益无穷。 下面一小节结论述了软件设计为什么困难的某些原因,并指出了如何组合使用结构化设计, 面向对象设计和其它设计方法。 7.5.17.5.17.5.17.5.1 什么是往返什么是往返什么是往返什么是往返 你可能会有这样的体验:当你编写程序快结束时,你非常希望能有机会再重新编写一次, 因为在编写过程中你对问题又有了更深的理解。这对设计也是同样适用的,只不过在设计中这 个循环的周期更短,带来的好处也更大,因此,你完全可以在设计过程中进行几次往返。 “往返设计”一词抓住了设计是个迭代过程这一特点:通常你不会只从 A 点走到 B 点,往 往还需要再返回 A 点。 在用不同的设计方法对各种设计方案进行尝试的进程中,将从高层次的总体上和低层次的 细节上对问题进行观察。在从事高层次问题时获得的总体印象将会对你在低层次细节中的工作 有很大帮助;同时,在从事低层次问题时所获得的细节将为你对高层次的总体理解和作出总体 设计决定奠定下良好的基础。这种在高层次和低层次之间往返思维过程是非常有益的,由此而 产生的结构,将比单纯自顶向下或自底向上产生的结构要稳定得多。 许多程序员,都会在这一往返过程中遇到麻烦。从对系统的一个观察点转到另一个观察点 上,的确是很困难的,但这是进行有效设计所必需的,你可以阅读一下 Adams 于1980 年写的 一本叫“Conceptual Blockbusting”的书,来提高自己思维的灵活性。 7.5.2 7.5.2 7.5.2 7.5.2 设计是一个复杂的过程设计是一个复杂的过程设计是一个复杂的过程设计是一个复杂的过程 J.P Morgon 曾经说过人们在做事情时常常有两个原因:表面上冠冕堂皇的原因和真正的 原因。在设计中,最终结果往往看起来是井井有条的,似乎设计者从未犯过任何设计错误,事 第七章 高级结构设计 106 实上,设计过程很少有像最终结果那样井井有条。 设计是一个复杂的过程。因为你很难把正确答案与错误答案区分开来。如果你让三个人分 别设计同一个程序,他们带回来的往往是三个大相径庭的方案,而且其中每一个看起来都非常 适用。它是一个复杂的过程还因为你在设计过程中曾钻过许多死胡同、犯过许多错误。说它是 一个复杂的过程也是因为你不知道什么时候设计方案已经足够完善了。什么时候算完成呢?对 这个问题的通常答案是“当你没有时间时”。 7.5.3 7.5.3 7.5.3 7.5.3 设计是一个“险恶”的过程设计是一个“险恶”的过程设计是一个“险恶”的过程设计是一个“险恶”的过程 Horst Rittel 和 Melvin Webber 把“烦人”的问题,定义成只有通过解决它或者部分解决它, 才能给出明确定义的问题。这 个似是而非的定义事实上暗示着你不得不首先“解决”这 个问题, 对其有一个清楚的定义和理解,然后再重新解决一遍,以获得正确的解决办法。这一过程对软 件开发就像母爱和面包对你我一样必不可少。 在现实中,关于险恶问题的一个富于戏剧性的例子便是托卡马大桥的设计。在修建大桥时, 主要考虑的便是它应该能承受设计载荷并能抗 12 级大风。然而,没人想到风在吹过桥时会产生 “卡门旋涡”——一种特殊的空气动力学现象,从而使桥产生横向简谐振动。结 果 ,在 1 940 年 的一天,只在 7 级风的作用下,桥便因振动而坍塌了。 说它是险恶问题的典型例子是因为直到桥坍塌时,也没有一个设计师想到应该在这个设计 中考虑空气动力学问题。只有在建成大桥(解决问题)之后,才使得他们意识到了要考虑的这 一“额外”问题,经过重新设计,新桥至今依然屹立在河上。 你在学校中设计的程序和在实际工作中设计的程序最重要的不同是:在学校中遇到的程序 设计问题,几乎没有哪个是险恶的,教师留给你的程序作业都是预先想好让你一次即可完成的。 如果哪位教师留给你们一个程序作业,当你们完成后他又突然改变了作业题目,接着,当你即 将完成那个程序时,他又改变了主意,会怎么样呢?我想,如果有谁胆敢这样的话,你们肯定 会把它绞死。但在实际工作中,几乎总是这样。 7.5.4 7.5.4 7.5.4 7.5.4 设计是一个启发的过程设计是一个启发的过程设计是一个启发的过程设计是一个启发的过程 进行有效设计的关键是要认识到它是个启发的过程。设计中,总是吃一堑,长一智的。往 返设计的概念事实上解释了设计是个启发过程这一事实,因为你要把任何设计方法都只当成一 种工具。一种工具只对一种工作或者一种工作的某一部分才有效,其余的工具适合其它的工作, 没有一种工具是万能的。因此,你往往要同时使用几种工具。 一种很有效的启发工具就是硬算。不要低估它。一个有效的硬算解决方案总比优雅却不能 解决问题的方案要好。以搜索算法的开发为例,虽然在1946 年就产生了对分算法的概念,但 直 到16 年后,才有人找到了可以正确搜索各种规模的表的算法。 图示法是另一种有力的启发工具。一幅图抵得上一千个单词。你往往不愿用那一千个单词 而宁愿用一幅图,因为图形提供了比文字更高的抽象水平,有时或许你想在细节上处理某一问 题,但是,更常见的是在总体上处理问题。 往返设计的一个附加的启发能力是你在设计的头几次循环中,可以暂时对没有解决的细节 问题弃之不管,你不必一次就对一切都做出决定,应记住还有一个问题有待做出决定,但同时 要意识到,你目前还没有充分的信息来解决这个问题。为什么在设计工作的最后 10%的部分苦 第七章 高级结构设计 107 苦挣扎呢?往往在下一循环中它们会自然获得解决。为什么非要在经验和信息都不足的情况下 草率决定呢?你完全可以在以后等经验和信息丰富时做出正确决定。有些人对一次设计没能彻 底解决问题会感到很不舒服,但与其很不成熟地勉强解决问题,不如把问题暂放一个,待到信 息足够丰富时,再解决它。 最重要的设计原则之一是不要死抱着一种方法不放。如果编写 PDL 无效的话,那 么 就作图, 或用自然语言写出来,要么就写一小段验证程序,或者使用一种完全不同的方法,比如硬算解 决法,坚持用铅笔不停地写和画,大脑或许会跟上。如果这一切都无效,暂时放开这个问题。 出去自由自在地散散步,或者想一下别的,然后再回到这个问题上。如果你已经尽了全力但还 是一无所获,那么暂时不考虑这个问题往往会比坚持冥思苦想更快得到答案。最后,可以借鉴 其它领域中的方法来解决软件设计中的问题。关于问题解决中的启发方法的最初的一本专著是 G. P olya 的《How To solve in》一书(1957),Polya 的书推广了数学中解决问题的方法,表 7-1 就是对其所用方法的总结,本表摘自 Polya 的书中的类似的总结表: 表表表表 7777----1 1 1 1 怎样解决问题怎样解决问题怎样解决问题怎样解决问题 l.理解问题,你必须理解要解决的问题 问题是什么?条件是什么?数据是什么?有可能满足条件吗?已知条件足以确定未知 吗?已知条件是否不够充分?是否矛盾 7 是否冗余? 画一个图,引入恰当的符号,把条件的不同部分分解开。 2.设计一个方案。找到已知数据和未知之间的联系。如果不能找出直接联系的话,你可能不得 不考虑一些辅助问题,但最后,你应该找到一个解决方案。 以前你是否遇到过这个问题?或者是见过与它稍有不同的问题?是否知道与其相关的问 题?是否知道在这个问题中有用的定理? 看着未知!努力回忆起一个有着相同或类似未知的问题。这里有一个与此相关的你以前 解决过的问题,你能利用它吗?是能利用它的结论还是能用它的方法?是否该引入辅助 要素以使这个问题可以再用? 能否重新表述一下问题?能用另外一种方式表述它吗?返回到定义。 如果你无法解决这个问题,可以先试着解决一些别的问题,是否能想象出一个容易解决的 相关问题;一个广义些的问题或是一个更特殊的问题?一个相似的问题呢?能否解决问 题的一部分呢?仅保留一部分条件,忽略其余条件;未知可以被决定到什么程度?会发生 什么变化?能否从数据中推导出一些有用的东西?能否找出适于确定未知的其余数据? 能否改变数据或未知?同时改变两者呢?这样做能否使新的未知和新的数据更接近些? 是否使用了全部的数据?使用全部条件了吗?是否考虑了这个问题的全部必要条件? 3.执行你的计划。 执行你解决问题的计划,同时检查每一步工作。你是否可以认定每一步都是正确的?你 能证明这点吗? 4.回顾,检查一下答案。 你能检查一下答案吗?能检查一个论证吗?能否用另外一种方法推导出答案?能否一眼 就看出答案? 能否在其它问题中再利用本题的答案或者结论? 7.5.5 7.5.5 7.5.5 7.5.5 受迎的设计特点受迎的设计特点受迎的设计特点受迎的设计特点 高质量的设计往往有一些共同的特点。如果你能达到这些目标,那么可以认为你的设计也 是非常成功的。有些目标是互相矛盾的。但是这是设计的挑战所在,在相互矛盾的目标之间做 第七章 高级结构设计 108 出合理的折衷。某些高质量设计的特点同时也是高质量程序的特点——可靠性。其余的则是设 计所独有的。 以下是设计所独有的一些特点: 智力上的可管理性智力上的可管理性智力上的可管理性智力上的可管理性。对于任何系统来说,智力上的可管理性都是其重要目标之一。它对于 整个系统的完整性是非常重要的,并且会影响程序员们开发和维护系统的难易程度。 低复杂性低复杂性低复杂性低复杂性。低复杂性实际上是智力上的可管理性一部分,由于上述同样的原因,这点也很 重要。 维护的方便性维护的方便性维护的方便性维护的方便性。维护的方便性意味着设计时要为负责维护的程序员着想。在设计中,要不 停地想象维护程序中将会对你的设计提出的问题。应该把维护程序员当作你的听众,同时把系 统设计成明白易懂的。 最小的联系性最小的联系性最小的联系性最小的联系性。最小的联系性指的是按照保持子程序之间的联系最少的原则来设计,应该 利用强内聚,松散耦合和信息隐蔽等作为指导原则来设计系统,使其内部的联系性尽可能少。 最小的联系性可以极大地减小综合、测试和维护阶段的工作量。 可扩充性可扩充性可扩充性可扩充性。可扩充性指的是不必影响系统的内部结构,就可以对系统的功能进行强化,你 可以改变系统的某一部分而不影响其余部分,使得最大可能性变动对系统带来的影响最小。 可重复使用性可重复使用性可重复使用性可重复使用性。 可重复使用性指的是把系统设计成其中许多部分是可以被其它系统借用 的。 高扇入高扇入高扇入高扇入。高扇入指的是对于一个给定的子程序来说,应该有尽可能多的子程序调用它。高 扇入表明一个系统在低层次上充分利用了功能子程序。 低或中等程度扇出低或中等程度扇出低或中等程度扇出低或中等程度扇出。低或中等扇出指的是对一个确定的子程序来说,它所调用的子程序应 该尽可能地少。高扇出(大约 7 个以上)说明一个子程序控制了许多其它子程序,因此可能是 很难理解的。而中等扇出则表明一个子程序只把任务交给了数量较少的其它子程序,因此是比 较容易理解的。低扇出(少于 4 个)看起来像是一个子程序没有把足够的任务交给其余的子程 序去做,但经验表明并不是这样。一项研究表明有 42%只调用一个子程序的子程序是没有错误 的,有 32%的调用 2~7 个子程序是没有错误的,而在调用 7 个以上子程序的情况中,只有12% 是没有错误的(Card, Church 和 Agresi,1986)。由此,Card 认为 0~2 个扇出是最优的。 可移植性可移植性可移植性可移植性。可移植性指的是把系统设计成很容易转到另外的环境下运行。 简练性简练性简练性简练性。简练性指的是把系统设计得没有任何多余部分。Voltaire 曾说过,当一本书不能删 掉,而不是不能添补任何内容时,才可以认为它已完成了。在软件中,这也是非常正确的,因 为当你对系统进行改进时,你不得不对冗余的代码进行开发、评审、测试和维护等等工作,而 且在开发软件的新版本时,新版本也不得不与这些冗余的代码兼容。最有害的观点是“多加入 些又不会有害,怕什么呢?” 成层设计成层设计成层设计成层设计。成层设计指的是尽量分解的层次是成层的,这样你可以在每一个单独的层次上 观察系统,同时也可以使观察的层次是连续的。也就是说当你在某一层次上观察系统时,不会 看到在其它层次上看到的东西。你会经常遇到某些子程序和软件在几个层次上起作用。这样会 使系统很混乱,应尽力避免。 如果在编写一个先进系统时,不得不借用许多旧的、设计得不好的代码,那么你可以在新 系统中建立一个层(layer),与那些旧代码相联接。精心设计这个层使它把旧代码的缺点隐含起 来,从而使新层表现了一整套连续的功能。然后,让程序的其余部分调用些子程序而不是直接 第七章 高级结构设计 109 调用旧代码。成层设计的好处是:(l)它可以使你避免与拙劣的旧代码直接打交道;(2)一旦 你想废弃那些旧代码中的子程序的话,只要修改一下接口层就可以了。 标准化技求。标准化技求。标准化技求。标准化技求。标准化技术是深受欢迎的。一个系统使用的奇特的、非标准的技术越多,当 别人第一次读它时就会越感到可怕,也越难理解。应该通过采用常用的、标准化的技术使得人 们在阅读它时是一种熟悉的感觉。 7.5.67.5.67.5.67.5.6 检查表检查表检查表检查表 高层次设计高层次设计高层次设计高层次设计 本表给出了在评估设计质量时,通常要考虑一些问题。本表是 3.4 节中结构设计检查表的 补充,这个表所考虑的主要是设计质量。3.4 节中的检查表则侧重于结构设计和设计内容。这 个表中的某些内容是相互重合的。 • 是否使用了往返设计方法,应从几个方案中选择最好的,而不是首次尝试就确定方案。 • 每个子程序的设计是否都和与其相关的子程序设计一致? • 设计中是否对在结构设计层次上发现但并未解决的问题作了充分说明? • 是否对把程序分解成目标或模块的方法感到满意? • 是否对把模块分解成子程序的方法感到满意? • 是否明确定义了子程序的边界? • 是否是按照相互作用最小的原则定义子程序的? • 设计是否充分利用了自顶向下和自底向上法? • 设计是否对问题域要素、用户接口要素、任务管理要素和数据管理要素进行了区分? • 设计是智力上可管理的吗? • 设计是低复杂性吗? • 程序是很容易维护的吗? • 设计是否将子程序之间的联系保持在最低限度? • 设计是否为将来程序可能扩展作了准备? • 子程序是否是设计成可以在其它系统中再使用的? • 低层次子程序是高扇入的吗? • 是否绝大多数子程序都是低或中等程度扇出的? • 设计易于移植到其它环境吗? • 设计是简练的吗?是不是所有部分都是必要的? • 设计是成层的吗? • 设计中是否尽量采用了标准化技术以避免奇特的、难以理解的要素? 7777....6666 小结小结小结小结 • 设计是一个启发的过程。固执地坚持某一种方法只会抑制创造力,从而产生低质量的程。 坚持设计方法上有一些不屈不挠的精神是有益的,因为这可以迫使你对这种方法进行充 分理解。但是,一定要确信你是在不屈不挠而不是顽固不化。 第七章 高级结构设计 110 • 好的设计是通过迭代逼近得到的:你尝试过的设计方案越多,你最终所确定的设计方案 也越好。 • 结构化设计比较适合于小规模的子程序组合,同时,它对于功能变化可能性比数据大的 问题也是较适用的。 • 面向对象设计更适于子程序与数据的组合,通常在比结构化设计抽象程度更高些的层次 上适用。它尤其适合于数据变动可能性大于功能变动可能性的问题。 • 设计方法仅是一种工具,你对工具运用得好坏决定了你所设计的程序的质量。利用不好 的设计方法,也可能设计出高质量的程序。而即使是好的方法,如果运用不当的话, 也 只能设计出拙劣的程序。但不管怎样,选择正确的工具更容易设计出高质量的软件。 • 许多关于设计的丰富而有用的信息都是在本书之外的。在这里所论述的,不过是冰山的 一角而已。 第八章 生成数据 111 第八章第八章第八章第八章 生成数据生成数据生成数据生成数据 目录目录目录目录 8.l 数据识别 8.2 自建数据类型的原因 8.3 自建数据类型的准则 8.4 使变量说明更容易 8.5 初始化数据的准则 8.6 小结 相关章节相关章节相关章节相关章节 变量命名:见第 9 章 使用变量时的一些考虑:见第 10 章 使用基本数据类型:见第 11 章 使用复杂数据类型:见第 12 章 在结构设计阶段定义数据:见 3.4 节 说明数据:见 19.5 节 数据结构布置:见 18.5 节 本章的内容既包括高层次的子程序、模块和程序设计中要考虑的问题,也包括对数据实现 问题基本要素的讨论。 数据结构在创建阶段能带来的收益大小,在某种程度上是由它对创建前的高层次工作影 响大小决定的。好的数据结构所带来的收益往往是在需求分析和结构设计阶段体现出来的。为 了尽可能地利用好的数据结构带来的收益,应在需求分析和结构设计阶段就定义主要数据结构。 数据结构的影响同时也是由创建活动所决定的。由创建活动来填平需求分析与结构设计 之间的壕沟是很正常也是很受欢迎的。在这种微观问题上,仅靠画几张蓝图来消除一致性的缺 陷是远远不够的。本章的其余部分将论述填平这一壕沟的第一步——生成进行此项工作的数据。 如果你是一位专家级的程序员,本章的某些内容对你来说可能已是司空见惯了。你可以浏 览一下标题和例子,寻找你不熟悉的内容看就可以了。 8.1 8.1 8.1 8.1 数据识别数据识别数据识别数据识别 有效生成数据的第一步是应该知道该生成什么样的数据结构。一张良好的数据结构清单是 程序员工具箱中的一件重要工具。对数据结构基本知识的介绍不在本书范围之内。但你可以利 用下面的“数据结构识别测试题”来看一下自己对其知道多少。 第八章 生成数据 112 8.1.1 8.1.1 8.1.1 8.1.1 数据结构识别测验题数据结构识别测验题数据结构识别测验题数据结构识别测验题 每遇到一个你所熟悉的词可以计 1 分。如果你认为你知道某个词但并不知道其确切内容, 计 0.5 分。当作完题后,把你的得分加到一起,然后再根据测验题后面的说明来解释你的得分。 ____抽象数据类型 ____文字型 ____数组 ____局部变量 ____B 树 ____查找表 ____位图 ____指针 ____逻辑变量(布尔变量)____队 ____字符变量 ____记录 ____命名常量 ____回溯 ____双精度 ____集合 ____枚举流 ____堆栈 ____浮点 ____字符串 ____散列表 ____结构化变量 ____堆 ____树 ____索引 ____联合 ____整型 ____数值链 ____链表 ____变体记录 ____最后得分 以下是得分解释办法(可随便些): 0~14 分 你是个初级程序员,可能是计算机专业一年级的学生,或者是一个正在自学 第一种语言的自学者。如果读一下后面列出的书的话,你将会学到很多。本 章的许多论述都是针对高级程序员的,如果你读完那些书再阅读本书将更 有益。 15~19 分 你是个中级程序员或是个健忘的富有经验的程序员虽然你对表中许多概念 都已经很熟了,但最好还是先阅读一下后面列出的书。 20~24 分 你是个专家级的程序员,你的书架上很可能已经插上了后面所列出的书。 25~29 分 关于数据结构,你知道的比我还多!可以考虑写一本你自己的专著(请别忘 了送我一本)。 30~32 分 你是个自大的骗子。“枚举流”、“回溯”和“数值链”并不是指数据结构 的,是我故意把它们加进去以增加难度的,在阅读本章其余部分之前,请先 阅读引言中关于智力诚实性的内容。 以下是关于数据结构的一些不错的书: Aho, Alfred V., John E. Hopcroft 《Data Structure and Algorithms, Reading,Mass》 Addison-Wesley,1983。 Reingold, Edward M 和 Wilfred J.Hansen《Data Structures》, Boston: Little, Brown, 1983。 Wirth, Niklaus,《Algorithms and Data Structures》, Englewood Cliffs,N.J.;Prentice Hall, 1986。 第八章 生成数据 113 8.28.28.28.2 自建数据类型的原因自建数据类型的原因自建数据类型的原因自建数据类型的原因 程序语言所赋予你的阐明自己对程序理解的最强有力的工具之一便是程序员定义的变量 类型。它们可以使程序更容易阅读。如果使用 C、Pascal 或其它允许用户定义类型的语言,那 么一定要利用这一点。如果使用的是 Fortran,Generic Basic 等不允许用户定义变量类型的语言, 那接着看下去,或许读完本节后你就想换一种语言了。 为了说明自定义类型的效力,让我们来看一个例子。假设你正编写一个把 x、y、z 坐标转 换为高度、经度、纬度坐标的程序,你认为可能会用到双精度浮点数,但是在十分确定之前, 你只想用单精度浮点数。这时,可用 C 中的 typedef 语句或 Pascal 中的 type 说明来为坐标专门 定义一种变量(当然也可用其它相当的语言)。以下是在 C 中是如何建立这种变量的: typedef float Coordinate_t; /* for coordinate variables */ 这个类型定义说明了一种新的类型,Coordinate_t,在功能上它与 float 是一样的,为了使 用这种新类型,应用它来说明变量,就像用 float 来预先定义一种类型一样。以下是一个用 C 写 成的例子: Routine1(...) { Coordinate_t latitude; /* latitude in degrees */ Coordinate_t longitude; /* longitude in degrees */ Coordinate_t elevation; /* elevation in meters from earth center */ ... } ... Routine2(...) { Coordinate_t x; /* x coordinate in meters */ Coordinate_t y; /* y coordinate in meters */ Coordinate_t z; /* z coordinate in meters */ ... } 在这段代码中,变量 x,y,z 和变量 latitude,longitude,elevation 都是 Coordinate_t 类型的。 现在,假设程序发生了变化,发现最终还是得用双精度变量。由于你为坐标专门定义了一 种类型,因此所要做的就是改变一下类型定义而已。而且只需在一个地方进行改动;在 typedef 语句中。下面是改变后的类型定义: typedef double Coordinate_t; /* for coordinate variables */ ——原浮点已改为双精度类型 下面来看第二个例子,它是用 Pascal 写成的。假设你正在生成一个工资发放系统,其中雇 员的名字至多有 30 个字母。你的用户告诉你从来没有任何人的名字长度超过 30 个字母。你是 否该在程序中把数字 30 作为文字型变量来使用呢?如果你这样作的话,那你就过于轻信你的用 第八章 生成数据 114 户了。更好的办法是为雇员名字定义一种类型: Type EmployeeName_t = arrayy[1..3] of char; 当引入数组或字符串时,最好定义一个命名常量来表示数组或字符串的长度,然后 在类型定义中使用这个命名常量,在程序中,你会许多次用到了这个常量——以下 是你将使用它的第一个地方,它看起来是这样的: Const Namelenght_c = 30;——这里是命名常量的说明 … Type EmployeeName_t = array[1..NameLength_c] of Char;——这里是命名常量使用的地方 一个更有说服力的例子是将自定义类型与信息隐蔽的思想结合在一起。在某些情况 下,你想隐蔽的信息是关于数据类型的。 在坐标例子中用 C 写成的程序已经在走向信息隐蔽的途中了。如果你总是用 Coordinate_t 而不是用 float 或 double,事实上已经隐蔽了数据的类型。在 C 或 Pascal 中,这些便是语言本身 能为信息隐蔽所做的一切,其余部分便是你或后来的使用者必须遵守这个规则,不查看 Coordinate_t 的定义。C 和 Pascal 所赋予的都是广义的而不是狭义的信息隐蔽能力。 其它像 Ada 和 C++等语言则更进一步支持狭义的信息隐蔽。下面是在 Ada 语言中,用包来 定义 Coordinate_t 的代码: package Transformation is type Coordinate_t is private; ——这个语句说明 coordinate_t 作为包的专用说明 … 以下是在另一个包中使用 Coordinate_t 的代码: with Transformation: … procedure Routine1(…)… latitude: Coordinate_t; longitude: Coordinate_t; begin -- statements using latitude and longitude … end Routine1; 注意 Coordinate_t 在包定义中是说明为专用的,这意味着只有 Transformation 包中的专用部 分才知道 Coordinate_t 的定义,而程序其余部分则都不知道。在有一群程序员的开发环境中, 只有包的定义部分才是开放的。对于从事另一个包的程序员来说,他是不可能查寻 Coordinate_t 的类型的。在这里,信息是狭义隐蔽的。 这些例子已经阐明了建立自己的类型的几条理由: · 使得改动更加容易。建立一种新类型工作量极小,但这却可以带来极大的使用灵活性。 第八章 生成数据 115 · 避免过度分散的信息分布。硬性类型会使程序中充斥着数据类型细节,而不是使其集 中在一个地方。这正是 6.2 节中所讨论的集中化原则。 · 为了增加可靠性。在 Ada 和 Pascal 中,可以定义类似 Age_t = 1~99 的类型。然后,编 译程序会产生运行检查信息,以保证 Age_t 类型总是在 1~99 的范围内。 · 为了补偿语言的弱点。如果语言中不具备某种定义好的类型,可以自己定义它。例如, C 中不含逻辑类型,通过建立你自己的类型,很容易弥补这个缺点: typedef int Boolean_t; 8.3 8.3 8.3 8.3 自建数据类型的准则自建数据类型的准则自建数据类型的准则自建数据类型的准则 以下是几条生成自己的类型时应牢记的准则: 建立具有面向功能名称的类型建立具有面向功能名称的类型建立具有面向功能名称的类型建立具有面向功能名称的类型。应避免用暗指计算机本身数据类型的名称。要使用代表实 际问题某一部分的名称。在上例中,为坐标建立的新类型命名就很恰当,因为它代表了客观世 界中的实体。同样,你也可以为现金、工资发放代号、年龄等客观世界中的实体建立新变量。 要避免使用含有已定义变量类型的名称要避免使用含有已定义变量类型的名称要避免使用含有已定义变量类型的名称要避免使用含有已定义变量类型的名称。比如像 BigInteger 和 LongString 等指的是计算机 数据而不是客观世界中实体的名称就应避免使用。建立自己的类型其最大优点是,可以在程序 及其实现语言之间建立一个绝缘层,而指向程序语言类型的名称则会破坏这个绝缘层,使用已 定义的类型不会带来任何优点。面向问题的名称可以增加易改进性,并且使数据说明成为自注 释的。 避免使用已定义类型避免使用已定义类型避免使用已定义类型避免使用已定义类型。如果类型存在变动的可能性,那么除了在 typedef 和 type 定义之外, 不要再使用已定义的类型。建立面向功能的类型是非常容易的,而改变程序中该类型的数据是 非常困难的。而且,当使用自建的面向功能类型说明变量时,也同时对变量进行了说明。 Coordinate_x 所告诉你的关于 x 的信息要比 float x 多得多。因此应尽可能使用自建类型。 不要对已定义类型重新定义不要对已定义类型重新定义不要对已定义类型重新定义不要对已定义类型重新定义。改变标准类型的定义往往令人困惑。例如,语言中已经有了 Integer 类型,而你又自建了叫作 Integer 的类型。这样,程序的阅读者往往会记住你所定义的 Integer 的含义,而仍把它当作语言中的标准 Integer 类型。 定义替换类型以增强移植性定义替换类型以增强移植性定义替换类型以增强移植性定义替换类型以增强移植性。与避免重新定义标准类型的建议相反,你可能想为标准类型 定义一种替换类型,从而使得在不同的硬件环境下变量所代表的都是同一实体。例如,你可以 定义 INT 类型来代替标准的 int 类型,它们之间的唯一区别便是字母的大小写。但当你把程序 移到新的硬件环境下时,你只要重新定义一个 INT 类型,就可以在新的硬件环境下使得数据类 型与旧环境下相容。 如果你所用的语言是不区分大小写的,你将不得不使用其它办法来对标准类型和替换类型 加以区别。 使用其它类型来建立新类型使用其它类型来建立新类型使用其它类型来建立新类型使用其它类型来建立新类型。你可以在已经建立的简单类型的基础上建立复杂类型。这种 变量类型可以进一步推广你用原来类型所达到的灵活性。 8.4 8.4 8.4 8.4 使变量说明更容易使变量说明更容易使变量说明更容易使变量说明更容易 本节论述的是怎样才能更顺利地进行变量说明。显然,这是件很容易的事情,你甚至会认 第八章 生成数据 116 为在书中专门用一节来论述这个问题是小题大做。但不管怎样,你毕竟在建立变量上花费了许 多时间,因此,养成这方面的良好习惯以避免不必要的失败和徒劳的努力是非常必要的。 8.4.1 8.4.1 8.4.1 8.4.1 使用模框(使用模框(使用模框(使用模框(templatetemplatetemplatetemplate)进行变量说明)进行变量说明)进行变量说明)进行变量说明 在一个文件中存储一个变量说明模框。需要说明新的变量时,你可以把这个模框调入程序, 对其进行编辑以说明新变量。下面是用 C 写成的一个模框: extern * *; /* */ static * *; /* */ * *; /* */ 这个模框有几个优点。首先,很 容易从中选择出与你要求最接近的行,然后删掉其余各行; 第二,每行中“*”的作用是占有位置,这使得进入每行的编辑位置都非常容易;第三,如果你 忘记了更改“*”,那么一定会产生语法错误,从而起到了提醒的作用;第四,使用模框可以保 持说明形式的一致性;最后,预留的注释空格将提醒你在说明变量时进行注释,这简化了以后 的程序注释工作。 不过,不要以为你必须采用与上例完全相同的模框,尽管按自己的想法去建立自己的模框, 只要它能降低变量说明工作量、增强代码可读性并且使调试更容易就可以了。我的一个朋发给 自己找出了一个注释变量的理由。她名叫 Chris,她的模框是这样的: * * { Chris is a jerk! } 8.4.2 8.4.2 8.4.2 8.4.2 隐式说明隐式说明隐式说明隐式说明 有些语言具有隐含变量说明功能。例如,如果在 Basic 或 Fortran 中你没有说明就使用了某 些变量,那么编译程序将自动说明。 隐式说明是所有语言特性中的最具危害性的特性之一。 如果你用 Fortran 或 Basic 编过程序,那你一定知道要找出 ACCTNO 的值不正确是多么的 困难。而 你 最 后却发现这是由于 ACCTNUM 被重新初始化为零的原因,如果你所用的语言不要 求对变量做出说明,那么,这种错误是很常见的。 而如果你所用的程序语言要求对变量作出说明,那么在出现上例中的错误之前,你必须首 先先犯两个另外的错误才行。首先,你要错误地同时把 ACCTNUM 和 ACCTNO 放入了子程序 中;其 次,你要错误地在子程序的说明段中同时说明了这两个变量。第二个错误是很难出现的, 因此,要再犯上例中的错误几乎是不可能的。要求你对变量进行显式说明的语言其主要优点之 一,便是可以使你在使用变量时更加谨慎。但如果你用的是具备隐式说明功能的语言,那你该 怎么办呢?以下给出了一些指导原则: 关闭隐式说明功能关闭隐式说明功能关闭隐式说明功能关闭隐式说明功能。有些编译程序允许关闭这一功能。比如,在 Fortran 中,可以使用一个 叫 IMPLICIT NONE 的语句,这并不是 ANSI FORTRAN77 中的标准语句,但许多 Fortran 中都 有这一扩展。 说明所有变量说明所有变量说明所有变量说明所有变量。即使编译程序不要求,但你每次使用新变量时,都要坚持对它做出说明。 这样并不能避免所有的错误,但是至少可以避免某些错误。 使用命名约定使用命名约定使用命名约定使用命名约定。建立一个关于常用后缀(如 NUM 和 NO)的命名约定,这样,当你想要 第八章 生成数据 117 用一个变量时就不会误了两个。 检查变量名检查变量名检查变量名检查变量名。利用你的编译程序或其它工具软件产生的参考表。许多编译程序都会列出你 在子程序中使用的所有变量,从而使你发现 ACCTNUM 和 ACCTNO 中的错误。同时,它也会 指出说明但并未使用的变量。 8.5 8.5 8.5 8.5 初始化数据的准则初始化数据的准则初始化数据的准则初始化数据的准则 在编程中,最大的错误原因之一便是对数据不恰当的初始化。开发有效地避免初始化错误 的技术,可以节约大量的调试时间。 不恰当初始化产生的问题,是由某一变量带有你没有预料的初值引起的。这可能是由下述 原因中的任何一种引起的: · 这个变量未被赋过任何值。其值是在程序开始运行时,由在它的内存中的位置偶然的 值决定的。 · 变量的值已经过时了。变量在某点中被赋过值,但是这个值已不再有效了。 · 变量的一部分被赋了值,而另一个部分没有被赋值。如果你使用的是指针变量,那么 常见的错误是用指针分配了内存,却忘记了对指针所指的变量进行初始化。这与对变 量根本不赋值的效果是一样的。 这种情况往往有几种表现形式。你可能初始化了结构的某一部分而没有初始化另一部分; 也可能会分配内存,然后初始化变量,但指向变量的指针却没有被初始化,这意味着你是随意 地拿一段内存并赋予它们一些值,这段内存可能是存储数据的,也可能是存储代码的,也可能 是被操作系统所占用的。这种错误的表现形式是多种多样的,而且它们之间往往每次都有着天 壤之别。这使得调试指针错误要比调试其它错误困难得多。 下面是如何避免初始化错误的一些准则: 检查输入参数的有效性检查输入参数的有效性检查输入参数的有效性检查输入参数的有效性。初始化的另一种有价值的形式是检查输入参数的有效性。在赋给 输入参数任何值之前,要首先确认它是合理的。 在使用变量的位置附近对其进行初始化在使用变量的位置附近对其进行初始化在使用变量的位置附近对其进行初始化在使用变量的位置附近对其进行初始化。有些程序员喜欢在子程序开始的一个地方对所有 变量进行初始化,以下是用 Basic 语言写成的例子: ' initialize all variables Idx = 0 Total = 0 Done = False ... 'lots of code using Idx and Total ... 'code using Done while not Done ... 第八章 生成数据 118 另外一些程序员则尽可能在每一次用到变量的位置附近对其进行初始化,下面也是用 Basic 写成的例子: Idx = 0 'code using Idx ... Total = 0 ── Total 在使用位置附近初始化 'code using Total ... Done = False ── Done 在使用位置附近初始化 'code using Done while not Done ... 第二个例子要好于第一个,其中有几个原因。在第一个例子中,当运行到使用 Done 的代 码时,Done 很有可能已经被改变过了。即使在初次写程序时不会出现这种情况的话,那么以后 你对其进行修改时,很可能会出现这种情况。第一种方法的另一个问题是,把所有变量集中进 行初始化,会给人以这些参数将在程序中随处都被用到的印象,而事实上 Done 只在程序结束 前才被用到;最后,当对程序进行改动时(这是很可能的,起码在调试时要修改),可能会把 Done 包含在循环中,从而需要对 Done 重新进行初始化,在这种情况下,第二个例子中的代码 不会有什么太大的变化。而第一个例子中的代码则可能会产生讨厌的初始化错误。 这也是邻近原则的一个例子:把相关的操作放在一起。这一原则也适用于把对代码的注释 放在相应的代码附近,把循环设定代码放在循环附近,把注释放在非循环代码中。 要特别注意计数器和累加器要特别注意计数器和累加器要特别注意计数器和累加器要特别注意计数器和累加器。变量 i、j、k 和 Sum、Total 等往往代表计数器和累加器。常 见的错误是在下次用到它们时,忘记了对其进行清零操作。 查找需要查找需要查找需要查找需要重新进行初始化的地方重新进行初始化的地方重新进行初始化的地方重新进行初始化的地方。首先问自己一下有哪些地方需要重新进行初始化。重新 初始化的原因可能是由于变量在循环中被用过多次,也可能是由于变量在对子程序的调用中间 要保持原值且需清零。如果需要重新初始化的话,要确保初始化语句是在被重复的代码段中。 对命名常量只初始化一次,用可执行代码初始化变量对命名常量只初始化一次,用可执行代码初始化变量对命名常量只初始化一次,用可执行代码初始化变量对命名常量只初始化一次,用可执行代码初始化变量。如果要用变量来模仿命名常量,那 么在程序的开始对它们进行一次初始化是可以的,在 Fortran 中可以用 Data 语句来做到这点。 在其它语言中,可以在 Startup()子程序中初始化它们。 应在使用变量的位置附近的可执行代码对其进行初始化。最常见的改动之一是改动某一子 程序,变一次调用它为多次调用它。在 DATA 语句或 Startup()子程序中被初始化的变量,在程 序中不能被重新初始化。 按照所说明的对每个变量进行初始化按照所说明的对每个变量进行初始化按照所说明的对每个变量进行初始化按照所说明的对每个变量进行初始化。虽然其地位无法与在变量使用位置附近对其初始化 相比,但是按照所说明的初始化变量,仍然是防错性编程中的一件有力工具。如果你养成习惯, 那就可以有效地防止初始化错误。下例就保证了 student_name 在每次调用含有它的子程序时, 都将被重新初始化。 第八章 生成数据 119 char student_name[NAME_LENGTH+1] = {'\0'}; /* full name of student */ 利用编译程序的警告信息利用编译程序的警告信息利用编译程序的警告信息利用编译程序的警告信息。许多编译程序都会对使用未初始化的变量进行警告。 设置编译程序使其自动初始化所有变量设置编译程序使其自动初始化所有变量设置编译程序使其自动初始化所有变量设置编译程序使其自动初始化所有变量。如果你的编译程序支持这种选择项,那么让它对 所有变量进行初始化是非常简单的。但是,由于对编译程序的依赖性。当把程序移到另一台编 译程序不同的机器上时,则有可能产生问题。这时要确保你对所用的编译程序进行了注释,否 则是很难发现程序对编译程序有依赖性。 使用内存存取检查程序来查找无效的指针使用内存存取检查程序来查找无效的指针使用内存存取检查程序来查找无效的指针使用内存存取检查程序来查找无效的指针。在某些操作系统中,操作系统代码会自动查找 无效指针,在其它情况下,就只有依靠自己了。不过,你也不一定非得靠自己。可以买一个内 存存取检查程序来检查程序中的指针操作。 在程序开始初始化工作内存在程序开始初始化工作内存在程序开始初始化工作内存在程序开始初始化工作内存。把工作内存初始化到某一个值可以帮助发现初始化错误。可 以采取以下任何一种方法: · 可以用程序内存填充程序来赋予内存某一已知值,这个值用 0 比较好,因为它保证未 初始化指针指向低内存,比较容易发现它们,在 80X86 处理器中,16 进制 0CCH 比 较好,因为它是机器的断点中断码。如果是在调试中运行数据而不是代码,你就会进 入断点。使用值 0CCH 的另一个优点是在内存转储中它很容易辨认。 · 如果你使用的是内存填充程序,可以每次改变一个填充值,用这种方法来检查一下运 行环境下隐藏的错误。 · 可以用程序在软件运行时初始化工作内存。虽然使用内存填充程序的目的是把错误暴 露出来,但这种技术的目的则是隐藏它们。通过每次把工作内存由相同的值充满,可 以保证在每次运行时程序不会被启动时的随机因素干扰。 8.5.l 8.5.l 8.5.l 8.5.l 检查表检查表检查表检查表 数据生成数据生成数据生成数据生成 生成类型生成类型生成类型生成类型 · 程序中是否对每种可能变动的数据都使用了不同的类型? · 类型名称是面向客观世界中的实体类型而不是面向程序语言中的类型吗? · 类型名称是否有足够的表现力来帮助说明数据? · 避免重新定义已定义的类型了吗? 说明数据说明数据说明数据说明数据 · 是否使用了模框为简化数据说明工作?并用其来统一说明形式? · 如果所用的语言支持隐式说明,是否采取了补救措施? 初始化初始化初始化初始化 · 是否每个子程序都对输入参数进行了检查以保证其有效性? · 是否在使用变量的位置附近对其进行初始化的? · 是否恰当地对计数器和指针进行了初始化?是否在必要时对其进行了重新初始化? · 在反复执行的代码段中,是否对变量进行了恰当地重新初始化? · 用编译程序编译代码时是否是无警告的? 第八章 生成数据 120 8888....6 6 6 6 小结小结小结小结 · 在你的工具箱中需要一张全部数据结构的清单,以便用最合适的方法处理每一种问题。 · 建立自己的数据类型,以增加程序的可变动性,并使其成为自说明的。 · 数据初始化很容易产生错误,因此应采用本章推荐的技术来避免由意外初始值所产生的错 误。 第九章 数据名称 121 第九章第九章第九章第九章 数据名称数据名称数据名称数据名称 目录目录目录目录 9.1 选择名称 9.2 特定数据类型命名 9.3 命名约定 9.4 非正式命名约定 9.5 匈牙利命名约定 9.6 短名称 9.7 要避免的名称 9.8 小结 相关章节相关章节相关章节相关章节 生成数据:见第 8 章 使用变量时要考虑的问题:见第 10 章 说明数据:见 19.5 节 格式化数据说明:见 18.5 节 本章论述的是如何对数据进行恰当命名。这个主题对有效编程是非常重要的,但是在进行 恰当命名时要考虑的几十个问题中,我却只读过对其中两到三个的讨论。大多数编程课本只用 几段来论述名称缩写的选择问题,讲的也都是些关于这方面的陈词滥调,而完全寄希望于你自 己去解决问题。对于另一个极端,使用过多关于命名的信息将你淹没,我认为简直就是犯罪。 9.19.19.19.1 选择名称选择名称选择名称选择名称 在给变量命名时,你不能像给小狗起名时那样仅仅挑有趣或好听的名字。但除了实体不同 之外,变量与变量名和狗与狗名实际是同一回事。 因此,一个变量的好坏在很大程度上是由其名字决定的。所以在选择变量名时一定要谨慎。 以下是一段使用不恰当变量名的例子(用 C 写成); X = X - XX; XXX = Aretha + SalesTax( Aretha ); X = X + LateFee(X1,X)+ XXX; X = X + Interest(X1,X); 这段代码是干什么的?X1,XX 和 XXX 代表的是什么呢?Aretha 的意思是什么?如果某 人告诉你,这段程序是根据收支平衡和新的购买情况来计算顾客和全部账单的,那么你将使用 什么变量来代表那些新购买活动呢? 第九章 数据名称 122 以下是一个同样内容的新程序,这里,上述问题就很容易回答了。 Balance = Balance - LastPayment; MonthlyTotal = NewPurchases + SalesTax( NewPurchases ); Balance = Balance + LateFee( CustomerID, Balance ) + MonthlyTotal; Balance = Balance + Interest( CustomerID,Balance ); 通过比较两段代码,我们发现好的变量名是易读、易记而且是恰当的。可以使用几条通用 原则来达到这些目的。 9.1.1 9.1.1 9.1.1 9.1.1 命名时要考虑的最重要问题命名时要考虑的最重要问题命名时要考虑的最重要问题命名时要考虑的最重要问题 在给变量命名时,考虑的问题是变量名称是否完全而又准确地描述了变量所代表的实体。 一个有效的方法是用自然语言(如英语)将变量所代表的实体描述出来。往往这一描述本身便 是最好的名称,因其不含缩写它很容易读懂,又由于它是对实体的全面描述。因此不会与其它 实体相混淆,而且因为它与概念相近,所以也很容易记。 比 如 要 用 一 个 变 量 来 代 表 美 国 奥 林 匹 克 队 的人数,你可以对其命名为 NumberofPeople0nTheUSOlympicTeam。对代表自 1896 年以来国家队在奥林匹克运动会上最多 得分的变量可以用 MaximumNumberofPointsSince1986 作为其名称。而用 InterestRate 或 Rate 作 为代表目前利率的变量名要比用 r 或 x 好得多。 你应该可以发现这些名字的两个特点。首先,它们很容易解释。事实上,你根本不需要解 释,它们的意思是一目了然的;第二条则是其中有些名字很长,长得根本不实用。稍后我们将 讨论这一问题。 下面是一些变量名的例子,同时列出了好的和坏的。 变量代表的实体 恰当的名称 不恰当的名称 火车速度 Velocity、TrainVelocity、VolocityInMPH VELT,V,TV,X,X1 今天日期 CurrentDate、CrntDate CD,Current,C,X 每页行数 LinesPerPage LPP , Lines , X , X1 CurrentDate 和 CrntDate 是恰当的名字,因为它们全面准确地描述了“今天日期”这 一 含义。 而且,它们用的是明显的单词。程序员们往往忽视使用平常的词,而事实上这是最简单的解决 办法;CD 和 C 太简短了,说明不了任何问题;Current 并没有说明现在的什么?是总统还是赌 马的结果?Date 像是一个不错的名字,但究竟是什么时候的 Date?是基督出生那天吗?X,X1 在任何情况下几乎都是不好的名字,因为它们通常都是代表未知量的,如果你要它代表其它实 体时,往往会引起误会。 9999.1.2 .1.2 .1.2 .1.2 面向问题面向问题面向问题面向问题 一个好记的名字通常是面向问题而不是解决问题的。一个恰当的名字往往说明是“什么” 而不是“怎样”。通常,如果一个名称指向计算的一方面而不是指向问题,那么可以认为之是个 “怎样”而不是“什么”的名称。要避免使用这种名称而要使用面向问题的名称。 雇员数据的记录可称为 InputRec 或 EmPloyeeData,InputRec 是一个计算机术语,指的是输 入和记录;EmPloyeeData 指的是问题域而不是计算方面。同样,对一个表示打印机状态的变量 第九章 数据名称 123 来说,BitFlag 要比 PrinterReady 专业化得多。在计帐系统中,CalcVal 要比 Sum 更加专业化。 9.1.3 9.1.3 9.1.3 9.1.3 最佳名称长度最佳名称长度最佳名称长度最佳名称长度 名称的最佳长度应介于 MaximumNumberOfPointsSincel896 和 x 之间。太短 的名字 往 往 表示不出完整的意思,而用 x1 和 x2 来表示的问题,即使你能找出 x1 代表的是什么,也很难发 现 x1 和 x2 之间有什么联系。太长的名字难以输入,而且会对软件的可视结构产生破坏作用。 Gorla 和 Benander 在1990 年发现当变量名长度在10 到16 个字母时,COBOL 程序最容易 调试。而变量名称长度在 8 到 20 个字母时,程序调试容易几乎是一样的。这一准则并不是说你 必须把所有变量名长度都限制在 9 到15 或10 到16 个字母之间。但这确实要求你检查一下,程 序中变量名长于这个标准的那些变量,确保清楚程度符合需求。 以下是对一些变量名的评价,或许会给你一些启迪: 名称太长的: NumberofPeopleOnTheUS0lympicTeam NumberOfSeatsInTheSaddleDome MaximumNumberOfPointsSince1896 名称太短的: N,NP,NTM N,NS,NSISD M,MP,Max,Points 合适的: NumTeamNumbers,TeamMbrCount,cTeamMbrs NumSeatslnDome,SeatCount,cSeat MaxTeamPoints,Record Points,cPoints 9.1.4 9.1.4 9.1.4 9.1.4 变量名的作用域变量名的作用域变量名的作用域变量名的作用域 短的变量名总是不好的吗?当然不总是。当你用 i 作为一个变量名时,这个较短的长度就 可以说明某些问题,比如,这个变量是一个临时变量,只在有限的操作范围内才是有效的。 当程序员读到这样一个变量时,他应该能猜到这个变量只在几行内使用。如果你把某个变 量称为“i”,你就等于在说“这个变量仅作为循环计数器或数组索引数使用,在这几行代码外 没有任何意义”。 由 W.J Hansen 进行的一项调查表明,较长的名字适于较常使用的变量或全局变量。而较 短的名字则适于局部变量或循环变量(1980)。但短名字会产生许多问题,有些程序员把避免使 用它们作为防错性编程的准则。 9.1.5 9.1.5 9.1.5 9.1.5 变量名中的计算值限定词变量名中的计算值限定词变量名中的计算值限定词变量名中的计算值限定词 许多程序中含有带有计算值的变量:totals,averages,maximums 等等。如果你用限定词诸 如(Ttl,Sum,Avg 等)来改动变量,那要把它们放在最后。 这种方法有几个优点。第一,变量名中最有意义的部分——表达变量名大部分意义的部分, 第九章 数据名称 124 被放在最前面;第二,通过建立这种约定,在同一程序中同时使用 TtlRevenue 和 RevenueTtl 会避免因此而引起的混淆;第三,像 RevenueTtl,ExpenseTtl,RevenueAvg 和 ExpenseAvg 这 样的一系列名字有一种令人愉快的相似感。而像 TtlRevenue,ExpenseTtl,RevenueAvg 和 AvgExpense 则不具备这种相似性。最后,一致性可以改善可读性并简化了维护。 这个准则的例外是 Num 放在变量名前面时表示全部,如 NumSales 表示的全部商品的数额; 当 Num 放在变量名后面时,它表示下标,SaleNum 表示现在标出的商品是第几个。Numsales 末尾的 S 也是表示两者意义区别的一个标志。但是,由于过分频繁地使用 Num 会产生混淆,所 以往往用 Count 来表示总数,而用 Index 来表示下标。这样,SalesCount 表示的是卖出的总数, 而 Saleslndex 则指的是卖出的某一种特定商品。 9.1.6 9.1.6 9.1.6 9.1.6 变量名变量名变量名变量名中的反义词中的反义词中的反义词中的反义词 应恰当使用反义词。使用关于反义词的命名约定可以帮助保持连续性,同时也可以提高可 读性。像 first/last 这样一组反义词是很容易理解的,但 first/end 就有些让人摸不着头脑了,以下 是一些比较容易理解的反义词。 add/remove begin/end create/destroy insert/delete first/last get/release increment/decrement put/get up/down lock/unlock min/max next/previous old/new open/close show/hide source/destination source/target start/stop 9.2 9.2 9.2 9.2 特定数据类型命名特定数据类型命名特定数据类型命名特定数据类型命名 除了对数据命名通常需要考虑的一些问题之外,对特殊数据类型必须给予特殊的考虑。本 书将论述循环变量、状态变量、临时变量、逻辑变量、枚举变量和命名常量的命名问题。 9.2.l 9.2.l 9.2.l 9.2.l 循环变量命名循环变量命名循环变量命名循环变量命名 由于几乎每个程序中都含有循环,因此,对循环变量的命名问题加以专门考虑是十分必要 的。 简单循环中的循环控制变量的名字往往也是十分简单的,常用的是 i、j、k 等。下面是一个 Pascal 循环的例子: for i:=FirstItem to LastItem do Data[i] := 0; 如果这个变量还要在循环外使用,那么应该用比 i、j、k 更能说明问题的名称。比如,你正 从文件中读取记录,并且要知道已经读取了多少个记录,那么用 RecordCount 作为其名称似乎 更合适些,请看下面的这个 Pascal 程序: RecordCount:= 0 while not eof(InputFile) do 第九章 数据名称 125 begin RecordCount := RecordCount + 1; ReadLn ( Score [ Recordcount ] ) end; { lines using RecoudCount } … 如果循环体长度较长的话,那就很容易使人忘记它代表的是什么,因此最好给循环控制变 量一个富有意义的名字。由于经常进行更改,扩展和拷贝等代码到另一个程序中,因此,大多 数有经验的程序员都避免用 i、j、k 这类的名字。 使循环体变长的一个常见原因是嵌套。因此,对于一个有多重嵌套的循环,最好给循环控 制变量以较长的名字以便改善其可读性: for TeamIndex := i to TeamCount to begin for EventIndex := 1 to EventCount[ TeamIndex ] do Score[ TeamIndex, EventIndex ] := 0 end; 通过精心对循环控制变量进行命名,可以避免它们的交叉:当你想用 i 时误用了 j,或者 想用 j 时却又误用了 i。这样做也可以使对数组的存取操作更为明了。Score [ TeamIndex, EvenIndex ]显然要 Score[i,j]更能说明问题。总之,应尽量避免使用 i、j、k 来命名。如果不得 不使用它们的话,那除了把它们用作循环控制变量之外,最好不再用作别的变量名。这一约定 是众所周知的,如果不遵守它只会引起别人的困惑。 9.2.2 9.2.2 9.2.2 9.2.2 状态变量命名状态变量命名状态变量命名状态变量命名 状态变量描述的是程序所处的状态。下面论述了它们的命名原则。 用比 flag 更好的名称来命名变量,最好不用 flag 作为状态变量的名字。之所以要避免使用 flag 作为标志名称,是因为它不能告诉你关于这个标志的任何信息。为了清楚起见,应该给标 志赋值,并且用枚举类型、命名常量或当作命名常量使用的全局变量对其进行测试。下面是一 个在 C 语言中不恰当命名标志的例子。 if( Flag )... if( StatusFlag & 0x0F )... if( PrintFlag == 16 )... if( ComputerFlag == 0 )... Flag = 0x1; StatusFlag = 0x80; PrintFlag = 16; ComputerFlag = 0; 如果这个程序不是你写的,而且也没有注释告诉你的话,你是无法知道类似 statusFlag = 0x80 之类的语句到底是要干什么的,而且你也无法知道 Statusflag 和 0x80 到底是什么意思。以 第九章 数据名称 126 下是功能与内容相同但清楚得多的程序: if ( DataReady )... if ( CharacterType & PRINTABLE_CHAR )... if ( ReportType == AnnualRpt )... if ( RecalcNeeded == TRUE )... DataReady = TRUE; CharacterType = CONTROL_CHARACTER; ReportType = AnnualRpt; RecalcNeeded = FALSE; 显然,第二个例子中 CharacterType = CONTROL_CHARACTER 的意义要比第一个中 StatusFlag = 0x80 的意义要清楚得多。同样,第二个例子中的条件语句 if ( ReportType == AnnualRpt ) 也显然要比第一个中的 if ( PrintFlag == 16 )清楚得多。第二个例子表明,你可借 助预先命名的常量或枚举类型来使用这种方法。以下是如何利用枚举类型和命名常量来设置上 例中用到的值,仍用 C 来实现: /* values for DataReady and RecalcNeeded */ #define TRUE 1 #define FALSE 0 /* values for CharacterType */ #define LETTER 0x01 #define DIGIT 0x02 #define PUNCTUATION 0x04 #define LINE_DRAW 0x08 #define PRINTABLE_CHAR ( LETTER | DIGIT | PUNCTUATION | LINE_DRAW ) #define CONTROL_CHARACTER 0x80 /* values for ReportType */ Typedef enum { DailyRpt,MonthlyRpt,,QuarterlyRpt, AnnualRpt,AllRpts}REPORT_TYPE_T; 当你发现自己“侦破”了一段代码时,应该考虑对变量重新命名。侦破一 杀案是可以 的,但你不应该去“侦破”一段代码。代码应该是具有良好可读性的。 9.2.3 9.2.3 9.2.3 9.2.3 临时变量命名临时变量命名临时变量命名临时变量命名 临时变量用来保存中间运算结果,如用作暂时保留某个位置或保留内务操作值。通常把它 们叫做 TEMP 或 X 等没有什么意义的名字。临时变量的使用往往标志着程序员还没有完全理解 第九章 数据名称 127 程序。而且,由于名义上给了它一个“临时”的状态,因而程序员们在处理它们时往往会采取 漫不经心的态度,从而增大了出错机会。 要警惕“临时”变量要警惕“临时”变量要警惕“临时”变量要警惕“临时”变量。通常,暂时保留某些值是完全必要的。但如果在你的程序中出现很 多临时变量的话,则说明你对它们在程序中作用和使用它们的目的还不清楚。先让我们看一下 下面用 C 语言写成一个例子: /* Compute root of a quadratic equation. This assumes that ( b^2 - 4 * a * c ) is positive. */ Temp = sqrt ( b^ 2 - 4 * a * c ); root[0] = ( -b + Temp )/( 2 * a ); root[1] = ( -b – Temp )/( 2 * a ); 把由公式 sqrt ( b^2 - 4 * a * c ) 计算出来的值储存起来是个不错的想法,尤其是在后面还有 两处用到了它的情况下。但是用 TEMP 作为它的名称不能告诉你关于这个变量意义的任何信息。 一个较好的作法是下面这个例子: /* Compute roots of a quadratic equation. This assumes that ( b^2 – 4 * a * c )is positive */ Discriminant = sqrt ( b^2 - 4 * a *c ); root[0] = ( -b + Discriminant )/( 2 * a ); root[1] = ( -b - Discriminant )/( 2 * a ); 事实上两段代码是完全一样的,但是通过采用更准确也更能说明问题的变量名,大大改善 了其可读性。 9.2.4 9.2.4 9.2.4 9.2.4 逻逻逻逻辑变量命名辑变量命名辑变量命名辑变量命名 以下命名逻辑变量的几条准则: 记住一些典型的逻辑变量名。记住一些典型的逻辑变量名。记住一些典型的逻辑变量名。记住一些典型的逻辑变量名。以下是些非常有用的逻辑变量名: Done。用 Done 来表示某项工作已经完成了。这个变量可以表示子程序或循环是否已经完 成。当某项工作没有完成时,把 Done 的值赋为 False;当某项工作已经完成时,把 Done 的值 赋为 True。 Error。用 Error 来表示发生了错误。当没有错误时将 Error 的值赋为 False;当有错误时将 其 赋为 True。 Found。用 Found 来表示是否找取了某个值。当搜寻数组来查找某一值或搜寻某一文件表 查找某一雇员的识别卡时,没有找到某一值时将其值赋为 False,而一旦找到这个值。则把 Found 值赋为 True。 Success。用 Success 来表示某一操作是否已成功地完成。当某一程序无法完成时,将 Success 的值置为 False;而当某一操作已经完成时,将 Success 的值置为 True。如果可能的话,可以用 比 Success 更准确更具有表达力的名称来代替它,用这个新名称应可以精确地表达出到底是什 么已 成功地完成了。比如当某一程序完成处理后 就可以认为是成功时,就可以用 Processingcomplete 来代替 Success。如果当找到某一值就可以为程序是成功的时,可以用 Found 来代替它。 第九章 数据名称 128 用旧含非真即假的名字来给逻辑变量命名用旧含非真即假的名字来给逻辑变量命名用旧含非真即假的名字来给逻辑变量命名用旧含非真即假的名字来给逻辑变量命名。像 Done 或 Success 等之所以是恰当地逻辑变量 名,是因为它们的状态是非真(True) 即假 (False)的。某项工作或者完成了或者没完成;或者 是成功的或者不成功,不会有第三种状态。而类似 Status 或 SourceFile 等则是不恰当的名字, 因为看不出它们的状态是非真即假的。如果 Status 的值是 True,那它是否意味着某种物质有状 态呢?任何物质都有状态。或者说当其值是 True 时,意味着某种物质的状态很好,而为 False 时则意味着状态不好?仅从 Status 这个名称是无法回答这些问题的。 为清楚起见,最好用 Error 或 Status_OK 等来代替 Status;用 SourceFileAvailable 或 SourceFileFound 来代替 Source。 有些程序员喜欢用 Is 用为逻辑变量名的前缀,于是变量名就成了一个问句:IsDone? IsError?IsSuccess?当用 True 或 False 来回答上述问题时,就等于给变量赋了值。这样做的好 处是可以避免那些不恰当的名称。如 IsStatus 显然没有任何意义。 使用肯定的逻辑变量名使用肯定的逻辑变量名使用肯定的逻辑变量名使用肯定的逻辑变量名。否定式的变量名如 NotFound、NotDone 和 Notsuccessful 等在“非” 运算中是很难读懂的,如: if not NotFound 这类名字应该用 Found,Done,Successful 等来代替,以方便“非”运算。 9.2.5 9.2.5 9.2.5 9.2.5 枚举类型命名枚举类型命名枚举类型命名枚举类型命名 当使用枚举型变量时,可以通过使用相同的前缀或后缀表示某一类型的元素都是属于同一 组的,如下面的这段 Ada 程序所示: type COLOR is ( COLOR_RED, COLOR_GREEN,COLOR_BLUE ); type PLANET is ( PLANET_ERATH, PLANET_MARS, PLANET_VENUS ); 9.2.6 9.2.6 9.2.6 9.2.6 常量命名常量命名常量命名常量命名 对常量来说,应该用它所代表的抽象实体而不是数值来命名。FIVE 是一个很不恰当的常量 名称(不管 它代表的数值是否 是 5.0 ); CYCLES_NEEDED 则是个恰 当 的 名称, CYCLES_NEEDED 可以等于 5.0 也可以等于 6.0,而 Five = 6.0 则是个荒唐的语句。由 于 同样的 原因,BAKERS_DOZEN 也是很不恰当的变量名,而 MAX_DONUTS 则要恰当得多。 9.3 9.3 9.3 9.3 命名约定命名约定命名约定命名约定 有些程序员往往坚持标准和约定,这是有其原因的。然而,某些原则和约定过于刻板而且 往往是无效的,这只会破坏你的创造力和程序的质量,这实在是个不幸,因为有效的标准和约 定是你的工具箱中最为有效的工具之一。这一节将讨论为什么及什么时候和怎样建立你自己的 命名标准。 9.3.19.3.19.3.19.3.1为什么要建立约定为什么要建立约定为什么要建立约定为什么要建立约定 约定可以带来如下好处: · 它们可以使更多的东西成为独立的。通过做出一个总体决定而不是许多局部决定,你 第九章 数据名称 129 可以把精力放到更重要的程序特性上。 · 它们可以帮助借鉴其它项目的经验并移植自己的经验。相似的名字可以使你更容易并且更 自信地猜测陌生变量的功用。 · 它们可以使你更快地熟悉新项目的代码。与一套连贯的而不是各式各样、东拼西凑的 代 码打交道显然要容易得多。 · 防止一变量多名。如果没有命名约定,你很可能给一个变量取两个或更多的名字。例如, 你可以把所有点的个数称作 PointTl 又称作 Ttl_Points。这对于你来说可能没什么,因为 你是程序的作者。但对以后要读这个程序的程序员来说,这很可能会使人困惑。 · 弥补语言的缺陷。你可以利用命名约定来仿效命名常量或枚举类型,这一约定可以区分局 部、模块和全局变量,也可以并入编译程序所不支持类型的信息。 · 命名约定还可以强化相关项之间的联系。如果你使用的是结构化数据,那么编译程序会自 动考虑到这一点;如果所用的语言并不支持结构化数据,可以通过命名约定来补充它。像 AddrmPhone 和 Name 等名称并不表示变量是相联系的。但如果你决定所有的雇员数据变量 名都要用 EmP 作为前缀,那 么 毫无疑问,EmpAddr、EmpPhme 和 EmpName 就是联系的变量了。 通过建立伪结构化数据,弥补语言的缺陷。 关键是有约定总比没有约定好,哪怕约定是随意的也罢,约定的效力并不是由某一确定的 约定,而是由约定存在决定的,它可以增加代码的结构并减少你的担心。 9.3.2 9.3.2 9.3.2 9.3.2 什么时候使用命名约定什么时候使用命名约定什么时候使用命名约定什么时候使用命名约定 关于这个问题并没有一成不变的答案。但在以下几种情况下,使用命名约定我认为还是值 得的。 · 当同时有几个程序员从事一个项目时。 · 计划把程序交给另一个程序员进行修改和维护时(这时命名约定是必不可少的)。 · 当你的程序将由其它程序员来评审时。 · 当程序规模过大,需要按部分来考虑它时。 · 当一个项目中要频繁使用某些不常见的词汇,而又想开始编码时。 命名约定总是有益的,上述准则可以帮助你确定在某一项目中命名约定使用的广泛程度。 9.3.3 9.3.3 9.3.3 9.3.3 正式程度正式程度正式程度正式程度 不同约定的正式程度是不同的。一个简单的约定可能只有一句话,“使用有意义的名称”。 略微正式的约定将在下节讨论。更正式些的约定将 9....5 节中论述。一般来说,约定正式程度是 由从事一个程序的程序员人数、程序的规模和程序的预测生存期决定的,在小型程序中,严格 的约定往往是不必要的。如果是需要几个人协作(可能是在开始,也可能是在程序生存期内的 某个时间)的程序,那么可读性往往要依赖正式的命名约定来保证。 9.4 9.4 9.4 9.4 非正式命名约定非正式命名约定非正式命名约定非正式命名约定 绝大多数项目都采用如下所述的非正式命名约定。 第九章 数据名称 130 9.4.19.4.19.4.19.4.1 与语言无关的约定准则与语言无关的约定准则与语言无关的约定准则与语言无关的约定准则 以下是一些与语言无关的约定准则: 标识全局变量标识全局变量标识全局变量标识全局变量。常见的编程问题之一是误用全局变量。可以在所有的全局变量前面都加 上 g_作为前缀来解决。比如看到 g_Running Total 时,程序员就会知道这是一个全局变量,从而 把它作为全局变量对待。 标识模块变量标识模块变量标识模块变量标识模块变量。模块变量是在模块内部供几个子程序使用的变量。要能清楚地表明它既不 是全局变量也不是局部变量。这可以用在变量前加 m_作为前缀来解决。在 C 中,你可以通过在 任何子程序外说明 Static 变量来建立模块层次的数据。这样的变量对文件中子程序来说都是可 用的,但文件外的子程序则无法使用。 标识类型定义标识类型定义标识类型定义标识类型定义。类型的命名约定需要有两个功能:它们要明确地指出某一名称是类型名称, 同时要可以避免类型名称与变量名称相冲突。为了达到这两个要求,使用前缀或后缀不失为一 个好办法。在 Pascal 中,可以使用小写的_t 来表示类型名称,例如 Color_t 或 Menu_t。在 C 中用这种办法稍有些困难,常 见 的 办法是用大写字母组合如 COLOR 或 MENU 来表示类型名,但 这 可能与命名预处理程序常量相混淆。而“_t”这一约定已经被标准类型 size_t 所采用了,因此可 以用“_T"表示类型名称,如 COLOR_T 和 MENU_T。 标识命名常量标识命名常量标识命名常量标识命名常量。需要对命名常量加以标识,以便可以使你知道是在用一个变量(其值可能 变动)还是在用一个命名常量给某个变量赋值。在 Pascal 或 C 中,你还可能是在用函数给某一 变量赋值。这些语言,并不要求函数使用括号。而在 C 中,即使是不带参数的函数也必须使用 括号。 给命名常量命名的一种办法是“_C”作为其名称的后缀。在 Pascal 中,用这种方法可以产 生出像 MaxRecs_C 或 MaxlinesPerPage_ C 之类的名字。在 C 中,你可以用“_C”作为其后缀。 标识枚举类型标识枚举类型标识枚举类型标识枚举类型。与命名常量同样的原因,枚举类型也需要被标识出来。即将其与变量、命 名常量和函数加以区别。常用的方式是用“_e”或“_E”作为后缀。 标识输入参数标识输入参数标识输入参数标识输入参数。有时输入参数会被错误改动。在像 C 或 Pascal 这样的语言中,当一个被改 变过的值返回调用子程序时,必须予以明确地说明。在 C 中,这是用“*”说明的,在 Pascal 中用的是 VAR,Ada 语言中使用的是 out 限定词。在其它语言如 Fortran 中,如果你改变了一个 输入值,那么不管你是否愿意,它都将被返回,但假如你建立了命名约定,在约定中规定只用 于输入的参数前面要采用 IP 作为前缀,那么当发现在等号左边出现了带有 IP 前缀的变量时, 你就可以知道发生了错误。比如在程序中看到 IPMAX = IPMAX+l 语句,就可以立刻认定它是 错误的,因为前缀 IP 表明 IPMAX 的值是不允许变动的。 对名字作格式化以增强可读性对名字作格式化以增强可读性对名字作格式化以增强可读性对名字作格式化以增强可读性。增强可读性常见的两项技术是用分隔字符或大写字母将单 词分隔开来。例如,GymnasticsPointTotal 或 Gymnastic_Point_Total 的可读性显然是要强于 GYMNASTICSPOINTTOTAL。C、PASCAL、Ada 和其它语言都允许大小写混用的方式。C、 Pascal 和其它语言都允许使用的下划线“_”作为分隔符。 尽量不要混合使用这两项技术,那样会降低可读性。如果坚持采用其中任何一种,那么你的 代码的可读性将大为改观,关于变量名第一个字母是否应该大写的问题,激烈的争论已经持续 了很长时间(是 TotalPoints 好还是 totalPoints 好?)。但是只要保持一致性,我认为二者区 别实际上并不大。 第九章 数据名称 131 9.4.29.4.29.4.29.4.2 与语言有关的命名约定与语言有关的命名约定与语言有关的命名约定与语言有关的命名约定 应当遵守所使用语言的标准命名约定。 你可以发现许多书中都讲述了语言的形式准则。关于 C、Pascal 和 Fortran 的准则将在下 面予以论述。 C 约定约定约定约定 有些命名的约定是只适用于 C 的,可以在 C 中使用这些约定,也可以改变它以使其适应其 它语言。 · c 和 ch 是字符变量 · i 和 j 整型下标 · n 是数量 · p 是指针 · s 是字符串 · 预处理程序宏指令是以全部大写来表示的,这通常扩展到包含 typedef · 变量和子程序名称都是小写的 · 下划线“_”用做分隔符 这些是在 UNIX 操作系统下用 C 语言编程的一些通用约定。在不同的环境下,这些约定是 有区别的。在 Microsoft Windows 环境下,C 程序员们往往愿意使用匈牙利命名约定,并且对变 量名称是大小写混用的。在 Macintosh 环境下,C 程序员们往往乐于使用类 Pascal 的命名约定 给子程序命名。因为 Macintosh 工具箱和操作系统子程序都是为 Pascal 接口设计的。 PascalPascalPascalPascal 约定约定约定约定 Pascal 中只有几条特殊的约定,你可以在 Pascal 中使用它们,也可以改进它们以适应其它 语言需要。 · i、j 和 k 是整型下标 · 变量和子程序名称是以大小写混用的形式出现的 FortranFortranFortranFortran 约定约定约定约定 Fortran 有一些语言本身固有的命名约定,在 Fortran 中可以使用它们。但我想如果不把它们 扩展到其它语言的话,全世界都会因此而感激你的。 · 以字母 I 到 N 开头的变量名是整型的 · I、J 和 K 只能作为循环控制变量 · X、Y 和 Z 是浮点数 9.49.49.49.4....3333 命名约定举例命名约定举例命名约定举例命名约定举例 当命名约定准则长达几页时,它们看起来是非常复杂的。事实上,命名约定是完全不必要 复杂到可怕程度的,你可以改进它们以适应你的需要。变量名应包括三个方面的信息: · 变量内容(它代表的是什么) 第九章 数据名称 132 · 变量数据类型(整型、浮点等) · 变量在程序结构中的位置——例如,定义变量的模块名称或者一个表示变量是全局的 前缀。 以下是 C 和 Pascal 中采用前面讲过的命名原则进行命名的一些例子。我并不是想把这些约 定推荐给你,而是想给你一些变量名应包括哪些信息的想法。 Pascal 命名约定示例命名约定示例命名约定示例命名约定示例 约定类别约定类别约定类别约定类别 约定内容约定内容约定内容约定内容 LocalVariable 局部变量名是大小写混用的。这个名称应与潜在的数据类型无关,且应 指出这个变量代表的到底是什么 RoutineName() 子程序也是大小混用的(具体讨论见 5.2 节) m_ModuleVariable 仅供某一模块子程序使用的变量以 m_作为前缀 g_GlobalVariable 全局变是用 g_为前缀 Constant_C 命名常量以_C 作为后缀 Type_t 类型以_t 作为后缀 Base_EnumeratedType 枚举类型是以其基本类型的助记符 作为前 缀 的,如 Color_Red, Color_Blue C 命名约定示例命名约定示例命名约定示例命名约定示例 约定类别约定类别约定类别约定类别 约定内容约定内容约定内容约定内容 GlobalRoutineName() 公用程序名称是大小写混合使用的 _FileRoutineName() 供某一个模块专用的子程序以下划线作为前缀 LocalVariable 局部变量名是大小写混用的。其名称应与数据类型无关且应指明变 量代表的究竟是什么 _FileStaticVariable 模块(文件)变量以一个下划线作前缀 GLOBAL_GlobalVariable 全局变量是以定义它的模块(文件)名称的全大写形式助记符作为 前缀的,如 SCR_Dimension LOCAL_CONSTANT 仅供某一子程序或模块 (文件) 专用的命名常量是全大写的 GLOBAL_CONSTATNT 全局命名常量名字是全大写的,且以它的模块(文件)名称 助记符的大写形式来作前缀的,如 SCR_MAXROWS TYPE 类型定义是全大写的 LOCAL_MACRO() 仅供某一子程序或模块(文件)专用的宏定义是大写的 GLOBAL_MACRO() 全局宏定义是大写的且以它的模块(文件)名称助记符大写形式为 前缀 9999....5555 匈牙利命名约定匈牙利命名约定匈牙利命名约定匈牙利命名约定 匈牙利命名约定是一整套对子程序和变量进行命名的详细约定。这一约定广泛应用在 C 语 言中,特别是在 Microsoft Windows 环境下用 C 编程时,之所以称其为“匈牙利”是因为遵循这 一 约定的命名看起来像是外文。而且其发明者 Charles Simonyi 原来是匈牙利人。 匈牙利命名主要包括三个部分:基本类型、一个或更多的前缀、一个限定词。在下面的例 子是在 C 中采用匈牙利命名约定写成的,你很容易就可以对其加以改进以扩展到其它语言中。 第九章 数据名称 133 9.5.19.5.19.5.19.5.1 基本类型基本类型基本类型基本类型 基本类型是指待命名变量的数据类型。基本类型名称通常不指向程序语言中任何一种已定 义的标准数据类型。基本类型是一种更抽象的数据类型,基本类型名称指向的可能是窗口、屏 幕区和字体等实体。在匈牙利名称中只能使用一种基本类型。 基本类型是用你为某一特定程序建立的简写代码来表示的,然后对其作标准化以便在这一 程序中继续使用。以下是在一个文字处理程序中你可能用到的基本类型: 基本类型 含义 wn 窗口 scr 屏幕区 fon 字体 ch 字符(不是 C 意义上的字符,而是这个文字处理程序 将用来在文件中代表字符数据结构意义上的) pa 段落 使用匈牙利基本类型时,同时也定义了与基本类型使用同样缩写的数据类型。因此,如果 使用了如上表列出的基本类型,就会看到像这样的数据说明: WN wnMain; SCR scrUserWorkspace; CH chCursorPosition; 9999....5555....2222 前缀前缀前缀前缀 前缀放在基本类型前面并描述了变量的使用。与基本类型不同的是前缀在某种程度上是标 准化的。表 9-1中示出了一级标准匈牙利前缀: 表表表表 9999----1111 匈牙利变量名称前缀匈牙利变量名称前缀匈牙利变量名称前缀匈牙利变量名称前缀 前缀 含义 a 数组 c 数目(如记录、字符等等的个数等) e 数组的元素 g 全局变量 h 处理 i 数组下标 m 模块层次上的变量 p(lp,np) 指针(长指针、近指针——对于 Intel 机器) 前缀是小写的,并且放在变量名中基本类型的前面。如果需要的话,可以把它与基本类型 或与其自身组合使用。例如,一个表示窗口的数组,可以用“a”来表示它是个数组,用“wn” 表示窗口,因此可以用“awn”来作这个数组的名称;而对窗口的操作可称为 hwn;cwn 是窗口 的数目;cfon 是字体的种类等等。 9999....5555....3333 限定记号限定记号限定记号限定记号 匈牙利名称的最后一个要素是限定词。限定调是名称的描述部分。没有使用匈牙利约定 第九章 数据名称 134 时也可利用这部 分来补 偿 。在上前面给出的例子——wnMain 、 scrUserWorkspace 和 chCursorPosition 中,Main、UserWorkspace 和 CursorPosition 都是限定词。在本章前面给出的关 于变量命名的准则也适用于限定词。 除了自己生成的限定词以外,匈牙利约定中还对容易在处理时产生混淆的概念给出了标准 化限定词。在表 9-2 中,表示了些标准限定词。 表表表表 9999----2222 匈牙利约定中的标准限定词匈牙利约定中的标准限定词匈牙利约定中的标准限定词匈牙利约定中的标准限定词 限定词限定词限定词限定词 含义含义含义含义 Min 数组或其它表中绝对的第一个元素 First 数组中需要进行处理的第一个元素。First 含义与 Min 相近,但 First 是相 对操作的第一个,而 Min 则是指数组本身的第一个元素 Last 在一个数组中需要最后进行处理的元素。Last 是 First 的反义词 Lim 在一个数组中需要处理的元素上界。与 Last 一样,Lim 也是 First 的反义 词,但与 Last 不同的是,Lim 代表的是数组的不完全上界;而 Last 代表 的则是最后一个元素。通常,Lim 等于 Last 加 1 Max Max 指的是数组或表列中的绝对最后一个元素而不是相对操作的最后一 个元素 限定词能够而且应该与基本类型和前缀联合使用。例如,PaReformat 指的是要重新格式化 的段落;apaRefotmat 指的是要重新格式化的段落数组。而 ipaReformat 则指的是需要重新格式 化的段落的数量。一个 for 循环来重新格式化段落的 C 程序如下: for ( theReformat = paFirstReformat; ipaReformat <= paLastReformat; ipaReformat++; )… 可以用 PaLimReformat 来代替上例中的变量名 PaLastReformat 重写相同的循环,这时,由 于 Lim 和 Last 的区别,在确定循环是否结束的检查中,将用“<”来代替“<=”。即用: ipaReformat < PaLimReformat 来代替 ipaReformat <= PaLastReformat 9.5.49.5.49.5.49.5.4 匈牙利名称举例匈牙利名称举例匈牙利名称举例匈牙利名称举例 以下是利用匈牙利约定产生的变量名。其中变量名的基本类型部分采用的是前面文字处理 程序示例中的基本类型。 变量名变量名变量名变量名 意义意义意义意义 ch 字符变量(并不是 C 语言意义上字符,而是指文字处理程序中用来 在文件中代替字符的数据结构) achDelete 要删去的一个字符数组 ich 字符数组的下标 ichMin 字符数组中绝对第一个元素的下标 第九章 数据名称 135 ichFirst 字符数组中第一个需要进行某种操作的元素的下标 echDelete 字符数组中产生的某一元素,如 ecbDelete = acbDelete [icbFirst]的结果 pachInsert 要插入字符数组中的指针 ppach 指向某一字符数组指针的指针 cchInser 要插入字符的数量 cscrMenu 用作菜单屏幕区的数量 hscrMenu 对用作菜单屏幕区的操作 mhserUserInput 用户输入屏幕区的模块层次上的操作(所有在这一模块中的子程序 都可对这一变量进行存取操作) ghscrMessages 为获得信息而对屏幕区进行的全局操作 注:上表中某些变量名不含限定词。虽然省略限定词是很常见的,但我们并不提倡这样做, 应尽可能使用限定同。 9.5.5 9.5.5 9.5.5 9.5.5 匈牙利约定优点匈牙利约定优点匈牙利约定优点匈牙利约定优点 匈牙利约定与其它命名约定一样,拥有由命名约定所带来的一切共同优点。由于有这样多 的标准名称,因此在任何一个单个子程序或程序中要特殊记忆的名字是非常少的。匈牙利约定 完全可以在不同项目中采用。 匈牙利约定可以使得在命名中容易产生定义的区域变得准确清楚。特别是约定中对 First, Min,Last,Max 和 Lim 的准确区分在实际中是尤其有帮助的。匈牙利约定可以使人对编译程序 无法检查的抽象数据类型进行检查:cpaReformat[i]很可能是错误的,因为 cpaReformat 不是数 组,而 apaReformat[i]则可能是正确的,因为 apaReformat[i]是数组。 匈牙利约定可以在类型不严格的语言或环境中对类型进行说明。例如,在 Windows 环境下 编程时,需要你放弃许多类型,这极大地限制了编译程序进行严格类型检查的能力。而建立约 定则可以对环境的这一弱点作出补偿,匈牙利约定还可以使名称更简洁,可以用 CMedals 而不 用 TotalMedals 来代表奖牌的数量,使用 pNewScore,而不是用 NewScorePtr 命名一个新分数指 针。 9.5.6 9.5.6 9.5.6 9.5.6 匈牙利约定缺点匈牙利约定缺点匈牙利约定缺点匈牙利约定缺点 一些版本的匈牙利约定事实上忽视了用抽象数据类型作为基本类型。它们以程序语言中整 型、长整型、浮点数和字符串为基础来建立基本类型。匈牙利约定基本类型事实上是没有什么 价值的,因为它使得程序员陷入对类型进行人工检查的困扰之中,而不是让编译程序对类型进 行更加快速而又准确的检查。 这种形式匈牙利约定的另一个问题是它把数据的意义与其表现联系在一起。比如,说明某 一变量是整型的,把它改为长整型的时,不得不改动这一变量的名称。 匈牙利约定的最后一个问题是它鼓励了懒惰、不含什么信息的变量名的出现。当程序员用 hwnd 来命名对窗口的操作时,往往忽视了他所指的到底是哪种窗口、对话框、菜单还是帮助区 的屏幕?显然用 hwndmenu 要比 hwnd 清楚得多。以变量的意义为代价来获得对其类型的精确 描述显然是愚蠢的。不过好在可以用加限定词的办法来同时获得完整的意义和精确的类型。 第九章 数据名称 136 9.6 9.6 9.6 9.6 短名称短名称短名称短名称 从某种程度上来说,使用短名称的意向是早期语言的遗留物。较老的语言如汇编、Basic 和 Fortran 语言把变量名长度限制在七到八个字母之间,从而迫使程序员们不得不使用短名称。在 现代语言如 C、Pascal 和 Ada 中,事实上可以使用任意长度的名称,因此,此时已没有任何必 要再使用短名称。 如果确实不得不使用短名称的话,要注意某些使用短名称的方法要好于其它的。可以通过 去掉不必要的单词、使用短符号或者使用其它缩写技术来建立恰当的变量短名称。可以使用任 意一种缩写技术。但最好多熟悉几种缩写技术因为没有任何一种方法是万能的。 9.6.1 9.6.1 9.6.1 9.6.1 缩写使用的总体准则缩写使用的总体准则缩写使用的总体准则缩写使用的总体准则 以下是使用缩写的几项准则,其中有某些准则是与其它准则相密的,因此不要试图一次使 用其中所有的技术。 · 使用标准的缩写(常用缩写,如列在字典缩写表中的)。 · 去掉所有的非大写元音字母(如 Computer 写成 Cmptr,Screen 写成 Scrn,Integer 写 成 Inter 等)。 · 使用每个单词的头一个或头几个字母。 · 截掉每个单词头一至三个字母后面的其余字母。 · 使用变量名中每一个有典型意义的单词,最多可用三个单词。 · 每个单词的第一个和最后一个字母。 · 去掉无用的后缀——ing,ed 等等。 · 保留每个音节中最易引起注意的发音。 · 反复交替地使用上述技术,直到变量名长度缩短至 8 到 20 个字母为止,或者到你所 用语言规定的长度为止。 9.6.2 9.6.2 9.6.2 9.6.2 语音缩写语音缩写语音缩写语音缩写 有些人喜欢根据单词的发音而不是拼写来进行缩写。如把 skating 写成 sk8ting,brightlight 写成 bilite,before 写成 b4 等等。这更像是在让人们破译密码,因此我不主张采用这种方法,但 作为一种技术,你可以尝试一下“破译”出如下语音缩写的含义: ILV2SK8 XMEQWK S2DTM8O NXTCd TRMN8R 9.6.3 9.6.3 9.6.3 9.6.3 关于缩写的建议关于缩写的建议关于缩写的建议关于缩写的建议 进行缩写时很容易陷入误区。以下是避免出现这种情况的几条准则: 不要通过拿掉单词中一个字母进行缩写不要通过拿掉单词中一个字母进行缩写不要通过拿掉单词中一个字母进行缩写不要通过拿掉单词中一个字母进行缩写。多敲一个字母费不了多少精力,而由此损失的可 读性却往往是巨大的。如把 June 写成“Jun”或“July”、“Jul”等都是不值得的。而且如果总是 使用这种只省略一个字母的缩写,很容易使人忘记你是否省略掉了一个字母。因此,或者多省 略几个字母,或者使用全称。 缩写应保持一致性缩写应保持一致性缩写应保持一致性缩写应保持一致性。应坚持使用同一缩写。例如,是使用 Num 或者 No,但不要两者混用。 第九章 数据名称 137 同样,也不要时而缩写某一名称,又时而不缩写。比如,假设已经用了 Num 就不要再同时使用 Number 了。 使用容易发音的缩写使用容易发音的缩写使用容易发音的缩写使用容易发音的缩写。如应使用 xPos 而不是 Xpsn,用 CurTotal 而不是 ntTtl。可以用能否 在电话中让对方明白的方法来检验缩写名称,如果不能的话,最好换一个比较容易说的缩写。 避免会引起错误发音的组合避免会引起错误发音的组合避免会引起错误发音的组合避免会引起错误发音的组合。如为表示 B 的结束,应使用 ENDB 而不要使用 BEND。如果 使用了良好的分隔技术,则不必理会这条准则。如 B_END,BEnd 或 b_end 都不会导致错误发 音。 近义词来避免命名冲突近义词来避免命名冲突近义词来避免命名冲突近义词来避免命名冲突。在使用短名称常碰到的一个问题是命名冲突——对不同的名称使 用了同一缩写。如 fired 和 fullrevenuedisbursal,假设对缩写的要求是限定在三个字母的话,那 么很可能两者都会被缩写成 fri,从而产生冲突。 避免这个问题的方法之一是使用近义词。如可以用 dismissed 来代替 fired,用 complete revenuedisturb 来代替 fullrevenuedistural 便可以解决上面的问题。 用注解表来说明短名称用注解表来说明短名称用注解表来说明短名称用注解表来说明短名称。在只允许使用简短名称语言中,可以通过加入一个注解表来对变 量名称的缩写加以说明,可以把注解表作为代码开始的注释块,以下是一个 Fortran 中使用注解 表的例子: C*************************************************************** C Translation Table C C Variable Meaning C -------- ------- C XPOS X-Coordinate Position ( in meters ) C YPOS Y-Coordinate Position ( in meters ) C NDSCMP Needs Computing ( =0 if no computation is needed; C =1 if computation is needed ) C PTGTTL Point Grand Total C PTVLMX Point Value Maximum C PSCRMX Possible Score Maximum C*************************************************************** 多从读程序者而不是写程序者的角度去考虑变量名称多从读程序者而不是写程序者的角度去考虑变量名称多从读程序者而不是写程序者的角度去考虑变量名称多从读程序者而不是写程序者的角度去考虑变量名称。你可以通过隔一段时间再阅读一下 程序的方法来检查一下是否需要费很大精力才能弄清变量的含义。如果是这样的话,应改进命 名技术来解决这一问题。 9.7 9.7 9.7 9.7 要避免的名称要避免的名称要避免的名称要避免的名称 以下是应该避免的几种名称: 避免容易产生误会的名称或缩写避免容易产生误会的名称或缩写避免容易产生误会的名称或缩写避免容易产生误会的名称或缩写。要确认变量名称是清楚的。FALSE 通常是指 TRUE 的反 义词,如果用它当“Fig and Almond Season”的缩写显然是不合适的。 避免含义相同或相近的名字避免含义相同或相近的名字避免含义相同或相近的名字避免含义相同或相近的名字。如果可以交换两个变量的名称而不致影响程序,那说明这两 个变量都应重新命名。如 Input 和 InVal,RecNum 和 NumRecs 等每两个的意义都很相近,如果 第九章 数据名称 138 在同一程序中同时使用它们来命名两个变量,就非常容易引入某些难以察觉的错误。 避免使用含义不同但拼写相似的名称避免使用含义不同但拼写相似的名称避免使用含义不同但拼写相似的名称避免使用含义不同但拼写相似的名称。如果发现两个变量名称拼写相似但含义不同,那应 对其中一个重新命名或改变缩写技术,比如 ClientsRecs 和 ClientsReps 这样的名称就应避免。因 为它们之间只有一个字母不同,而且这一字母很难辨别,两个名称之间至少应有两个字母不同, 或者把不同的字母放在词首或词尾。如用 ClientRecords 和 ClientsReports 来分别代替上述两个 名称显然要好得多。 避免使用发音相同或相近的名称避免使用发音相同或相近的名称避免使用发音相同或相近的名称避免使用发音相同或相近的名称。如 Wrap 和 rap。因为这将使你在与同事讨论问题时遇到 很多麻烦。 避免在名称中使用数字避免在名称中使用数字避免在名称中使用数字避免在名称中使用数字。如果变量名中的数字的确很有意义的话,应使用数组而不应使用 单个变量。如果使用数组不合适的话,那么使用含有数字的变量名更不合适。比如,应避免使 用 FILE1、FILE2 和 Total1、Total2 这类名字。可以用很多办法来区分两个变量,但不要采用在 变量名末尾加数字的方法。我不敢说应绝对禁止在变量名中使用数字,但起码你应尽全力避免 这种用法。 避免在名称中改写字母避免在名称中改写字母避免在名称中改写字母避免在名称中改写字母。记住单词的拼写是一件困难的事情,而记住改了字母的单词则更 困难。例如,通过改写字母把 highlight 写成 hilite 以节省三个字母,将使得读者很难记住这个 单词被改写成什么样了,是 Hilite,还是 Hai-a-lai-t?谁知道呢? 避免常见的容易拼写错的单词避免常见的容易拼写错的单词避免常见的容易拼写错的单词避免常见的容易拼写错的单词。Absense、acummulate、acsend、calender、conceive、defferred、 definate、independance、occassionally、prefered、reciept、superseed 等是英语中经常容易拼写错 误的,绝大多数英语词典中都列有常见的容易拼写错的单词。应避免在变量名中使用这些单词, 以避免因拼写错造成程序中的错误。 不要单纯通过大写来区分变量名不要单纯通过大写来区分变量名不要单纯通过大写来区分变量名不要单纯通过大写来区分变量名。如果使用的是可以区分大小的语言,可能会试图用 Frd 来代替 fired,用 FRD 来代替 final review duty,用 frd 来代替 full revenue disbursal,应放弃这种 做法。尽管每个名字都是唯一的,但其中每个名称所代替的意义则是任意且容易混淆的。谁能 知道 Frd,FRD 和 frd 分别对应的是 fired,final review duty 和 full revenue disbursal 而不是按其 它顺序来对应的呢? 避免使用标准子程序名和已定义的变量名避免使用标准子程序名和已定义的变量名避免使用标准子程序名和已定义的变量名避免使用标准子程序名和已定义的变量名。所有的语言都要求保留其标准子程序名和已 定义变量名,应注意避免使用这些子程序和变量。比如,下面这段代码在 PL/I 中是合法的,但 若你真的这样的话,那你一定是一个十足的傻瓜: if if = then then then = else; else else = if; 不要使用与变量所代表的实体没有任何联系的名字不要使用与变量所代表的实体没有任何联系的名字不要使用与变量所代表的实体没有任何联系的名字不要使用与变量所代表的实体没有任何联系的名字。像 Margaret 或 Coolie 之类的变量名事 实上保证了除你之外没有其它任何人能理解它的。不要用你的女朋友、妻子或朋友的名字作为 变量名,除非这个程序是关于你的男朋友、妻子或朋友的。即使真的是这样的话,你也应该意 识到他们是有可能变化的,因此用通用些的名字如:BoyFriend、wife 或 FavoriteBeer 会更好。 避免使用含有难以辨认字符的变量名称避免使用含有难以辨认字符的变量名称避免使用含有难以辨认字符的变量名称避免使用含有难以辨认字符的变量名称。要知道有些字符是非常相象的,很难把它们区分 开来。如果两个变量名的唯一区分便是一个或两个这种字符,那么你区分这些变量时就会感到 第九章 数据名称 139 十分困难。例如,请尝试一下把下表每一组中与其它两个变量名不同的一个找出来。 变量名表变量名表变量名表变量名表 EyeChart1 EyeChartI EyeChart1 TTLCONFUSION TTLC0NFUSION TTLCONFUSION Hard2Read HardzRead Hard2Read GRANDTOTAL GRANDTOTAL 6RANDTOTAL Ttl5 TtlS TtlS 如上表所示,难以区分的字符有"l"和"1"、"1"和"I"、"."和","、"0"和"o";"S"和"5" 、"G" 和"6"等。 9.8 9.8 9.8 9.8 小结小结小结小结 · 恰当的变量名是可读性好的必要条件之一。特殊的变量如循环变量和状态变量要予以 特殊考虑。 · 命名约定可以区分局部、模块和全局变量。同时它还可以区分类型名称,比如可以对 命名常量、枚举类型和变量加以区分。 · 不管你从事的是哪种项目,都应该采用命名约定。所采用的命名约定取决于程序的规 模和从事这一程序的程序员的人数。 · 匈牙利约定是一种非常有效的命名约定,比较适于大规模项目和程序。 · 在现代编程语言中几乎不需要采用缩写技术。 9.8.1 9.8.1 9.8.1 9.8.1 检查表检查表检查表检查表 通用命名约定通用命名约定通用命名约定通用命名约定 · 变量名称是否完全准确地描述了变量代表的是什么? · 变量名是否指向是客观世界中的问题,而不是关于这问题的用程序语言表达解决方案? · 变量名称是否是够长,使得你不必破译它? · 变量名中如果含有计算限定词的话,是否将其放在最后? · 是否在名称中用 Count 或 Index 来代替了 Num? 对特殊类型数据的命名对特殊类型数据的命名对特殊类型数据的命名对特殊类型数据的命名 · 循环变量的名称是有意义的吗?(如果循环体较长是嵌套循环的话,应用有含义的名 称来代替 i、j、k 之类的名称) · 是否用更富于含义的名称来代替了被叫作"tempotarg"的临时变量? · 当逻辑变量的值是"True"时,它的名称是否充分表达了其含义? · 是否用前缀或后缀来表明了某些枚举类型是一类的?如用 Color 来作 ColorRed, ColorGreen,ColorBlue 等枚举类型的前缀。 · 命名常量的名称是否是指向它们代表的实体而不是它们所代表的数值的? 命名约定命名约定命名约定命名约定 · 命名约定是否区分了局部、模块和全局数据? 第九章 数据名称 140 · 命名约定是否对类型名称、命名常量、枚举类型和变量进行了区分? · 在不支持强化仅供子程序输入参数的语言中,命名约定是否对这类参数进行了标识? · 命名约定是不是与程序语言的标准约定尽可能地相容? · 对于语言中没有强制的子程序中仅做输入的参数,是否约定将它标识了? · 是否对名称进行了格式化以增强程序的可读性? 短名称短名称短名称短名称 · 代码是否使用了长名称?(除非有必要使用短名称) · 是否避免了只省略一个字母的缩写? · 所有单词保持缩写的连续性了吗? · 所有的名称都是容易发音的吗? · 是否避免了会引起错误发音的名称? · 是否在注释表中对短变量名进行了注释? 避免如下这些常见的命名错误了吗避免如下这些常见的命名错误了吗避免如下这些常见的命名错误了吗避免如下这些常见的命名错误了吗 · 易引起误会的名称 · 含义相似的名称 · 仅有一或两个字母不同的名称 · 发音相似的名称 · 使用数字的名称 · 对单词作改写以使其比较短的名称 · 英语中常拼写错的名称 · 标准库子程序或已定义的变量名又定义了 · 完全是随意的名称 · 含有难以辨识字母的名称 第十章 变 量 141 第十章第十章第十章第十章 变变变变 量量量量 目录目录目录目录 10.1 作用域 10.2 持久性 10.3 赋值时间 10.4 数据结构与控制结构的关系 10.5 变量功能单一性 10.6 全局变量 10.7 小结 相关章节相关章节相关章节相关章节 生成数据:见第 8 章 数据命名:见第 9 章 使用基本数据类型:见第 11 章 使用复杂数据类型:见第 12 章 格式化数据说明:见 18.5 节 说明数据:见 19.5 节 由于前面一章叙述的都是数据名称问题,你可能会认为恰当地命名变量之后便完事大吉了。 绝非如此!命名仅仅是开始,你使用变量的方法也是非常重要的。 如果你是个富有经验的程序员的话,那么本章的内容对你尤其有用。在你完全清楚替代方 案之前,很容易在开始时使用有害的技术。然后,即使在你知道如何避免它们时,出于习惯仍 会继续使用它们。有经验的程序员们将会发现10.5 节“变量功能单一性”和 10.6 节“全局变 量”的讨论对他们来说是非常有趣的。 10.110.110.110.1 作用域作用域作用域作用域 作用域指的是变量名称的影响范围,也可称之为可见性,即在程序中某一变量被知道和提 及的范围。作用域有限或很小的变量只在程序的一小部分中被知道,如:一个只有在一个小循 环中用到的循环变量。作用域大的变量则在程序中的许多地方被知道,如:在一个程序中被到 处使用的雇员信息表。 不同的语言处理作用域的方式是不同的。在 Basic 某些实现中,所有变量都是全局的。因 此在这种情况下你无法对变量作用域进行任何控制,这也是 Basic 的主要缺点之一。在 C 中, 变量可以是对块(用大括号括起来的部分)可见的,也可以是分别对子程序、源文件或整个程 序可见的。在 Ada 中,变量可以分别是对块、亚程序、包、任务、单元或整个程序中可见的。 以下是一些关于作用域(可见性)的常用准则: 第十章 变 量 142 尽可能减小作用域尽可能减小作用域尽可能减小作用域尽可能减小作用域。你所采取的方法往往取决于你“方便性”和“可管理性”这两个问题 的看法。许多程序员喜欢用全局变量。因为全局变量存取很方便而且程序员们不必围着参数表 和模块命名规则转。事实上,这种存取方便性是与由全局变量引入的危险共存的。 另一些程序员则尽可能使用局部变量,因为局部变量可以提高可管理性。你所隐含的信息 越多,那么需要记在心中的东西就越少,而需要记住的东西越少,那 么犯错误的机会也就越少, 因为许多细节都不需要再进行记忆了。 “方便性”和“可管理性”之间的区别可以理解为是强调写程序还是强调读程序。扩大变 量作用域事实上的确可以方便写程序,但是一个任意子程序都可以在任意时刻访问任一个变量 的程序,往往要比使用模块化子程序的程序难懂得多。在这种程序中,你无法单纯理解一个子 程序,你必须同时也理解与这个子程序分享全局变量的其它子程序。这样的程序不仅难读,而 且也很难调试和修改。 因此,你必须尽可能地减小变量的作用域。如果能将变量的作用域限制在一个子程序之内 的话,那是再好不过的了,如果你无法把它限制在一个模块中的话,那就利用存取子程序来使 几个模块分享这一数据。总之,应尽量避免使用全局变量以减小作用域。 把对某一变量的引用集中放置把对某一变量的引用集中放置把对某一变量的引用集中放置把对某一变量的引用集中放置。某些研究人员认为把对某一变量的访问放得越近,那么对 程序阅读者的精神压力也就越小。这一想法有很大的直觉吸引力——你每次只需注意比较少的 变量。以下是由这一想法产生的几项准则: 应恰好在某一循环前初始化循环中用到的变量,而不是在含有这个循环的子程序开头对其 中用到的变量进行初始化。这样作可以使你在修改循环时,同时想起对相应的变量的初始化进 行修改;或者在这一循环外再嵌套一个循环时,此时外部循环每执行一次时,都会对内部循环 用到的变量进行初始化,而不会出现只初始化一次的错误。 要在用到某一变量时才对它进行赋值。你可能有过费尽心机地寻找某一变量到底是在哪被 赋值的体验。因此,越清楚地表示出变量赋值的地方越好。 下例指出了在一个计算日收入的子程序中,是怎样把对同一变量的引用集中放置,以便方 便地寻找它们的。第一个例子是违反这一原则的一个C语言子程序: void SummarizeData(...) { ... ... GetOldData(OldData, &NumOldData); GetNewData(NewData, &NumNewData); TtlOldData = Sum(OldData,NumOldData); TtlNewData = Sum(NewData,NumNewData); 语句使用两组变量 PrintOldDataSummary(OldData,TtlOldData,NumO1dData); PirntNewDataSummary(NewData,TtlOldData,NumNewData); SaveOldDataSummary(TtlOldData,NumOldData); SaveNewDataSummary(TtlNewData,NumNewData); ... ... } 在上例中,你不得不同时注意 OldData、NewData、NumOldData、NumNewData、TtlOldData 第十章 变 量 143 和 TtlNewData 六个变量,而且又是在这样短的一段程序中。下面的例子指出了如何把这一数量 减少到只有三个: void SummariseDaily( ... ) { GetOldData(OldData, &NumOldData); TtlOldData = Sum(OldData, NumOldData); PrintOldDataSummary(OldData,TtlOldData,NumOldData); SaveOldDataSummary (TtlOldDataNumOldData); ... GetNewData( NewData, &NumNewData); TtlNewData = Sum( NewData, NumNewData); PrintNewDataSummary( NewData,TtlNewData,NumNewData); SaveNewDataSummary( TtlNewData, NumNewData ); ... } 如果像上例这样把程序分用两块,那么每一块都要比原来的块要短,而且其中的变量也要 少得多。这两个块都很容易理解,而且如果需要把这段程序分成几个子程序的话,两个具有较 少变量的块本身就是定义得很好的子程序。 10.210.210.210.2 持久性持久性持久性持久性 “持久性”指的是某一数据的使用寿命。持久性有几种表现形式。如下所示: · 与某一个块或子程序的生存期一样长。C中的auto变量或Pascal中的局部变量就属 于这种情况。 · 其生存期取决于你的意愿。Pascal中用New()产生的变量直到dispose()它们时才失效。 在C中用malloc()产生的变量也将持续到free()它们时才失效。 · 与程序的生存期一样长。绝大多数语言中的全局变量都属于这种情况。比 如 c中的static 变量和 Turbo Pascal中“类型化的常量”(类型化常量是对Pascal的非标准推广)。 · 永远有效。这些变量可能包括你在程序再次执行之间存储在数据库中的变量。例如, 在一个交互式的程序中用户可以定义屏幕的颜色,可以把这些颜色存在一个文件中, 在每次程序加载时再将其调出,现在有少数几种语言支持这种持久性。 假定的某个变量的持久性要长于其实际持久性时,就会出现问题。变量就像放在冰箱中的 牛奶,你认为它可以保存一星期,但有时可以保存一个月,有时则五天就坏了。变量的持久性 也是同样不可测的。当变量的有效生存期已经结束时,还试图重新使用它,它还会保持原值吗? 有些情况下变量中的值已经被改变了。这可以使你意识到自己的错误,而在另一些情况下,计 算机还让变量保持原值,从而让你认为自己正确地使用了变量。 以下是可以使你避免这种错误的几个步骤; · 在程序中加入调试代码来检查变量的值是否合理。如果不合理的话。产生一个警告 使用 OldData 的语句 使用 NewData 的语 第十章 变 量 144 信息来提示检查不恰当的变量初始化。 · 在写代码时假定变量已经失效。比如,退出子程序时某一变量等于某个值,当再次进 入子程序时不要假定这个变量仍保持原值。当然,当你用语言的特定功能来使变量保 持原值时这一原则不适用,比如用 C 中的 static 如来实现这一功能。 · 养成在恰好使用某一变量之前对其进行初始化的习惯。如果发现使用的变量附近没有 对它的初始化,那么你就要小心了。 10.310.310.310.3 赋值时间赋值时间赋值时间赋值时间 一个对程序的维护性和可读性有深远影响的主题是“赋值时间”——把变量的值和变量联 系在一起的时间。是在写程序时把它们联系在一起?还是在编译、加载或者程序运行时把它们 联系在一起? 应该尽可能地晚一些将它们联系在一起。通常,越是晚一些给变量赋值,代码的灵活性便 越大。下面是在可能的最早时间——程序写成时对变量进行赋值例子(用C写成): TestID=47; 由于47是一个程序的常数值,因此在代码写好时47便与TestID联系在一起了。像上例那样 编码赋值的想法是很有害的,因为如果当这里的47改变时,程序其余用到47且必须与TestID的 值相同的地方很可能会出现语法错误。 以下是一个稍晚些赋值的例子,即在编译时进行赋值: #define MAX_ID 47 ... TestID = MAX_ID; MAX_ID是一个宏或是命名常量,当编译时编译程序会用一个值来代替它。如果所用的语 言支持这种用法的话,应尽量这样用,因为这种方法要好于前面的用47来硬性赋值。它使得改 变MAX_ID值变得很容易,因为你只需要在一个地方作出改动就可以了,而且不会影响程序性 能。 下面是在最后时刻赋值的例子,即在运行时赋值。 TestID = MaxID; 程序中MaxID是一个在程序运行时被赋值的变量。这样做的灵活性和可读性也要好于前面 的硬性编译赋值。下面是另一个在运行时赋值的例子: TestID = ReadFileForMaxID(); 上例中的ReadFileForMaxID()是一个在程序运行时从一个文件中读取数值的子程序。这一 例子假定在程序开始运行之前要用到的值已经被放在文件中了。显然这样作的灵活性和可读性 也要好于前述的硬性编码赋值的例子。不必通过改动程序来改变TestID的值,而只要改动一下 存储该值的文件就可以了。这种方法常用于用户可以定义应用环境的交互式系统中,用户的定 义被存储在一个文件中,当程序运行时从文件中读取定义。 下面是在程序运行时进行赋值的最后一种形式: TestID = GetMaxIDFromUser(); 上例中 GetMaxIDFromUser()是一个采用交互方式从用户那里读取数值的子程序。这种方 第十章 变 量 145 法的可读性和灵活性要远远好于硬性编码赋值。为改变TestID值根本不需作出任何改动,只要 在程序运行时由用户输入另外一个值就可以了。从上述在运行时赋值的例子可以看出,即使同 样是在运行时赋值的方式,变量与其值联系到一起的具体时间也是不同的。最后一个例子中的 赋值可以在程序运行中任一时刻进行,它取决于用户被要求输入TestID值的时间。 10.410.410.410.4 数据结构与控制结构的关系数据结构与控制结构的关系数据结构与控制结构的关系数据结构与控制结构的关系 许多研究者都曾试图努力找出数据结构与控制结构之间的通用关系,这其中最成功的是英 国计算机学家 Michael Jackson。Jackson的技术,主要是通过一种系统的方法把数据结构变换为 控制结构。他的方法在欧洲得到了充分发展并被广泛应用。本书无法详细论述Jackson的理论, 但可以对这种理论所基于的数据和控制流之间的调节关系作一概述。 从可用的数据和输出该是什么样子的想法开始 然后对程序进行定义,使其把输入转化为输出 Jackson勾画出了三种类型数据与相应的控制结构之间的关系。 程序中顺序性程序中顺序性程序中顺序性程序中顺序性数据可以转化为顺序性语句数据可以转化为顺序性语句数据可以转化为顺序性语句数据可以转化为顺序性语句。数列是由一组按某一特定顺序使用的数据组成 的。如果用排成一列的五条语句来处理五个不同的数值,那么它们就是顺序性语句,如果需要 从某一文件只读取雇员的名字、社会保险号码、地址、电话号码和年龄等五个数据,那你将在 程序中使用顺序性语句来从文件中读取这些顺序性数据。 程序中的选择性数据可以转换为程序中的选择性数据可以转换为程序中的选择性数据可以转换为程序中的选择性数据可以转换为if和和和和case语句语句语句语句。通常,选择性数据指的是在任一特定时刻, 几个数据中的某一个会出现——将选定其中的某一个数据。相应的程序必须用If-Then-Else语句 或Case语句进行选择操作。比如在一个工资发放系统中,你可能需要按某一雇员是按小时计酬 第十章 变 量 146 顺序性数据是指按某一指定顺序进行处理的数据 还是按固定薪水计酬的对其进行不同的处理。程序中的模式就与数据中的模式相容。 对选择性数据你可以使用两个中的任何一个,但不能同时 程序中的重复性数据可以转化为程序中的重复性数据可以转化为程序中的重复性数据可以转化为程序中的重复性数据可以转化为for、、、、repeat和和和和while循环结构循环结构循环结构循环结构。重复性数据指的是同一类型 要重复几次的数据。通常,这类数据是作为记录存储在文件或数组中的,比如从文件中读取的 一列社会保险号码。重复性数据将与读取它们的循环相容。 重复性数据的操作需要重复 真实的数据可能是上述几种类型数据的组合,这时可以用上述几种操作的组合处理复杂的 数据结构。 10101010....5555 变量功能单一性变量功能单一性变量功能单一性变量功能单一性 可以通过几种“巧妙”的办法使变量具有一个以上的功能,但最好不要使用这种所谓的巧 妙办法。 应使每一个变量只具有一个功能应使每一个变量只具有一个功能应使每一个变量只具有一个功能应使每一个变量只具有一个功能。有时人们会试图在两个不同的地方使用同一变量来进行 两个不同的活动。通常,变量名对其中的一个活动来说是不恰当的,或者在两种情况下都充当 了“临时变量”的角色(且是用无意义的X或Temp来命名的)。下面给出了一个用C语言写成的 一个临时变量具有两个功用的例子: /* Compute roots of a quadratic equation. 第十章 变 量 147 This code assumes that ( b * b - 4 * a * c ) is positive. */ Temp = sqrt(b * b - 4 * a * c); root[0] = (-b + Temp)/(2 * a); root[1] = (-b - Temp)/(2 * a); ... /* swap the roots */ Temp = root[0]; root[0] = root[1]; root[1] = Temp; 这段程序的问题是:头几行代码中的Temp与后几行代码中的Temp是什么关系呢?事实上它 们之间没有任何关系,可是,这样使用它会使人们误以为它们之间存在着某种联系,从而产生 困惑,以下是对上面程序的改进: /* Compute roots of a quadratic equation. This code assumes that (b^2 - 4 * a * c) is positive. */ Discriminant = sqrt(b * b - 4 * a * c); root[0] = (-b + Discriminant)/(2 * a); root[1] = (-b - Discriminant)/(2 * a); ... /* swap the roots */ Oldroot = root[0]; root[0] = root[1]; root[1] = Oldroot; 避免使用具有隐含意义的变量避免使用具有隐含意义的变量避免使用具有隐含意义的变量避免使用具有隐含意义的变量。一个变量具有一个以上功用的另一种情况是同一变量取值 不同时,其代表的意义也不同。如变量PageCount代表是已经打印的页数,但当它等于-l时则表 示发生了错误;变量CustomerID代表的是顾客号码,但当它的值超过500,000时,你要把 CustomerId的值减掉500,00O以得到一个过期未付款的帐号;而BytesWritten一般表示的是某一输 出文件的字节数,但当其值为负时,则表示的是用于输出磁盘驱动器数目。 应避免使用上述具有隐含意义的变量。技术上把这种滥用称之为“混合关联”。使用同一变 量来承担两项任务意味着对其中一项任务来说变量类型是错误的。比如上例中的PageCount,在 正常情况下代表页数时是整型的,而当它等于-1表示发生了错误时,则是把整型变量当作逻辑 型来用了。 即使你清楚这种用法,别人也往往很难理解。如果用两个变量来分别承担两项工作的话, 其意义的清晰性将会使你感到满意,并且,也不会在存储上有额外的麻烦。 保证所有说明的变量保证所有说明的变量保证所有说明的变量保证所有说明的变量。与用一个变量来承担两项工作相反的另一个极端是根本不用它,而 研究表明未被使用的变量往往是与出错率高相联系的。因此,要养成确实用到每一个说明变量 的习惯。某些编译程序会对说明但未用到的变量给出警告。 第十章 变 量 148 10.610.610.610.6 全局变量全局变量全局变量全局变量 全局变量在程序的任何地方都可以进行存取。有时它也被非正式地用来指可存取范围大于 局部变量的变量,如在某个单一文件中可以随处存取的模块变量。但是,在单独一个文件中随 处可存取,本身并不能表示某一变量是全局的。 绝大多数有经验的程序员都认定使用全局变量要比局部变量危险,同时他们也认为利用几 个子程序来存取数据是非常有益的。尽管对使用全局数据的危险性有许多附和的意见,但研究 发现全局变量的使用与错误率上升之间并无联系。 即使使用全局变量是没有危险的,使用它也决非最好的编程方法。本书其余部分将讨论由 此而引入的问题。 10.6.1 10.6.1 10.6.1 10.6.1 伴随全局变量的常见问题伴随全局变量的常见问题伴随全局变量的常见问题伴随全局变量的常见问题 如果不加选择地使用全局变量,或者不使用它们就感到很不方便,那么你很可能还没有充 分意识到信息隐蔽和模块化的好处。模块化和信息隐蔽可能并不是最终真理,但它们对程序的 可读性和维护性的贡献是令其它技术望尘莫及的,一旦你懂得了这一点,你就会使用与全局变 量关系尽可能少的子程序和模块。 对全局数据的疏忽改变对全局数据的疏忽改变对全局数据的疏忽改变对全局数据的疏忽改变。你可以会在某处改变全局变量的值,而在别处又会错误地以为它 仍保持着原值,这就是所谓的副作用,例如在下面的Pascal程序段中,TheAnswer就是个全局变 量: TheAnswer := GetTheAnswer; ——TheAnswer是一个全局变量 OtherAnswer := GetOldAnswer; ——GetOtherAnswer改变了TheAnswer AverageAnswer := (TheAnswer + OtherAnswer )/2; ——AverageAnswer是错误的 你可能以为对GetOtherAnswer的调用并没有改变TheAnswer的值,如果真的是这样的话,那 么第三行中的平均就是错误的。事实上,对GetOtherAnswer的调用,的确改变了TheAnswer的值, 因此程序中有一个需要改正的错误。 伴随全局变量的奇怪的别名问题伴随全局变量的奇怪的别名问题伴随全局变量的奇怪的别名问题伴随全局变量的奇怪的别名问题。“别名”指的是用两个或更多的名称来叫某一变量。当 全局变量被传入子程序,然后又被子程序既用作全局变量又用作参数时,就会产生这种问题。 以下是一个用到全局变量的Pascal子程序: Procedure WriteGlobal (VAR InputVar:Integer); begin Globa1Var := InputVar + 1; Writeln( 'InputVariable:', InputVar); writeln( 'GlobalVariable:', Globe1Var); end; 下面是一个把全局变量当作变元来调用上面子程序的程序: WriteGlobel(GlobalVar); 由于WriteGlobal把InputVar加1后得到了GlobalVar,你会以为GlobalVar要比InputVar大l,但下面 第十章 变 量 149 却是足以令你大吃一惊的运行结果: Input Variable: 2 G1obal Variable: 2 这里令人难以置信的是:事实上GlobalVar和InputVar是同一个变量,由于GlobalVar是通过 调用子程序被传入WriteGlobal()的,因此它被用两个不同的名字提及了,或者说它是被“别名” 了,从而Writeln()的结果与你想要的便是大相径庭的了。虽然你用两个不同的名字来区分全局 变量,但它们还是将同一变量打印了两次。 有全局数据的代码重入问题有全局数据的代码重入问题有全局数据的代码重入问题有全局数据的代码重入问题。通过不止一个控制进入代码,已经变得越来越普遍了。在 Microsoft Windows、Apple Macintosh和OS/2 Presentation Manager及递归子程序中都用到了这种 代码。代码重入使得全局变量不仅可能被子程序们分享,而且可能被同一程序的不同拷贝所分 享。在这种环境下,你必须保证即使在某一程序的多个拷贝都在运行的情况下,全局数据仍保 持着它原来的意义。这是一个很有意义的问题,你可以根据后面推荐的技术来避免这一问题。 全局数据妨碍重新使用的代码全局数据妨碍重新使用的代码全局数据妨碍重新使用的代码全局数据妨碍重新使用的代码。为了从另一个程序中借用某段代码,首先你要从这个程序 中把这段代码取出来,然后把它插入要借用它的程序中。理想的情况是你只需把要用的模块或 单个子程序拿出来放入另一个程序中就可以了。 但全局变量的引入则使这一过程变得复杂化了。如果你要借用的模块或子程序使用了全局 变量,你就不能简单地把它拿出来再放入另一个新程序了。这时你必须对新程序或旧的代码进 行修改以使得它们是相容的。如果想走捷径的话,那最好对旧代码进行修改,使其不再使用全 局数据。这样作的好处是下次再要借用这段代码时就非常方便了。如果不这样作的话,那你就 需要改动新程序,在其中建立旧有的模块或子程序要用到的全局数据。这时全局变量就像病毒 一样,不仅感染了旧程序,而且随着被借用的旧程序中的模块或子程序传播到了新程序中。 全局变量会损害模块性和可管理性全局变量会损害模块性和可管理性全局变量会损害模块性和可管理性全局变量会损害模块性和可管理性。开发大于几百行规模软件的一个主要问题便是管理的 复杂性,使其成这可管理的唯一办法便是将程序成为几个部分,以便每次只考虑其中的一个部 分。模块化便是将程序分为几部分的最有力工具之一。 但是全局数据却降低了你进行模块化的能力。如果使用了全局数据,不能做到每次只集中 精力考虑一个子程序吗?当然不能,这时你将不得不同时考虑与它使用了相同全局数据的其余所 有子程序,尽管全局数据并没有摧毁程序的模块性,使它减弱了模块性,仅凭这一点就该避免 使用它。 10.6.210.6.210.6.210.6.2 使用全局数据的理由使用全局数据的理由使用全局数据的理由使用全局数据的理由 在某些情况下全局数据也是很有用的: 保存全局数值,有时候需要在整个程序中都要用到某些数据保存全局数值,有时候需要在整个程序中都要用到某些数据保存全局数值,有时候需要在整个程序中都要用到某些数据保存全局数值,有时候需要在整个程序中都要用到某些数据。这些数据可能是反映程序状 态的——在交互式状态时还是批处理状态?是正常状态还是错误修正状态?它们也可能是在整 个程序中都要用到的信息,如程序中每一个子程序都要用到的一个数据表。 代替命名常量代替命名常量代替命名常量代替命名常量。尽管C、Pascal等绝大多数现代语言都支持命名常量,但仍有一些语言不支 持,这时,可以用全局变量来代替命名常量。例如,你可以通过分别给TRUE和FALSE赋值”1” 和”0”采用它们代替常量型值1和0,或者通过LinesPerPage = 66这一语句把每页行数(66行) 赋给LinesPerPage,从而用LinesPerPage来取代66。通过使用这种方法,可以改善代码的可读性和 第十章 变 量 150 易改动性。 方便常用数据的使用方便常用数据的使用方便常用数据的使用方便常用数据的使用。有时候需要非常频繁地使用某一个变量,以至于它几乎出现在每一 个子程序的参数表中,与其在每个子程序的参数表中都将这个变量写一次,倒不如使它成为全 局变量方便。在这种情况下,这一变量几乎是到处都被存取的,不过,很少有这种情况,更多 的情况是它是被一组有限的子程序存取的,这时你可以将这些子程序及数据装入一个模块,这 将在稍后详细讨论。 消除“穿梭”数据消除“穿梭”数据消除“穿梭”数据消除“穿梭”数据。有时候把某个数据传入一个子程序中仅仅是为了使它可以把这一数据 传入另一个子程序中,当这个传递链中的某个子程序并没有用到这个数据时,这个数据就被叫 做“穿梭数据”。使用全局变量可以消除这一现象。 10.6.3 10.6.3 10.6.3 10.6.3 怎样降低使用全局数据的危险怎样降低使用全局数据的危险怎样降低使用全局数据的危险怎样降低使用全局数据的危险 你可能会认为下面这些准则让人感到很不自由也很不舒服,或许的确是这样,但我想你一 定知道“良药苦口利于病”这句话吧? 先使所有变量都成为局部的,然后再根据需要先使所有变量都成为局部的,然后再根据需要先使所有变量都成为局部的,然后再根据需要先使所有变量都成为局部的,然后再根据需要把其中某一些改为全局变量。把其中某一些改为全局变量。把其中某一些改为全局变量。把其中某一些改为全局变量。首先使所有变 量针对单个子程序来说都是局部的。如果发现还需要在别的地方使用它,那么在使它成为全局 变量之前应先使它成为模块变量。如果最后发现必须使它成为全局变量,你可以这样作,但必 须确认这是迫不得已的。如果开始就使某一变量成为全局的,那么你决不会再把它变成局部的, 而很可能如果开始把它用成局部的话,你永远也不会再把它变为全局的。 区分全局和模块变量。区分全局和模块变量。区分全局和模块变量。区分全局和模块变量。如果某些变量是在整个程序中存取的,那么它们就是真正的全局变 量。而某些变量只在一组子程序中存取,事实上是模块变量。在指定的那组子程序中,对模块 进行任何存取操作都是可以的,如果其它子程序要使用它的话,应通过存取子程序来进行。即 使是程序语言允许,也不要像对待全局变量那样对模块变量进行存取操作。要在自己的耳边不 停地说“模块化!模块化!模块化!”。 建立使你一眼即可识别出全局变量的命名约定。建立使你一眼即可识别出全局变量的命名约定。建立使你一眼即可识别出全局变量的命名约定。建立使你一眼即可识别出全局变量的命名约定。如果使用全局变量的操作是十分明显的, 可以避免许多错误。如果不只是出于一个目的使用全局变量(如既作为变量又用替换命名常量), 那一定要保证命名约定可以区分这些不同目的。 建立一个清楚标出所有全局变量的注释表。建立一个清楚标出所有全局变量的注释表。建立一个清楚标出所有全局变量的注释表。建立一个清楚标出所有全局变量的注释表。建立标识全局变量的命名约定,对表明变量的 功用是非常有帮助的,而一个标有所有全局变量的注释表则是读你程序的人最强有力的辅助具 之一(Glass 1982)。 如果你用的是如果你用的是如果你用的是如果你用的是 Fortran 语言,那么仅使用标号语言,那么仅使用标号语言,那么仅使用标号语言,那么仅使用标号 COMMON 语句,不要使用空白语句,不要使用空白语句,不要使用空白语句,不要使用空白 COMMON。。。。 空白 COMMON 可以使任意一个子程序存取任意一个变量。使用命名的 COMMON 来详细规定 可以存取特定 COMMON 数据的特定子程序,这是一种在 Fortran 中模拟使用模块数据的方法。 研究表明这种方法是很有效的。 使用加锁技术来控制对全局变量的存取。使用加锁技术来控制对全局变量的存取。使用加锁技术来控制对全局变量的存取。使用加锁技术来控制对全局变量的存取。与多用户数据库环境下,当前值的控制方式类似, 在全局变量被使用或更新之前锁定要求,这个变量必须被“登记借出”,在变量被使用过之后, 再被“登记归还”,在它不能被使用期间(已经被登记借出),如果程序其它部分企图要求将它 登记借出,那么加锁/开锁子程序将打印出错误信息。 加锁技术在开发阶段是有用的。加锁技术在开发阶段是有用的。加锁技术在开发阶段是有用的。加锁技术在开发阶段是有用的。当程序最终成为产品时,程序应该被改动来做比打印更有 意义的工作,使用存取子程序来存取全局变量的好处之一,就是使你可以方便地实现加锁/开 第十章 变 量 151 锁技术,而如果不加限制地存取全局变量的话,就很难实现这一技术。 不要通过把数据放人庞大的变量,同时又到处传递它来掩盖你使用了全不要通过把数据放人庞大的变量,同时又到处传递它来掩盖你使用了全不要通过把数据放人庞大的变量,同时又到处传递它来掩盖你使用了全不要通过把数据放人庞大的变量,同时又到处传递它来掩盖你使用了全局变量的事实。局变量的事实。局变量的事实。局变量的事实。把 什么都放入一个巨大的结构,可能从字面上满足了避免使用全局变量这准则,但这只是表面文 章,事实上这样做,得不到任何真正模块化的好处,如果你使用了全局变量的话,那就分开使 用它,不要企图用臃肿的数据结构来掩盖它。 10.6.4 10.6.4 10.6.4 10.6.4 用存取子程序来代替全局数据用存取子程序来代替全局数据用存取子程序来代替全局数据用存取子程序来代替全局数据 用全局数据能作的一切,都可以通过使用存取子程序来做得更好,存取子程序是建立在抽 象数据类型和信息隐蔽的基础上的。即使不愿使用纯粹的抽象数据类型,仍然可以通过使用存 取子程序来对数据进行集中控制并减少因改动对程序的影响。 存取子程序的优点存取子程序的优点存取子程序的优点存取子程序的优点 以下是使用存取子程序的优点: · 可以对数据进行集中控制。如果你以后又找到了更合适的数据结构,那么不必在每一处涉 及到数据的地方都进行修改,而只修改存取子程序就可以了,修改的影响可以被限制在存 取子程序内部。 · 可以把所有对数据的引用分隔开来,从而防止因其错误造成的影响蔓延。使用类似 Stack.array[stack.top]new_element 的语句来把元素压入堆栈时,很容易忘记检查堆栈是否溢 出从而造成错误。如果使用存取子程序,例如 push stack( new_element ),就可以把检查堆 栈是否溢出的代码写入 push_stack()子程序,这样每次调用子程序都可以对堆栈自动进行检 查,而你则可以不必再考虑堆栈溢出问题。 · 你可以自动获得信息隐蔽带来的好处。即使你不是为了信息隐蔽才使用存取子程序的,它 也是信息隐蔽的范例性技术之一。你可以改变存取子程序的内部而不必改动程序的其余部 分。打个比方,存取子程序使你可以改变房屋的内部陈设而不会变动房屋的外观,这样你 仍然可以很容易便找着你的家。 · 存取子程序很容易转换为抽象数据类型。存取子程序的一个优点是:它可以得到很高的抽 象级,而直接存取全局变量却难以做到。例如,你可以用存取子程序 ifPageFull()来代替 语句 if lineCount > Maxlines,虽然这是个很小的收益,但是大量的这类差别便积聚成了高 质量软件与东拼西凑到一起的软件之间的不同之处。 怎样使用存取子程序怎样使用存取子程序怎样使用存取子程序怎样使用存取子程序 以下是关于存取子程序理论与应用简短论述:将数据隐含在模块中,编写可以使你访问并 修改数据的子程序,数据所在模块之外的子程序要求存取数据时,应让它通过存取子程序而不 直接地存取模块内的数据。比如,假设有一个状态变量你可以通过两个存取子程序 Getstatus() 和 SetStatus()来对其进行存取操作。 以下是关于存取子程序使用的几条较为详细的准则: 要求所有子程序来对数据进行存取操作要求所有子程序来对数据进行存取操作要求所有子程序来对数据进行存取操作要求所有子程序来对数据进行存取操作。通过存取子程序将数据结构隐含起来。通常需要 两个子程序,一个读取数据的值,而另一个用于赋给它新值。除去几个可以直接存取数据的服 务性子程序,其它子程序都应通过存取子程序接口来对数据进行存取。 第十章 变 量 152 不要把所有的全局数据都放入同一个模块中不要把所有的全局数据都放入同一个模块中不要把所有的全局数据都放入同一个模块中不要把所有的全局数据都放入同一个模块中。如果你把所有的全局数据都归成一个大堆, 并编写对其存取的子程序,的确可以消除由全局数据带来的问题,但同时也抛掉了信息隐蔽和 抽象数据类型所带来的优点。编写存取子程序之前,应先考虑一下每一全局数据应属于哪一个 模块,然后把这个全局数据、相应的存取子程序和其关联的子程序放入那个模块中。 在存取子程序中建立某种程度的抽象。在存取子程序中建立某种程度的抽象。在存取子程序中建立某种程度的抽象。在存取子程序中建立某种程度的抽象。在数据所代表的意义层次上而不是计算机本身的实 现细节层次上建立存取子程序,可以使你更容易应付可能的变动。 请比较下列两组语句: 直接使用复杂数据 通过存取子程序使用复杂数据 node=node.next node=nearestNeighbor(node) node=node.next node=nextEmployee(node) node=node.next node=nextRatele(node) Event=EventQueue[QueueFront] Event=HighestprioirtyEvent( ) Event=EventQueue[QueueFront] Event=LowesPriotityEvent( ) 表中前三个语句对中,抽象的存取子程序告诉你的信息要比数据结构所告诉你的多得多。 如果直接使用数据结构,那么一次需要做工作就太多了。你必须在表示出数据结构本身正在做 什么(移到链表中的下一个链)的同时,表示出正在对数据结构所代表的实体做什么(调用一 个邻居、下一个雇员或税率),这 对 于 简单数据结构来说是很重的负担。把信息隐蔽在存取子程 序后面,可以使代码自己指出这些,并且可以使得其它人从问题域而不是实现细节的层次上来 阅读程序。 把对数据的所有存取保持在同一抽象水平上。把对数据的所有存取保持在同一抽象水平上。把对数据的所有存取保持在同一抽象水平上。把对数据的所有存取保持在同一抽象水平上。如果你用了某一存取子程序对某一数据进行 了一项操作,那么对这一数据的其它操作也应通过存取子程序来实现。比如若是通过存取子程 序来从数据结构中读取数值的,那么对该数据结构的写操作也应通过存取子程序来实现。又比 如假设你通过调用 initstack()子程序将元素压入堆栈,但你接着又用 value=array(strack.top)来 获得堆栈的一个入口,那么此时对数据的观察点便不连续了。这种不连续性使别人很难理解你 的程序。因此,要保持对数据所有存取操作抽象水平的一致性。 在上表中的后两个语句对中,两个事件排队的操作是平行进行的。在队列中插入一个事件 将比表中其它操作都更复杂,因为你不得不改变队列的前后顺序,调整现存事件以便为它腾出 空间,再写几行代码以便找到插入它的地方等等,从一个序列中移出一个事件几乎是同样麻烦 的。因此,在编码时如果把复杂操作放入子程序,而其余操作直接对数据进行,将产生对数据 结构拙劣的、非平等的使用。请比较下面的两组语句: 对复杂数据的非平行使用对复杂数据的平行使用 对复杂数据的非平行使用 对复杂数据的平行使用 Event=EventQueue[QueueFront] Event=HightestPriorityEvent( ) Event=EventQueue[QueueBack] Event=LowestPriorityEvent( ) AddEvent(Event) AddEvent(Event) EventCount=EventCount-1 RemoveEvent(Event) 应注意这些准则适用许多模块和子程序构成的大型程序。在小一些的程序中,存取子程序 第十章 变 量 153 的地位也会相应降低。但不管怎样,实践已经证明,存取子程序是增强程序灵活性并避免由全 局变量带来问题的有效手段之一。 10.6.5 10.6.5 10.6.5 10.6.5 检查表检查表检查表检查表 使用数据时通常要考虑的一些问题使用数据时通常要考虑的一些问题使用数据时通常要考虑的一些问题使用数据时通常要考虑的一些问题 一般数据一般数据一般数据一般数据 · 是否已经使变量的作用域尽可能地小? · 是否把对变量的使用集中到了一起? · 控制结构与数据结构是相对应的吗? · 每个变量是否有且仅有一个功能? · 每个变量的含义都是明确的吗?是否保证了每个变量都没有隐含的意义? · 每一个说明过的变量都被用到了吗? 全局变量全局变量全局变量全局变量 · 是否是在迫不得已的情况下,才使某些变量成为全局的? · 命名约定是否对局部、模块和全局变量进行了区分? · 是否说明了所有全局变量? · 程序中是否不含有伪全局变量——传往各个子程序的庞大而臃肿的数据结构? · 是否用存取子程序来代替了全局数据? · 是把存取子程序和数据组织成模块而不是随意归成一堆的吗? · 存取子程序的抽象层次是否超过了程序语言实现细节? · 所有相互有联系的存取子程序,其抽象程度都是一致的吗? 10.7 10.7 10.7 10.7 小结小结小结小结 · 尽量减小变量的作用域。把对变量引用集中到一起,应尽量使变量成为局部或模块的, 避免使用全局变量。 · 使每个变量有且仅有一个功能。 · 并不是因为全局数据危险才避免使用它们,之所以避免用它是因为可以用更好的技术 来代替它。 · 如果全局数据确实不可避免的话,应通过存取子程序来对其进行存取操作。存取子程 序不仅具备全局变量和全部功能,而且可以提供更多的功能。
还剩78页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

ecjtuxuan

贡献于2010-11-23

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf