与本书相关的checklist表资源在:http://download.csdn.net/source/2403574
第一部分 打好基础
关于错误处理:
有人估计程序中高达90%的代码是用来做错误处理,处理异常情况,或者做簿记(housekeeping)工作,意味着只有10%的代码是用来处理常规情况的(Shaw in Bentley 1982)
构架的总体质量:
优秀的软件架构很大程度上是与机器和编程语言无关的。不可否认的是,你不能忽视构建的环境。无论如何,要尽可能的独立于环境,这样你就能抵抗对系统进行过度架构(overarchitect)的诱惑
架构应该踏在对系统“欠描述/underspecifying”和“过度描述/overspecifying"之间的那条分界线上。没有哪一部分架构应该得到比实际需要更多的关注,也不应该过度设计(overdesigned)
架构应该明确指出有风险的区域
前期准备阶段:
程序员的一部分的工作是教育老板和合作者,告诉他们软件开发过程,包括在开始编程之前进行充分准备的重要性
你所从事的软件项目的类型对构建活动的前期准备有重大影响--许多项目应该是高度迭代式得,某些应该是序列式
第四章 关键的”构建“决策
第二部分 创建高质量的代码
第五章 软件构件中的设计
你在学校中所开发的程序和你在职业生涯中所开发的程序的主要差异就在于,学校里的程序所解决的设计问题很少(如果有的话)是险恶的。学校里给你的编程作业都是为了让你能从头到尾之直线前进。
说设计了无章法,是因为在此过程中你会采取很多错误的步骤,多次误入歧途 -- 你会犯很多的错误。事实上,出错正是设计的关键所在--在设计阶段犯错并加以改正,其代价要比在编码后才发现同样的错误并彻底修改低得多。
设计就是确定取舍和调整顺序的过程,在现实世界里。设计者工作的一个关键内容便失去衡量彼此冲突的各项设计特性
设计的要点,一部分是在创造可能发生的情况,而另一部分又是在限制可能发生的事情。
管理复杂度的重要性:
当没人知道对一处代码的改动会对其他代码带来什么影响时,项目就快要停止进展了
作为软件开发人员,我们不应该试着在同一时间把整个程序都塞进自己大脑。
所有软件设计技术的目标都是把复杂问题分解成简单的部分
理想的涉及特征:
· 最小的复杂度
· 易于维护
· 松散耦合
· 可扩展性
· 可重用性
· 高扇入(让大量的类使用某个给定的类)
· 低扇出(让一个类里少量或适中的使用其它的类)
· 可移植性
· 精简性: 精简性意味着设计出的系统没有多余的部分。因为任何多余的代码也需要开发,复审和测试,并且当修改了其它代码之后还要重新考虑他们。要问这个关键的问题:”这虽然简单,但把它加进来之后会损害什么呢“
· 层次性:是你能在任意的层面上观察系统,并得到某种具有一致性的看法,设计出来的系统应该能在任意层次上观察而不需要进入其他层次。
· 标准技术
信息隐藏:
隐藏设计决策对于减少”改动所影响的代码“而言是至关重要的。
信息过度分散的一个例子是 在系统内部到处都有雨人机交互相关的内容。最好是把人机交互逻辑集中到一个单独的类,报或者子系统中
找出容易改变的区域:
1. 找出看起来容易变化的项目。如果需求做得好,那么其中就应该包含一分钱在变化的清单,以及其中每一项变化发生的可能性。
2. 把容易变化的项目分离出来。把第一步中找出的容易变化的组件单独划分成类,或者和其它容易同时发生变化的组件划分到统一各类中
3. 把看起来容易变化的项目隔离开来。
可以在使用状态变量时增加至少两层的灵活性和可读性:
不要使用布尔变量作为状态变量,请用枚举变量
使用访问器子程序取代对状态变量的直接检查
要做多少设计才够:
对于实施正式编码阶段前的设计工作量和设计文档的正规程度,很难有个准确的定论。有很多因素,如团队的经验,系统的预定寿命,想要得到的可靠度,项目的规模和团队的大小等等都需要考虑进去。 并不是一定都要细化程度非常高的设计的。
通常最大的设计问题不是来自于那些我认为是很困难的,并且在其中作出了不好的设计的区域;而是来自于那些我认为是很简单的,而没有做出任何设计的区域。
第六章 可以工作的类(Working Classes)
类的基础:抽象数据类型(ADTs) Abstract Data Types
抽象数据类型是指一些数据以及对这些数据所进行的操作的集合。
6.2 良好的类接口:
需要好的抽象和好的封装。
封装是一个比抽象更强的概念。抽象通过提供一个可以让你忽略实现细节的模型来管理复杂度,而封装则强制阻止你看到细节--即便你想这么做
良好的抽象需要你:
a. 不要对类的使用者作出任何假设,类的设计和实现应该符合在类的接口中所隐含的契约。
b. 避免使用友元,一般情况下友元类会破坏封装,因为它让你在同一时刻需要考虑更多的代码量,增加了复杂度
c. 让阅读代码比编写代码更方便
d. 要格外警惕从语义上破坏封装性。语义上的封装不好导致调用方法不是依赖于类的公开接口,而是依赖于类的私用实现。
e. 留意过于紧密的耦合关系:
§ 尽可能限制类和成员的可访问性
§ 避免友元类,因为他们之间是紧密耦合的。
§ 把基类中把数据声明为private 而不是Protected,以降低派生类和基类之间耦合的程度
§ 避免在类的公开接口中暴露成员数据
§ 要对从语义上破坏封装性保持警惕。
6.3 有关设计和实现的问题
Containment( "has a " relationships)
inheritance ("is a " relationships)
包含往往比继承更可取,除非你是要对“is a”模型建模。
注意: 避免让继承体系过深,过深的继承层次会显著导致错误率的增长。过深层次增加了复杂度,而这恰恰与继承所应解决的问题相反。还有就是基类中的所有数据曾预案最好都定义为Private而不是protected.
在不能决定的时候,对于浅拷贝,深拷贝时更好的选择。
Summary of reasons to create a class
1. 对现实世界中的对象建模
2. 对抽象对象建模
3. 降低复杂度 *****************
4. 隔离复杂度****************
5. 隐藏实现细节***************
6. 限制变化所影响的范围*************
7. 隐藏全局数据
8. 让参数传递更顺畅************
9. 创建中心控制点
10. 让代码更容易重用****************
11. 为程序族做计划
12. 把相关操作放在一起
13. 实现特定的重构
与特定语言相关的问提
在java中,所有方法默认是可以覆盖的,方法必须被定义成final才能阻止派生类对他进行覆盖。在c++中,模式是不能被覆盖的,
基类的方法必须被定义为virtual才能被覆盖。而在visualbasic中,基类的子程序必须被定义为overridable,而派生类中的子程序必须要用overrides关键字
第七章 高质量的子程序
创建子程序的正当理由:
· 降低复杂度
· 引入中间,易懂的抽象
· 避免代码重复
· 支持子类化
· 隐藏顺序
· 隐藏指针操作
· 提高可移植性
· 简化复杂的布尔判断
7.4 子程序可以写多长: 理论上认为的子程序最佳最大长度通常是一屏代码或打印出来一到两页的代码,也就是大约50到150行
7.5 如何使用子程序参数:
子程序之间的接口是最易出错的部分之一。 程序中39%的错误都是属于内部接口错误--也就是子程序间互相通信时所发生的错误。以下是一些可以减少接口错误的指导原则:
1。按照输入-修改-输出的顺序排列参数,这和c函数库中所有的把会被修改的参数列在最前面的规则是不同的(可以考虑自己创建In和out的关键字)
2。如果几个子程序都用了类似的一些参数,应该让这些参数的排列顺序保持一致
3。使用这些参数,如果不适用,应该从子程序的接口中删去。
4。把状态或出错变量放在最后
5。不要把子程序的参数用做工作变量,这样很危险,应该使用局部变量
如:
int sample(int inputval )
{
int workingval = inputval;
workingval = workingval * currentmultiplier ( workingval );
...
return workingval;
}
6。在接口中队参数的假定加以说明。在子程序内部和调用子程序的地方同时对所做的假定进行说明是值得的。 (一种闭住是还好的方法,实在代码中使用断言(assertions));
7。把子程序的参数个数限制在大约7个以内,如果你发现一只需要传递很多参数,这就说名字程序之前的耦合太过紧密了 。
8。确保实际参数与形式参数相匹配。
7.6 special considerations in the use of functions
函数是指有返回值的子程序;过程是指没有返回值的子程序。在c++中,通常把所有子程序都称为 “函数”; 然而,那些返回值类型为void的函数在语义上其实就是过程。
setting the function's return value
检查所有可能的返回路径。确认在所有情况下该函数都会返回值。
不要返回指向局部数据的引用后指针。
7.7 Macro routines and inline routines
把宏表达是整个包含在括号内 如: #define Cube( a ) ( (a)*(a)*(a))
把含有多条语句的宏用大括号括起来 一个宏可以含有多条语句,如果你把它当做一条语句是永久会出错。
如:
#define LookUpEntry ( key, index ) { /
index = (key -10 ) / 5 ; /
index = min ( index, MAX_INDEX ); /
}
...
for ( entryCount = 0; entryCount < numEntries; entryCount ++ )
LookUpEntry( entryCount, tableIndex[ entryCount ] );
通常认为,用宏来代替函数调用的做法具有风险,而且不易理解--这是一种很糟糕的编程实践--因此,除非必要,否则还是应该避免使用这种技术。
用给子程序命名的方法来给展开后代码形同子程序的宏命名,以便在需要是可以用子程序来替代宏。
像c++这样的现代编程语言提供大量可以取代宏的方案:
o const 可以用于定义常量
o inline可以用于定义可悲便以为内核的代码的函数
o template可以用于以类型安全的方式定义各种标准操作,如min, max等
o enum可以用于定义枚举类型
o typedef 可以用于定义简单的类型替换
Inline routines
inline子程序允许程序员在编写代码时把代码当成子程序,但编译器在编译期间通常会把每一处调用inline子程序的地方都转换为插入内嵌的代码(inline凑得)。 因为便面乐子程序调用的开销,因此inline机制可以产生非常高效的代码。
节制使用Inline子程序。 inline子程序违反了封装原则,因为c++要求程序员把inline子程序的实现代码写在头文件里,从而也就把这些实现细节暴露给了所有使用该头文件的程序员。
inline子程序呀要求在调用子程序的每个地方都生成该子程序的全部代码,这样无论inline子程序是长是短,都会增加整体代码的长度,这也会带来其自身的问题。
为了性能原因使用Inline子程序的底线是:剖测(profile)代码并衡量性能上的改进,如果预期获得的性能收益不能说明为“剖测代码以验证性能改进”操心是值得的,那也就没有理由再牺牲代码质量而使用inline子程序了。
第八章 防御式编程 Defensive Programming
防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。
8.2 断言 Assertions
可以建立自己的断言机制
c++ example: a marco to implement the assertions
#define ASSERT( condition, message ) { /
if ( !(condition) ) { /
LogError ( "Assertion failed: ", /
#condition, message ); /
exit( EXIT_FAILURE ); /
} /
}
使用断言的指导建议
用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况。
避免把需要执行的代码放到断言中
。。。。
8.3 错误处理技术
· 返回中立值,换用下一个正确的数据,返回与前次相同的数据,换用最接近的合法值
· 把警告信息记录到日志文件中
· 返回一个错误码
· 调用错误处理子程序或者对象
· 显示出错信息
· 关闭程序
8.4 异常
异常和继承有一点是相同的,就是:审慎明智地使用时,他们都可以降低复杂度;而草率粗心的使用时,只会让代码变得几乎无法理解。
· 用异常通知程序的其他部分,发生了不可忽略的错误。
· 只有在罕见甚至永远也不该发生的情况下才抛出异常。
· 避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获。
· 在恰当的抽象层次抛出异常
· 在异常消息中加入关于导致异常发生的全部信息
· 避免使用空的catch语句
java示例:忽略异常的错误做法
try {
....
} catch ( AnException exception ) {
}
java示例:忽略异常的正确做法
try {
...
} catch ( AnException exception ) {
LogError( "Unexpected exception" );
}
· 了解所有函数库可能抛出的异常
· 考虑创建一个集中的异常报告机制
8.5 隔离程序,使之包容有错误造成的损害
隔栏的使用使断言和错误处理有了清晰的区分。隔栏外部的程序应该使用错误处理技术,在那里对数据做的任何假定都不安全。而隔栏内部就应该使用断言,因为传进来的数据应该已经在通过隔栏时被清理过了
8.6 辅助调试的代码
采用进攻式编程:
· 确保断言语句是程序终止运行
· 完全填充分配到的所有内存,这样可以检测到内存非配错误
· 完全填充已分配到的所有文件或流,这样可以让你排查出文件格式错误
· 删除一个对象之前把它填满垃圾数据
· 让程序把他的错误日志文件用电子邮件发给你--如果这对你所开发的软件适用的话
移除调试辅助的代码
常用的可以用内置的预处理器
如:
#define DEBUG
#if defined( DEBUG )
...
#endif
#define DEBUG
#if defined( DEBUG )
#define DebugCode ( code_fragment ) { code_fragment }
#else
#define DebugCode ( code_fragment )
#endif
...
DebugCode (
statement 1;
statement 2;
...
statement n;
);
可以使用调试存根(debugging stubs)
如:
//开发阶段
void CheckPointer ( void *pointer ) {
//step1 check
//step2 check
....
}
//产品代码中
void CheckPointer ( void *pointer ) {
//no code, just return to caller
}
8.7 确定在产品代码中该保留多少防御式代码
· 保留那些检查重要错误的代码
· 去掉检查席位错误的代码(去掉并不知永久删除,而是通过版本控制,预编译器开关等来编译不包含这段特定代码的程序。如果程序所占空间不成问题,你也可以把错误检查代码保留下来,但应该让他不动声色的把错误信息记录在日志文件里)
· 去掉会导致程序硬性崩溃的代码
· 保留可以让程序稳妥崩溃的代码(这两点意思是,可以崩溃,但至少要留出时间给用户来保存工作成果)
· 为你的技术支持人员记录错误信息
· 确认留在代码中的错误消息是友好的(一种常用方法是,通知用户说发生了“内部错误”,再留下可供报告该错误的电子邮件地址或电话号码即可)
第九章 伪代码编程过程 The Pseudocode Programming Process
9.3 通过伪代码变成过程创建子程序
编写子程序的代码:
写出子程序的声明 |
编写第一条和最后一条语句,然后将伪代码转换为高层次的注释 |
每条注释下面填充代码 |
检查代码 |
收尾工作 |
以伪代码开始 |
按需重复 |
完成 |
检查代码:
· 在脑海中检查程序中的错误
· 编译子程序(在你一能让自己相信一个子程序是正确是在编译它,可以避免在匆忙中完成代码):
o 把编译器的警告级别调到最高
o 使用验证工具(如lint)
o 消除产生错误消息和警告的所有根源
· 在调试器中逐行执行代码
· 测试代码(test case)
· 消除错误