代码大全笔记:创建高质量的代码

5 软件构建中的设计

  “深入一种语言去编程”,而非“在一种语言上去编程”。区别在于,不要将自己的思想受限于语言特性,而是首先决定表达的思想,然后使用特定的语言特性来表达这些实现。

  软件的首要技术革命:管理复杂度。管理复杂度的两个行之有效的方法

  • 把任何人同一时间需要处理的本质复杂度的量减到最少。

  所有软件设计的目标都是把复杂问题分解成简单的部分。 子系统间的相互依赖越少,你就越容易在同一时间只专注于问题的一小部分。 精心设计的对象关系是关注点相互分离,从而使你在每一时刻只专注于一件事情。在软件架构层次上,可以将整个系统分解为多个子系统来降低问题的复杂度,为了减少子系统间的相互依赖,子系统尽量设计成松散耦合。要避免做成聪明的设计,因为聪明的设计常常是难于理解的。 应该做出简单且易于理解的设计。 如果你的程序设计方案不能让你在专注于程序的一部分时安心的忽视其他部分的话,这一设计就没有什么作用了。

  • 不要让偶然复杂度无谓的快速增长。

    偶然的复杂性包括后期需求更改,需求不明确等。可以通过软件过程加以控制。

  编程首先是要让人理解的,其次才是计算机。

  子系统的设计:从管理复杂度的角度触发,要对子系统间的通信加以限制,否则子系统间的耦合会使划分子系统的好处消失。 系统层的设计应该是无环图。

启发式设计方法:

1、 抽象:是一种能让你在关注某一概念的同时可以放心的忽略其中的一些细节的能力——在不同的层次上处理不同的细节。

2、 封装:抽象是“从更高层次的细节来看待一个对象”,封装是“除此之外,你不能看到对象的任何其他层次的细节”。 封装帮助你管理复杂度的方法是不让你看到那些复杂度。

3、 信息隐藏:信息隐藏的好处是,对减少“改动影响的代码量”至关重要。需要隐藏的类型一般包括:

  隐藏复杂度:这样你就不用去对付它,除非需要的时候。

  隐藏变化源:这样变化发生时,其影响就被限制在局部的范围内。

4、 信息隐藏的障碍:

  信息过度分散:通过子程序来统一管理过度分散的信息。

  循环依赖: A 类调用 B 类,B 类又调用 A 类。

  在设计的时候,养成问“我该隐藏些什么?”的习惯。

  找出容易改变的区域

  把不稳定的区域隔离出来,从而把变化所带来的影响限制在一个子程序,类或者包的内部。 

1、 一些容易变化的区域:

   业务规则,  硬件依赖, 非标准语言特性,困难的设计区域和构建区域,状态变量, 数据量的限制:

  不要使用布尔变量作为状态变量,换用枚举类型;使用访问器子程序取代对状态变量的直接检查。

2、 预料不同程度的变化:

  应该让变化的影响或范围与发生该变化的可能性成反比,即:如果变化容易发生而又容易做出计划,你就应该在它上面多下功夫,不要浪费过度的精力在不太可能发生而又很难做出计划的变化上。

3、 找出容易变化的区域的一个好的方法:

    首先找到程序中可能对用户有用的最小子集,这一最小子集构成了系统的核心,不容易发生变化。接下来,有微小的步伐扩充这个系统。当你考虑功能上的改进时,同时也考虑质的变化。这些潜在的改进区域就构成了系统中的潜在的变化。

   保持松散耦合

1、 耦合度表示类和类之间或者子程序和子程序之间关系的紧密程度,耦合度设计的目标是创建出小的,直接的,清晰的类或子程序,使他们与其他类或子程序之间的关系尽可能的灵活,这就被称作“松散耦合” 。

2、 模块间好的耦合关系会松散到恰好能使一个模块能够很容易地被其他模块使用。

3、 请尽量使你创建的模块不依赖或者很少依赖其他模块。

4、 耦合标准:

    规模:指模块间的连接数。规模越小,耦合度越松散。

    可见性:模块间连接的显著程度。连接要尽可能的明显。

    灵活性:模块间的连接是否容易改动。

总结: 一个模块越容易被其他模块调用,那么他们之间的耦合关系就会越松散。创建系统架构时,按照“尽可能的缩减相互连接”的准则来分解程序。

5、 耦合的种类 :

  简单数据参数耦合:可以接受。

  简单对象耦合:一个模块实例化一个对象。

  对象参数耦合:对象作为参数传递数据,比数据参数耦合更紧密。

  语义耦合:一个模块不仅使用了另一个模块的用法元素,而且还使用了有关那个模块内部工作细节的语义知识。

6、 松散耦合的关键之处在于, 一个有效的模块提供出了一层附加的抽象(是不是这个抽象屏蔽了内部的细节,而是对此模块的调用更容易) 。一旦你写好了它,你就可以想当然的去使用它,这样就降低了整体系统的复杂度。 使你在同一时间只关注一件事情。如果一个模块的使用要求你同时关注几件事情——内部工作的细节,对全局数据的修改,不确定的功能点——那么就失去了抽象的能力,模块所具有的管理复杂度的能力也削弱或完全丧失了。 类和子程序是降低复杂度的首选和最重要的智力工具,如果他们没有帮助你简化工作,那就是他们的失职。

查阅常用的设计模式:

1、 设计模式通过提供现成的抽象来减少复杂度。

2、 设计模式通过把常见解决方案的细节予以制度化来减少出错。

3、 设计模式通过多种设计方案而带来启发性的价值。

4、 设计模式通过把设计对话提升到一个更高的层次上来简化交流。

5、 陷阱:强迫让代码适应某个模式,或者为了模式而模式。这两者都不可取,好的设计应该是巧好能够解决现实问题的设计。

其他的启示方法:

1、 高内聚性:类内部的子程序或子程序内部的代码在支持一个中心目标的紧密程度。

2、 构造分层结构:通过分层来管理复杂度,同一时间,只关注本层的细节,而不是在所有的时间都要考虑所有的细节。

3、 严格描述类契约:?

4、 分配职责:考虑:该怎样为对象分配职责,对象应该隐藏什么信息。

5、 为测试而设计:考虑,怎样设计系统会更容易测试?类似于测试驱动开发。

6、 避免失误:在设计时,考虑失败的案例,有可能有反面的启发意义。

7、 画一个图

8、 保持设计的模块化:

  使用启发式设计方法的原则:

1、 不要卡在一个方法上。

2、 你无须马上解决整个难题。 

5.4 设计实践

  通常可以获得良好结果的工作步骤

1、 迭代:从不同的视角来考虑问题。

2、 分而治之:增量式改进。

3、 自上而下和自下而上:区别在于,前者是一种分解策略,后者是一种合成策略。

4、 建立设计原型。

5、 合作设计。

设计做多少才够

1、 两个问题:一个设计的层次(详细程度) ,另一个是记录设计的文档的正规程度。

2、 受影响的因素包括:项目规模(正比) ,项目生存周期(正比) ,项目成员经验(反比)

3、 最大的失误来自于我认为自己已经做得很充分,可事后却发现还是做得不够,没能发现其他的一些设计挑战。

4、 最大的设计问题通常不是我认为是很困难的,并且在其中做了不好的设计的区域;而是我认为是简单的,而没有做出任何设计的区域 。

5、 程序化的活动容易把非程序化的活动驱逐出去—— Gresham法则

6、 宁愿花费80%的精力去创建和探索大量的备选方案, 而用 20%的时间去创建并不是很精美的文档,也不愿意把         20%的精力花在创建平庸的设计方案上,而把 80%的精力用于对不良的设计进行抛光润色。

记录设计成果

1、 把设计文档插入到代码中;

2、 用 wiki 来记录设计讨论和决策;

3、 写总结邮件;

4、 使用数码相机保存设计结果。这种方法可以考虑一下。

5、 UML图。

5.5 对流行的设计方法的评论

  1、设计一切和不做设计都是不可取的,比较合理的做法是,预先做一点设计 ,或者预先做足够的设计。

  2、如果你无法判断最佳的设计量,请记住:设计所有细节和不做设计一定是不对的。

  P.J. Plauger :“你在应用某种设计方法的时候越教条化,你所能解决的现实问题就会越少。请把设计看出是一个险恶的,杂乱的和启发式的过程(没有教条化的规则可以遵循,能够巧好解决现实问题的方案才是最佳的方案) 。不要停留于你所想到的第一套方案,而是去寻求合作,探求简洁性,在需要的时候做出原型,迭代,并进一步迭代。你将对你的设计成果感到满意。”

  由此,对概要设计文档,形式上没有必要要求太高,设计的层次上,应该是更高层面上的概要设计。

  开发流程当中,概要设计阶段对设计结果进行评审还有一个好处, 就是在设计者向评审人员讲解设计方案时,有助于他对自身的设计方案重新审视而获得不一样的设计思路。

本章要点

软件的首要技术使命就是管理复杂度。以简单性作为努力目标的设计方案对此最有帮助。

简单性可以通过两种方式来获取:一是减少在同一时间所关注的本质性复杂度的量,二是避免生成不必要的偶然的复杂度。

设计是一种启发式的过程。固执于某一种单一方法会损害创新能力,从而损害你的程序。

好的设计都是迭代的。你尝试设计的可能性越多,你的最终设计方案就是变得越好。

信息隐藏是个非常有价值的概念。通过询问 “我应该隐藏什么,能够解决很大困难的设计问题”。 

第六章,可以工作的类

   成为高效程序员的一个关键就在于:当你开发程序任何一部分代码时,都能安全的忽视程序中尽可能多的其余部分。

6.1 类的基础:抽象数据类型( ADTs)

  抽象数据类型定义:ADT(abstract data type),是指一些数据以及对这些数据所进行的操作的集合。

使用 ADT 的益处:

1、 可以隐藏实现细节。

2、 改动不会影响到整个程序。

3、 让接口提供更多的信息。

4、 更容易提高性能。

5、 让程序的正确性更显而易见。

6、 让程序更具有自我说明性。

7、 无须在程序内部到处传递数据。

8、 你可以像在显示世界中那样操作实体,而不用在底层实现细节上操作它。

6.2 良好的类接口

  创建高质量类的第一步,可能也是最重要的一步: 创建一个好的接口。

好的抽象

  定义类的时候,把 public 放在最前面,使用类的时候,一下就可以看到公共的方法。

1、 类的接口应展现一致的抽象层次。 每一个类应该实现一个 ADT ,并且仅实现这个 ADT 。

  如果你发现一个类不止实现了一个 ADT ,或者不能确定究竟它实现了何种 ADT ,你就应该把这个类重新组织为一个或多个定义更加明确的 ADT 。

  所谓层次一致,就是要所有的接口都是围绕 ADT 中的“ data”来展开的。并且,要对实现的细节进行隐藏。

2、 一定要理解类实现的抽象是什么。

3、 提供成对的服务。大多数接口都有相应的,相等的,相反的操作。

4、 把不相关的信息转移到其他类中

5、 尽量让接口可编程,而不是表达语义 。编程部分有接口中的数据类型和其他属性构成,编译器强制要求他们。语义部分则由“本接口将会怎样被使用”的假定组成,编译器无法强制检查,比如,接口 A 应该在接口 B 调用前调用,否则会引起崩溃;调用接口 A前需要设置全局变量。断言可以将语义部分转换为编程部分?

6、 谨防在修改时破坏接口的抽象。流程编辑中的 Node类现在有点杂乱了。

7、 不要添加与接口抽象不一致的公用成员,每次向类中的接口中添加子程序时,问问“这个子程序与现有接口提供的抽象一致吗?” ,如果不一致,该如何解决?我遇到过类似的问题,可以参考一下设计模式

8、 同时考虑内聚性和抽象性。

“关注类的接口所表现出来的抽象,比关注类的内聚性更有助于深入的理解类的设计。 ”

良好的封装

  “设计精良的模块和设计糟糕的模块的唯一最大区别,就是对其他模块隐藏本模块内部数据和其他实现细节的程度。 ”—— Joshua Bloch 。

  封装是比抽象更强的一个概念。 抽象通过让你忽略实现细节的模型来管理复杂度,而封装则强制阻止你看到细节。

1、 尽可能限制类和成员的可访问性。让可访问性尽可能的低是促成封装的原则之一。如果无法确定子程序的访问级别(公用,私用,受保护) ,经验之举是 采用最严格且可行的访问级别 。更好的建议:采用那种访问级别能够最好的保护接口抽象的完整性?

2、 不要公开暴露成员数据。

3、 避免把私用的实现细节放入类的接口中。不要在 private 中暴露内部细节,一个做法就是可以在 private 中定义一个指针,指向一个附属类中,附属类中实现了有关的细节。

4、 不要对类的使用做出任何的假设。即尽量让接口可编程,而不是表达语义。

5、 避免使用友元类。 State 模式中按照正确的方式使用友元类有助于管理复杂度。但一般友元会破坏封装 。他让你在同一时刻考虑更多的代码。

6、 不要因为一个子程序仅使用公用子程序,就把它归入公开接口。

7、 让阅读代码比编写代码更方便。

8、 要格外警惕从语义上破坏封装性。语义上对封装性的破坏很大: 它们让调用代码不是依赖于类的公开接口,而是依赖于类的私用实现。每当你发现自己是通过查看类的内部实现来得知该如何使用这个类的时候,你就不是针对接口编程了, 而是透过接口针对内部实现编程了。如果你透过接口来编程的话,封装性就被破坏了,而一旦封装性开始遭到破坏,抽象能力也就遭殃了。

9、 留意过于紧密的耦合关系。建议:在基类中把数据声明为 private,而不是 protect,以降低派生类和基类的耦合关系。避免在公开接口中暴露成员数据。要对从语义上破坏封装性保持警惕。

  紧密的耦合性总是发生在抽象不严谨或封装遭到破坏的时候。

6.3 有关设计和实现的问题

这一节主要是关于类内部的设计和实现。

包含( “有一个”的关系)

  包含表示一个类含有一个基本数据元素或对象,继承比包含复杂的多,不是因为继承比包含更好。包含才是面向对象中的主力技术。

1、 包含表示有一个的关系。

2、 在万不得已是通过 private 继承来实现有一个的关系。

3、 警惕有超过约 7 个数据成员的类。如果超过 7(+ -)2 个成员,考虑把这个类分解成更小的类。简单数据成员上限 9 个,复杂对象 5 个。主要的原理就是分而治之,降低单个复杂度。

继承( “是一个 ”的关系)

  使用继承时考虑:

1、 对以每一个成员函数,它应该对派生类可见吗?它应该有默认的实现吗?这一默认实现能被覆盖吗?

2、 对每个成员数据:是否对派生类可见?

  更详细的说明:

1、 用 public 继承来实现 “是一个”的关系。能否用 private 来实现继承, 会有什么效果?三种不同的继承: 1)public     类型继承。表示是一个的关系。 2)private 实现继承,基类中所有的公共接口和公共成员都是派生类中的私有接口。 3)protect     继承,所有的公用成员和方法都将成为派生类的 protcet 成员。

2、 要么使用继承并进行详细的说明,要么就不使用它。 使用继承程序增加复杂度,是一种危险的技术,要尽量少用。就像我的 service     interface 一样,现在有点过于复杂。

3、 遵循替换原则: 派生类必须能够通过基类的接口而被使用,且使用者无须了解两者之间的差异     。如果程序遵循替换原则,继承就能够成为降低复杂度的一个强大工具,因为它让程序员关注于对象的一般特性而不必担心细节。

4、 确保只继承需要继承的部分:     1)抽象且可覆盖——继承接口,不继承实现(纯虚接口)。2)可覆盖——继承接口,默认实现,并且可以覆盖默认实现(虚拟接口) 。3)不可覆

盖——继承接口,默认实现,但是不可覆盖(一般 public 接口)。继承接口和继承实现都可以继承。如果只是使用实现,则最好使用包含(类的组合)形式。

5、 不要覆盖一个不可覆盖的成员函数——派生类中的成员函数和基类中不可覆盖函数不要重名。

6、 把公用的接口,数据,操作放到继承树中尽可能高的位置,这样方便派生类的使用。高的标尺:根据抽象性来决定,如果把一个子程序移到更高的层次后会破坏该层对象的抽

象性,就要停止了。

7、 只有一个实例的类是值得怀疑的。有可能把对象和类混为一谈了。考虑能否创建新的对象(派生类中的差异通过数据而非定义新的类来表达)。单件模式例外。

8、 只有一个派生类的基类也值得怀疑。我的 GNodeData 设计不是很合理。 考虑是否可以把它在优化一下。这样是在做“提前设计”——试图去预测未来的需要,而又常常没有真正了解为了到底需要什么。为未来变化要做的不是创建几层额外的基类,而是让眼下的工作成功尽可能的清晰,简单,直截了当。 不要创建任何并非绝对必要的几层结构。

9、 派生后覆盖了某子程序,当中其中没有进行任何操作,这种情况也值得怀疑。这种情况通常是基类中的设计有问题。破坏接口语义,破坏接口抽象,增加维护难度。

10、 避免继承体系过深。 过深的继承体系会导致复杂度的增加,与首要技术使命背道而驰。

11、尽量使用多态,避免大量的类型检查。要根据不同的情况的。

12、 让所有的数据是 private,而非 protected。继承会破坏封装。增加基类和派生类间的复杂度。继承往往会让你和程序员的首要技术使命 (管理复杂度) 背道而驰。 从控制复杂度的角度说 ,你应该对继承持有非常歧视的态度。

  总结:何时可以使用继承,何时使用包含:

1、 如果多个类共享数据而非行为,应该创建这些类可以包含的共用对象。

2、 如果多个类共享行为而非数据,应该使用继承。

3、 如果多个类即共享数据,又共享行为,应该让它们从一个共同的基类继承而来,并在基类中定义共用的数据和子程序。

4、 当你先通过基类控制接口时,使用继承。当你想自己控制接口是,使用包含。

成员函数和数据成员

1、 让类中子程序的数量尽可能的少。需要和其他竞争的因素权衡一下:过深的继承体系,类中大量调用子程序。

2、 禁止隐式的产生你不需要的成员函数和运算符。

3、 减少类所调用的不同子程序的数量。

4、 对其他类的间接调用尽可能的少。

构造函数

1、 如果可能,应该在所有的构造函数中初始化所有的数据成员。

2、 用私用( private)构造函数来强制实现单件属性。

3、 优先采用 深层 复本(深拷贝)(deep copies) ,除非论证可行,才采用浅层复本。 

6.4 创建类的原因

1、 为现实世界中的对象建模。

2、 为抽象的对象建模。抽象对象能够对具体的对象提供抽象。 “从现实世界中提炼出抽象的概念”。设计出恰当的抽象对象是面向对象设计中的一项主要挑战。

3、 降低复杂度。这是创建类的最重要的理由。通过抽象,封装,信息隐藏降低复杂度。

4、 隔离复杂度。将复杂的东西,隔离在一定的范围内。

5、 隐藏实现细节。

6、 限制变动的影响范围。

7、 隐藏全局数据。把全局数据放到类的接口后面,通过访问器子程序( accessroutine)来操控全局数据有很多好处: 可以修改数据结构而无须修改程序本身; 监视对数据的访问 ;会使你思考数据是否是全局的;你会发现,全局数据只是对象数据而已。

8、 让参数传递更顺畅。用类中多个子程序共享数据,代替数据的大量传递。

9、 建立中心控制点。集中控制有助于隐藏细节。

10、 让代码更易于重用。精心分解的一组类,比一个功能很强大的大类更利于重用。

11、为程序族做计划。其实是一种预先设计的方法。根据实际情况,把需要更改的东西放在一个类中。后面出现更改需求的时候,直接替换这个类。

12、 把相关操作包装到一起,

13、 为某种特定的重构。

应该避免的类:

1、 避免创建万能类。

2、 消除无关紧要的类。

3、 避免用动词命名类。只有行为,没有数据的类不是一个真正类。

6.5 与具体编程语言相关的问题

6.6 超越类:包

  如果我们有更好的工具把对象聚合起来,我们就可能更好的朝着抽象和封装的目标前进了。

  可以创建自己的包,如果编程语言不支持包。

1、 用于区分公共的类和私用的类的命名规则。

2、 为了区分每个类所属的把而制定的命名规则和 /或代码组织规则。

3、 规定什么包可以用其他什么包的规则,包括是否可以使用继承和包含等。

  这个变通之法展示的是“在一种语言上编程”和“深入一种语言去编程”之间的区别的好例子。 

本章要点

1、 类的接口应该提供一致的抽象。很多问题都是由于违背了该原则而引起的。

2、 类的接口应该隐藏一些信息——如某个系统接口、某项设计决策、或一些实现细节。

3、 包含往往比继承更为可取——除非你要对“是一个”的关系建模。

4、 继承是一种有用的工具,但它会增加复杂度,这有违于软件的首要技术使命——管理复杂度。

5、 类是管理复杂度的首先工具,要在设计类时给予足够的关注,才能实现这一目标。

第七章,高质量的子程序

  子程序是为实现一个特定的目的而编写的一个可被调用的方法或过程。

7.1 创建子程序的正当的理由

1、 降低复杂度。当内部循环或内部嵌套层次很深的时候,就要考虑从子程序中提取出新的子程序了。子程序以其抽象,封装,信息、细节的隐藏来实现管理复杂度的功能。

2、 引入中间、易懂的抽象。通过命名将一段代码重组织,使其更易理解。

3、 避免代码重复。

4、 支持子类化。便于子类覆盖。

5、 隐藏顺序。

6、 隐藏指针操作。指针操作可读性通常很差,且容易出错。

7、 提高可移植性。

8、 简化复杂的布尔判断。

9、 改善性能。隐藏实现的细节,有助于后面用更好的算法进行重新实现。

10、 确保子程序都很小

  总之,创建高质量的子程序,有助于使用抽象、封装,信息细节隐藏的手段,来降低复杂度。除此之外的理由:

1、 隔离复杂度。

2、 隐藏实现细节。

3、 限制变化带来的影响。

4、 隐藏全局数据: 是对全局数据的范围都通过子程序来实现。 避免直接对全局数据的范围 。

5、 形成中央控制点。

6、 促进可重用的代码。

7、 达到特定的重构的目的。

  编写有效子程序时,一个最大的心里障碍是不情愿为一个简单的目的而编写一个简单的子程序。事实证明,一个很好而有小巧的子程序会非常有用:既可以提高可读性,又便于扩展。 

7.2 在子程序上的设计

    在设计类的时候,抽象和封装已经很大程度上取代了内聚性。但是在子程序这一层次上,内聚性仍是常用的启发式方法。

    对于子程序而言,内聚性是指子程序中各个操作之间的联系的紧密程度。我们的目标是让子程序只把一件事情做好,不再做任何其他事情。

    内聚性是针对操作的概念。即操作具有内聚性。通常,一个操作指一个语句,或一个函数调用。一个是简单的操作,一个是复杂的操作。

    内聚性的一些概念,理解概念有助于思考如何让子程序尽可能的内聚。

1、 功能的内聚性:让一个子程序仅执行一项操作。这是最强的也是最好的内聚性。其他的内聚性则不够理想。

2、 顺序上的内聚性:子程序内包含按照特定顺序执行的操作,这些步骤需要共享数据,而只有全部执行完毕后,才完成了一下完整的功能。 顺序上的内聚性设计成功能上的内聚性需要对顺序进行分解,形成多个功能更加单一的子程序 。

3、 通信上的内聚性:子程序中的不同操作使用了同样的数据,当不存在其他任何联系。 优化的方法是将子程序分解为多个子程序。

4、 临时的内聚性:子程序中含有一些因为需要同时执行才放到一起的操作。 优化方法是把临时性的子程序看成是一系列事件的组织者,即让它直接调用其他的子程序,而不是直接执行所有的操作。另外,可以通过函数的命名,表达子程序完成的功能。避免使用多个操作叠加的方法命名,因为它暗示子程序只有巧合的内聚性。

  除功能的内聚性外,其他类型的内聚性都是不可取的。要想办法避免。

1、 过程上的内聚性:子程序中的操作是按特定的顺序进行的,除此外没有任何联系。对子程序分解,把不同的功能纳入不同的子程序中,让调用方的子程序具有单一而完整的功能。

2、 逻辑上的内聚性:若干操作被放入同一个子程序中,通过传入的控制标志选择执行其中的一项操作。各个操作间没有任何的逻辑关系,这些操作只是被包含在一个很大的 case或 if 语句中。如果此子程序只是分发命令,根据命令调用不同的子程序,则这种做法也是不错的。其他的不好的情况,可以通过分解来进行优化。分解为多个独立的子程序。

3、 巧合的内聚性:子程序内部的各个操作间没有任何的关联。内聚性考虑的是在一个子程序内部,操作的关联程度。编写功能上的内聚性的子程序几乎总是可能的,因此把注意力集中于功能上的内聚性,从而得到最大的收获。

如果一个子程序中,局部变量的个数超过 7 个,则有可能是此子程序设计出现问题,有可能要优化一下。

7.3 好的子程序名字

1、 好的子程序的名字能够清晰的描述子程序所做的一切。

2、 描述子程序所做的所有事情 子程序的名字应该能够描述其所有的输出结果及副作用。

3、 避免使用无意义的,模糊或表述不清的动词—— handle, perform, output, process, dealwith等。很多情况下,使用模糊不清的动词作为子程序的名称的根本原因是程序本身就不是完成一个清晰的功能,这个时候,就要考虑重新组织该程序,并且选择准确的词语描述子程序。

4、 不要仅通过数字来形成不同的子程序名称。

5、 根据需要确定子程序名称的长度。研究表名,变量名的最佳长度是 9 到 15 个字符(过短则无法表达准确其含义,过长则有碍阅读) 。子程序命名的重点是含义清晰。长短要视名字是否清晰而定。

6、 给函数命名是要对返回值有所描述。最好能精确描述返回的结果。

7、 给过程起名时使用语气强烈的动词加宾语的形式。如果是面向对象,且动词是对对象的操作,则不用加宾语。

8、 准确使用仗词(成对的词语) :close/open。

9、 为常用操作确立命名规则。

7.4 子程序可以有多长

    子程序的长度尽量控制在 200 行以内。在这个范围内,代码长度不是子程序的关键因素,其他的降低复杂度的因素才是关键的。

7.5 如何使用子程序的参数

    子程序间的接口是程序中最容易出错的部分之一。

1、 按照输入 -输出 -修改的顺序排列参数:仅作输入参数 -即做输入又作输出 -仅作输出。

2、 考虑创建自己的 OUT 和 IN 关键字:有弊端, C++中可以使用 const来表达只是输入的含义。

3、 如果几个程序都用了类似的一些参数,应该让这些参数的排列顺序保持一致。

4、 使用所有的参数。如果没有使用,则从接口中删除。

5、 把状态或出错变量放在最后。

6、 不要把子程序的参数用作工作变量。

7、 在接口中对参数的假定加以说明。比注释更好的做法是加断言: 1)参数是仅用于输入,输出还是修改; 2)表示数量的参数的单位; 3)如果没有用枚举,则要说明状态代码和错误值的含义。 4)所能接收的范围。 5)不应该出现的特定数值。

8、 把子程序的参数限定在大约 7 个以内。 7 是一个神奇的数值,人很难记住超过 7 个单位的信息。如果子程序的参数过多,则要考虑使用设计的方法进行优化。

9、 可以考虑采用某种表示输入,输出,修改的命名规则。

10、 为子程序传递用以维持其接口抽象的变量或对象。考虑这个问题的时候, 角度应该是这个 子程序的接口要表达何种抽象 。如果传递对象能够表达子程序的抽象,就传递对象,如果传递变量能够表达抽象,就传递变量。如果传递对象,但是在调用子程序前后经常需要进行装配和卸载,或者传递变量,但是经常要修改参数表,则要考虑是否要换一种方式来设计接口。

11、使用具名参数

12、确保形式参数和实际参数匹配。 C语言存在这个问题, c++,java 不存在。因为他们是强类型的语言。 

7.6 使用函数时要特别考虑的问题

  在现代编程语言中,函数是指有返回值的子程序,过程是指没有返回值的子程序。

1、 什么时候使用函数, 什么时候使用过程: 把子程序的调用和对状态值的判断清楚的分开 ,降低此语句的复杂度。如果一个子程序的主要用途就是返回其名字所指明的返回值,那么就使用函数,否则,使用过程。

2、 设置函数的返回值: 1)检查所有的路径,设置一个默认的函数返回值。 2)不要返回指向局部的应用和指针。

7.7 宏子程序和内联子程序

1、把宏表达式包含在整个括号内: #define cube(a) ((a)*(a)*(a))

2、把多条语句的宏用括号括起来。

3、用子程序的命名方法来命名宏,便于后面替换。

4、不到万不得已,不要用宏来替换子程序。

5、限制使用内联子程序。 Inline 关键字可以支持提高性能。

本章要点

1、 创建子程序的最主要的目的是提高程序的可管理性, 当然也有其他一些好的理由。 其中,节省代码空间只是一个次要的原因; 提供可读性、 可靠性和可修改性等原因更重要一些 。

2、 有时候,把一些简单的操作写成独立的函数也非常有价值。

3、 子程序可以按照其内聚性分为很多种类,而你应该让大多数子程序具有功能上的内聚性,这是最佳的一种内聚性。

4、 子程序的名字是它的质量的指示器,如果名字糟糕但恰如其分,说明这个子程序的设计很差劲。如果名字糟糕而且又不准确,那么它就反映不出是干什么的。不管怎么样,糟糕的名字都意味着程序需要修改。

5、 只有在某个子程序的主要目的是返回由其名字所描述的特定结果时,才使用函数。

6、 细心的程序员会非常谨慎的使用宏,而且只用在万不得已时。 

第八章,防御式编程

  防御式驾驶:你永远不能确定另外一位司机将要做什么。你要承担其保护自己的责任,哪怕是其他司机犯的错误。

  防御式编程的主要思想: 子程序应该不因传入错误数据而被破坏,哪怕是有其他子程序产生的错误数据。

8.1 保护数据免受非法数据的破坏

    1、检查所有来源于子程序外部的数据的值。

    2、检查所有输入参数的值。

    3、决定如何处理错误。

8.2 断言

    断言是指在开发期间使用的、 让程序在运行时进行自检的代码。 断言只在开发和维护阶段使用。

  建立自己的断言,使用断言的建议:

1、 用错误处理代码来处理预期会发生的状况,用断言来处理绝对不应该发生的状况。断言是用来检查用于不该发生的情况,而错误处理代码是用来检查不太可能经常发生的非正常情况,这些情况在写代码的时候就可以预料到的,且产品代码中也要处理这种情况。断言用来检查程序中的 bug,如果断言触发,则肯定是程序中存在 bug。错误处理代码对反常情况做成反映。可以把断言看成主动的可执行的注释。错误处理代码处理系统外部的异常数据。如果数据来自可信的内部,则可以使用断言来检查。

2、 避免把需要执行的代码放入断言中。

3、 用断言来注释并验证前条件和后条件。前条件调用方代码在调用子程序或类之前要保证的条件;后条件是指被调用方代码执行结束后要保证的条件。可以用断言来检查前条件和后条件。

4、 对于高健壮性的代码,应该先使用断言再处理错误。同时使用两者。

8.3 错误处理技术

    断言用于处理代码中不应该出现的错误。错误处理技术对反常情况做出响应。

1、 返回中立值

2、 换用下一个正确的数据。

3、 返回与前次相同的数据。

4、 换用最接近的合法值。

5、 把警告信息记录日志中。

6、 返回一个错误码。

7、 调用错误处理子程序或对象。

    健壮性与正确性:

    正确性意味着永远不返回不准确的结果。健壮性意味着要不断尝试采取某些措施,以保证软件能够持续的运行下去。我们的软件更倾向于健壮性。

    高层次设计对错误处理方式的影响:

    在软件架构的时候,就要设计一个统一的错误处理决策。一般情况下,函数返回错误,而调用者则要根据返回值进行处理。

    防御式编程的全部的重点就在于防御那些你未曾预料到的错误。   

8.4 异常

1、 用异常通知程序的其他部分,发送了不可忽略的错误。异常有一种无法被忽略的通知机制。

2、 只有真正例外的情况下才抛出异常。和断言类似,处理不可能发生的情况。

3、 不能用异常来推卸责任。应该在局部处理的,不要抛出来。

4、 避免在构造和析构函数中抛出异常。

5、 在恰当的抽象层次抛出异常。把抛出的异常认为是接口的一部分。要符合接口的抽象。抛出的异常不能破坏封装,不能破坏抽象,暴露细节。

6、 在异常消息中加入关于导致异常发生的全部信息。

7、 避免是空的 catch 语句。

8、 了解所有的函数库抛出的异常。

9、 考虑建立一个集中的异常报告机制。

10、 把项目中对异常的使用标准化。

11、考虑异常的替代方案。

8.5 隔离程序,使之包容有错误造成的损害

    将某些接口设定为安全边界,对穿越边界的数据进行安全的检查。

    让软件的某些部分处理“不干净的”数据,让另外一部分处理干净的数据,即可让大部分的代码无须在承担检查数据的责任。

    在类的层次采用此方法:类的公用方法假定数据是不安全的,私用方法假定数据是安全的。

    将输入数据转化为适当的类型。

    隔栏使用的是错误处理错误,处理错误的数据。隔栏内部则使用断言,检查程序错误,而非数据错误。

    隔栏的使用体现了架构层次上如何处理错误。

8.6 辅助调试的代码

  用辅助代码来辅助调试程序。

  尽早引入辅助调试的代码,包括对通道检查,通道状态检查。资源使用情况等。

  采用进攻式编程:在开发节点让他显现出来,而在产品运行时,让它能够自我恢复。

1、 确保断言能使程序终止运行。

2、 完全填充分配到的所有错误。

3、 完全填充分配到的所有的文件或流。

4、 确保每一个 case语句中的 default 分支或 else分支都能产生严重的错误。

5、 在删除一个对象前把它填满垃圾数据。

    计划移除辅助调试代码:

1、 使用 ant 或 make 工具。

2、 使用内置的与处理器。

3、 编写自己的预处理器。

4、 使用调试存根。

8.7 确定在代码中保存多少防御式代码

1、 保留那些检查重要错误的代码。

2、 去掉检查细微错误的代码

3、 去掉可以导致程序硬性崩溃的代码。

4、 保留可以稳妥的崩溃的代码。

5、 为你的技术支持人员记录错误信息。

6、 确保代码中的错误信息是友好的。

本章要点

1、 最终产品代码对错误的处理比“垃圾进,垃圾出”要复杂的多。

2、 防御式编程技术可以让错误更容易发现,更容易修改,并减少错误对产品代码的破坏。

3、 断言可以帮助人尽早的发现错误,尤其是在大型系统和高可靠性的系统中。 

第九章,伪代码编程过程

9.1 创建类和子程序的步骤概述

    创建一个类的步骤

1、 创建类的总体设计:定义类的特定职责,定义类说要隐藏的秘密,以及精确的定义类的接口所代表的抽象概念,决定这个类是否要从其他类派生出来,以及是否运行其他类从它派生;支持这个类中关键的公用方法,标识并设计出类所需要的重要的数据成员。

2、 创建类的子程序:

3、 复查并测试。

    创建子程序的步骤:

创建类的步骤:确定类的职责;确定类对外的接口;确定类隐藏的数据;设计对外接口,

包括设计数据;为对外接口编写伪代码;为支持对外接口,编写使用接口。

9.2 伪代码

    伪代码编写原则:

1、 用类似英语的语句精确描述特定的操作。可以使用中文。

2、 避免使用目标编程语言中的语法元素。使用比代码层次略高的语言来描述。

3、 在本意(意图)上编写伪代码。用伪代码去描述解决问题的方法意图。

4、 在一个足够低的层次上编写伪代码。如果层次过高,会隐藏代码中的问题细节。

9.3 通过伪代码编程过程创建子程序

    设计子程序:

1、 检查先决条件。

2、 定义子程序需要解决的问题。

1) 这一子程序将要隐藏的信息。

2) 传给这项子程序的各项输入。

3) 从子程序得到的输出。

4) 在调用程序之前确保有关的前条件成立。

5) 在子程序将控制权交回给后条件之前,确保其后条件成立。

3、 为子程序命名。好的子程序名是优秀子程序的标志之一。如果你在给程序起名的时候犯难,通常就表明这个子程序的目标还没有明确。

4、 决定如何测试子程序。

5、 在标准库中搜寻可用的功能。

6、 考虑错误处理。

7、 考虑效率问题。效率上最主要的优化还在于高层的设计,不要在微不足道的点滴上浪费时间。

8、 研究算法和数据类型。

9、 编写伪代码。首先写一个函数头部注释。要能够一句话描述程序的目的。

10、 考虑数据。

11、在伪代码中试验一些想法,留下最好的想法。最初的伪代码可能层次还是很高,要进行持续的分解。持续的精化和分解伪代码,知道你觉得再写伪代码实在是浪费时间为止。

编写子程序的代码:

    检查代码:

1、 在脑海中检查程序的错误。

2、 编译子程序。在构建的后期才开始进行编译。在编译之前要确保代码错误很少。不要陷入“拼凑加编译”开发怪圈。匆匆忙忙的更改以使编译通过往往后遗留很多后面需要花费大量精力去修改的 BUG。

3、 测试代码

4、 修改错误。

收尾工作:

1、 检查子程序的接口:确认所有的输入,输出数据都参与了计算,并且所有的参数也都用到了。

2、 检查整体的设计质量。

3、 检查变量。

4、 检查逻辑和语句。

5、 检查布局。

6、 检查文档。

7、 出去冗余注释。

本章要点

1、 创建类和子程序的过程通常都是一个迭代的过程。在创建子程序的过程中获得的认识通常会反过来影响类的设计。

2、 编写好的伪代码要使用易懂的英语,要避免使用特定的编程语言中才有的特性,同时要在意图的层面上写伪代码(即描述该做上面,而不是怎么去做) 。

3、 伪代码编程过程是一个行之有效的详细工具。它同时让编码工作更容易。伪代码直接转化为注释,同时确保注释的准确性和实用性。

4、 不要只停留在你第一次的设计方案上,反复使用伪代码做出多种方案,然后选出其中最佳的一种设计方案。

5、 每一个步骤完成后,都要检查你的工作,并且鼓励其他人来检查。这样可以用最低的成本发现错误。

原文地址:https://www.cnblogs.com/flysong/p/9082384.html