代码大全(二)
第1部分 打好基础
本部分内容
第1章 欢迎进入软件构建的世界
第2章 用隐喻来更充分地理解软件开发
第3章 三思而后行:前期准备
第4章 关键的“构建”决策
一、欢迎进入软件构建的世界
内容:
什么是软件构建
软件构建为何如此重要
如何阅读本书
你一定知道“构建”一词在软件开发领域之外的含义。“构建”就是“建筑工人(construction workers)”在建设一栋房屋、一所学校、及至一座摩天大楼时所做的工作,在你年轻时,可能也曾用“硬纸板(construction paper)”构建过什么东西。按照一般的用法,“构建”是指建设的过程。构建过程可能包含有计划、设计、检查工作的一些方面,但在多数时候,“构建”就是指创建事物过程中动手的那些部分。
1、什么是软件构建
开发计算机已是一个复杂的过程。在过去25年间,研究者已经认识到在软件开发过程中的各种不同的活动(activity):
定义问题
需求分析
规划构建
软件架构
详细设计
编码与调试
单元测试
集成测试
集成
系统测试
保障维护
构建活动主要关注于编成与调试,但也包含详细设计、单元测试、集成测试以及其他一些活动
构建活动中的具体任务(task)
验证有关的基础工作已经完成,因此构建活动可以顺利地进行下去。
确定如何测试所写的代码
设计江编写类(class)和子程序(routine)
创建并命名变量(variable)和具名常量(named constant)
选择控制结构(control structure),组织语句块
对你的代码进行单元测试和集成测试,并排除其中的错误
评审开发团队其他成员的底层设计和代码,并让他们评审你的工作
润饰代码,仔细进行代码的格式化和注释
将单独开发的多个软件组件集成为一体
调整代码(tuning code),让它更快、更省资源
非构建活动主要包括管理(management)、需求分析、软件架构设计、用户界面设计、系统测试、以及维护。
2 、软件构建为何如此重要
构建活动是软件开发的主要组成部分
构建活动是软件开发的核心活动
把主要精力集中于构建活动,可以大大提高程序员的生产效率
构建活动的产物——源代码——往往是对软件的唯一精确描述
构建活动是唯一一项确保会完成的工作
要点:
1、软件构建是软件开发的核心活动;构建活动是每个项目中唯一一项必不可少的工作。
2、软件构建的主要活动包括;详细设计、编码、调试、集成、开发者测试(developer testing)(包括单元测试和集成测试)。
3、构建也常被称作“编码”和“编程”。
4、构建活动的质量对软件的质量有着实质性的影响。
5、最后,你对“如何进行构建”的理解程序,决定了你这名程序员的优秀程序——这就是本书其余部分的主题了。
二、用隐喻来更充分地理解软件开发
内容:
隐喻的重要性
如何使用软件隐喻
常见的软件隐喻
计算机科学领域中有着所有学科中最为丰富多彩的语言。你走进一间安全严密、温度精确控制在20度的房间,并在里面发现了病毒、特洛伊木马、蠕虫、臭虫、逻辑炸dan、崩溃、论坛口水战、双绞线转换头,还有致命错误......,这此形象的隐喻(比喻)描述了软件领域中各种特定的现象和事物。像这样生动活泼的隐喻还能够描述更加广泛的现象。借助这些隐喻,我们能更深刻地理解软件开发的过程。
2、如何使用软件隐喻
与其说一个软件隐喻像是一张路线图,还不如说它是一盏探照灯。它不会告诉你到哪里去寻找答案 ,而仅是告诉你该如何寻找答案。隐喻的作用更像是启示(启发、试探法),而不是算法。
算法是一套定义明确的指令,使你能完成某个特定的任务。算法是可预测的、确定性的、不易变化的。一个告诉你如何从A点到达B点的算法,不会让你绕路,不会让你额外地经过D、E、F等地方,更不会让你停下来闻闻玫瑰花或喝杯咖啡。
而启发式方法是一种帮你寻求答案的技术,但它给出的答案是具有偶然性的,因为启发式方法仅仅告诉你该如何去找,而没有告诉你要找什么,它并不告诉你该如何直接从A点到达B点,它甚至可能连A点和B点在哪里都不知道。实际上,启发式方法是穿着小丑儿外套的算法;它的结果不太好预测,也更有趣,但不会给你什么30天无效退款的保证。
3、常见的软件隐喻
软件中的书法:写作代码
软件的耕作法:培植系统
软件的牡蛎养殖观点:系统生长
软件构建:建造软件
组合各个隐喻
要点:
1、隐喻是启示而不是算法。因此它们往往有一点随意
2、隐喻把软件开发过程与其他你熟悉的活动联系在一起,帮助你更好地理解。
3、有些隐喻比其他一些隐喻更贴切。
4、通过把软件的构建过程比作是房屋的建设过程,我们可以发现,仔细的准备是必要的,而大型项目和小型项目之间也是有差异的。
5、通过把软件开发中的实践比作是智慧工具箱的工具,我们又发现,每位程序员都有许多工具,但并不存在一个能适用于所有工作的工具,因地制宜地选择正确工具是成为能有效编程的程序员的关键。
6、不同隐喻彼此并不排斥,应当使用对你最有益处的某种隐喻组合。
三、三思而后行:前期准备
内容:
前期准备的重要性
辨明你所从事的软件的类型
问题定义的先决条件
需求的先决条件
架构的先决条件
花费在前期准备上的时间长度
在开始建造房子之前,施工人员会审视蓝图(包含所有细节信息的设计详图),查看是否获得了全部(建筑)许可证,并测量房屋的地基。施工人员建造摩天大楼是一种方法,建造普通住宅用另一种方法,建造犬舍用第三种方法。无论何种项目,都会对“准备工作”进行剪裁,使之符合项目的特定需要;在构建活动开始之有,准备工作要做周全。
本章描述软件构建必须做的准备工作。就像修建建筑物一样,项目的成败很大程度上在构建活动开始之前就已经注定了。如果地基没打好,或者计划不充分,那么你在构建期间能做的无非是尽量让损害最小罢了。
1、前期准备的重要性
使用高质量的实践方法是那些能创造高质量软件的程序员的共性。这些高质量的实践方法在项目的初期、中期、末期都强调质量。
如果你在项目的末期强调质量,那么你会强调系统测试
如果你在项目的中期强调质量,那么你会强调构建实践
如果你在项目的初期强调质量,那么你就会计划、要求并且设计一个高质量的产品。
关于开始构建之前要做前期准备的绝对有力且简明的论据
设想你已经到过“问题定义”之山,与名为“需求”之人同行数里,在“架构”之泉面前脱下脏兮兮的外套,然后在“前期准备”之纯净水中沐浴。那么你就会知道,在实现一个系统之前,你需要理解“这个系统应该做什么”,以及“它该如何做这些”。
2、辨明你所从事的软件的类型
不同种类的软件项目,需要在“准备工作”和“构建活动”之间做出不同的平衡。三种常见的软件项目种类:商业系统(Intenet站点)、使命攸关的系统(软件工具)、性命攸关的嵌入式系统(航空软件)
开发商业系统的项目往往受益于高度迭代的开发法,这种方法的“计划、需求、架构”活动与“构建、系统测试、质量保证”活动交织在一起。性命攸关的系统往往需求采用更加序列式的方法——“需求稳定”是确保“超高等级的可靠性”的必备条件之一。
迭代的开发法:随着着项目的进展不断找出错误,在项目进行过程中一点点地吸收消化返工,这样使得总体成本较低。
序列式的方法:仅仅依赖于测试来发现缺陷,将绝大部分缺陷修正工作推迟到项目结束的时候进行,使得成本较高。
3、问题定义的先决条件
在开始构建之前,首先要满足的一项先决条件是,对这个系统是解决的问题做出清楚的陈述。这有时称为“产品设想”、“设想陈述”、“任务陈述”或者“产品定义”。这里将它称为“问题定义”。
“问题定义”只定义了“问题是什么”,而不涉及任何可能的解决方案。这是一个很简单的陈述,可能只有一到时两项,并且听起来应该你一个问题。像“我们跟不上Gigatron的订单了”这样的句子听起来你是个问题,而且确实是一个很好的问题定义。而“我们需要优化数据自动采集系统,使之跟上Gigatron的订单”这种句子是糟糕的问题定义。它听起来不像是问题,倒像是解决方案。
问题定义应该用客户的语言来书写,而且应该从客户的角度来描述问题。
“未能定义问题”的处罚是,你浪费了大量时间去解决错误的问题。这是双重处罚,因为你也没有解决正确的问题。
4、需求的先决条件
“需求”详细描述软件系统应该做什么,这是达成解决方案的第一步。“需求”活动也称为“需求开发”、“需求分析”,“分析”、“需求定义”、“软件需求”、“规格书”、“功能规格书”、“规格”
为什么要有正确的需求:明确的需求有助于确保用户(而不是程序员)驾驭系统的功能。如果没有好的需求,你可能对问题有总体的把握,但却没有击中问题的特定方面。
在构建期间处理需求变量:
使用本节末尾的需求核对表来评估你的需求的质量
确保每一个人都知道需求变量的代价
建立一套变量控制程序
使用能适应变量的开发方法
放弃这个项目
注意项目的商业案例
核对表:需求
这张需求核对表包含了一系列的问题——问问自己项目的需求工作做得如何。在开始构建之前,用这份列表做一次“心智健全”检查,看看你的地基到底有多坚固——用“需求里氏震级”来衡量。
针对功能需求
1、是否详细定义了系统的全部输入,包括其来源、精度、取值范围、出现频率等?
2、是否详细定义了系统的全部输出、包括目的地、精度、取值范围、出现频率、格式等?
3、是否详细定义了所有输出格式(Web页面、报表,等等)?
4、是否详细定义了所有硬件及软件的外部接口?
5、是否详细定义了全部外部通信接口,包括握手协议、纠错协议、通信协议等?
6、是否列出了用户想要做的全部事情?
7、是否详细定义了每个任务所用的数据,以及每个任务得到的数据?
针对非功能需求(质量需求)
1、是否为全部必要的操作,从用户的视角。详细描述了期望响应时间?
2、是否详细描述了其他与计时有关的考虑,例如处理时间、数据传输率、系统吞吐量?
3、是否详细定义了安全级别?
4、是否详细定义了可靠性,包括软件失灵后的后果、发生故障时需要保护的至关重要的信息、错误检测与恢复的策略等?
5、是否定义了机器内存和剩余磁盘空间的最小值?
6、是否详细定义了系统的可维护性,包括适应特定功能的变更、操作环境的变更、与其他软件接口的变更能力?
7、是否包含对“成功”的定义?“失败”的定义呢?
需求的质量
1、需求是用用户的语言书写的吗?用户也这么认为吗?
2、每条需求都不与其它需求冲突吗?
3、是否详细定义了相互竞争的特性之间的权衡——例如,健壮性与正确性之间的权衡?
4、是否避免在需求中规定设计(方案)?
5、需求是否在详细程序上保持相当一致的水平?有些需求应该更详细地描述吗?有些需求应该更粗略地描述吗?
6、需求是否足够清晰,即使转交给一个独立的小组去构建,他们也能理解吗?开发者也这么想吗?
7、每个条款都与待解决的问题及其解决方案相关吗?能从每个条款上溯到它在问题域中对应的根源吗?
8、是否每条需求都是可测试的?是否可能进行独立的测试,以检验满不满足各项需求?
9、是否详细描述了所有可能对需求的改动,包括各项改动的可能性?
需求的完备性
1、对于在开始开发之前无法获得的信息,是否详细描述了信息不完全的区域?
2、需求的完备度是否能达到这种程度:如果产品满足所有需求,那么它就是可接受的?
3、你对全部需求都感到很舒服吗?你是否已经去掉了那些不可能实现的需求——那些只是为了安抚客户和老板的东西?
四、关键的“构建”决策
内容:
1、选择编程语言
2、编程约定
3、你在技术浪潮中的位置
4、选择主要的构建实践方法
第3章“三思而后行,前期准备”讨论了设计蓝图和建筑许可证在软件业里的等价物。你可能对那此准备工作没有多少发言权,所以第3章关注的焦点是确定“当构建开始后你需要做什么”。
本章关注的焦点是程序员和技术带头人个人必须(直接或间接)负责的准备工作。在向工地进发之前,如何选择适用的工具别在你的腰带上,你的手推车里该装哪些东西?本章讨论的就是这些事务在软件中的等价物。
1、选择编程语言
研究表明,编程语言的选择从多个方面影响生产率和代码质量。
2、编程约定
在高质量软件中,你可以看到“架构的概念完整性”与“其底层实现”之间的关系。“实现”必须与(指导该实现的)“架构”保持一致,并且这种一致性是内在的、固有的。这正是变量名称、类的名称、子程序名称、格式约定、注释约定等这些针对“构建活动”的指导方针的关键所在。
3、你在技术浪潮中的位置
在成熟的技术环境下——浪潮的末尾,21世纪最初10所的中期的网络编程——我们受益于丰富的软件开发基础设施。
在技术浪潮的前期——20世纪90年代中期的网络编程——情况正好相反,可选择的编程语言非常少。
“深入一种语言去编程”的程序员首先决定他要表达的思想是什么,然后决定如何使用特定语言提供的工具来表达这些思想。
4、选择主要的构建实践方法
“构建”有一部分准备工作,就是决定在这么多的可选的实践方法中,你想要强调哪些。下面的核对表总结了在“构建”过程中,应该有意识地使用或者排斥的特定编程实践。
核对表:主要的构建实践
编码
你有没有确定,多少设计工作将要预先进行,多少设计工作在键盘上进行(在编写代码的同时)?
你有没有规定诸如名称、注释、代码格式等“编码约定”?
你有没有规定的由软件架构确定的编码实践,比如如何处理错误条件、如何处理安全性事项、对于类接口有哪些约定、可重用的代码遵循哪些标准、在编码时考虑多少性能因素等?
你有没有找到自己在技术浪潮中的位置,并相应调整自己的措施?如果必要,你是否知道如何“深入一种语言去编程”,而不受限于语言(仅仅“在一种语言上编程”)?
团队工作
你有没有定义一套集成工序——即,你有没有定义一套特定的步骤,规定程序员在把代码check in(签入)到主源码(代码库)中之前,必须履行这些步骤?
程序员是结对编程、还是独自编程,或者这二者的某种组合?
质量保证
程序员在编写代码之前,是否先为之编写测试用例?
程序员会为自己的代码写单元测试吗(无论先写还是后写)?
程序员在check in代码之前,会用调试器单步跟踪整个代码流程吗?
程序员在check in代码之前,是否进行集成测试?
程序员会复审(review)或检查别人的代码吗?
工具
你是否选用了某种版本控制工具?
你是否选定了一种语言,以及语言的版本或编译器版本?
你是否选择了某个编程框架(framework,如J2EE或Microsoft.NET),或者明确地决定不使用编程框架?
你是否决定允许使用非标准的语言特性?
你是否选定并拥有了其他将要用到的工具——编辑器、重构工具、调试器、测试框架(test framework)、语法检查器等?
要点:
1、每种编程语言都有其优点和弱点。要知道你使用的语言的明确优点和弱点。
2、在开始编程之前、做好一些约定。“改变代码使之符合这些约定”是近乎不可能的。
3、“构建的实践方法”的种类比任何单个项目能用到的要多。有意识地选择最适合你的项目的实践方法。
4、问问你自己,你采用的编程实践是对你所用的编程语言的正确响应,还是受它的控制?请记得“深入一种语言去编程”,不要仅“在一种语言上编程”。
5、你在技术浪潮的位置决定了哪种方法是有效的——甚至是可能用到的。确定你在技术浪潮中的位置,并相应调整计划和预期目标。
第2部分 创建高质量的代码
五、软件构建中的设计
理想的设计特征
高质量的设计具有很多常见特征。如果你能实现所有这些目标,你的设计就真的非常好了。这些目标之间会相互抵触,但这也正是设计中的挑战所在———在一系列相互竟争的目标之中做出一套最好的折中方案。有些高质量设计的特征也同样是高质量程序的特征,如可靠性和性能等。而有些则是设计范畴内的特征。
下在就列出一些设计范畴内的特征:
最小的复杂度:正如刚刚说过的,设计的首要目标就是要让复杂度最小。要避免做出“聪明的”设计,因为“聪明的”设计常常都是难以理解的。应该做出简单且易于理解的设计。如果你的设计方案不能让你在专注于程序的一部分时安心地忽视其他部分的话,这一设计就没有什么作用了。
易于维护:易于维护意味着在设计时为做维护工作的程序员着想。请时刻想着这些维护程序员可能会就你写的代码而提出的问题。把这些程序员当成你的听众,进而设计出能自明的系统来。
松散耦合:松散耦合意味着在设计时让程序和各个组成部分之间关联最小。通过应用类接口中的合理抽象、封装性及信息隐藏等原则,设计出相互关联尽可能最少的类。减少关联也就减少了集成、测试与维护时的工作量。
可扩展性:可扩展性是说你能增强系统的功能而无须破坏其底层结构。你可以改动系统的某一部分而不会影响到其他部分。越是可能发生的改动,越不会给系统造成什么破坏。
可重用性:可重用性意味着所设计的系统的组成部分能在其他系统中重复使用。
高扇入:高扇入就是说让大量的类使用某个给定的类。这意味着设计出的系统很好地利用了在较低层次上的工具类(utility classes)。
低扇出:低扇出就是说一个类里少量或适中地使用其他的类。高扇出(超过约7个)说明一类使用了大量其他的类,因此可以变得过于复杂。研究发现,无论考虑某个子程序调用其他子程序的量,还是考虑某个类使用其他类的量,低扇出的原则都是有益的。
可移植性:可移植是说应该这样设计系统,使它能很方便地移植到其他环境中。
精简性:精简性意味着设计出的系统没有多余的部分,伏尔泰曾说,一本书的完成,不在它不能再加入任何内容的时候,而在不能再删去任何内容的时候。在软件领域中,这一观点就更正确,因为任何多余的代码也需要开发、复审和测试,并且当修改了其他代码之后还要重新考虑它们。软件的后续版本也要和这些多余代码保持向后兼容。要问这个关键的问题:“这虽然简单,但把它加进来之后会损害什么呢”?
层次性:层次性意味着尽量保持系统各个分解层次的层次性,使你能在任意的层面上观察系统,并得到某种具有一致性的看法。设计出来的系统应该能在任意层次上观察而不需要进入其他层次。举例来说,假设你正在编写一个新系统,其中用到很多设计不佳的旧代码,这时你就应该为新系统编写一个负责同旧代码交互的层。在设计这一层时,要让它能隐藏旧代码的低劣质量,同时为新的层次提供一组一致的服务。这样,你的系统其他部分就只需与这一层进行交互,而无须直接同旧代码打交道了。在这个例子中,层次化设计的益处有:(1)它把低劣代码的烂泥潭禁闭起来;(2)如果你最终能抛弃或者重构旧代码,那时就不必修改除交互层之外的任何新代码。
标准性:一个系统所依赖的外来的、古怪的东西越多,别人在第一次想要理解它的时候就越是头疼。要尽量用标准化的、常用的方法,让整个系统给人一种熟悉的感觉。
关于设计启发的总结
下面是对主要的设计中的启发式方法的总结:
寻找现实世界的对象(object,物体)
形成一致的抽象
封装实现细节
在可能的情况下继承
藏住秘密(信息隐藏)
找出容易改变的区域
保持松散耦合
探寻通用的设计模式
下列的启发式方法有时也很有用
高内聚性
构造分层结构
严格描述类契约
分配职责
为测试而设计
避免失误
有意识地选择绑定时间
创建中央控制点
考虑使用蛮力
画一个图
保持设计模块化
六、为什么有这么多关于继承的规则
下面来总结一下何时可以使用继承,何时又该使用包含
1、如果多个类共享数据而非行为,应该创建这些类可以包含的共用对象。
2、如果多个类共享行为而非数据,应该让它们从共同的基类继承而来,并在基类里定义共用的子程序。
3、如果多个类既共享数据也共享行为,应该让它们从一个共同的基类继承而来,并在基类里定义共用的数据和子程序。
4、当你想由基类控件接口时,使用继承;当你想自己控制接口时,使用包含。
七、高质量的子程序
什么是子程序?子程序是为实现一个特定的目的而编写的一个可被调用的方法(method)或过程
7.2 在子程序层上设计
内聚性:对子程序而言,内聚性是指子程序各种操作之间联系的紧密程序。有些程序员更喜欢使用“强度”这一术语。
功能上的内聚性:是最强也是最好的一种内聚性,也就是说让一个子程序仅执行一项操作。例如:GetCustomerName(),这样的子程序都是高度内聚性的。当然,以这种方式来评估内聚性,前提是子程序所执行的操作与其名字相符---如果它还做了其他操作,那么它就不够内聚,同时其命名也有问题。
除此之外,还有其他一些种类的内聚性人们却通常认为是不够理想的。
顺序上的内聚性:是指在子程序内包含有需要按特定顺序执行的操作,这些步骤需要共享数据,而且只有在全部执行完毕后才完成了一项完整的功能。
通信上的内聚性:是指一个子程序中的不同操作使用了同样的数据,但不在在其他任何联系。例如某个子程序先根据传给它的汇总数据打印一份汇总报表,然后再把这些汇总数据重新初始化,那么这个子程序就具有通信上的内聚性;因为这两项操作只是因为使用了相同的数据才彼此产生联系。
临时的内聚性:是指含有一些因为需要同时执行才放到一起的操作的子程序。典型的例子有;Startup()、Shutdown()、CompleteNewEmployee()等。有些程序员认为临时的内聚性是不可取的,因为它们有时与不良的编程实践相关----比如说在Startup()子程序里塞进一大堆互不相关的代码等。
过程上的内聚性:是指一个子程序中的操作是按特定的顺序进行的。一个例子是依次获取员工的姓名、住址、和电话号码的子程序。这些操作执行的顺序之所以重要,只是因为它和用户按屏幕提示而输入数据在顺序相一致。另一个子程序用来获取员工的其他数据。这段程序也具有过程上的内聚性,因为它把一组操作赋以特定的顺序,而这些操作并不需要为了除此之外的任何原因而彼此关联。
逻辑上的内聚性:是指若干操作被放入同一个子程序中,通过传入的控制标志选择执行其中一项操作。之所以称之为逻辑上的内聚性,是因为子程序的控制流或所谓“逻辑”是将这些操作放到一起的唯一原因-----它们都被包在一个很大的 if 语句或case语句中,而不是因为各项操作之间有任务逻辑关联。认为是逻辑上的内聚性的标志属性就是各项操作之间的关联,因此,似乎更应称其为“缺乏逻辑的内聚性”。
巧合的内聚性:是指子程序中的各个操作之间没有任务可以看到的关联。它也可称为“无内聚性”或“混乱的内聚性”。
要点:
1、创建子程序最主要的目的是提高程序的可管理性,当然也有其他一些好的理由,其中,节省代码空间只是一个次要原因;提高可文读性、可靠性和可修改性等原因都更重要一些。
2、有时候,把一些简单的操作写成独立的子程序也非常有价值。
3、子程序可以按照其内聚性分为很多类,而你应该让大多数子程序具有功能上的内聚性,这是最佳的一种内聚性。
4、子程序的名字是它的质量的指示器。如果名字糟糕但恰如其分,那就说明这个子程序设计得很差劲。如果名字糟糕而且又不准确,那么它就反映不出程序是干什么的。不管怎样,糟糕的名字都意味着程序需要修改。
5、细心的程序员会非常谨慎地使用宏,而且只在万不得已时才用。
八、防御式编程
要点:
1、最终产品代码中对错误的处理方式要比“垃圾进、垃圾出”复杂很多。
2、防御式编程技术可以让错误更容易发现、更容易修改,并减少错误对产品代码的破坏。
3、断言可以帮助人尽早发现错误,尤其是在大型系统和高可靠性的系统中,以及快速变化的代码中。
4、关于如何处理错误输入的决策是一项关键的错误处理决策,也是一项关键的高层设计决策。
5、异常提供了一种与代码正常流程角度不同的错误处理手段。如果留心使用异常,它可以成为程序员们知识工具箱中的一项有益补充,同时也应该在异常和其他错误处理手段之间进行权衡比较。
6、针对产品代码的限制并不适用于开发中的软件。你可以利用这一优势在开发中添加有助于更快地排查错误的代码。
九、伪代码编程过程
本章将从微观上关注编程过程-------也就是关注创建单独的类及其子程序的特定步骤,这些步骤对任何规模的项目来说都十分关键。
1、创建类和子程序的步骤概述
创建一个类的步骤
创建类的总体设计
创建类中的子程序
复审并测试整个类
创建子程序的步骤
设计子程序
检查设计
编写子程序的代码
检查代码
2、伪代码
伪代码这个术语是指某种用来描述算法、子程序、类或完整程序的工作逻辑的、非形式的、类似于英语的记法。伪代码编程过程则是一种通过书写伪代码而更高效地创建程序代码的专门方法。
3、通过伪代码编程过程创建子程序
设计子程序
编写子程序的代码
检查代码
收尾工作
按照需要重复上述步骤
4、伪代码编程过程的替代方案
下面是其它专家推荐的一些不同的方法。你即可以用这些方法替代伪代码编程过程,也可以把它们用作对伪代码编程过程的补充。
测试先行开发
重构
契约式设计
东拼西凑
要点:
1、创建类和子程序通常都是一个迭代的过程。在创建子程序的过程中获得认识常常会反过来影响类的设计。
2、编写好的伪代码需要使用易懂的英语,要避免使用特定编程语言中才有的特性,同时要在意图的层面上写伪代码(即描述该做什么,而不是要怎么去做)。
3、伪代码编程过程是一个行之有效的做详细设计的工具,它同时让编码工作更容易。伪代码会直接转化为注释,从而确保了注释的准确度和实用性。
4、不要只停留在你所想到的第一个设计方案上。反复使用伪代码做出多种方案,然后选出其中最佳的一种方案再开始编码。
5、每一步完成后都要检查你的工作成果,还要鼓励其他人帮你检查。这样你就会在投入精力最少的时候,用最低的成本发现错误。
第3部分 变量
十、使用变量的一般事项
1、数据认知
创建有效数据的第一步是了解所要创建数据的种类。
2、轻松掌握变量定义
关闭隐藏声明
声明全部的变量
遵循某种命名规则
检查变量名
3、变量初始化原则
不合理地初始化数据是产生编程错误的常见根源之一。
在声明变量的时候初始化
在靠近变量第一次使用的位置初始化它
理想情况下,在靠近第一次使用变量的位置声明和定义该变量。
在可能的情况下使用final或者const
在类的构造函数里初始化该类的数据成员
检查是否需要重新初始化
一次性初始化具名常量:用可执行代码来初始化变量
使用编译器设置来自动初始化所有变量
利用编译器的警告信息
检查输入参数的合法性
使用内存访问检查工具来检查错误的指针
在程序开始时初始化工作内存
4、作用域
“作用域”可以看作是一种衡量变量的知名度的方法;
使变量引用局部化
尽可能缩短变量的“存活”时间
减小作用域的一般原则
有关缩小变量作用域的说明
5、持续性
“持续性”是对一项数据的生命期的另一种描述。持续性具有多种形态。
6、绑定时间
对程序维护和更改有很深远影响的一个话题就是“绑定时间”,对变量与数据绑定的时间进行一下总结。( 编码时、编译时、加载时、对象实例化时、即时),一般而言,绑定时间越早灵性性就会越差,但复杂度也会越低。
7、数据类型和控制结构之间的关系
jackson描绘出了三种类型的数据和相应控制结构之间的关系。
A、序列型数据翻译为程序中的顺序语句
B、选择型数据翻译为程序中的 if 和 case 语句
C、迭代型数据翻译成程序中的 for、repeat、while等循环结构
8、为变量指定单一用途
通过使用一些巧妙的方法,可以给一个变量赋予多种职责。不过你最好还是远离这些奇技淫巧。
每个变量只用于单一用途
避免让代码具有隐含含义
确保使用了所有已声明的变量
要点:
1、数据初始化过程很容易出错,所以请用本章描述的初始化方法避免由于非预期的初始值而造成的错误。
2、最小化每个变量的作用域。把同一变量的引用点集中在一起。把变量限定在子程序或类的范围之内。避免使用全局数据。
3、把使用相同变量的语句尽可能集中在一起。
4、早期绑定会减低灵活性,但有助于减小复杂度。晚期绑定可以增加灵活性,同时增加复杂度。
5、把每个变量用于唯一的用途。
十一、变量名的力量
核对表:变量命名
名字完整并准确地表达了变量所代表的含义吗?
名字反映了现实世界的问题而不是编程语言方案吗?
名字足够长,可以让你无须苦苦思索吗?
如果有计算值限定符,它被放在名字的最后吗?
名字中用Count或者Index来代替Num了吗?
为特定类型的数据命名
循环下标的名字有意义吗(如果循环的长度超出了一两行代码或者出现了嵌套循环,那么就应该是i/j或k以外的其他名字)?
所有的“临时”变量都重新命以更有意义的名字了吗?
当布尔变量的值为真时,变量名能准确表达其含义吗?
枚举类型的名字中含有能够表示其类别的前缀或后缀了吗?例如,把Color_用于Color_Red,Color_Green,Color_Blue等了吗?
具名常量是根据它所代表的抽象实体而不是它所代表的数字来命名的吗?
命名规则
规则能够区分局部数据、类的数据和全局数据吗?
规则能够区分类型名、具名常量、枚举类型和变量名吗?
规则能够在编译器不强制检测只读参数的语言里标识出子程序中的输入参数吗?
规则尽可能地与语言的标准规则兼容吗?
名字为了可读性而加以格式化吗?
短名字
代码用了长名字吗?
是否避免只为了省一个字符而缩写名字的情况?
所有单词的缩写方式都一致吗?
名字能够读出来吗?
避免使用容易被看错或者读错的名字吗?
在缩写对照表里对短名字做出说明吗?
常见命名问题:你避免使用......
容易让人误解的名字
有相近含义的名字
只有一两个字符不同的名字
发音相近的名字
包含数字的名字
为了缩短而故意拼错的名字
英语中经常拼错的名字
与标准子程序名或者预定义变量名冲突的名字
过于随意的名字
含有难读的字符的名字
要点:
1、 好的变量名是提高程序的可读性的一项关键要素。对特殊种类的变量,比如循环下标和状态变量,需要加以特殊的考虑。
2、 名字要尽可能地具体。那些太模糊或者太通用以致于能够用于多和目的的名字通常都是很不好的。
3、命名规则应该能够区分局部数据、类数据和全局数据。它们还应当可以区分类型名、具名常量、枚举类型名字和变量名。
4、无论做哪种类型项目,你都应该采用某种变量命名规则。你所采用的规则的种类取决于你的程序的规模,以及项目成员的人数。
5、现代编程语言很少需要用到缩写。如果你真的要使用缩写,请使用项目缩写词典或者标准前缀来帮助理解缩写。
6、代码阅读的次数远远多于编写的次数。确保你所取的名字更侧重于阅读方便而不是缩写方便。
十二、基本数据类型
核对表:基本数据类型
数值概论
代码中避免使用神秘数值吗?
代码考虑了除零错误吗?
类型转换很明显吗?
如果在一条语句中存在两个不同类型的变量,那么这条语句会像你期望的那样求值吗?
代码避免了混合类型比较吗?
程序编译时没有警告信息吗?
整数
使用整数除法的表达式能按预期的那样工作吗?
整数表达式避免整数溢出问题吗?
浮点数
代码避免了对数量级和相差大的数字做加减运算吗?
代码系统地阻止了舍入错误的发生吗?
代码避免对浮点数做等量比较吗?
字符和字符串
代码避免使用神秘字符和神秘字符串吗?
使用字符串时避免了off-by-one(缓冲溢出错误) 错误吗?
C代码把字符串指针和字符数组区别对待了吗?
C代码遵循了把字符串声明为CONSTANT + 1 长度的规则了吗?
C代码在适当的时候用字符数组来代替指针了吗?
C代码把字符串初始化为NULL来避免无终端的字符串了吗?
C代码用strncpy()代替strcpy()吗?strncat()和strncmp()呢?
布尔变量
程序用额外的布尔变量来说明条件判断了吗?
程序用额外的布尔变量来简化条件判断了吗?
枚举类型
程序用枚举类型而非具名变量来提高可读性、可靠性和可修改性吗?
当变量的用法不能仅用true和false表示的时候,程序用枚举类型来取代布尔变量了吗?
针对枚举类型的测试检测了非法数值吗?
把枚举类型的第一项条目保留为“非法的”了吗?
具名常量
程序用具名常量而不是神秘数值来声明数据和表示循环界限吗?
具名常量的使用一致吗?--------没有在有些位置使用具名常量又在其他位置使用文字量?
数组
所有的数组下标都没有超出数组边界吗?
数组引用没有出现 off-by-one 错误吗?
所有多维数组的下标的顺序都正确吗?
在嵌套循环里,把正确的变量用于数组下标来避免循环下标串话了吗?
创建类型
程序对每一种可能变化的数据分别采用不同的类型吗?
类型名是以该类型所表示的现实世界实体为导向,而不是以编程语言类型为导向的吗?
类型名的描述性足以强,可以帮助解释数据声明吗?
你避免重新定义预定义类型吗?
与简单地重定义一个类型相比,你考虑过创建一个新类吗?
要点:
1、使用特定的数据类型就意味着要记住适用于各个类型的很多独立的原则。用本章的核对表来确认你已经对常见问题做了考虑。
2、如果你的语言支持,创建自定义类型会使得你的程序更容易修改,并更具有自描述性。
3、当你用typedef或者其等价方式创建了一个简单类型的时候,考虑是否更应该创建一个新的类。
十三、不常见的数据类型
内容:
结构体:这一术语指的是使用其他类型组建的数据。
指针:指针的使用是现代编程中最容易出错的领域之一,以至于你java\c#和VB这些没有提供指针数据类型。
全局数据:全局数据可以在程序中任意一个位置访问。作用域比局部变量更广的变量。
核对表:使用不常见数据类型的注意事项
结构体
你使用结构体而不是使用单纯的变量来组织和操作相关的数据吗?
你考虑创建一个类来代替使用结构体吗?
全局数据
所有的变量是否都是局部的或者是类范围的?除非绝对有必要才是全局的?
变量的命名规则能把局部数据、类数据和全局数据区分开吗?
你对所有的全局变量都加以文档说明了吗?
避免使用伪全局数据,即被四处传递且包含有杂乱数据的巨大对象吗?
用访问器子程序来取代全局数据吗?
把访问器子程序和数据组织到类里面吗?
访问器子程序提供了一个在底层数据类型实现之上的抽象层了吗?
所有相关的访问器子程序都位于同一抽象层之上吗?
指针
把指针操作隔离来子程序里吗?
指针引用合法吗?或者说指针有可能成为空悬指针吗?
代码在使用指针之前检查它的有效性吗?
在使用指针所指向的变量之前检查其有效性吗?
指针用完后被设置为空值吗?
就可读性而言,代码用了所有需要使用的指针变量吗?
链表中的指针是按正确的顺序加以释放吗?
程序分配了一片保留的内存后备区域,以便在耗尽内存的时候能够优雅地退出吗?
是不是在没有其他方法可用的情况下最终才使用指针的?
要点:
1、结构体可以使得程序更简单、更容易理解,以及更容易维护。
2、每当你打算使用结构体的时候,考虑采用类是不是会工作的更好。
3、指针很容易出错。用访问器子程序或类以及防御式编程实践来保护自己的代码。
4、避免用全局变量,不只是因为它们很危险,还是因为你可以用其他更好的方法来取代它们。
5、如果你不得不使用全局变量,那么就通过访问器子程序来使用它。访问器子程序能为你带来全局变量所能带来的一切优点,还有一些额外好处。
第4部分 语句
十四、组织直线型代码
核对表:组织直线型代码
代码使得语句之间的依赖关系变得明显吗?
子程序的名字使得依赖关系变得明显吗?
子程序的参数使得依赖关系变得明显吗?
如果依赖关系不明确,你是否用注释进行了说明吗?
你用“内务管理变量”(housekeeping variables)来检查代码中关键位置的顺序依赖关系了吗?
代码容易按照自上而下的顺序阅读了吗?
相关的语句被组织在一起吗?
把相对独立的语句组放进名自的子程序里吗?
要点:
1、组织直线型代码的最主要原则是按照依赖关系进行排列。
2、可以用好的子程序名、参数列表、注释、以及------如果代码足够重要------内容管理变量来让依赖关系变得更明显。
3、如果代码之间没见有顺序依赖关系,那就设法使相关的语句尽可能地接近。
十五、使用条件语句
核对表:使用条件语句
if-then语句
代码的正常路径清晰吗?
if-then测试对等量分支的处理方式正确吗?
使用了else子句并加以说明吗?
else子句用得对吗?
用对了if和else子句,即没把它们用反吗?
需要执行的正常情况是位于if而不是else子句里吗?
if-then-else-if语句
把复杂的判断封闭到布尔函数调用里了吗?
先判断最常见的情况了吗?
判断包含所有的情况吗?
if-then-else-if是最佳的实现吗?比case语句还要好吗?
case语句
case子句排序得有意义吗?
每种情况的操作简单吗?必要的时候调用了其他子程序吗?
case语句检测的是一个真实的变量,而不是一个只为了滥用case语句而刻意制造变量吗?
默认子句用的合法吗?
用默认子句来检测和报告意料之外的情况了吗?
在C、C++或者Java里,每一个case的末尾都有一个break吗?
要点:
1、 对于简单的if-else语句,请注意if子句和else子句的顺序,特别是用它来处理大量错误的时候。要确认正常的情况是清晰的。
2、对于if-then-else语句串和case语句,选择一种最有利于阅读的排序。
3、为了捕捉错误,可以使用case语句中的default子句(默认子句),或者使用if-then-else语句串中的最后那个else子句。
4、各种控制结构并不是生来平等的。请为代码的每个部分选用最合适的控制结构。
十六、控制循环
内容:
选择循环的种类:
循环控制:
轻松创建循环——由内而外
循环和数组的关系:
核对表:循环
循环的选择和创建
在合适的情况下用while循环取代for循环了吗?
循环是由内到外创建的吗?
进入循环
是从循环头部进入的循环的吗?
初始化代码是直接位于循环前面吗?
循环是无限循环或者事件循环吗?它的结构是否清晰?
避免使用像for i = 1 to 9999 这样的代码吗?
如果这是一个C++\C\或Java中的for循环,那么把循环头留给循环控制代码了吗?
循环的内部
循环是否用了“{ }”或其等价物来括上循环体,以防止因修改不当而出错吗?
循环体内有内容吗?它是非空的吗?
把内务处理集中地放在循环开始或者循环结束处了吗?
循环像定义良好的子程序那样只执行一件操作吗?
循环短得足以一目了然吗?
循环的嵌套层次不多于3层吗?
把长循环的内容提取成单独的子程序吗?
如果循环很长,那么它非常清晰吗?
循环下标
如果这是一个for循环,那么其中的代码有没有随意修改循环下标值?
是否把重要的循环下标植保存在另外的变量里,而不是在循环体外使用该循环下标?
循环下标是序数类型(整数)或者枚举类型------而不是浮点类型吗?
循环下标的名字有意义吗?
循环避免了下标串话问题吗?
退出循环
循环在所有可能的条件下都能终止吗?
如果你建立了某种安全计数器标准,循环使用安全计数器了吗?
循环的退出条件清晰吗?
如果使用了break或者continue,那么它们用对了吗?
要点:
1、循环很复杂。保持循环简单将有助于别人阅读你的代码。
2、保持循环简单的技巧包括:避免使用怪异的循环、减少嵌套层次、让入口和出口一目了然、把内务操作代码放在一处。
3、循环下标很容易被滥用。因些命名要准确,并且要把它们各自仅用于一个用途。
4、仔细地考虑循环,确认它在每一种情况下都运行正常,并且在所有可能的条件下都能退出。
十七、不常见的控制结构
内容:
子程序中的多处返回:
递归:
goto:
针对不常见控制结构的观点:
核对表:不常见的控制结构
return
每一个子程序都仅在有必要的时候才使用return吗?
使用return有助于增强可读性吗?
递归
递归子程序中包含了停止递归的代码吗?
子程序用安全计数器来确保该子程序能停下来吗?
递归只位于一个子程序里面吗?
子程序的递归深度处于程序栈容量可以满足的限度内吗?
递归是实现子程序的最佳方法吗?它要好于简单的迭代吗?
goto
是否只有在万不得已时候才使用goto?如果用了goto,是否仅仅是出于增强可读性和可维护性呢?
如果是出于效率因素而使用goto,那么对这种效率上的提升做出衡量并且加以说明了吗?
一个子程序里最多只用了一个goto标号吗?
所有的goto都向前跳转,而不是向后跳转吗?
所有的goto标号都用到了吗?
要点:
1、多个return可以增强子程序的可读性和可维护性,同时可以避免产生很深的嵌套逻辑。但是使用它的时候要多加小心。
2、递归能够很优雅地解决一小部分问题,对它的使用也要倍加小心。
3、在少数情况下,goto是编写可读和呆维护代码的最佳方法。但这种情况非常罕见。除非万不得已,不要使用gogo。
十八、表驱动法
内容:
表驱动法使用总则
直接访问表
索引访问表
阶梯访问表
表查询的其他示例
核对表:表驱动法
你考虑过把表驱动法作为复杂逻辑的替换方案吗?
你考虑过把表驱动法作为复杂继承结构的替换方案吗?
你考虑过把表数据存储在外部并在运行期间读入,以便在不修改代码的情况下就可以改变这些数据吗?
如果无法用一种简单的数组索引(像agc示例中那样)去访问表,那么你把计算访问键值的功能提取成单独的子程序,而不是在代码中重复地计算键值吗?
要点:
1、表提供了一种复杂的逻辑和继承结构的替换方案。如果你发现自己对某个应用程序的逻辑或者继承树关系感到困惑,那么问问自己是否可以通过一个查询表加以简化。
2、使用表的一项关键决策是决定如何去访问表。你可以采取直接访问、索引访问或者阶梯访问。
3、使用表的另一项关键决策是决定应该把什么内容放入表中。
十九、一般控制问题
内容:
布尔表达式
复合语句(语句块)
空语句
驯服危险的深层嵌套
编程基础:结构化编程
控制结构与复杂度
核对表:控制结构相关事宜
表达式中用的是true和false,而不是1和0吗?
布尔值和true以及false做比较是隐匿进行的吗?
对数值做比较是显示进行的吗?
有没有通过增加新的布尔变量、使用布尔函数和决策表简化表达式吗?
布尔表达式是用肯定形式表达的吗?
括号配对吗?
在需要用括号来明确的地方都使用括号吗?
把逻辑表达式全括起来了吗?
判断是按照数轴顺序编写的吗?
如果适当的话,java中的判断用的是a.equals(b)方式,而没有用a==b方式吗?
空语句表述的明显吗?
用重新判断部分条件、转换成if-then-else或者case语句、把嵌套代码提取成单独的子程序、换用一种更面向对象的设计或者其他的改进方法来简化嵌套语句了吗?
如果一个子程序的决策点超过10个,那么能提出不重新设计的理由吗?
要点:
1、使布尔表达式简单可读,将非常有助于提高你的代码的质量
2、深层次的嵌套使得子程序变得难以理解。所幸的是,你可以相对容易地避免这么做。
3、结构化编程是一种简单并且仍然适用的思想:你可以通过把顺序、选择和循环三者组合起来而开发出任何程序。
4、将复杂度降低到最低水平是编写高质量代码的关键。
第5部分 代码改善
本章部分内容
第20章 软件质量概述
第21章 协同构建
第22章 开发者测试
第23章 调试
第24章 重构
第25章 代码调整策略
第26章 代码调整技术
二十、 软件质量概述
内容:
软件质量的特性
改善软件质量的技术
不同质量保障技术的相对效能
什么时候进行质量保证工作
软件质量的普遍原理
核对表:质量保证计划
是否确定出对项目至关重要的特定质量特性了?
是否让其他人意识到项目的质量目标了?
是否能够区分质量的外在特性和内在特性?
是否考虑过某些特性与其他特性相互制约或相互促进的具体方式?
在软件开发的每一个阶段,项目是否要求针对不同错误类型使用不同的错误检测技术?
项目计划中是否有计划有步骤地保证了软件在开发各阶段的质量?
是否使用了某种质量评估方法,并由此确定质量是改善了还是下降了?
管理层是否能理解为了质量保证在前期消耗额外成本,目的就是在项目后期减少成本?
要点:
1、开发高质量代码最终并没有要求你付出更多,只是你需要对资源进行重新分配,以低廉的成本来防止缺陷出现,从而避免低价高昂的修正工作。
2、并非所有的质量保证目标都可以全部实现。明确哪些目标是你希望达到的,并就这些目标和团队成员进行沟通。
3、没见有任何一种错误检测方法能够解决全部问题,测试本身并不是排除错误的最有效方法。成功的质量保证计划应该使用多种不同的技术来检查各种不同类型的错误。
4、在构建期间应当使用一些有效的质量保证技术,但在这之前,一些具有同样强大功能的质量保证技术也是必不可少的。错误发现越早,它与其余代码的纠缠就越少,由此造成的损失也越小。
5、软件领域的质量保证是面向过程的。软件开发与制造业不一样,在这里并不存在会影响最终产品的重复的阶段,因此,最终产品的质量受到开发软件所用的过程的控制。
二十一、协同构建
内容:
协同开发实践概要
结对编程
正式检查
其他类型的协同开发实践
结对编程:在进行结对编程的时候,一位程序员敲代码,另外一位注意有没有出现错误,并考虑某些策略性的问题,例如代码的编写是否正确,正在编写的代码是否所需等。结对编程最初是由极限编程所普及推广的,现在已经使用得相当广泛了。
核对表:有效的结对编程
是否已经有一个编码规范,以便让程序员始终把精力集中到编程,而不是编码风格的讨论上。
结对的双方是否都积极地参与。
是否避免了滥用结对编程,而是选择那些能够从中获得好处的工作进行结对编程。
是否有规律地对人员和工作任务进行轮换。
结对组合是否在开发速度和个性方面互相匹配。
是否有一个组长专注于项目管理以及与项目外其他人的沟通。
正式检查:详查(正式检查)是一种特殊的复查,种种迹象表明它在侦测缺陷方面特别有效,并且相对测试来说更加经济合理。
核对表:有效的详查
你是否有一个核对表,能让评论员将注意力集中于曾经发生过问题的领域?
你是否专注于找出错误,而不是修正它们?
你是否考虑制定某些视角或者场景,以帮助评论员在准备工作的时候集中注意力?
你是否给予评论员足够的时间在详查会议之前进行准备,是否每一个人都做了准备?
是否每一个参与者都扮演一个明确的角色-----主持人、评论员及记录员等?
会议是否以某种高效的速度进行?
会议是否限制在两个小时以内?
是否所有详查会议的参与者都接受了如何进行详查的针对性培训,是否主持人接受了有关主持技巧方面的针对性培训?
是否将每次详查所发现的错误数据都收集起来,使你能调整本组织以后使用的核对表?
是否收集了准备速度和详查速度方面的数据,以便你去优化以后的准备和详查工作?
是否每次详查中被指派下去的活动都被正确跟进了,无论是通过主持人自己还是一次重新详查?
管理层是否理解他们不应该参与详查会议?
是否有一个用于保证修正正确性的跟进计划?
其他类型的协同开发实践:走查、代码阅读、公开演示
二十二、开发者测试
内容:
开发者测试在软件质量中的角色
开发者测试的推荐方法
测试技巧锦囊
典型错误
测试支付工具
保留测试记录
核对表:测试用例
类和子程序所对应的每一项需求是否都有相应的测试用例?
类和子程序所对应的每一个设计元素是否都有相应的测试用例?
每行代码是否被至少一个测试用例所测试?你是否通过计算测试到每行代码所需的最少测试用例数量来验证这一点?
所有已定义--已使用路径是否至少被一个测试用例测试过了?
是否测试过那些不太可能正确的数据流模式,例如已定义--已定义,已定义--已退出以及已定义--已销毁?
是否有一张常见错误列表,并据此编写测试用例以检测过去经常出现的错误?
所有的简单边界是否都已经测试过了:最大、最小以及off-by-one?
是否测试了组合边界------即,多个输入数据的组合导致输出数据过小或者过大?
测试用例是否检查了数据类型错误,例如一个薪水记账程序里的雇员数量是负数?
是否测试了那些中规中矩的典型数值?
是否测试了最小正常形式?
是否测试了最大正常形式?
是否检查了与旧数据的兼容性?以及是否对旧硬件、旧操作系统版本以及其他旧版本软件的接口进行了测试?
测试用例是否容易手工校验?
要点:
1、开发人员测试是完整测试策略的一个关键部分。独立测试也很重要,但这一主题超出了本书的范围。
2、同编码之后编写测试用例相比较,编码开始之前编写测试用例,工作量和花费的时间差不多,但是后者可以缩短缺陷--侦测--调试--修改这一周期。
3、即使考虑到了各种可用的测试手段,测试仍然只是良好软件质量计划的一部分。高质量的开发方法至少和测试一样重要,这包括尽可能减少需求和设计阶段的缺陷。在检测错误方面,协同开发的成效至少与测试相当。这此方法所检测错误的类型也各不相同。
4、你可以根据各种不同的思路来产生很多测试用例,这些思路包括基础测试、数据流分析、边界分析、错误数据类型以及正确数据类型等。你还可以通过猜测错误的方式得到更多的测试用例。
5、错误往往集中在少数几个容易出错的类和子程序上。找出这部分代码,重新设计和编写它们。
6、测试数据本身出错的密度往往比被测试代码还要高。查找这种错误完全是浪费时间,又不能对代码有所改善,因此测试数据里面的错误更加让人烦恼。要像写代码一样小心地开发测试用例,这样才能避免产生这种问题。
7、自动化测试总体来说是很有用的,也是进行回归测试的基础。
8、从长远来看,改善测试过程的最好办法就是将其规范化,并对其进行评估,然后用评估中获得的验证教训来改善这个过程。
二十三、调试
内容:
调试概述
寻找缺陷
修正缺陷
调试中的心理因素
调试工具---明显的和不那么明显的
核对表:关于调试的建议
寻找缺陷的方法
使用所有可用数据来构造你的假设
不断提炼产生错误的测试用例
在自己的单元测试族中测试代码
借助可以获得的任何工具
用不同的方式重现错误
通过产生更多的数据来构造更多的假设
利用证伪假设的测试结果
用头脑风暴的方式找出可能的假设
在桌上放一个记事本,把需要尝试的事情列出来
缩小被怀疑有问题的代码区域
对之前出现过问题的子类和子程序保持警惕
检查最近修改的代码
扩展被怀疑有问题的代码区域
采用增量集成
检查常见的缺陷
和其他人一起讨论你的问题
抛开问题休息一下
在使用快速肮脏调试法的时候,要设置一个时间上限
列出所有的蛮力调试方法,逐条应用
解决语法错误的方法
不要太信任编译器信息中给出的行号
不要太信任编译器信息
不要太信用编译器所给出的第二条出错信息
分而治之,各个击破
使用具有语法分析功能的编辑器来找出位置错误的注释和引号
修改缺陷的方法
在动手之前先理解程序
理解整个程序而非具体问题
验证对错误的分析
放松一下
要保存最初的源代码
治本,而非治标
只有当理由充分的时候才去修改代码
一次只做一个改动
检查自己所做的修订
添加单元测试来暴露代码中的缺陷
找出类似的缺陷
调试的一般方法
你是否会把调试看做是能让你更好地理解程序、错误、代码质量各解决问题方法的良机?
你是否会避免采用随机尝试查找错误或迷信式的调试方法(不听话的机器,奇怪的编译器错误,月圆时才会出现的编程语言的隐藏缺陷,失效的数据,忘记做的重要改动,一个不能正常保存程序的疯狂的编辑器,这种行为描述为迷信式的编程)?
你是否假设错误是你自己造成的?
你是否使用了科学的方法将间歇性的错误稳定下来?
你是否使用了科学的方法来寻找缺陷?
你在寻找缺陷的时候会使用多种不同的方法么?还是每次都是用相同的方法?
你会验证你的修改是否正确么?
你会在调试中使用编译器警告信息、执行性能分析、利用测试框架和交互式调试方法么?
要点:
1、调试同整个软件开发的成败息息相关。最好的解决之道是使用本书中介绍的其他方法来避免缺陷的产生。然而,花点时间来提高自己的调试技能还是很划算的,因为优秀和拙劣的调试表现之间的差距至少是10:1。
2、要想成功,系统化的查找和改正错误的方法至关重要。要专注于你的调试工作,让每一次测试都能让你朝着正确的方向前进一步。要使用科学的调试方法。
3、在动手解决之前,要理解问题的根本。胡乱猜测错误的来源和随机修改将会让你的程序陷入比刚开始调试时更为糟糕的境地。
4、将编译器警告级别设置为最严格,把警告信息所报告的错误都改正。如果你忽略了明显的错误,那么要改正那些微妙的错误就会非常麻烦。
5、调试工具对软件开发而言是强有力的支持手段。找出这些工具并加以应用,当然,请记得在调试的时候开动脑筋。
二十四、重构
内容:
软件演化的类型
重构简介
特定的重构
安全的重构
重构策略
重构简介核对表:重构的理由
代码重复
子程序太长
循环太长或者嵌套太深
类的内聚性太差
类的接口的抽象层次一致
参数表中参数太多
类的内部修改往往局限于某个部分
需要对多个类进行并行修改
对继承体系的并行修改
需要对多个case语句进行并行修改
相关的数据项只是被放在一起,没有组织到类中
成员函数更多使用了其他类的功能,而非自身类的
过于依赖基本数据类型
一个类不做什么事
一连串传递流浪数据的子程序
中间人对象什么也不干
某个类同其他类关系过于密切
子程序的命名太差
数据成员被设置为公用
派生类仅仅使用了基类的一小部分成员函数
用注释来掩饰拙劣的代码
使用了全局变量
在子程序调用前使用设置代码,调用后使用收尾代码
程序包含的某些代码似乎在将来某个时候才会被用到
特定的重构
核对表:重构总结
1、数据级的重构
用具名常量来代替神秘数值
用更明确或更具有信息量的名字来重命名变量
将表达式内联化
用函数来代替表达式
引入中间变量
将多用途变量转换为多个单一用途变量
使用局部变量实现局部用途而不是使用参数
将基础数据类型转化为类
将一组类型码转化为类或是枚举类型
将一组类型码转化为含派生类的类
将数组转化为对象
封闭群集
用数据类替代传统记录
2、语句级的重构
分解布尔表达式
将复杂的布尔表达式转换为命名精确的布尔函数
将条件语句中不同部分中的重复代码合并
使用break或return而不是循环控制变量
在嵌套的if-then-else语句中一旦知道结果就立刻退出,而不是仅仅赋一个返回值
用多态来代替条件语句(尤其是重复的case语句)
创建并使用空对象代替对空值的检测
3、子程序的重构
提取子程序
将子程序代码内联化
将冗长的子程序转化为类
用简单的算法替代复杂的算法
增加参数
减少参数
将查询操作同修改操作区分开
合并功能相似的子程序,并用参数来区分他们
通过传递不同的参数使用子程序体现不同的功能
传递整个对象而非特定成员
传递特定成员而非整个对象
封装向下转型操作
4、类实现的重构
将值对象改变引用对象
将引用对象改为值对象
用数据初始化来代替虚函数
改变成员函数或数据的位置
将特定代码提出生成派生类
将相似的代码合并起来放到基类中
5、类接口的重构
将某成员子程序放到另一个类中
将一个类转化为两个
删除某个关系
隐藏委托关系
去掉中间人
用委托代替继承
用继承代替委托
引入外部子程序
引入扩展类
封装暴露在外的成员变量
对不能修改的成员去掉set()函数
隐藏在类的外部不会使用的成员函数
封装不会用到的成员函数
如果基类和派生类的代码实现相似,将二者合并
6、系统级的重构
为无法控制的数据创建明确的索引源
将单向类联系改为双向类联系
将双向的类联系改为单向类联系
使用工厂函数而非简单的构造函数
用异常代替错误代码,或者反其道而行之
重构策略
核对表:安全的重构
每一改变都是系统改变策略的一部分吗?
在重构之前,你保存了初始代码了么?
你是否保持较小的重构步伐?
你是否同一时间只处理一项重构?
在重构时你是否把要做的事情一条条列了出来?
你是否设置了一个停车场,把你在重构时所想到的任何东西记下来?
在每次重构后你会重新测试么?
如果所做的修改非常复杂,或者影响到了关键代码,你会重新检查这些修改吗?
你是否考虑过特定重构的风险,并以此来调整你的重构方法?
你所做的修改是提升还是降低了程序的内在质量?
你是否避免了将重构作为先写后改的代名词,或者作为拒绝重写拙劣代码的托词?
要点:
1、修改是程序一生都要面对的事情,不仅包括最初的开发阶段,还包括首次发布之后。
2、在修改中软件的质量要么改进,要么恶化。软件演化的首要法则就是代码演化应当提升程序的内在质量。
3、重构成功之关键在于程序员应学会关注那些标志着代码需要重构的众多的警告或“代码臭味”。
4、重构成功的另一要素是程序员应当掌握大量特定的重构方法
5、重构成功的最后要点在于要有安全重构的策略。一些重构方法会比其他重构方法更好。
6、开发阶段的重构是提升程序质量的最佳时机,因为你可以立刻让刚刚产生的改变梦想变为现实。请珍惜这些开发阶段的天赐良机。
二十五、代码调整策略
内容:
性能概述
代码调整简介
蜜糖和哥斯拉(像蜜糖一样黏,你哥拉斯一样大的问题)
性能测量
反复调整
代码调整方法总结
代码调整方法总结
如果还对代码调整能否有助于提高某个程序的性能心存疑虑,按照以下的步骤去做吧。
1、用设计良好的代码来开发软件,从而使程序易于理解和修改。
2、如果程序性能很差
a、保存代码的可运行版本,这样你才能回到“最近的已知正常状态”;
b、对系统进行分析测量,找出热点;
c、判断性能拙劣是否源于设计、数据类型或算法上的缺陷,确定是否应该做代码调整,如果不是,请跳回第一步;
d、对步骤c中所确定的瓶颈代码进行调整;
e、每次调整后都对性能提升进行测量;
f、如果调整没有改进代码的性能,就恢复到步骤a保存的代码(通常而言,超过一半的调整尝试都只能稍微改善性能甚至造成性能恶化)。
3、重复步骤2。
核对表:代码调整策略
程序整体性能
你是否考虑通过修改需求来提高性能?
你是否考虑通过修改程序的设计来提高性能?
你是否考虑通过修改类的设计来提高性能?
你是否考虑过减少程序同操作系统的交互从而提高性能?
是否考虑过避免I/O操作以提高性能?
是否考虑使用编译型语言替代解释型语言以提高性能?
是否考虑过使用不同的硬件来提高性能?
是否仅仅将代码调整看做是解决问题的最后一招?
代码调整方法
在开始调整代码之前,程序是完全正确的么?
在调整之前是否测量过性能瓶颈在什么地方?
是否记录了每一次修改所产生的效果?
如果没有带来预期的性能提高,你是否放弃了所做的代码调整改变?
你是否对每一个性能瓶颈进行不止一次的修改尝试-----也就是说,你是在反复进行代码调整么?
要点:
1、性能只是软件整体质量的一个方面,通常不是最重要的。精细的代码调整也只是实现整体性能的一种方法,通常也不是决定性的。相对于代码本身的效率而言,程序的架构、细节设计以及数据结构和算法选择对程序的运行速度和资源占用的影响通常会更大。
2、定量测量是实现性能最优化的关键。定量测量需要找出能真正决定程序性能的部分,在修改之后,应当通过重复测量来明确修改是提高还是降低了软件的性能。
3、绝大数的程序都有那么一小部分代码耗费了绝大部分的运行时间。如果没有测量,你不会知道是哪一部分代码。
4、代码调整需要反复尝试,这样才能获得理想的性能提高。
5、为性能优化工作做好准备的最佳方式就是在最初阶段编写清晰的代码,从而使代码在后续工作中易于理解和修改。
二十六、代码调整技术
内容:
逻辑
循环
数据变换
表达式
子程序
用低级语言重写代码
变得越多,事情反而越没变
1、逻辑
在知道答案后停止判断
按照出现频率来调整判断顺序
用查询表替代复杂表达式
使用惰性求值
2、循环
将判断外提
合并:就是把两个对相同一组元素进行操作的循环合并在一起。好处就是把两个循环的总开销减少至单个循环的开销。
展开:循环展开的目的是减少维护循环所需要做的工作。
尽可能减少在循环内部做的工作
哨兵值:当循环的判断条件是一个复合判断的时候,你可以通过简化判断来节省代码运行时间。如果该循环是一个查找循环,简化方法之一就是使用一个哨兵值,你可以把它放到循环范围的未尾,从而保证循环一定能够中止。
把最忙的循环放在最内层
削减强度:削减强度意味着用多次轻量级运算(例如加法)来代替一次代价高昂的运算(例如乘法)。
3、数据变换
数据类型的改变同样可以成为减少程序规模和改进执行速度方面的利器。
使用整数而不是浮点数。
数组维度尽可能少。
尽可能减少数组引用。除了避免对二维或三维数组的访问,减少对数组的访问总是有好处的。
使用辅助索引:使用辅助索引的意思就是添加相关数据,使得对某种数据类型的访问更为高效。
使用缓存机制:缓存机制就是把某些值存起来,使得最常用的值会比不太常用的值更容易被获取。
4、表达式
程序里的很多工作都是在数据或逻辑表达式中实现的。复杂的表达式往往代价昂贵。
利用代数恒等式:例如 not a and not b = not(a or b),如果选择第二个表达式,就避免一次not操作
削弱运算强度:用加法代替乘法、用乘法代替幂乘、.............、用移位操作代替整数乘2或除2。
编译期初始化
小心系统函数:系统函数运行起来很慢,提供的精度常常也是根本不需要的。
使用正确的常量类型
预先算出结果
删除公共子表达式:如果发现某个表达式老是在你面前出现,就把它赋给一个变量,然后在需要的地方引用该变量,而非重新计算这个表达式。
5、子程序
代码调整的利器之一就是良好的子程序分解。
将子程序重写为内联
6、用低级语言重写代码
有一句亘古不变的箴言也不能不提:当程序遭遇性能瓶颈的时候,你应该用低级语言重写代码。
核对表:代码调整方法
同时改善代码执行速度和规模
用查询表替换复杂逻辑
合并循环
使用整型变量而非浮点变量
在编译时初始化数据
使用正确的常量类型
预先计算结果
删除公共子表达式
将关键子程序代码转化为某种低级语言代码
仅仅提高代码执行速度
在知道答案后就停止执行判断
根据各种情况的出现频率对 case 语句和 if-then-else 串排序
比较相似逻辑结构的性能
使用惰性求值
将循环中的 if 判断转到外部
展开循环
将循环内部所做的工作减少到最低限度
在查找循环中使用哨兵
把执行最为频繁的循环放在嵌套循环的最里面
减轻内层循环的强度
将多维数组改为一维数组
最大限度减少数组索引
为数据类型扩充索引
对频繁使用的值进行缓存
利用代数恒等式
降低逻辑和数学表达式的强度
注意系统调用
用内联子程序重写代码
要点:
1、优化结果在不同的语言、编译器和环境下有很大差异。如果没有对每一次的优化进行测量,你将无法判断优化到底是帮助还是损害了这个程序。
2、第一次优化通常不会是最好的。即使找到了效果很不错的,也不要停下扩大战果的步伐。
3、代码调整这一话题有点类似于核能,富有争议,甚至会让人冲动。一些人认为代码调整损害了代码可读性和可维护性,他们绝对会交期弃之不用。其他人则认为只要有适应的安全保障,代码调整对程序是有益的。如果你决定用本章所述的调整方法,请务必谨慎行事。
第6部分 系统考虑
本章部分内容
第27章 程序规模对构建的影响
第28章 管理构建
第29章 集成
第30章 编程工具
二十七、程度规模对构建的影响
内容:
交流和规模
项目规模的范围
项目规模对错误的影响
项目规模对生产率的影响
项目规模对开发活动的影响
1、交流和规模
如果项目中只有你一个人,那么唯一的交流路径就是你和你顾客的交流,随着项目成员数目的增加,交流路径的数量也随之增加,但是二者的关系并不是加性的,而是乘性的,即交流路径的条数大致正比与人数的平方。
2、项目规模的范围
你所从事的项目的规模具有典型吗?项目规模的范围很宽,这也就意味着不能把任何一种规模视为典型。
3、项目规模对错误的影响
项目的规模会影响错误的数量,也会影响错误的类型。你也许不曾想到错误类型也会受到影响,然而随着项目规模的增大,通常更大一部分错误要归咎于需求和设计。
5、项目规模对开发活动的影响
如果你从事的是一个单人项目,那么对项目成败影响最大的因素就是你自己。如果你在一个具有25个人的项目中工作,那么你仍然可能是最大的影响因素,但更多可能的是,没人能够独享成功的奖牌,组织结构对项目成败的影响力更大。
活动比例和项目规模:项目越大,所需进行的各种活动的种类也会急剧变化。
程序、产品、系统和系统产品:代码行数和团队规模并不是影响项目大小的仅有因素。另一个更敏感的影响因素是最终软件的质量和复杂度。
方法论和规模:各种方法论都被用于不同大小的项目。对于小项目,方法论的应用显得很不经意并且趋于本能。对于大项目,它们的应用变得十分严格,并且计划得非常仔细。
要点:
1、随着项目规模的扩大,交流需要加以支持。大多数方法论的关键点都在于减少交流中的问题,而一项方法论的存亡关键也应取决于它能否促进交流。
2、在其他条件都相等的时候,大项目的生产率会低于小项目。
3、在其他每件都相等的时候,大项目的每千行代码错误率会高于小项目
4、在小项目里的一些看起来“理当如此”的活动在大项目中必须仔细地计划。随着项目规模扩大,构建活动的主导地位逐渐降低。
5、放大轻量级的方法论要好于缩小重量级的方法论,最有效的办法是使用“适量级”方法论。
二十八、管理构建
内容:
鼓励良好的编码实践
配置管理
评估构建进度表
度量
把程序员当人看
管理你的管理者
1、鼓励良好的编码实践
设定标准的考虑事项
鼓励良好的编码实践的技术
2、配置管理
什么是配置管理:配置管理是“系统化地定义项目工件和处理变化,以使项目一直保持其完整性”的实践活动。它的另一种说法是“变量控件”。其中的技术包括评估所提交的更改、追踪更改、保留系统在不同时间点的各历史版本。
需求变易和设计变更
软件代码变更
工具版本
机器配置
备份计划
核对表:配置管理
概要
你的软件配置管理计划是否用于帮助程序员,并能将额外负担降至最低。
你的软件配置管理方法是否避免了对项目的过度控制?
你是否将一些变更请求聚成一组?无论采用非正式的方法(如创建一份未决更改的列表)还是更加系统的方法(如设立变更控制委员会)。
你系统地评估了每一项提交的更改对成本、计划和质量的影响吗?
你是否把重大的变更看做是需求分析还不够完备的警报信号吗?
工具
你用版本控件软件来促进配置管理吗?
你用版本控件软件来减少团队工作的协调问题吗?
备份
你定期地备份项目中的所有资料吗?
你定期地把备份项目备份数据转移到off-site storage 里了吗?
所有的资料,包括源代码、文档、图表和重要的笔记都得到备份了吗?
你测试过备份与恢复的过程吗?
3、评估构建进度表
评估的方法
评估构建的工作量
对进度的影响
评估与控制
4、度量
度量软件项目有很多种方法。以下是对项目进行度量的两项根本原因
任何一种项目特征(attribute)都是可以用某种方法来度量的,而且总会比根本不度量好得多。
留心度量的副作用
反对度量就是认为最好不要去了解项目中到底在发生什么
5、把程序员当人看
程序员们怎样花费时间
性能差异与质量差异(个体差异、团队差异)
信仰问题(编程语言、缩进风格、大括号的摆放位置、所用的集成开发环境、注释风格、效率与可读性的取舍、对方法的选择--例如,Scrum-极限编程-渐进云交付、编程工具、命名习惯、对goto的使用、对全局变量的使用、量度---特别是有关生产力的量度,如每天编写的代码行数)
物理环境
6、管理你的管理者
在软件开发中,非技术出身的管理者随处可见,具有技术经验但却落后于这个时代10年(以上)的管理者也比比皆是。技术出色并且其技术与时俱进的管理者实属凤毛麟角。如果你正在为一位这样的管理者工作,那么就尽可能地保住你的工作吧。这可是非常难得的待遇。
要点:
1、好的编码实践可以通过“贯彻标注”或者“使用更为灵活的方法”来达到。
2、配置管理,如果应用得当,会使程序员的工作变得更加轻松。特别包括变量控制。
3、好的软件评估是一项重大挑战。成功的关键包括采用多种方法、随着项目的开展而修缮评估结果,以及很好地利用数据来创建评估等。
4、度量是构建管理成功的关键。你可以采取措施度量项目的任何方面,而这要比根本不度量好得多。准确的度量是制定准确的进度表、质量控制和改进开发过程的关键。
5、程序员和管理人员都是人,在把他们当人看的时候工作得最好。
二十九、集成
内容:
集成方式的重要性
集成频率---阶段式集成还是增量集成
增量集成的策略
Daily Build(每日构建)与冒烟测试
1、集成方式的重要性
在软件以外的工程领域,正确集成的重要性已广为人知。一起事故戏剧性地展示了糟糕的集成的危险,华盛顿大学的露天足球场在建设时中途坍塌。因为它在建造时不能支撑自己的重量。很可能在完工后它会足够牢固,但是它在建造顺序是错误的------这是一个“集成”错误。它必须在每一步都要足够牢固才行。如果你按错误的顺序构建并集成软件,那么会难于编码、难于测试、难于调试。
2、 集成频率---阶段式集成还是增量集成
阶段式集成
增量集成
增量集成的益处:易于定位错误、及早在项目里取得系统级的成果、改善对进度的监控、改善客户关系、更加充分地测试系统中的各个单元、能在更短的开发进度计划内建造出整个系统。
3、增量集成的策略:自顶向下集成、自底向上集成、三明治集成、风险导向的集成、功能导向的集成、T-型集成。
集成方法的小结:请不要像教条一样遵循前面提到的任务过程,而应该为特定项目剪裁一套独一无二的策略。
4、Daily Build(每日构建)与冒烟测试
无论你选用哪种集成策略,dayil build和冒烟测试都是软件集成的好方法。每天都将各个(源)文件编译、链接并组合为一个可执行程序,然后对这个程序进行冒烟测试,即执行一种相对简单的检查,看看产品在运行时是否“冒烟”。
每日构建:每日构建最关键的部分是"daily/每天"。
检查失败的build
每天进行冒烟测试
让冒烟测试与时俱进
将daily build 和冒烟测试自动化
成立build小组
仅有当意义时,才将修订(revisions)加入build中
......但是加等太久才将修订加入进来
要求开发人员在把他的代码添加到系统之前,进行冒烟测试
为即将添加到build的代码准备一块暂存区
惩罚破坏build的人
在早上发布build
即使有压力,也要进行daily build和冒烟测试
哪些项目能用daily build 过程:持续集成
核对表:集成
集成策略
该策略是否指明了集成子系统、类、子程序时应该采用的最优顺序?
集成的顺序是否与构建顺序协调,以便在适当的时候准备好供集成的类?
该策略是否易于诊断缺陷?
该策略是否使脚手架最少?
所选的策略是否好于其他方式?
组件之间的接口是否有明确定义?(定义接口不是集成的任务,但要验证这些接口的定义是否正确。)
Daily build与冒烟测试
项目是否经常build------理想情况下,每天build一次------以支持增量集成?
每次build后是否都运行冒烟测试,让你知道这个build能否工作?
你是否已使build和冒烟测试自动进行?
开发人员是否频繁地check in他们的代码------两次check in之间最多间隔一两天?
冒烟测试是否与代码同步更新,随代码发展而发展?
破坏build是罕见事件吗?
是否在有压力的情况下,也对软件进行build和冒烟测试?
要点:
1、构建的先后次序和集成的步骤会影响设计、编码、测试各类的顺序。
2、一个经过充分思考的集成顺序能减少测试的工作量,并使调试更容易。
3、增量集成有若干变型,而且------除非项目是微不足道的------任何一种形式的增量集成都比阶段式集成好。
4、针对每个特定的项目,最佳的集成步骤通常是自顶向下、自底向上、风险导向及其他集成方法的某种组合。T-集成和竖直分块集成通常都能工作得很好。
5、daily build能减少集成的问题,提升开发人员的士气,并提供非常有用的项目管理信息。
三十、编程工具
内容:
设计工具
源代码工具
可执行码工具
工具导向的环境
打造你自己的编程工具
工具幻境
现代化的编程工具减少了构建所需的时间。使用最前沿的工具集---并熟悉你所用的工具---能使用生产力增加50%还不止。编程工具也能降低编程中必须的单调乏味的琐碎事务的劳动量。
工具幻境:当你听到某个工具厂商宣称“这一新工具将会消除计算机程序设计时”,躲开它!或者对这种厂商的幼稚的乐观主义一笑置之。
核对表:编程工具
你有一套有效的IDE吗?
你的IDE集成了:源代码控制、build/测试/除错工具,以及其他有用的功能吗?
你有能自动进行常用的重构操作的工具吗?
你是否使用版本控制工具,对源代码、内容、需求、设计、项目计划及其他的项目构件进行管理?
如果你正面对超大型的项目,你是否使用了数据字典或者其他“包含系统中使用的各个类的权威描述”的中央知识库。
当可以用到代码库是时,你是否考虑用它来代替“编写定制代码”?
你是否充分利用了交互式除错器?
你是否使用make或者他“依赖关系控件软件”,用来高效并可靠地build程序?
你的测试环境包含有自动化的测试框架、自动测试生成器、覆盖率监视器、系统扰动器、diff工具,以及缺陷跟踪软件吗?
你有没有制造过定制工具-----能满足特定项目的需求的那种,特别是能自动执行重复任务的工具?
总而言之,你的工作环境有没有从“充足的工具支援”中获益?
要点:
1、程序员有时会在长达数年的时间里忽视某些最强大的工具,之后才发现并使用之。
2、好的工具能让你的日子过得安逸很多。
3、下面这些工具已经可用了;编辑、分析代码质量、重构、版本控制、除错、测试、代码调整。
4、你能打造许多自己用的专用工具。
5、好的工具能减少软件开发中最单调乏味的工作的量,但它不能消除对“编程”的需要,虽然它会持续地重塑(reshape)“编程的含义”。
第7部分 软件工艺
本部分内容
第31章 布局与风格
第32章 自说明代码
第33章 个人性格
第34章 软件工艺的话题
第35章 何处有更多信息
三十一、布局与风格
内容:
基本原则
布局技术
控制结构的布局
单条语句的布局
注释的布局
子程序的布局
类的布局
本章转向计算机编程的美学问题---程序源代码的布局。编排出色的代码会带来视觉上和思维上的愉悦,这是非程序员的人不能感受到时的。而精雕细琢代码、使之达到美观的程序员们,却会从这一过程得到艺术上的满足。
核对表:布局
一般问题
格式化主要是为了展现代码的逻辑结构吗?
你的布局方案能统一地运用吗?
你的布局方案能让代码易于维护吗?
你的布局方案是否有利于代码的可读性?
控件结构的布局
你的代码中避免begin-end对或 { } 的又重缩进了吗?
相邻的块之间用空行分隔了吗?
对复杂表达式格式化时考虑到可读性吗?
对只有一条语句的块的布局始终如一吗?
case语句与其他控制结构的格式化保持一到了吗?
对goto语句的格式化是否让其显眼了呢?
单条语句的布局
为逻辑表达式、数组下标和子程序参数的可读性而使用空格了吗?
不完整的语句在行末是以明显有错的方式结束了吗?
后续行按照标准数目缩进了吗?
每行顶多只有一条语句吗?
所写的每个语句都没有副作用吗?
每行顶多只声明一个数据吗?
注释的布局
注释与其所注释的代码的缩进量相同吗?
注释的风格使用维护吗?
子程序的布局
你对每个子程序参数的格式化方式便于看懂、修改、注释吗?
采用空行分隔子程序的各部分了吗?
类、文件和程序的布局
多数类和文件之间是一一对就的关系吗?
如果文件内有多个类,各类中的子程序按类分组了吗?各类都清楚标识了吗?
文件中的子程序用空行清楚地分开了吗?
在没有更好的组织形式的场合,所有子程序都按字母顺序排列了吗?
要点:
1、可视化布局的首要任务是指明代码的逻辑组织。评估该任务是否实现的指标包括准确性、一致性、易读性和易维护性。
2、外表悦目比起其他指标是最不重要的。然而,如果其他指标都达到了,代码又质量好,那么布局效果看上去也会不错。
3、Visual Basic具有纯代码风格,而java的传统做法就是使用纯块风格,所以若用这些语言编程,就请使用纯代码风格。C++中,模拟纯代码块或者 begin-end 块边界都行之有效。
4、结构化代码有其自身目的。始终如一地没用某个习惯而少来创新。不能持久的布局规范只会损害可读性。
5、布局的很多方面涉及信仰问题,应试着将客观需要和主观偏好区分开来。定出明确的目标 ,在些基础了再讨论风格参数的选择。
三十二、自说明代码
内容:
外部文档
编程风格作文档
注释或不注释
高效注释之关键
注释技术
IEEE标准(电子与电器工程师协会)
核对表:自说明代码
类
你的类接口体现出某种致的抽象吗?
你的类名有意义吗,能表明其中心意图吗?
你的类接口对于如何使用该类显而易见吗?
你的类接口能抽象到不需要考虑其实现过程吗?能把类看成是黑盒吗?
子程序
你的每个子程序名都能准确地指示该子程序确干些什么吗?
你的各子程序的任务明确吗?
若各子程序中自成一体后更有用,你都将其各自独立出来吗?
每个子程序的接口都清晰明了吗?
数据名
类型名描述有助于说明数据声明吗?
你的变量名有意义吗?
变量只用在其名字所代表意义的场合吗?
你的循环变量名能给出更多信息,而不是 i、j、k 之类的吗?
你用了名字有意义的枚举类型,而非临时拼凑的标识或者布尔变量吗?
用具名常量代替神秘数值或者字符串了吗?
你的命名规范能区分类型名、枚举类型、具名常量、局部变量、类变量以及全局变量吗?
数据组织
你根据编程清晰的需要,使用了额外变量来提高清晰度吗?
你对某变量的引用集中吗?
数据类型简化到了最低复杂度吗?
你是通过抽象访问子程序(抽象数据类型)来访问复杂数据吗?
控制
代码中的正常执行路径很清晰吗?
相关的语句放在一起了吗?
相对独立的语句组打包成子程序了吗?
正常情况的处理位于 if 语句之后,而非在 else 子句中吗?
控制结构简单明了,以使复杂度最低吗?
每个循环完成且仅完成一个功能,是像定义良好的子程序那么做吗?
嵌套层次是最少吗?
逻辑表达式通过额外添加布尔变量、布尔函数和功能表简化了吗?
布局
程序的布局能表现出其逻辑结构吗?
设计
代码直接了当吗?是不是避免自作聪明或新花样?
实现细节尽可能隐藏了吗?
程序是尽可能采用问题领域的术语,而非按照计算机科学或者编程语言的术语编写的吗?
5、注释技术
注释单行
注释代码段
注释数据声明
注释子程序
注释类、文件和程序:
类、文件和程序的共同特征是它们都包含多个子程序。文件或类应包含彼此相关的一组子程序;程序包含了所有的子程序。各情况下注释的任务就是要对文件、类或程序内容提供有意义的概括说明。
程序注释以书本为范例:
“序”:为一组常于文件头的注释,起到介绍程序的作用。它和书的序功能相同,提供有关程序的情况。
“目录”:给出顶层文件、类和子程序(即“章”),它们可以是清单形式,就如同书本列出章节那样,也可以结构图的形式表示。
“节”:则是子程序内的各单位,例如子程序声明、数据声明和可执行语句。
“交叉引用”:是代码的“参阅......”映射,其中包含行号。
6、IEEE(电子与电器工程师协会)标准
软件开发标准
软件质量保标准
管理标准
标准综述
核对表:好的注释技术
一般问题
别人拿起你的代码就能立刻明白其意吗?
你的注释是在解释代码用意,或概括代码在做什么,而非简单重复代码吗?
采用了伪代码编程法减少注释时间吗?
是重写有玄机的代码,而非为其做注释吗?
你的注释是否能同代码一起更新?
注释清楚正确吗?
你的注释风格便于修改注释吗?
语句和段落
代码避免用行尾注释了吗?
注释是着力说明为什么而非怎么样吗?
注释为将要阅读代码的人们做好准备了吗?
每个注释都其用处吗?删掉抑或改进了多余的、无关紧要的或随意的注释没有?
是否注释了代码的非常规之处?
避免使用缩略语了吗?
主次注释区别明显吗?
含错代码和未公开的代码特性有注释吗?
数据声明
对数据声明的注释说明了数值单位吗?
数值数据的取值范围注释出来了吗?
注释出的编码含义吗?
对输入数据的限制有注释吗?
对位标志做注释了吗?
在各全局变量声明的地方对其做注释了吗?
各全局变量是通过命名规范、注释(或两者兼用)来标识其意义吗?
神秘数值是否以具名常量代替,而非只是标注之?
控制结构
控制语句都注释了吗?
冗长或者复杂的控制结构结尾处有注释吗?抑或可能的话,简化之从而省去注释了吗?
子程序
各子程序的意图都注释出了吗?
子程序的其他有关情况(诸如输入输出数据、接口假设、局限性、纠错、全局效果和算法来源)都注释出来了吗?
文件、类和子程序
程序有简短的文档(就像在“以书本为范例”中说明的那样)给出程序组织的概述吗?
每个文件的用途都有说明吗?
作者姓名、email及电话号码在代码清单中都有吗?
要点:
1、该不该注释是个需要认真对待的问题。差劲的注释只会浪费时间,帮倒忙;好的注释才有价值。
2、源代码应当含有程序大部分的关键信息。只要程序依然在用,源代码比其他资料都能保持更新,故而将重要信息融入代码是很有用处的。
3、好代码本身就是最好的说明。如果代码太糟,需要大量注释,应先试着改进代码,真至无须过多注释为止。
4、有的注释风格需要许多重复性劳动,应舍弃之,改用易于维护的注释风格。
三十三、个人性格
内容:
本人性格是否和本书话题无关
聪明和谦虚
求知欲
诚实
交流与合作
创造力和纪律
懒惰
不如你相像中那样起作用的性格因素
习惯
每个行业的工程人员都了解其工作用的工具及材料和各种限制。如果是电气工程师,就该知道各种金属的电导率,以及电压表的上百种用法;如果是建筑工程师,就该知道木材、混凝土和钢铁的承载量。
假如你是软件工程师,基本的建造材料就是你的聪明才智,主要工具就是你自己。建筑工程师对建筑物结构进行详细的设计,然后将设计蓝图交给其他人施工;而你一旦从细节上对软件做好设计后,软件就生成出来了。编程的整个过程如同建造空中楼阁一样——这是人们能做的纯粹脑力劳动之一。
因此,当软件工程师研究其工具和原材料的本质时,会发现其实是在研究人的智力、性格,这些可不像木材、混凝土和钢材是有形的东西。
3、求知欲
在开发过程中建立自我意识
试验
阅读解决问题的有关方法
在行动之前做分析和计划
学习成功项目的开发经验
阅读文档
阅读其他书本期刊
同专业人士交往
向专业开发看齐——第一级入门级、第二级中级、第三级熟练级、第四级技术带头人级(具有第三级的专业才学),并明白编程工作中只有15%用来和计算机交互、其余都是与人打交道的。
4、诚实
不是高手不假装是高手
乐于承认错误
力图理解编译器的警告,而非弃之不理
透彻理解自己的程序,而不要只是编译看看能否运行
提供实际的状况报告
提供现实的进度方案,在上司面前坚持自己的意见
9、习惯
我们的精神品德即非与生俱来,也非逆于天性......其发展归因于习惯......我们要学的任何东西都通过实际做的过程学到......如果人们建的房子好,他们就成为好的建设者;而造的房子不好时,他们就是差的建设者......所以小时候形成了怎样的习惯关系很大——正是它会造成天壤之别,或者说就是世上所有差异之源。
要点:
1、人的个性对其编程能力有直接影响
2、最有关系的性格为:谦虚、求知欲、诚实、创建性和纪律,以及高明的偷懒。
3、程序员高手的性格与天分无关,而任何事都与个人发展相关。
4、出乎意料的是,小聪明、经验、坚持和疯狂既有助也有害。
5、很多程序员不愿主动吸收新知识和技术,只依靠工作时偶尔接触新的信息。如果你能抽出少量时间阅读和学习编程知识,要不了多久就能鹤立鸡群。
6、好性格与培养正确的习惯关系甚大。要成为杰出的程序员,先要养成良好习惯,其他自然水到渠成。
三十四、软件工艺的话题
内容:
征服复杂性
精选开发过程
首先为人写程序,其次才是为机器
深入一门语言去编程
借助规范集中注意力
基于问题域编程
当心落石
迭代、反反复复,一次以一次
汝当分离软件与信仰
1、征服复杂性:致力于降低复杂度软件开发的核心。第5章“软件构建中的设计”说明了管理复杂度是软件的首要技术使命。
例:A、在架构层将系统划分为多个子系统,便让思绪在某段时间内能专注于系统的一小部分
B、仔细定义类接口,从而可以忽略类内部的工作机理
C、保持类接口的抽象性、从而不必记住不必要的细节
D、避免全局变量,因为它会大大增加总是需要兼顾的代码比例
E、避免深层次的继承,因为这样会耗费很大精力
F、避免深度嵌套的循环或条件判断,因为它们都能用简单的控制结构取代,后者占用较少的大脑资源。
G、别用goto,因为它们引入非顺序执行,多数人都不容易弄懂。
H、小心定义错误处理的方法,不要滥用不同的错误处理技术。
I、以系统的观点对待内置的异常机制,后者会成为非线性的控制结构。异常如果不受约束地使用,会和goto一样难以理解。
J、不要让类过度膨胀,以致于占据整个程序。
K、子程序应保持短小
L、使用清楚、不言自明的变量名,从而大脑不必费力记住诸如“ i 代表账号下标,j 代表顾客下标,还是另有它意?”之类的细节。
M、传递给子程序的参数数目应尽量少。更重要的是,只传递保持子程序接口抽象所必需的参数。
N、用规范和约定来使大脑从记忆不同代码的随意性、偶然性差异中解脱出来。
O、只要有可能,一般情况下应避免第5章所说的“偶然性困难”。
3、首先为人写程序,其次才是为机器
你的程序(名词)好似迷宫一样令人摸不清方向,夹杂着各种花招和无关注释。对比:我的程序
我的程序(名词)算法精准,结构紧凑,性能好,注释清晰得体。对比:你的程序
4、深入一门语言去编程,不浮于表面
不要将编程思路局限到所用语言能自动支持的范围。杰出的程序员会考虑他们要干什么,然后才是怎样用手头的工具去实现他们的目标。
6、基于问题域编程
另一个处理复杂性的特殊方式就是尽可能工作于最高的抽象层次。其中一种方法就是针对编程所要解决的问题来工作,而非针对计算机科学的解决方案。
将程序划分为不同层次的抽象
好的设计使你可以把很多时间集中在较高层,而忽略较低层的细节
第0层:操作系统的操作和机器指令
第1层:编程语言结构和工具
第2层:低层实现结构
第3层:低层问题域
第4层:高层问题域
问题域的低层技术
1、在问题域使用类,来实现有实际意义的结构
2、隐藏低层数据类型及其实现细节的信息
3、使用具名常量来说明字符串和文字量的意义
4、对中间计算结果使用中间变量
5、用布尔函数使复杂逻辑判断更清晰
7、当心落石
编程中的警告信息能提醒程序中可能的错误,但它们一般不会像路标“当心落石”那样显眼。
要点:
1、编程的主要目的之一是管理复杂性。
2、编程过程对最终产品有深远影响。
3、合作一发要求团队成员之间进行广泛沟通,甚于同计算机的交互; 而单人开发则是自我交流,其次才是与计算机。
4、编程规范一旦滥用,只会雪上加霜;使用得当则能为开发环境带来良好机制,有助于管理复杂性和相互沟通。
5、编程应基于问题域而非解决方案,这样便于复杂性管理。
6、注意警告信息,将其作为编程的疑点,因为编程几乎是纯粹的智力活动。
7、开发时迭代次数越多,产品的质量越好。
8、墨守成规的方法有悖于高质量的软件开发。请将编程工具箱中填满各种编程工具,不断提高自己挑选合适工具的能力。
三十五、何处有更多信息
内容:
关于软件构建的信息
构建之外的话题
期刊
软件开发者的阅读计划
参加专业组织
读到这儿时,你应已对行这有效的软件开发实践有了不少的了解。实际上你还能获取比这些多得多的信息。你犯的错误别人早已犯过,要是不想自讨苦吃,就读读他们的书吧,这样能够避免再走弯路,并找出解决问题的新方法。
3、期刊
初阶程序员技术杂志
高阶程序员学术期刊
专题出版物:专业出版物、通俗出版物、
4、软件开发者的阅读计划
入门级
熟练级
专业级
5、参加专业组织
了解编程的最好途径是与其他程序员交往。最好是面向实践的组织是IEEE计算机社区。请访问www.computer.org。
相关推荐
《代码大全2》是Steve McConnell的经典著作,被誉为程序员必读的书籍之一,它深入探讨了软件开发中的各种最佳实践和编程技巧。这本书的核心是提升代码质量和可维护性,旨在帮助程序员编写出更高效、更易读、更易于...
《代码大全2》是Steve McConnell的经典著作之一,中文版的发布为广大的中文读者提供了深入理解和提升编程技能的宝贵资源。这本书共计966页,内容丰富,详细讲解了软件开发过程中的各种最佳实践和常见问题,旨在提高...
根据上述提供的文件信息,我们可以推测"代码大全2中文版"是一本与编程和技术相关的书籍或资料。尽管描述和标签中并未给出具体的细节,但从内容中提取的信息点来看,我们可以构建一篇与软件开发相关的文章,尽管与...
根据提供的信息,我们可以了解到这本书名为《代码大全2中文版》,是一本关于编程和软件开发的经典著作。虽然描述中并未提供具体的知识点,但从书名及其所属领域出发,我们可以推测本书可能涵盖的重要知识点,并进行...
[代码大全2中文版(完整清晰版)]
《代码大全2》是编程领域的一本经典著作,由Steve McConnell撰写,中文版和英文版皆有,旨在帮助程序员提升代码质量和开发效率。这本书详细探讨了软件开发过程中的多个重要方面,包括设计、编码、测试以及重构等。...
《代码大全 2 中文版》是一本深受程序员喜爱的经典编程书籍,由Steve McConnell撰写,中文版为国内程序员提供了深入理解编程艺术的便利。这本书旨在提高程序员的编码技能,无论你是初学者还是经验丰富的开发者,都能...
代码大全2中文版 azw3版本 kindle版本 超好用,放在kindle里直接看,放在kindle pc阅读器里,电脑直接看
《代码大全2》是编程领域的一本经典著作,由Steve McConnell撰写,全面涵盖了软件开发过程中的各种实践和原则,旨在帮助开发者提升代码质量,实现更高效、可维护的编程。这本书深入探讨了从编程基础到高级设计的诸多...
《代码大全2》是Steve McConnell的经典著作,被誉为软件开发者的必备参考书之一。该书的CHM(Compiled HTML Help)格式版本包含了大量的编程实践、设计原则和最佳编码习惯,旨在帮助程序员提高代码质量和效率。CHM...
《代码大全2》是Steve McConnell的经典著作,被誉为程序员必读的书籍之一,它全面而深入地探讨了软件开发过程中的编码实践和技巧。这本书的第二版尤其受到关注,因为它不仅涵盖了第一版的内容,还更新了许多现代编程...
代码大全2 英文版
代码大全2中文版(完整清晰版)]_部分1
《代码大全2中文版》是一本深受欢迎的编程实践指南,尤其在C#开发者中具有广泛影响力。这本书的完整清晰版提供了丰富的编程指导和最佳实践,旨在帮助程序员编写出更高效、可读性更强的代码。以下是根据书中的内容...
根据提供的文件信息,“代码大全2(中文版)”这本书似乎是一本关于编程实践与软件开发技术的专业书籍。尽管描述部分的信息较为简略且部分内容仅提供了一个字母“s”,但我们可以根据书名及其潜在涵盖的主题来推测书...