《代码大全第二版》学习笔记(一)

来源:互联网 发布:python 递归深度 编辑:程序博客网 时间:2024/06/05 20:03

第一部分 打好基础

架构的典型组成部分

1.         程序组织 program Organization

2.         主要的类 Major Classes

3.         数据设计 Data Design

4.         业务规则 Business Rules

5.         用户界面设计 User Interface Design

6.         资源管理 Resource Management

7.         安全性 Security

8.         性能 Performance

9.         可伸缩性 Scalability

10.      互用性 Interoperability

11.      国际化/本地化 Internationalization/Localization

12.      输入输出 Input/Output

13.      错误处理 Error Processing

14.      容错性 Fault Tolerance

15.      架构的可行性 Architectural Feasibility

16.      过度工程 Overengineering

17.      关于买还是造的决策 Buy-vs.-Build Decisions

18.      关于复用的决策 Reuse Decisions

19.      变更策略 Change Strategy

20.      架构的总体质量 General Architectural Quality

第二部分 创建高质量的代码

第六章 可以工作的类

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

考虑类就是抽象数据类型再加上继承和多态两个概念。

6.2 良好的类接口

好的抽象:

1.         类的接口应该展现一致的抽象层次。

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

3.         提供成对的操作。

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

5.         尽可能让接口可编程,而不是表达语义。接口不应当依赖于什么前提。

6.         谨防在修改时破坏接口的抽象。

7.         不要添加与接口抽象不一致的公用成员。

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

良好的封装

1.         尽可能地限制类和成员的可访问性。

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

3.         避免把私用的实现细节放入类的接口中。

如:

class Employee {public:…Employee ( … );…FullName GetName() const;String GetAddress() const;…private:EmployeeImplementation* m_implementation;   // 这样就把实现细节隐藏在指针之后了。}

4.         不要对类的使用者做任何假设。

5.         避免使用友元类。

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

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

8.         要格外警惕从语义上破坏封装性。调用方代码应当依赖于类接口,而不是类的私用实现。

9.         留意过于紧密的耦合关系。

6.3 有关设计和实现的问题

包含(“has a”关系)

1.         通过包含来实现has a的关系。

2.         在万不得已时通过private继承来实现has a关系。

3.         警惕有超过约7个数据成员的类。

继承(“is a”关系)

1.         public继承来实现is a的关系。

2.         要么使用继承并详细说明,要么就不要用它。

3.         除非派生类真的是一个更特殊的基类,否则不应该从基类继承。

4.         确保只继承需要继承的部分。

5.         不要覆盖重载一个不可覆盖重载的成员函数。

6.         把共用的接口、数据及操作放到继承树中尽可能高的位置。

7.         只有一个实例的类是值得怀疑的。考虑能否只创建一个新的对象而不是一个新的类。特例:Singleton模式。

8.         只有一个派生类的基类也值得怀疑。

9.         派生后覆盖了某个子程序,但在其中没做任何操作,也值得怀疑。

10.     避免让继承体系过深。

11.     尽量使用多态,避免大量的类型检查。

12.     让所有的数据都是private,而非protected

何时使用继承,何时使用包含

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

2.         如果多个类共享行为而非数据,应该让它们从共同的基类继承而来,并在基类里定义共用的子程序。

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

4.         当你想由基类控制接口时,使用继承;当你想自己控制接口时,使用包含。

成员函数和数据成员

1.         让类中子程序的数量尽可能少。

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

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

4.         对其他类的子程序的间接调用要尽可能少。

5.         一般来说,应尽量减小类和类之间相互合作的范围。如:所实例化的对象的种类、在被实例化对象上直接调用的不同子程序的数量、调用由其他对象返回的对象的子程序的数量。

构造函数

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

2.         private构造函数来强制实现singleton属性。

3.         优先使用deep copies,除非论证可行,才采用shallow copies

6.4 创建类的原因

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

2.         为抽象的对象建模。

3.         降低复杂度。

4.         隔离复杂度。

5.         隐藏实现细节。

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

7.         隐藏全局数据。

8.         让参数传递更顺畅。

9.         建立中心控制点。

10.     让代码更易于重用。

11.     为程序族做计划。

12.     把相关操作包装到一起。

13.     实现某种特定的重构。

应该避免的类

1.         避免创建万能类。

2.         消除无关紧要的类。

3.         避免用动词命名的类。

要点

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

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

3.         包含往往比继承更为可取——除非你要对is a的关系建模。

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

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

第七章 高质量的子程序

子程序是为实现一个特定的目的而编写的一个可被调用的方法method或过程procedure。如C++中的函数、宏。

7.1 创建子程序的正当理由

1.         降低复杂度。

2.         引入中间、易懂的抽象。

3.         避免代码重复。

4.         支持子类化。

5.         隐藏顺序。

6.         隐藏指针操作。

7.         提高可移植性。

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

9.         改善性能。

10.     确保所有的子程序都很小。

7.3 好的子程序名字

1.         描述子程序所做的所有事情。

2.         避免使用无意义的、模糊或表达不清的动词。

3.         不要仅通过数字来形成不同的子程序名字。

4.         根据需要确定子程序名字的长度。

5.         给函数命名时要对返回值有所描述。Pen.CurrentColor()

6.         给过程起名时使用语气强烈的动词加宾语的形式。PrintDocument()Document.Print()

7.         准确使用对仗词。

8.         为常用操作确立命名规则。

7.4 子程序可以写多长

小于200行。

7.5 如何使用子程序参数

1.         按照输入-修改-输出的顺序排列参数。

2.         考虑自己创建inout关键字。

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

4.         使用所有的参数。

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

6.         不要把子程序的参数用作工作变量。应明确引入工作变量。

7.         在接口中对参数的假定加以说明。断言优于注释。如:参数是输入、修改、输出;表示变量的参数的单位如厘米;如果没有用枚举类型,应说明状态代码和错误值的含义;所能接受的数值的范围;不该出现的特定数值。

8.         把子程序的参数个数限制在大约7个以内。

9.         考虑对参数采用某种表示输入、修改、输出的命名规则。

10.     为子程序传递用以维持其接口抽象的变量或对象。

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

C++中所有子程序都称为函数。但返回值类型为void的函数在语义上其实就是过程。

什么时候使用函数,什么时候使用过程

outputStatus = report.FormatOutput( formattedReport );if( outputStatus = Success ) then …

如果一个子程序的主要用途就是返回由其名字所指明的返回值,那么就应该使用函数,否则就应该使用过程。

设置函数的返回值

1.         检查所有可能的返回路径

2.         不要返回指向局部数据的引用或指针。

7.7 宏子程序和内联子程序

1.         把宏表达式整个包含在括号内。如 #define Cube(a) ( (a) * (a) * (a) )

2.         把含有多条语句的宏用大括号括起来。

#define LookupEntry(key, index) { \index = (key – 10) / 5; \index = min(index, MAX_INDEX); \index = max(index, MIN_INDEX); \}

3.         用给子程序命名的方法来给展开后的代码形同子程序的宏命名,以便在需要时可以用子程序来替换宏。

宏子程序在使用上的限制

C++中取代宏的方案:const, inline, template, enum, typedef(简单的类型替换)

节制使用inline routine。违反封装。

第八章 防御式编程

子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。

保护程序免遭非法输入数据的破坏

三种处理进来垃圾的方法:

1.         检查所有来源于外部的数据的值。

2.         检查子程序所有输入参数的值。

3.         决定如何处理错误的输入数据。

断言Assertions

可用断言检查如下这类假定:

1.         输入参数或输出参数的取值处于预期的范围内。

2.         子程序开始或结束执行时文件或流逝处于打开或关闭的状态。

3.         子程序开始或结束执行时,文件或流的读写位置处于开头或结尾处。

4.         文件或流已用只读、只写或可读可写方式打开。

5.         仅用于输入的变量的值没有被子程序所修改。

6.         指针非空。

7.         传入子程序的数组或其他容器至少能容纳X个数据元素。

8.         表已初始化,存储着真实的数值。

9.         子程序开始或结束执行时,某个容器是空的或满的。

10.     一个经过高度优化的复杂子程序的运算结果和相对缓慢代代码清晰的子程序的运算结果相一致。

11.     

建立自己的断言机制

改善C++中的ASSERT

#define ASSERT(condition, message) { \if(!(condition)) { \LogError(“Assertion failed: “, \#condition, message) \exit(EXIT_FAILURE); \} \}

使用断言的指导建议

1.         用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况。

2.         避免把需要执行的代码放到断言中。以防被编译器略过。

3.         用断言来注解并验证前条件和后条件。

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

8.3 错误处理技术

处理那些预料中可能要发生的错误。可用的技术。

1.         返回中立值

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

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

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

5.         把警告信息记录到日志文件中。

6.         返回一个错误码。如:设置一个状态变量的值;用状态值作为函数的返回值;用语言内建的异常机制抛出一个异常。

7.         调用错误处理子程序或对象。把错误处理都集中在一个全局的错误处理子程序或对象中,代价是紧密耦合。

8.         当错误发生时显示出错信息。

9.         用最妥当的方式在局部处理错误。

10.     关闭程序。适用于safety_critical的应用程序。

健壮性与正确性

正确性:永不返回不准确的结果,哪怕不返回结果也比返回不准确的结果好。

健壮性:不断尝试采取某些措施,以保证软件可以持续地运转下去,哪怕有时结果不够准确。

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

应该在整个程序里采用一致的方式处理非法的参数。

8.4 异常

把代码中的错误或异常事件传递给调用方代码。

1.         用异常通知程序的其他部分,发生了不可忽略的错误。

2.         只在真正例外的情况下才抛出异常。因复杂度会增加。

3.         不能用异常来推卸责任。

4.         避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获。

5.         在恰当的抽象层次抛出异常。抛出的异常也是程序接口的一部分,和其他具体的数据类型一样。

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

7.         避免使用空的catch语句。

8.         了解所用函数库可能抛出的异常。

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

10.     把项目中对异常的使用标准化。如:C++可以考虑只抛出从std::exception基类派生出的对象;创建项目的特定异常类,用作所有可能抛出异常的基类,这样就能把记录日志、报告错误等操作集中起来并标准化。

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

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

在输入数据时将其转换为恰当的类型。

格栏与断言的关系

隔栏外部的程序应使用错误处理技术,在那里对数据做的任何假定都是不安全的。

隔栏内部的程序应使用断言技术,因为传进来的数据应该已在通过隔栏时被清理过了。

8.6 辅助调试的代码

不要自动地把产品版的限制强加于开发版之上。应该在开发期间牺牲一些速度和对资源的使用,来换取一些可以让开发更顺畅的内置工具。

尽早引入辅助调试的代码。

采用进攻式编程。在开发阶段让它显现出来,而在产品代码运行时让它能够自我恢复。

计划移除调试辅助的代码。比如:

1.         使用类似antmake这样的版本控制工具和make工具。

2.         使用内置的预编译器。例如,C++中:

#define DEBUG#ifdef(DEBUG)#define DebugCode( code_fragement ) { code_fragment }#else#define DebugCode( code_fragement )#endif…DebugCode(statement 1;statement 2;…statement n;);

3.         编写你自己的预处理器。

4.         使用调试存根(debuging stubs)。如写一个CheckPointer()子程序,对传入指针全面检查。正式发布时把这个子程序置空。

8.7 确定在产品代码中该保留多少防御式代码

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

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

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

4.         保留可以让程序稳妥地崩溃的代码。

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

6.         确认留在代码中的错误消息是友好的。

8.8 对防御式编程采取防御的姿态

考虑何时用,以及优先级。

第九章 伪代码编程过程

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

创建一个类的步骤

创建子程序的步骤

9.2 伪代码

1.         用类似英语的语句来精确描述特定的操作。

2.         避免使用目标编程语言中的与法院诉。

3.         在本意的层面上编写伪代码。用伪代码去描述解决问题的方法的意图,而不是去写如何在目标语言中实现这个方法。

4.         在一个足够低的层次上编写伪代码,以便可以近乎自动地从它生成代码。层次太高会掩盖代码中的问题细节。

伪代码将来可变为注释。

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

设计子程序

1.         检查先决条件。

2.         定义子程序要解决的问题。如:这一子程序将要隐藏的信息;传给这个子程序的各项输入;从该子程序得到的输出;在调用程序之前确保有关的前条件成立(如输入数据的取值位于特定范围之内、有关的流已经初始化、文件已经打开或者关闭、缓冲区已经填满或清空等);在子程序将控制权交回调用方程序之前,确保其后条件的成立(如输出数据位于特定范围之内、流已经初始化、文件已经打开或者关闭、缓冲区已填满或清空等)。

3.         为子程序命名。

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

5.         在标准库中搜寻可用的功能。与其在那些别人写成了博士论文的东西上浪费时间,还不如花几分钟去看看别人已经写成的代码。

6.         考虑错误处理。所有可能出错的化解。

7.         考虑效率问题。大部分情况下,并没有特别的重要。通常白费功夫。主要的优化还是在于完善高层的设计,而不是完善每个子程序。

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

9.         编写伪代码。

10.     考虑数据。把关键的数据类型定义好,对于设计一个子程序的逻辑很有用。

11.     检查伪代码。

12.     在伪代码中试验一些想法,留下最好的想法(迭代)。

编写子程序的代码

对编译子程序的建议:

1.         把编译器的警告级别调到最高。

2.         使用验证工具validators

3.         消除产生错误消息和警告的所有根源。

9.4 伪代码编码过程的替代方案

测试先行开发:先写测试用例。

重构:对代码进行一系列保持语义的变换和调整来提高代码的质量。

契约式设计:每一段程序都具有前条件和后条件。

东拼西凑:还是试试伪代码编程吧。

原创粉丝点击