18伪代码2
1. 设计子程序
1.1 检查先决条件
检查该子程序要做的工作是不是已经定义好了。
1.2 定义子程序要解决的问题
- 这一子程序将要隐藏的信息
- 传给这一子程序的各项输入
- 从该子程序得到的输出
- 在调用程序之前确保有关的前条件成立(入输入数据的取值位于特定的范围之内、有关的流已经初始化、文件已经打开或者关闭、缓冲区已经填满或清空等)
- 在子程序将控制权交回调用方程序之前,确保其后条件的成立(如输出数据位于特定范围之内、流已经初始化、文件已经打开或者关闭、缓冲区已填满或清空等)
1.3 为子程序命名
给子程序命名看似不重要,但好的子程序名却是优秀子程序的标志之一,起个好名字也并不那么容易。一般来说,子程序应该由一个清晰、无歧义的名字。如果你在给子程序起个好名字时犯难,通常就表明这个子程序的目标还没明确。
1.4 决定如何测试子程序
1.5 在标准库中搜寻可用的功能
1.6 考虑错误处理
1.7 考虑效率问题
在第一种情况下,也是对绝大多数系统而言,程序的效率并不是十分要紧。这时你只要看看子程序的接口是否经过很好的抽象,看程序的代码是否易读,这样在日后需要的时候你可以随时对它进行改进。如果封装做的很好,你就可以用更好的算法,或者一段极快、又节省资源、用低级语言所写的实现代码来替换原来既慢又耗费资源的、用高级语言所写的实现代码,同时还不会影响其他的子程序。
另一种情况只是在少数系统里出现——在那里性能非常重要。性能问题可能与稀缺的数据库连接、受限的内存、不可多得的句柄、严格的时限或者其他一些稀有的资源相关。架构应当指明每一个子程序(或者类)有多少资源科使用,以及它们必须以多快的速度执行其操作。
除了在以上两种常见情况下采取推荐的做法之外,在每个子程序上为效率问题卖力通常是白费功夫。最主要的优化还是在于完善高层的设计,而不是完善每个子程序。通常,只有当能够证明高层次设计确认无法满足系统的性能目标时,你才需要进行微观的优化——不过除非整个程序全部完成,否则你也不会知道这一点。因此不要在微不足道的点滴改进上浪费时间,除非你知道确实有必要这样去做。
1.8 研究算法和数据类型
如果在可用的程序库里没有所需的功能,它也许会在某本算法书里介绍过。因此,在你决定从头开始编写一段复杂的代码之前,查一下算法书看看有什么可用的内容。如果要采用一个已有明确定义的算法,则要保证把这个算法正确地转换为你所使用的变成语言。
1.9 编写伪代码
在完成了前述这些步骤之后,你可能还没写太多东西。其实这些步骤的主要目的是为你确立一个思路,这在你真正写程序的时候是很有帮助的。
从最一般的情况写起,向着更具体的细节展开工作。子程序最常见的部分是一段头部注释,用于描述这段程序应该做些什么,所有首先简要地用一句话写下该子程序的目的。写出这句话将帮助你澄清对该子程序的理解。如果连概要性的注释写起来都很困难,那就得提醒你自己,还得更好地理解这一子程序在整个程序中的角色才行。一般来说,如果很难总结出一个子程序的角色。你可能就应该考虑是否什么环节出了问题。
1.10 考虑数据
1.11 检查伪代码
在写完伪代码并设计完数据之后,花上几分钟时间复查你写的伪代码。然后抛开这写代码,想想你该如何向别人解释这些代码。
找别人老看你写的代码,或者让他来听你的解释。你可能会想,让别人来看 11 行伪代码是不是很傻?做了之后你就会惊讶的。因为与用编程语言写成的代码相比,伪代码会让那些你自己想当然的,还有那些高层次的错误更加明显。而且,与审阅 35 行的 C++ 或者 Java 代码相比,人们更乐意来审阅只有寥寥几行的伪代码。
1.12 在伪代码中试验一些想法,留下最好的想法(迭代)
在你开始编写代码之前,你尽可能用伪代码去尝试更多的想法。一旦你真正开始编码,你和你所写下的代码就会有感情,从而就更难以抛弃不好的设计再重头来过了。
通常的想法是,用伪代码反复描述这个子程序,直到用伪代码写出的句子已经足够简单了,你可以在每个句子下面填入代码,并把原来的伪代码变为代码文档为止。你最初写出的一些伪代码可能还是层次太高,这就需要进一步分解它。一定要进一步的分解它。如果还不确定该怎么编写代码,那就继续在为伪代码上下功夫,知道能确定为止。持续地精化和分解伪代码,直到你觉得再写伪代码(而不是真正的代码)实在是浪费时间为止。
2.编写子程序的代码
2.1 写出子程序的声明
2.2 把伪代码转变为高层次的注释
2.3 在每条注释下面填充代码
每一段注释产生出一行或多行代码。以这些注释为基础,每一代码块都形成了一套完整的思想。这些注释仍然保留下来,从一个更高的层次上对代码做出说明。
2.4 检查代码是否需要进一步分解
有的时候,你会发现几行伪代码展开后形成大量的代码。在这种情况下,你应该考虑采取以下两种方法中的一种。
(1)把这段注释下的代码重构成一个新的子程序。如果你发现由一段伪代码发展形成的代码量超出了预期,那么就把这些代码重构为一个单独的子程序。
(2)递归地应用伪代码编程过程。与其在一行伪代码下面编写数十行的代码,还不如花些时间把原来的那一行伪代码分解成更多行的伪代码,然后再在新写出的伪代码下面填入代码。
3.检查代码
在设计并实现了一个子程序之后,第三大步骤便是检查你所构建的代码是否正确。在这一阶段留下的任何错误,只能到以后的测试阶段才能发现。到那时再查找和修正这些错误可能就更昂贵了,因此你应该在此尽可能地发现所有的错误。
有的错误可能要一直到子程序完全写好之后才能够显现出来,这里面有很多种原因。伪代码里的错误可能在细节的实现逻辑中变得更明显。一个在伪代码时看上去很雅致的设计,可能用编程语言实现时变得不堪入目了。实现具体细节的时候也可能会揭示出架构、高层次设计或者需求中存在的错误。最后,在代码中也可能会出现各种各样的常见编码错误。
3.1 在脑海里检查程序中的错误
第一种正式的程序检查方法是在脑海中检查。请确保你检查到了所有可能的执行路径、端点和所有异常条件。你不但要自己检查,还要和一个或多个同伴一起检查。
编程爱好者和专业程序员之间最大的区别之一便是从迷信到理解的转变。这里所说的“迷信”并不是指一个程序会在月圆之夜让你心生惊恐或产生什么莫名其妙的错误。“迷信”指的是把对代码的感觉当做对它的理解。如果你发现自己经常怀疑某些错误是由编译器和硬件造成的,那么你就仍然处于迷信的阶段。
多年前的一项研究表明,只有月5%的错误可能是由硬件、编译器或者操作系统的原因造成的。今天这一比例甚至可能更低。已经从迷行转为理解的程序员们总会怀疑是自己的工作出了问题,因为他们知道,正是他们之中了95%的错误。理解没一行代码所起的作用,理解为什么需要这行代码。没有什么东西会仅仅因为看上去可行就是正确的。如果你不知道它们为什么可以工作,那么它很可能就是不能工作的——只是你还不知道罢了。
3.2 编译子程序
完成检查之后,就可以编译这个程序了。在这么久之后才开始编译程序,看上去效率不太高,因为代码早在几页之前就都写好了。不可否认,如果你在此之前就去编译这个子程序,让计算机去检查哪些未声明的变量、相互冲突的命名等,那么你也许确实可以省去一些工作。
然而,在整个构建过程的后期才开始编译能够给你带来很多的好处。主要的原因是,当你编译新写的代码时,你心中的一块秒表也就开动了。第一次编译完成以后,你就会开始给自己施压了:“只要再编译一次我就能把它搞定啦!”这种“只要再编译一次”的毛病会导致你对代码做出草率而可能带来错误的更改,从长远看反而要花掉你更多的时间。到你已能相信一个子程序是正确时再编译它,可以避免在匆忙中完成代码。
这本书的一个目的就是告诉你怎样脱离哪种先东拼西凑,然后通过运行来看看代码是否工作的怪圈。在确信程序能工作之前就开始编译它,这通常就是上面那种黑客思维的症状。如果你没有陷入这种“拼凑加编译”的怪圈,那就在你觉得合适的时候再去编译吧。不过你要清楚,大多数人都是在挣扎中通过“拼凑,编译,然后修改”的方法开发能工作的程序的。
下面是一些相关的知道建议。
(1)把编译器的警告级别调到最高。通过让编译器来检查错误,你可以很容易地查出大量细微的错误。
(2)使用验证工具,例如使用类似lint这样的工具检查C代码。
(3)消除产生错误消息和警告的所有根源。通常,大量的警告信息暗示着代码的质量欠佳,你需要尽量理解所得到的每一个警告。
3.3 在调试器中逐行执行代码
程序编译通过之后,要在编译器中逐行执行,以确保每行代码都在按照你所期望的方式执行。遵循这个简单易行的方法,你就能查出很多的错误。
3.4 测试代码
使用你在开发该子程序期间计划写的或者已写成的测试用例来测试代码。你可能需要开发一些脚手架来支持你的测试用例——也就是说,写一些对程序测试起辅助作用,而又不纳入最终的产品中的支持代码。脚手架可以是一套用测试数据来调用你的子程序的测试夹具子程序,也可以是一些供你的子程序调用的存根。
3.5 消除程序中的错误
一旦检测到错误,就一定要把它消除。如果此时你开发的程序仍是漏洞百出,那么很可能它就永远漏洞百出了。如果你发现一段程序的毛病不是一般的多,那请从头再来吧。对于一个毛病百出的程序而言,设计一个全新的方案是值得的。
4. 收尾工作
4.1 检查子程序的接口
4.2 检查整体的设计质量
确认下列事项:这个子程序只干一件事,并且把这件事做得很好;子程序之间是松散耦合的;子程序采用了防御式设计。
4.3 检查子程序中的变量
4.4 检查子程序的语句和逻辑
4.5 检查子程序的布局
确认你正确的使用了空白来明确子程序、表达式及参数列表的逻辑结构。
4.6 检查子程序的文档
确认那些由伪代码转化而来的注释仍然是准确无误的。