作者写这本书的首要目的,就是希望缩小本行业中一般商用实践与大师级人物及专家们之间的知识差距。许多强大的编程技术在被编程领域的大众接触之前,都已在学术论文和期刊里尘封了多年。
成功学大师拿破仑希尔说过:什么思想决定什么样行为;什么样行为决定什么样的习惯,什么样的习惯决定什么样性格,而什么性格决定什么样的命运。
本书给我印象较深刻的章节有:前期准备、软件构建中的设计、防御式编程、表驱动法。
前期准备中包括了需求、架构方面的知识;软件构建中的设计中包括了设计模式、设计方法知识;防御式编程包括了在实际编程中很少关注的处理程序容错性,让出错误更容易发现、更容易修改,并减少错误对产品代码的破坏;表驱动法介绍了一种编程模式,从表里面查找信息而不是使用逻辑语句。
一、打好基础
1、软件构建世界
软件开发的主要流程:问题定义-〉需求分析-〉规划构建-〉架构(概要设计)-〉详细设计-〉编码与调试-〉单元测试-〉集成测试-〉系统测试-〉交付或发布-〉软件维护
各个过程与软件构建的关系如下图:
2、用隐喻来更充分地理解软件开发
软件隐喻更像一个探照灯,而不是一个路牌。它并不能告诉你在哪找到答案,但却能告诉你如何能找到答案。更像一个启发式的方法而非一个算法。隐喻把软件开发过程与其他你熟悉的活动联系在一起,帮助你更好地理解。
一般的软件隐喻有以下类型:
1)写作代码
这种隐喻认为开发方式就像写信,如果写错了,抛弃即可。对于大些的项目明显不适用。
2)培植系统
这种隐喻认为开发就像种庄稼,耕地、播种、施肥、浇水、收割,一点一点的做,最后丰收。其核心为“每次做一点”,但缺点为无法对整个过程进行系统的控制。
3)系统生长
这种隐喻认为开发就像牡蛎产生珍珠的过程:增量式(增量的,迭代的,自适应的和演进的)。就像珍珠先有一个沙粒,然后再一点一点长成珍珠。软件开发也是,可以先搭起骨架,然后一点一点附着肌肉和皮肤,一次增加一点代码,最后形成系统。优点在于不做过多的承诺,缺点在于用它来描绘软件开发过程还是不恰当。
4)建造软件
这种隐喻认为软件开发就像建筑一样,需要计划,前期准备和执行。其实软件开发的很多术语都源于建筑,比如:软件架构,脚手架,构建等。同时你还可以把建筑的很多方面引申到软件开发上来。他们具有很多的相似性。
5)智力工具箱
这种隐喻是指人们在多年的开发过程中积累了大量的技术、技巧和诀窍。在开发过程中学的越多,脑中工具箱可用的工具就越多,以后合适的时候就可以拿出来用。
3、前期准备
不同种类的软件项目,需要在“准备工作”和“构建活动”之间做出不同的平衡。每一个项目都是独特的,但是项目可以归入若干种开发风格。表列出了三种最常见的软件项目种类,并且列出了各种项目最适合的典型实践。
三种常见的软件项目种类,及其典型的良好实践
软件种类 |
|||
商业系统 |
使命攸关的系统 |
性命攸关的嵌入式系统 |
|
典型应用 |
Internet站点 Intranet站点 库存管理 游戏 管理信息系统(MIS) 工资系统 |
嵌入式软件 游戏 Internet站点 盒装软件 软件工具 Web services |
航空软件 嵌入式软件 医疗设备 操作系统 盒装软件 |
生命周期模型 |
敏捷开发(极限编程、 Scrum、time-box 开发等等) 渐进原型(prototyping) |
分阶段交付 渐进交付 螺旋型开发 |
分阶段交付 螺旋型开发 渐进交付 |
计划与管理 |
增量式项目计划 随需测试与QA计划 非正式的变更控制 |
基本的预先计划 基本的测试计划 随需QA计划 正式的变更控制 |
充分的预先计划 充分的测试计划 充分的QA计划 严格的变更控制 |
需求 |
非形式化的需求规格 |
半形式化的需求规格 随需的需求评审 |
形式化的需求规格 形式化的需求检查 |
设计 |
设计与编码是结合的 |
架构设计 非形式化的详细设计 随需的设计评审 |
架构设计 形式化的架构检查 形式化的详细设计 形式化的详细设计检查 |
构建 |
结对编程或独立编码 非正式的check-in手续 |
结对编程或独立编码 非正式的check-in手续 随需代码评审 |
结对编程或独立编码 正式的check-in手续 正式的代码检查 |
测试与QA |
开发者测试自己的代码 测试先行开发 很少或没有测试(由单独的测试小组来做) |
开发者测试自己的代码 测试先行开发 单独的测试小组 |
开发者测试自己的代码 测试先行开发 单独的测试小组 单独的QA小组 |
部署 |
非正式的部署过程 |
正式的部署过程 |
正式的部署过程 |
在真实项目中,你会找到表中所列这三种主调的无数种变奏;无论如何,表中已经列举了它们的共性。开发商业系统的项目往往受益于高度迭代的开发法,这种方法的“计划、需求、架构”活动与“构建、系统测试、质量保证”活动交织在一起。
架构师吃掉需求,而设计师吃掉架构,而程序员则消化设计。IBM和其他公司的研究发现,平均水平的项目在开发过程中,需求会有25%的变化。
衡量需求做得如何:
针对功能需求
是否详细定义了系统的全部输入,包括其来源、精度、取值范围、出现频率等?
是否详细定义了系统的全部输出,包括目的地、精度、取值范围、出现频率、格式等?
是否详细定义了所有输出格式(Web页面、报表,等等)?
是否详细定义了所有硬件及软件的外部接口?
是否详细定义了全部外部通信接口,包括握手协议、纠错协议、通信协议等?
是否列出了用户想要做的全部事情?
是否详细定义了每个任务所用的数据,以及每个任务得到的数据?
针对非功能需求(质量需求)
是否为全部必要的操作,从用户的视角,详细描述了期望响应时间?
是否详细描述了其他与计时有关的考虑,例如处理时间、数据传输率、系统吞吐量?
是否详细定义了安全级别?
是否详细定义了可靠性,包括软件失灵的后果、发生故障时需要保护的至关重要的信息、错误检测与恢复的策略等?
是否详细定义了机器内存和剩余磁盘空间的最小值?
是否详细定义了系统的可维护性,包括适应特定功能的变更、操作环境的变更、与其他软件的接口的变更能力?
是否包含对“成功”的定义?“失败”的定义呢?
需求的质量
需求使用用户的语言书写的吗?用户也这么认为吗?
每条需求都不与其他需求冲突吗?
是否详细定义了相互竞争的特性之间的权衡——例如,健壮性与正确性之间的权衡?
是否避免在需求中规定设计(方案)?
需求是否在详细程度上保持相当一致的水平?有些需求应该更详细地描述吗?有些需求应该更粗略地描述吗?
需求是否足够清晰,即使转交给一个独立的小组去构建,他们也能理解吗?开发者也这么想吗?
每个条款都与待解决的问题及其解决方案相关吗?能从每个条款上溯到他在问题与中对应的根源吗?
是否每条需求都是可测试的?是否可能进行独立的测试,以检验满不满足各项需求?
是否详细描述了所有可能的对需求的改动,包括各项改动的可能性?
需求的完备性
对于在开始开发之前无法获得的信息,是否详细描述了信息不完全的区域?
需求的完备度是否能达到这种程度:如果产品满足所有需求,那么它就是可接受的?
你对全部需求都感到很舒服吗?你是否已经去掉了那些不可能实现的需求——那些只是为了安抚客户和老板的东西?
衡量架构做得如何:
针对个架构主题
程序的整体组织结构是否清晰?是否包含一个良好的架构全局观(及其理由)?
是否明确定义了主要的构造块(包括每个构造块的职责范围及其他构造块的接口)?
是否明显涵盖了“需求”中列出的所有功能(每个功能对应的构造块不太多也不太少)?
是否描述并论证了那些最关键的类?
是否描述并论证了数据设计?
是否详细定义了数据库的组织结构和内容?
是否指出了所用的关键的业务规则,并描述其对系统的影响?
是否描述了用户界面设计的策略?
是否将用户界面模块化,使界面的变更不会影响程序其余部分?
是否描述并论证了处理I/O的策略?
是否估算了稀缺资源(如现程、数据库连接、句柄、网络带宽等)的使用量,是否描
述并论证了资源管理的策略?
是否描述了架构的安全需求?
架构是否为每个类、每个子系统、或每个功能域(functionality area)提出空间与时间预算?
架构是否描述了如何达到可伸缩性?
架构是否关注互操作性?
是否描述了国际化/本地化的策略?
是否提供了一套内聚的错误处理策略?
是否规定了容错的办法(如果需要)?
是否证实了系统各个部分的技术可行性?
是否详细描述了过度工程的方法?
是否包含了必要的“买 vs. 造”的决策?
架构是否描述了如何加工被复用的代码,使之符合其他架构目标?
是否将架构设计得能够适应很可能出现的变更?
架构的总体质量
架构是否解决了全部需求?
有没有哪个部分是“过度设计”或“欠设计”?
整个架构是否在概念上协调一致?
顶层设计是否独立于用作实现它的机器和语言?
是否说明了所有主要的决策的动机?
序列式开发法与迭代式开发法选择
序列式开发法适用下列环境:
需求相当稳定。
设计直接了当,而且理解透彻。
开发团队对于这一应用领域非常熟悉。
项目风险很小。
“长期可预测性”很重要。
后期改变需求、设计和编码的的代价很可能较昂贵。
迭代式开发法适用下列环境:
需求并没有被理解透彻,或者出于其他理由你认为它是不稳定的。
设计很复杂,或者有挑战性,或者两者兼具。
开发团队对于这一应用领域不熟悉。
项目包含许多风险。
“长期可预测性”不重要。
后期改变需求、设计和编码的代价很可能较低。
4、关键的“构建”决策
语言的选择、编程约定
关键点:
输入一种语言去编程,而不是使用一种语言编程。在一种语言上编程的程序员将受制于程序语言提供的“构件",这样,如果工具是初级的,那么程序员的思想也会是初级的。而深入一种语言去编程的程序员会先决定他想表达的思想是什么,然后再决定如何使用特定的程序语言提供的工具来表达这些思想。
如果你使用的语言缺乏你所希望的构件,或者倾向于出现其他种类的问题,那深入一种语言编程的程序员应该发明自己的编程约定,命名标准,和程序类库来弥补特定语言自身的缺陷。
二、创建高质量的代码
5、软件构建中的设计
对优秀的设计师,他们共有的一项特质就是对变化的预期能力。好的程序设计所面临的最重要挑战之一就是适应变化。目标应该是把不稳定的区域隔离出来,从而把变化所带来的影响限制在一个子程序、类或者包的内部。下面给出你应该采取的应对各种变动的措施:
A、找出看起来容易变化的项目。如果需求做得很好,那么其中就应该包含一份潜在变化的清单,以及其中每一项变化发生的可能性。在这种情况下,找出潜在的变化就非常容易了。如果需求中没有包括潜在的变化,请业务规则、对硬件的依赖性、输入和输出、被标准的语言特性、困难的设计区域和构建区域、状态变量等方面考虑变化。
B、把容易变化的项目分离出来。把第一步中找出的容易变化的组件单独划分成类,或者和其他容易同时发生变化的组件划分到同一个类中。
C、把看起来容易变化的项目隔离出来。设法设计好类之间的接口,使其对潜在的变化不敏感。设计好类的接口,把变化限制在类的内部,且不会影响类的外部。任何使用了这个将会发生变化的类的其他类都不会察觉到变化的存在。类的接口应该肩负起保护类的隐私的职责。
设计模式:
创建型模式
Singleton:模式解决实体对象个数问题,除Singleton之外,其他创建型模式解决的都是new所带来的耦合关系。
Factory mothod(工厂方法) 、abstract factory (抽象工厂)、Builder都需要一个额外的工厂类来负责实例化"
易变对象".而Prototype则是通过原型(一个特殊的工厂类)来克隆“易变对象”。
结构型模式
Adapter模式注重转换接口,将不吻合的接口适配对接。
Bridge模式注重分离接口与其实现,支持多维度变化。
Composite模式注重统一接口,将“一对多”的关系转化为“一对一”的关系。
Decorator模式注重稳定接口,在此前提下为对象扩展功能。
Facade模式注重简化接口,简化组件系统与外部客户程序的依赖关系。
Flyweight模式注重保留接口,在内部使用共享技术对对象存储进行优化。
Proxy模式注重假借接口,增加间接层来实现灵活控制。
行为型模式
Template Method模式封装算法结构,支持算法子步骤变化。
Strategy模式注重封装算法,支持算法的变化。
State模式注重封装与状态相关的变化,支持状态的变化。
Memento模式注重封装对象状态的变化,支持状态保存/恢复。
Mediator模式注重封装对象间的交互,支持对象交互的变化。
Chain Of Responsibility模式注重封装对象责任,支持责任的变化。
Command模式注重将请求封装为对象,支持请求的变化。
Iterator模式注重封装集合对象内部结构,支持集合的变化。
Interpreter模式注重封装特定领域变化,支持领域问题的频繁变化。
Observer模式注重封装对象通知,支持通信对象的变化。
Visitor模式注重封装对象操作变化,支持在运行时为类层次结构动态添加新的操作。
理想的设计特征
最小的复杂度(Minimal complexity):设计的首要目标是让复杂度最小。要避免“聪明的”设计,因为“聪明的”设计常常都是难于理解的。应该做出简单且易于理解的设计。如果你的设计方案不能让你在专注于程序的一部分时安心地忽视其他部分的话,这一设计就没有什么作用了。
易于维护(Ease of maintenance):意味着在设计时为做维护工作的程序员着想。请时刻想着维护程序员可能会就你写的代码而提出的问题。把这些程序员当成你的听众,进而设计出能自解释的系统来。
松散耦合(loose coupling):意味着在设计时让程序的各个组成部分之间关联最小。通过应用类接口中的合理抽象、封装性及信息隐藏等原则,设计出相互关联尽可能最少的类。减少关联也就减少了集成、测试与维护工作量。
可扩展性(extensibility):是说你能增强系统的功能而无须破坏其底层结构。你可以改动系统的某一部分而不会影响到其他部分。越是可能发生的改动,越不会给系统造成什么破坏。
可重用性(reusability):意味着所设计的系统的组成部分能在其他系统中重复使用。
高扇入(high fan-in):是说让大量的类使用某个给定的类。这意味着设计出的系统很好地利用了在较低层次上的工具类(utility classes)。
低扇出(low fan-out):是说让一个类里少量或适中地使用其他的类。高扇出(超过约7个)说明一个类使用大量其他的类,因此可能变得过于复杂。
可移植性(portability):设计出的系统应该很方便地移植到其他环境中。
精简性(cleanness):设计出的系统没有多余的部分。任何多余的代码也需要开发、Review和测试,并且修改了其他代码后还要重新考虑这部分。
层次性(stratification):意味着尽量保持系统各个分解层的层次性,是你能在任意的层面上观察系统,并得到某种具有一致性的看法。
标准技术(Standard techniques):要尽量用标准化的、常用的方法,让整个系统给人一种熟悉的感觉。
设计中启发式方法的总结:
寻找现实世界的对象
形成一致的抽象
封装实现细节
在可能的情况下继承
信息隐藏
找出容易改变的区域
保存松散耦合
探寻通用的设计模式
设计策略
自上而下策略和自下而上策略的最关键的区别在于,前者是一种分解策略,后者是一种合成策略;前者从一般性的问题出发,把该问题分解成可控的部分。后者从可控的部分出发,去构造一个通用的方案。这两种方法都有各自的强项和弱项。
记录设计成果
把设计文档插入到代码里:在代码注释中写明关键的设计决策,这种注释通常放在文件或者类的开始位置。如果你同时使用类似于JavaDoc这样的文档提取工具,那么这种方法会确保设计文档对于开发这部分代码的程序员来说是立等可取的,同时也有助于程序员保持代码和设计文档之间的相当不错的同步。
用Wiki来记录设计讨论和决策:把你们的设计讨论写到项目的Wiki里去(Wiki是指一组可以由项目组所有成员用网络浏览器轻松编辑的网页)。尽管文字录入要比交谈麻烦一些,但这样会自动地记录下你们的设计讨论和设计决策。如果使用Wiki,你可以用图片来弥补文字讨论的不足,并链接支持该设计决策的网站、白皮书及其他材料。如果你的开发团队在地理位置上是分布式的,那么这种技术会非常有帮助。
写总结邮件:每次就设计展开讨论过后,请采取这种做法,即指派某人来写出刚才讨论的纲要——特别是那些决定下来的事项——然后发送给整个项目组。在项目的公共电子邮件文件夹里保留一份备份。
使用数码相机:在对设计进行文档化时有一个很常见的障碍,那就是用流行的画图工具画设计图表太麻烦。不过文档化可不仅限于“用漂亮的格式、正规的符号来记录设计”和“不做任何设计文档”这两种选择。把白板上画出的图表照成相片然后嵌入到传统的文档里,这样做可以带来事半功倍的效果,因为它的工作量只是用画图工具画设计图表的1%,而它的收益却能达到保存设计图表的80%。
保留设计挂图:如果你把设计记录在大的挂图上,那么你只需把这些挂图保存在方便的地方即可,或者采用更好的做法,把它们张帖在项目工作区域的墙上,让大家能够很容易地随时查阅和修改。
使用CRC(类、职责、合作者)卡片:另外一种技术含量较低的文档记录方案是使用索引卡片。在每张卡片上,设计者写下类的名称、职责和合作者(与这个类合作的其他类)。一个设计团队便按照这些卡片的内容展开工作,直到他们认为已经创建出一个好的设计方案为止。到那个时候,你只需把这些卡片保留下来,留待日后引用。索引卡片非常便宜,不吓人,易于携带,并且有助于促进团队合作(Beck 1991)。
在适当的细节层创建UML图:一种流行的绘制设计图的方法是由对象管理组织(Object Management Group)定义的统一建模语言(UML)(Fowler 2004)。UML提供了一套丰富的、形式化的表示法,可用于设计实体(entity)及其关系(relationship)。你可以用非正式的UML图来帮助讨论和发现设计思路。从最简单的草图开始,直到你最终选定了一套设计方案,才往其中增加细节。由于UML是标准化的,因此在交流设计观念时大家都能理解它,同时还能加快团队共同讨论各种设计方案的速度。
6、可以工作的类
包含(“有一个……”的关系)
继承(“是一个……”的关系)
研究表明,人们在做其他事情时,能够记住的离散项目的个数是7+-2个。如果一个类中包含有超过7个数据成员时,请考虑要不要把他分解成为几个更小的类。
创建类的合理原因:对现实世界中的对象建模、对抽象对象建模、降低复杂度、隔离复杂度、隐藏实现细节、限制变化所影响的范围、隐藏全局数据、让参数传递更顺畅、创建中心控制点、让代码更易于重用、让程序族做计划、把相关操作放到一起、实现特定重构。
要点:
类的接口应提供一致的抽象。很多问题都是由于违背该原则而引起的。
类的接口应该隐藏一些信息——如某个系统接口、某项设计决策、或者一些实现细节。
包含往往比继承更为可取——除非你要对“是一个/is a”的关系建模。
继承是一种有用的工具,但它却会增加复杂度。
类是管理复杂度的首选工具。
7、高质量子程序
要点:
子程序代码长度最好控制在200行之内,如果超过200行,会在可读性方面遇到问题。
把宏表达式整个包含在括号内,比如:#define Cube(a) (a*a*a)
创建子程序最主要的目的是提高程序的可管理性,当然也有其他一些好的理由。其中,节省代码空间只是一个次要原因:提高可读性、可靠性和可修改性等原因都更重要一些。
子程序可以按照其内聚性分为很多类(功能内聚、顺序内聚、通讯内聚、临时内聚过程内聚、逻辑内聚、巧合内聚),而你应该让大多数子程序具有功能上的内聚性,这是最佳的一种内聚性。
子程序的名字是它的质量的指示器。如果名字槽糕但是恰如其分,那就说明这个子程序设计得很差劲。准确使用对应词,下面列出一些常见的对应词组:
add/remove increment/decrement open/close
begin/end insert/delete show/hide
create/destroy lock/unlock source/target
first/last min/max start/stop
get/put next/previous up/down
get/set old/new
只有在某个子程序的主要目的是返回由其名字所描述的特定结果时,才应该使用函数。
细心的程序员会非常谨慎地使用宏,而且只在万不得已时才用。
8、防御式编程
防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是有其他子程序产生的错误数据。简单点讲就是容错性。
要点:
最终产品代码中对错误的处理方式要对“垃圾进,垃圾出”复杂得多。
防御式编程技术可以让出错误更容易发现、更容易修改,并减少错误对产品代码的破坏。
断言可以帮助人尽早发现错误,尤其是在大型系统和高可靠性的系统中,以及快速变化的代码中。包含C++
、Java和Microsoft Visual Basic在内的很多语言都支持断言,比如C++中标准的aasert宏并不支持文本消息。下面实例给出了一个使用C++宏改进的ASSERT实现:
#define ASSERT(condition,message){
if(!(condition)){
LogError("Assertion failed:",!condition,message);
exit(EXIT_FAILURE);
}
}
关于如何处理错误输入的决策是一项关键的错误处理决策,也是一项关键的高层设计决策。常用处理技术:返回中立值、换用一一个正确的数据、返回与前次相同的数据、换用最接近的合法值、把警告信息记录到日志文件中、返回一个错误码、调用错误处理子程序或对象、当错误发生时显示出错信息、用最妥当的方式在局部处理错误、闭关程序。
异常提供了一种与代码正常流程角度不同的错误处理手段。如果留心使用异常,它可以成为程序员们知识工具箱中一项有益补充,同时也应该在异常和其他错误处理手段之间进行权衡比较。常使用try- catch语句、try- catch-fianlly语句。
针对产品代码的限制并不适用于开发中的软件。你可以利用这一优势在开发中添加有助于更快地排查错误的代码。
9、伪代码编程过程
伪代码编程过程有助于减少设计和编写文档所需的工作量,同时提高这两项工作的质量。
要点:
创建类和子程序通常都是一个迭代的过程。在创建子程序的过程中获得的认识常常会反过来影响类的设计。
编写好的伪代码需要使用易懂的英语,要避免使用特定编程语言中才有的特性,同时要在意图的层面上为伪代码(即描述该做什么,而不是要怎么去做)。
伪代码编程过程是一个行之有效的做详细设计的工具,它同时让编码工作更容易。伪代码会直接转化为注释,从而确保了注释的准确度和实用性。
三、变量
10、使用变量的一般事项
要点:
数据初始化过程很容易出错,所以请用本章描述的初始化方法来避免由于非预期的初始值而造成的错误。
最小化每个变量的作用域。把同一变量的引用点集中在一起。把变量限定在子程序或者类的范围之内。避免使用全局数据。
把使用相同变量的语句尽可能集中在一起。
早期绑定会减少灵活性,但有助于减少复杂度。晚期绑定可以增加灵活性,同时增加复杂度。
把每个变量用于唯一的用途。
11、变量名的力量
变量命名规则可以参考著名的匈牙利命名法。比如:
全局变量 g_
常量 c_
c++类成员变量 m_
静态变量 s_
指针 p
函数 fn
无效 v
句柄 h
长整型 l
布尔 b
浮点型 f
双字 dw
字符串 sz
短整型 n
双精度浮点 d
计数 c(通常用cnt)
字符 ch(通常用c)
整型 i(通常用n)
字节 by
字 w
实型 r
无符号 u
最大 Max
最小 Min
初始化 Init
临时变量 T(或Temp)
源对象 Src
目的对象 Dest
要点:
好的变量名提高程序可读性的一项关键要素。对特殊种类的变量,比如循环下标很状态变量,需要加以特殊的考虑。
名字要尽可能地具体。那些太模糊或者太通用以致于能够用于多种目的的名字都是很不好的。
命名规则应该能够区分局部数据、类数据和全局数据。它们还应当可以区分类型名、具名常量、枚举类型名字和变量名。
无论做那种类型项目,你都应该采用某种变量命名规则。你所采用的规则的种类取决于你程序的规模,以及项目成员的人数。
现在编程语言很少需要用到缩写。如果你真的要使用缩写,请使用项目缩写词典或者标准前缀来帮助理解缩写。
代码阅读的次数远远多于编写的次数。确保你所取的名字侧重于阅读方便而不是编写方便。
12、基本数据类型
数值、整数、浮点数、字符和字符串、布尔变量、枚举类型、具名常量、数组、创建类型
要点:
使用特定的数据类型就意味着要记住适用于各个类型的很多独立的原则。
如果你的语言支持,创建自定义类型会使得你的程序更容易修改,并更具有自描述性。
当你用typedef或者其等价方式创建了一个简单类型的时候,考虑是否更应该创建一个新的类。
13、不常见的数据类型
结构体、指针、全局数据
要点:
结构体可以使得程序更简单、更容易理解,以及更容易维护。
每当你打算使用结构体的时候,考虑采用类是不是会工作得更好。
指针很容易出错。用访问器子程序或者类以及防御式编程实践来保护自己的代码。
避免用全局变量,不只是因为它们很危险,还是因为你可以用其他更好的方法来取代它们。
如果你不得不使用全局变量,那么就通过访问器子程序来使用它。访问器子程序为你带来全局变量所能带来的一切优点,还有一些额外好处。
四、语句
14、组织直线型代码
顺序结构
要点:
组织直线型代码的最主要原则是按照依赖关系进行排列。
可以用好的子程序名、参数列表、注释。如果代码足够重要,内务管理变量来让依赖关系变得更明显。
如果代码之间没有顺序依赖关系,那就设法使相关的语句尽可能地接近。
15、使用条件语句
if-else,switch-case-default
要点:
对于简单的if-else语句,请注意if子句和else子句的顺序。
为了捕捉错误,可以使用case语句中的default子句,或者使用if-then-else语句中的最后那个esle子句。
16、控制循环
while for
要点:
循环很复杂。保持循环简单将有助于别人阅读你的代码。
保持循环简单的技巧包括:避免使用怪异的循环、减少嵌套层次、让入口和出口一目了然、把内务操作代码放在一处。
循环下标很容易被滥用。因此命名要准确,并且要把它们各自仅有用于一个用途。
仔细地考虑循环,确认它在每一种情况下都运行正常,并且在所有可能的条件下都能退出。
17、不常见的控制结构
子程序中的多出返回、递归、goto语句
要点:
多个return可以增强子程序的可读性和可维护性,同时可以避免产生很深的嵌套逻辑。
递归能够很优雅地解决一小部分问题。对它的使用要加倍小心。在使用递归的时候需要注意:确认递归能够停止、使用安全计数器防止出现无穷递归、留心栈空间、不要用递归去计算阶乘或者斐波那契数列。
除非万不得已最好不要使用goto,因为逻辑结构比较混乱。
18、表驱动法
表驱动法是一种编程模式,从表里面查找信息而不是使用逻辑语句。事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。对于简单的情况而言,使用逻辑语句更为容易和直白。但随着逻辑链的越来越复杂,查表法也是愈发显示地更具有吸引力。
要点:
表提供了一种复杂的逻辑和继承结构的替换的方案。如果你发现自己对某个应用程序的逻辑和继承树关系感到困惑,那么问问自己它是否可以通过一个查询表来加以简化。
使用表的一项关键决策是决定如何去访问表。你可以采取直接访问、索引访问、阶梯访问。阶梯访问是通过确定每项命中的阶梯层次确定其归类,它命中的“台阶”确定其类属。
使用表的另一项关键决策是决定应该把什么内容放入表中。
19、一般控制问题
要点:
使用布尔表达式简单可读,将非常有助于提高你的代码的质量。
深层次的嵌套使得子程序变得难以理解。所幸的是,你可以相对容易地避免这么做。
结构化编程是一种简单并且仍然使用的思想,你可以通过顺序、选择、循环三者组合起来而开发出任何程序。
将复杂度降低到最低水平是编写高质量代码的关键。
五、代码改善
20、软件质量
软件同时拥有内在和外在的质量特性。外在特性指的是该产品的用户所能够感受到的部分,包括正确性、可用性、效率、可靠性、完整性、适应性、精确性、健壮性。内在特性包括可维护性、灵活性、可移植性、可重用性、可读性、可测试性、可理解性。
21、协同构建
协同构建包括结对编程、正式检查、非正式技术复查、文档阅读,以及其他开发人员共同承担创建代码及其他工作产品责任的技术。
22、开发者测试
单元测试、组件测试、集成测试
回归测试、自动化测试
要点:
开发人员测试是完整测试策略的一个关键部分。
同编码之后编写测试用例相比较,编码开始之前编写测试用例,工作量和花费的时间差不多,但是后者可以缩短缺陷-侦测-调试-修正这一周期。
即使考虑到了各种可用的测试手段,测试仍然只是良好软件质量计划的一部分。
你可以根据各种不同的思路产生很多测试用例,这些思路包括基础测试、数据流分析、边界分析、错误数据类型以及正确数据类型等。你还可以通过猜测错误的方式得到更多的测试用例。
错误往往集中在少数几个容易出错的类和子程序上,找出这部分代码,重新设计和编写它们。
23、调试
寻找缺陷的方法:
使用所有可用数据来构造你的假设。
不断提炼产生错误的测试用例。
在自己的单元测试族中测试代码。
借助可以获得的任何工具。
用不同的方式重现错误。
通过产生更多的数据来构造更多的假设。
用头脑风暴的方式找出可能的假设。
在桌上放一个笔记本,把需要尝试的事情列出来。
缩小被怀疑有问题的代码区域。
对之前出现过问题的类和子程序保存警惕。
检查最近修改的代码。
扩展被怀疑有问题的代码区域。
采用增量集成。
检查常见的缺陷。
和其他人一起讨论你的问题。
抛开问题休息一下。
列出所有的蛮力调试方法,逐条应用。
解决语法错误的方法:
不要太信任编译器信息中给出的行号。
不要太信任编译器信息。
分而治之,各个击破。
使用具有语法分析功能的编译器来找出位置错误的注释和行号。
修正缺陷的方法:
在动手之前先理解程序。
验证对错误的分析。
放松一下。
要保存最初的源代码。
只有当理由充分的时候才去修改代码。
一次只做一个改动。
添加单元测试来暴露代码中缺陷。
要点:
在动手解决问题之前,要理解问题的根本。
将编译器警告级别设置为最严格,把警告信息说报告的错误都改正。
调试工具对软件开发而言是强有力的支持手段。
24、重构
数据级别重构:
用具名常量来代替神秘数值。
用更明确或更具信息量的名字来重命名变量。
将表达式内联化。
用函数来代替表达式。
引入中间变量。
将多用途变量转换为多个单一用途变量。
将一组类型码转化为类或枚举类型或含派生类的类。
将数组转化为对象。
封装群集。
用数据类替代传统记录。
语句级的重构:
分解布尔表达式。
将复杂的布尔表达式转换为命名精确的布尔函数。
将条件语句中不同部分中的重复代码合并。
子程序级的重构:
提取子程序。
将子程序代码内联化。
将冗长的子程序转化为类。
用简单的算法替代复杂算法。
类实现的重构:
将值对象改为引用对象。
用数据初始化来替代虚函数。
改变成员函数或者数据的位置。
将特定的代码提出生产派生类。
将相似的代码合并起来放到基类中。
类接口的重构:
将一个类转化成两个。
引入外部子程序。
引入扩展类。
封装暴露在外的成员变量。
系统级的重构:
使用工厂函数而非简单的构造函数。
要点:
重构成功的另一要素是程序员应当掌握大量特定的重构方法。
重构成功的最后要点在于要有安全重构的策略。
25、代码调整策略
代码调整方法:
1)用设计良好的代码来开发软件,从而使程序易于理解和修改。
2)如果程序性能很差。
a、保存代码的可运行版本,这样你才能回到“最近的已知正常状态”;
b、对系统进行分析测量,找出热点;
c、判断性能差劣是否源于设计、数据类型或算法上的缺陷,确定是否应该做代码调整,如果不是,请跳回第一步;
d、对步骤c中所确定的瓶颈代码进行调整;
e、每次调整后都要对性能提升进行测量;
f、如果调整没有改进代码的性能,就恢复到步骤a保存的代码。
3)重复步骤2。
要点:
性能只是软件整体质量的一个方面,通常不是最重要的。
绝大多数的程序都有那么一小部分代码耗费了绝大部分的运行时间。如果没有测量,你不会知道是哪一部分代码。
26、代码调整技术
要点:
优化结果在不同的语言、编译器和环境下有很大的差异。如果没有对每一次的优化进行测量,你将无法判断优化到底是帮助还是损害了这个程序。
第一次优化通常不会是最好的。即使找到了效果很不错的,也不要停下扩大战果的步伐。
六、系统考虑
27、程序规模对构建的影响
要点:
随着项目规模的扩大,交流需要加以支持。
放大轻量级的方法论要好于缩小重量级的方法论。最有效的办法是使用“适量级”方法论。
28、管理构建
配置管理、备份计划
要点:
好的软件评估是一项重大挑战。成功的关键包括采用多种方法、随着项目的开展而修缮评估结果。
度量是构建管理成功的关键。准确的度量是制定准确的进度表、质量控制和改进开发过程的关键。
程序员和管理人员都是人,在把他们当人看的时候工作得最好。
29、集成
集成是一种软件开发行为:将一些独立的软件组件组合为一个完整系统。
集成频率——阶段式集成与增量集成
增量集成有若干变型,而且任何一种形式的增量集成都比阶段式集成好。
针对每个特定的项目,最佳的集成步骤通常是自顶向下、自底向上、风险导向及其他集成方法的某种组合。T型集成和竖直分块集成通常都能工作得很好。
30、编程工具
设计工具(Rose、visio)、源代码工具(IDE集成工具、diff比较工具、版本控制工具、生产接口文档工具)、可执行工具(编译器与连接器、Build工具、程序库、代码生成工具、安装程序的工具)
测试,下列功能特性和工具有助于你进行有效的测试:
自动测试框架,如JUnit、NUnit、CppUnit等
测试用例的记录和回放工具
覆盖率监视器
系统扰动器(内存填充工具、内存“抖动”工具、内存访问检查器)
Diff工具(比较数据稳健、图像等)
缺陷注入工具
缺陷跟踪工具
要点:
好的工具能让你的日子过得安逸很多。
下面这些工具可用:编辑、分析代码质量、重构、版本控制、除错、测试、代码调整。
七、软件工艺
31、布局与风格
计算机编程美学话题——程序源代码的布局。布局好的代码看起来不仅感觉漂亮而且结构也清晰。
布局技巧
用空格可以提高可读性。空格包括空格、制表符、断开行及空行。用空格得到罗好的布局:
分组—— 一个代码段只能包含有关的、为完成某一任务而组织在一起的语句。
空格——在程序段的开头应当加空格表明
对方——把同属性的元素对齐。例如同一类语句的排成一条直线下来
缩排——当一个句子在逻辑上从属于上一个句子时,这个名子就比上一行退几格
要尽量的多用括号,有的时候表达式可能不需要使用括号,但加上括号会使表达的更清晰。
布局风格
控制语句下的一组语句要区分成一组,再括起来。纯块结构、行尾布局这些名词化了,其实都见过的,“行尾布局”没用过,感觉这个最不好。文中给出的缺点是:无连续性而且难修改。
控制结构布局
单条语句布局
单条语句常规限制不超过80个字符,原因:超过了很难读、80个字符也防止了深层嵌套、80字符内方便打印。单条语句布局建议:
逻辑表达式使用空格可增强可读性
用空格使数组下标更好读
用空格使子程序参数更好读
使续行明显 放在第一行那部分要清楚的表明它是语句的一部分。最好方法是第一行部分独立出来则有明显的语法错误
把紧密关联的元素放在一起
子程序调用的续行可后退标准格数
使续行的结尾易于发现
每行仅写一条语句
数据类型定义布局
注意数据类型定义的对齐
每行定义一个数据
有意识地安排定义顺序
注释布局
一个好的注释能极大提高程序的可读性,若注释不成功,则会帮倒忙。
注释行与相应的代码行同样缩排
把注释行至少用一个空行分开
子程序布局
如何将子程序布局好,以下几点作为参考:
用空行把子程序各部分分开
对子程序的参数用标准缩排
文件、模块和程序布局
把一板块放在一个文件里
把一个文件内的子程序区分清楚
如果文件里有多个模块,要把这些模块区分得清清楚楚
各子程序按照字母顺序排列
在C中,细心地组织源文件
要点:
可视化布局的首要任务是指明代码的逻辑组织。评估该任务是否实现的指标包括准确性、一致性、易读性和易维护性。
布局的很多方面涉及信仰问题。
32、自说明代码
33、个人性格
聪明和谦虚、求知欲、诚实、交流与合作、创造力和纪律、懒惰、坚持、经验、习惯。
一些培养求知欲和把学习当做第一要务的特定方法:在开发过程中建立自我意识、对编程和开发过程做试验、阅读解决问题的有关方法、在行动之前做分析和计划、学习成功项目的开发经验、阅读文档、阅读其他书本期刊、同专业人士交往。
最有关系的性格为:谦虚、求知欲、诚实、创造力和纪律、高明的偷懒。
很多程序员不愿意主动吸收新知识和技术,只依靠工作时偶尔接触新的信息。如果你能抽出少量时间阅读和学习编程知识,要不了多久就能鹤立鸡群。
好性格与培养正确的习惯关系很大。要成为杰出的程序员,先要养成良好的习惯,其他自然水到渠成。
什么思想决定什么样行为;什么样行为决定什么样的习惯,什么样的习惯决定什么样性格,而什么性格决定什么样的命运。
34、软件工艺的话题
合作开发要求团队成员之间进行广泛沟通,其次同计算机交互;而单人开发是自我交流,其次才是与计算机。
编程应基于问题域而非解决方案,这样便于复杂度管理。
开发时迭代次数越多,产品的质量越好。
墨守成规的方法有悖于高质量的软件开发。请将编程工具箱中填满各种编程工具,不断提高自己挑选合适工具的能力。
35、何处有更多信息
软件开发资料库由几类信息构成:一类核心数据解释有效编程的基础概念;相关的数据解释编程技术管理及知识背景;还有关于编程语言、操作系统、环境和硬件等面向特定主题的参考书等。
代码大全(第二版)中文版:http://download.csdn.net/source/1636975