代码大全
- 欢迎进入软件构建的世界
- 什么是软件构建:软件开发过程涉及众多环节,构建涉及其中的一部分环节,在这些环节中,有些是构建的核心(编码与测试),有些则只是部分覆盖(详细设计,单元测试,集成测试);
- 软件构建为何如此重要:构建占据最大的人工成本和时间成本,它是所有环节中唯一不可或缺的;它的成败,直接决定了整个软件成败;
- 如何阅读这本书
- 从头到尾读:从第2章开始
- 挑特定的主题读,然后向外延伸:从第6章“可以工作的类”开始;
- 不知道如何开始:阅读第3.2节,辨明所从事的软件的类型;
- 用隐喻来更充分的理解软件开发
- 建筑
- 三思而后行:前期准备
- 前期准备的重要性:好的前期准备,可以为后期的构建节省成本,不管是人力上还是时间上;
- 辨明你从事的软件的类型
- 不同类型的软件项目,需要在前期准备工作和构建工作之间所花的时间做出平衡;
- 三种类型:
- 商业系统:普通网站,库存管理,游戏,信息管理系统,工资系统
- 使命攸关的系统:嵌入式软件,游戏,盒装软件,软件工具,Web Services
- 性命攸关的系统:航空软件,嵌入式软件,医疗设备,操作系统,盒装软件;
- 开发方式:
- 迭代式开发,适用于需求不明确不稳定的场景
- 瀑布式开发,适用于需求稳定明确,设计直截了当,理解透彻的场景;
- 问题定义的先决条件
- 对这个系统要解决的问题做出清楚的陈述
- 以客户的语言进行陈述,例如:做为(角色),我想(目标),这样可以(利益);
- 陈述中应不涉及任何可能的解决方案;(后续的开发设计时,将需要基于问题层面进行思考和设计)
- 需求的先决条件
- 需求:详细描述软件系统应该做什么,明确的需求,用户可以自己把握软件能够做什么,而不是由程序员猜测,程序员之间也可以避免分歧;在早期发现错误的需求,改动成本最低,少则5-10倍,多则100倍;
- 减少需求变更的方法
- 使用一份需求规范核对表,对需求的质量进行评估,如果质量不合格,把它改到合格;
- 确保每个人都知道需求变更的费用代价和时间代价;当用户提出变更时,行动之前,给对方变更的成本表和进度表;
- 建立一套变更审批程序,比如设立一个变更控制委员会,通过审批机制的方式,让变更以更加有节奏的方式进行,而不是随意打乱当前的工作;
- 使用更能适应变更的开发方法,例如使用:高保真原型 + 敏捷开发;(项目上线后,正常会有一个业务流程,用户在这个流程中录入数据,一环一环的进行下去,开发的时候,最好能够按这个顺序进行,这样可以实现迭代交付,不然如果先完成了后边的流程,前面的流程缺失,无法实现迭代交付)
- 如果上面的方法都无法实施,则应该放弃这个项目;
- 考虑项目的商业价值,很多听上去很棒的功能点,当结合商业价值考虑的时候,可能它的实现就不是很有必要了;
- 需求核对表
- 针对功能需求
- 是否详细定义了全部输入?包括来源、精度、取值范围、频率等;
- 是否详细定义了全部输出?包括目的地、精度、取值范围、频率、输出格式等;
- 是否详细定义了所有的硬件和软件接口?
- 是否详细定义了所有的外部通信接口?
- 是否列出了所有用户想做的事情?
- 是否详细定义了每个任务需要用到的数据?以及每个任务要得到的数据?
- 针对非功能需求(质量需求)
- 可选性:操作是否必需
- 时间:用户期望的响应时间、处理时间、数据传输率、系统吞吐量等;
- 安全可靠性:安全级别、软件失灵的后果、重要信息保存、错误检测恢复;
- 空间:内存与磁盘大小需求;
- 可维护性:特定功能变更、操作环境变更、外部接口变更
- 定义:成功的定义、失败的定义
- 需求的质量
- 用户性:是否使用用户的语言来描述需求?用户是否也这么认为?
- 冲突与权衡:不同需求之间是否存在冲突?不同非功能特性的优先级?
- 不包含方案:需求是否不包含解决方案?
- 详细度:需求是否在详细程度上保持一致的水平?
- 清晰度:需求是否描述清晰,并可以交给另外的独立小组开发?他们能否理解?开发人员也这么认为吗?
- 相关性:每个需求条款是否与待解决的问题和解决方案相关?是否能够从条款中找到问题领域的根源?
- 可测性:每个需求是否可以测试?是否可以独立测试,以验证是否满足各项需求?
- 变更度:是否详细描述所有对需求可能的变更?以及变更的可能性?
- 需求的完备性
- 未知度:对于在开发之前无法详细了解的信息,是否注明?
- 完备度:是否只要产品满足了需求,即是可以接受的?
- 健康性:是否去除了那些只为了安抚客户或老板的不可能实现的需求?
- 针对功能需求
- 架构的先决条件
- 架构的质量决定了系统的概念完整性,它将工作分成几个部分,使多个开发者或者多个开发团队可以独立工作;
- 架构的组成部分
- 程序组织:定义程序的主要构造块,以及它们的主要责任,以概括的形式对系统做一个综述,并写上曾经考虑过的其他备选方案,以及选择当前方案的原因;
- 主要的类:80/20 法则,每个主要的类的责任,如何与其他类交互;包含类的继承体系,状态转换,对象持久化等描述(例如:客户端信息保持30秒更新一次);
- 数据设计:描述所用到的主要文件和数据表的设计,详细定义所用数据库的高层组织结构和内容(什么是高层组织结构?);
- 业务规则:如果架构的设计依赖于特定的业务规则,则应该详细描述这些规则,以及这些规则对设计的影响;
- 用户界面设计:模块化,让用户界面的变更不影响程序的其他部分;
- 资源管理:描述管理稀缺资源的计划,包括数据库的连接、线程、句柄等;估算正常情况和极端情况下的资源使用量(在资源紧张的驱动程序开发和嵌入式开发,这点尤为重要)
- 安全性:建立威胁模型;描述实现设计层面和代码层面的安全性的方法;包括:处理缓冲区的方法、处理非受信数据的规则、消息的加密、内存中数据的保护等;
- 性能:如果需要关注性能,则需求中应该详细定义性能指标;架构应该提供数据,解释为什么可以达到这些指标;如果某些部分达不到,则应指出风险;如果某些部分需要采用特定算法和数据结构以达到性能指标,也应该指出来;
- 可伸缩性:如何应对用户数量、服务器数量、网络节点数量、数据库记录的长度、交易量等的增长;如果系统不会增长,则架构应描述这一假设;
- 互用性:如果需要和其他软硬件共享数据或资源,则应描述如何完成这一任务;
- 本地化:如何翻译一个程序,以支持当地特定语言的工作;如何无需更改代码的维护不同语言所用的字符集;
- 输入输出:定义读取策略,look-ahead, look-behind, 或 just-in-time;
- 错误处理:纠正or检测、主动or被动、如何传播错误、错误消息的处理有何约定、如何处理异常、在哪个层次处理错误、每个类在验证其输入数据有效性时的责任、使用环境内建机制or自建;
- 容错性:如果出现误差,如何处理误差;
- 可行性:应论证系统的技术可行性,如果有某一方面无法实现,解释原因;务必在构建前解决掉这些风险;
- 过度工程:详细定义一种过度工程的方法,设计期望目标,避免有些类过度健壮,有些过于薄弱;
- 买or造的决策:采用现货供应的组件,还是自建组件,如果是后者,说明自建应该在哪些方面胜过现成的程序和组件;
- 复用的决策:如果使用已有的软件、用例、数据等,则应说明,如何对复用的软件进行加工,使之符合架构的目标;
- 变更策略:清楚的描述处理变更的策略,列出已经考虑过的有可能会增加的功能,并说明最有可能增加的功能,也是最容易实现的(因为已经提前考虑了);(延迟策略:如果某些决定目前不构成风险,则越晚做出决策越好)
- 架构的总体质量:目标清楚表述、描述所有主要决策的动机、与编程语言无关、明确指出有风险的区域、包括多个视角(例如建筑的正视图、平面图、结构图等)
- 花费在前期准备上面的时间长度
- 正常占10-20%的工作量,花20-30%的时间;需求越是清晰稳定,越降低后续构建的成本,减少给构建带来负面的影响
- 如果需求不清晰,则有必要将需求分析做为一个独立的项目来做,好比建筑行业中,独立的聘请设计单位进行图纸的绘制一样,设计与施工分开;
- 关键的构建决策
- 选择编程语言
- 熟悉的语言比不熟悉的语言要高30%的效率;
- 高级的语言更有利于专注要表达的思想,而不是表达的细节(例如措词);
- 编程约定:通过架构上的指导方针达到整体的协调统一,有利于团队协作和未来维护,包括变量名称、类的名称、子程序名称、格式约定、注释约定等,好比一幅画,如果由印象主义、古典主义、立体主义等多种风格构成,就会导致它的混乱和难懂;
- 你在技术浪潮中的位置:
- 早期:缺失的文档,不稳定的包,需要花相当多的时间关注工具本身;
- 后期:详细的文档,极少的BUG,可以用大部分时间编写功能;
- 深入一种语言去编程
- 可以用不同的方法实现相同的思想,重要的是,要有正确的思想;
- 选择编程语言
- 软件构建中的设计
- 设计中的挑战
- 设计是一个险恶的问题,因为它只有在做的过程中,才能更多的暴露问题本身;
- 需要不断试错;
- 需要进行取舍和优先级排序;
- 在外界资源有限的条件下工作;
- 具有不确定性,同一个问题,三个人有三种不同的解决方法;
- 它是一个启发式的过程,即需要不断的尝试新方法;
- 它是在迭代中形成的;
- 关键的设计理念
- 管理复杂度:非常重要,软件的第一位的技术使命;
- 区分外在暂时的难题和内在本质的难题;
- 通过抽象,将系统分为多个子系统来降低复杂度;保持子程序的短小精悍,有助于减少思考的负担;
- 理想的设计特征
- 高扇入(被很多人依赖),低扇出(少依赖他人);
- 最小的复杂度、精简性;
- 可拓展性:增强时不会破坏原有的结构(需要思考可能存在的拓展,需求中应考虑标注)
- 可移植性:可快速转移到其他系统使用,要么系统无关,要么针对具体系统设计单独增加一层抽象的接口;
- 可重用性:每个组成部分可以在其他系统中被使用;
- 层次性(多个等级结构),松散耦合(最少的通信渠道即是一个例子,即应低扇出),易于维护
- 使用标准的设计:减少使用古怪的东西,让别人熟悉容易理解和上手(如有可能,尽量使用简单通用的解法);
- 设计的层次:
- 系统、子系统和包、类、数据和子程序、子程序内部;
- 严格限制不同子系统之间的通信;原因:越少的通信渠道,越便于维护(低扇出);
- 常用的子系统:业务规则、数据库访问、用户界面、对系统的依赖性、应用程序;
- 管理复杂度:非常重要,软件的第一位的技术使命;
- 设计构造块:启发式方法
- 找出现实世界中的对象:不要想系统能做什么,而是它在模仿谁;
- 确定对象及其属性
- 确认对象可以进行的操作;
- 确定对象可以对其他对象进行的操作;
- 确定对象哪些部分对其他对象不可见;
- 确定对象的公开接口和不公开接口;
- 继承:当继承能简化设计时考虑使用继承;
- 隐藏信息:
- 实现自增 id 的例子,用全局变量+一行语句,还是一个独立的函数;
- 从“应该隐藏什么”出发,去思考如何设计;原因:这样会让因内部变化而给外部引用,带来最小的变化;如果暴露过多的内部细节,会导致当出现变化时,外部也出现大量的变化;(内部应该尽量设计高的抽象)
- 应对变化:
- 思考最有可能出现变化的部分,花费最多的时间抽象它,以便当变化出现时,改动成本最小;最不可能出现变化的部分,则花费时间最少;
- 思考一个模块的最核心功能的最小集合,它变化的可能性最小;然后在这个集合的基础上进行扩展,延伸的附加功能,出现变化的概率逐渐变大;
- 常见的变化:业务规则、对硬件的依赖、输入输出、难度高的设计和构建、状态变量、非标准的语言特性;
- 保持松散耦合
- 耦合标准:
- 规模:连接的数量,越小越好;
- 可见性:连接的显著程序,越公开越明显越好;
- 灵活性:是否容易修改,越容易越好;
- 总结:模块越容易被其他模块调用,则它们之间的关系越松散(理想的情况下,模块对其他模块的依赖越小,越是自洽,它跟其他模块的耦合越小,即低扇出);
- 耦合的种类
- 简单数据耦合:OK,只传一个或多个数据;
- 简单对象耦合:OK;只传一个简单对象;
- 对象参数耦合:一般,原因:需要了解对象具体有哪些参数,提高了复杂度(前后端的数据接口即是一个例子);
- 语义上的耦合:非常差,原因:需要了解另外一个模块的规则(这种耦合非常可怕,维护成本非常高);
- 耦合标准:
- 设计模式
- 优点:
- 前人也遇到了相似的问题,并思考了好的解决方案。相比自己重新造轮子,采用他们的方案可以省去时间和犯错;
- 方便了团队的交流,原因:模式提供了更高抽象层次的思路,让沟通更快速;
- 缺点:避免为了模式而模式,强迫代码去适应模式,而没有认真思考两个场景是否匹配;
- 按王垠的观点,设计模式一书中的20个模式,在很多动态语言里面已经透明化了,即内置成为其特点的一部分,导致感觉不到它们的存在;而 Java 语言由于其不能传递函数的局限性,导致需要做一些模式的设计;因此千万不能本末倒置,以为一定要用设计模式才是高级的,事实上很有可能把一件简单的事情搞得复杂化了,反而让其他人看不懂代码;
- 优点:
- 其他启发方法
- 高内聚性:子程序内部的方法紧密围绕类的中心目标;原因:内聚程序越高,越容易理解和记住代码的功能所在;如果分散,则让人费解;
- 构造分层结构:当对复杂的事物进行分层后,可以将大脑从大量的细节中解放出来,只关注当前层次的关键信息,有利于更好的思考问题;
- 严格描述类契约:在类的对外接口调用过程中,详细描述一个需要遵守的规则,有利于减少错误的发生(示例:如果你提供数据x,y,z,并承诺让这些数据具备a,b,c 的特征,则我将基于约束8,9,10 执行操作 1,2,3);
- 为测试而设计:问自己一个问题,如果为了方便更好的测试,系统会如何设计?原因:站在测试的角度进行思考,有可能会使得设计更加的规整,减少相互依赖和耦合,降低复杂度;
- 失败的案例:思考一遍前人失败过的案例,有利于避免在同一个地方摔倒;
- 有意识的选择绑定时间:将某个值,绑定到某个变量的时机;早绑定比较简单,但晚绑定则具备更多的灵活性;
- 创建中央控制点:将控制点放在一个集中的地方;原因:有利于后期的维护(例如将直接赋值设为变量的引用,将在一个统一的地方定义变量);
- 画一个图:原因:当使用图形的时候,就会逼迫大脑进行抽象的思考;
- 考虑使用蛮力:优雅的算法固然很好,但蛮力经常也可以到达目的地,虽然不怎么优雅,但时间相差甚少;
- 保持设计的模块化:类似函数式编程,给定预定的输入,得到预期的输出,而不用管里面发生了什么;思考如何将一堆黑盒子组装成一个系统;
- 分配职责:思考一个对象应该为什么负责,不为什么负责;
- 启发方法的原则
- 不要卡在单一的方法上。如果一个方法行不通,尝试其他方法,UML图,草图,测试,伪代码等;
- 无须马上解决所有的问题。如果有些问题很难,将它们放一放,等过一段时间再回来看看;
- 设计实践
- 迭代:每一次迭代,每一次从上而下和从下而上的换位思考,每一次尝试一种新的解决思路,都会带来不一样的洞察力。使得再一次设计的方案比上一次更好;没有最好,永远只有更好;
- 分而治之:程序很大很复杂,不要一下子考虑全部东西。每次只集中只解决一小片问题即可;
- 自上而下(分解)和自下而上(合成)结合使用;
- 制作原型:如果一个问题的答案不够显得易见,则可以考虑建立一个小的原型去试验它;此处陷阱:试图将原型的代码用于生产;应对方法:用其他语言来实现原型;
- 合作设计:如果是为了找到更好的解决方案,则可以先将自己的解决思路分享给其他人,然后听取他们的思考和反馈;三个臭皮匠,顶个诸葛亮;
- 设计要做多少?
- 取决于团队的经验丰富程度,如果高,则设计可低;如果低,则设计要高;
- 最大的设计风险,不来自于某个困难的问题,而是来自于对简单部分的轻视;很少出现因为设计过多而带来问题,问题常常来自那些设计不足的部分;
- 80%的时间应该用于寻找探索更好的设计方案,仅将20%的时间用来制作简单的文档,文档好看不重要,重要是好用;
- 记录设计成果
- 将设计文档插入到代码的注释段落中;
- 使用 Wiki 来记录;原因:特别方便异地的团队共享成果;
- 使用数码相机,将给一些草图拍照(大大减少了画正式图的时间,效果却很接近);
- 写总结邮件:开会后,将开会内容整理一下,发给相关人员;原因:这样一旦有疑问,大家知道在哪里可以查阅;
- 保留设计挂图;原因:贴在某个地方,大家可以随时查看;
- 在适当的细节层,创建 UML 图;
- 找出现实世界中的对象:不要想系统能做什么,而是它在模仿谁;
- 设计中的挑战
- 可以工作的类
- 抽象数据类型
- 抽象数据类型(ADT):ADT 的涵义其实相当广泛,它可以代表任何一个现实世界的实体,也即对象,它不仅含有数据,还包含了对数据的操作;
- 设计 ADT 时,尽量在最高的抽象层次上工作;
- 类是实现抽象的一种好方法,而 ADT 是实现类的基础;让我们可以专注一项事情上面,而忽略其他事情,不用关心它的细节,让我们更好的思考;
- 良好的类接口
- 设计类的第一步:设计接口,满足两点:合理的尽量高层次的抽象,隐藏抽象以下的细节;
- 原则
- 接口应该体现一致的抽象层次;
- 务必理解类所要实现的抽象是什么,并且要隐藏什么;
- 把不相关的信息,转移到其他类中;
- 提供成对的服务:但也不盲目增加反向操作;
- 尽可能让接口可编程,而不是表达语义:将规则转化成代码,让机器自动检查(例如使用 assert )(莫非即是防御性编程?),而不是靠语义约定,不然会埋下隐患;
- 不要添加与抽象不一致的公用成员进来;谨防在修改时,破坏了原来的抽象层次;
- 良好的封装
- 尽量限制类和成员的可达性:如果出现纠结的情况,遵循最严厉法;
- 不暴露成员数据;
- 类接口不包含类的实现细节:更好的做法,将类的接口和类的实现隔离开;
- 不对类的使用者做任何假设(防御性编程?)
- 避免使用友元类(因为友元类可以访问类的私有成员和保护成员,破坏了信息隐藏原则);
- 即使子程序仅使用公用子程序,也不要放入接口;原因:会破坏抽象;
- 让阅读代码比编写代码更方便;原因:不要为了方便破坏抽象,这样会导致代码难以阅读和理解;
- 格外警惕从语义上破坏封装性:调用方代码没有依赖类的接口,而是依赖类的实现细节;原因:这样会导致严重的后果,当类的内部实现细节发现变化时,就会马上出现错误;抵制诱惑:当根据类接口文档看不懂如何使用这个类的时候,应该让作者重写文档,而不是自己分析类的实现,然后基于这个实现去调用它;
- 留意过于紧密的耦合关系:Demeter 法则,仅使用一个点法则(只能访问直接的朋友,不能访问朋友的朋友);
- 设计和实现的问题
- 包含
- 通过包含来实现 has a 关系;
- 不到万不得已,不通过 private 继承来实现 has a 关系;原因:此时外层的包含类,可以访问 private 类的 protected 成员函数和数据,会破坏封装性,造成隐患;
- 警惕拥有超过 7 个数据成员的类;(如果超了,考虑创建子类)
- 继承
- 用 public 来实现 is a 关系;如果一个派生类,不打算遵守基类的全部接口,就不要使用继承;要么改成包含,要么考虑修改基类;
- 要么使用继承并详细说明,要么不要用它;如果某个类不可继承,在定义时写明不可继承(例如java 的 final)
- 遵循 LSP 替换原则(可替换原则):派生类必须能够通过基类的接口进行访问,且调用者无须了解差异;
- 确保只继承需要继承的部分;原因:如果只想继承实现,而不想继承接口,应使用包含的方式;
- 不要覆盖一个“不可覆盖”的成员函数;原因:这样会给使用的时候带来困惑,埋下隐患;
- 共用的接口、数据、操作,放在继承树中尽可能高的位置;原因:这样派生类可以更容易调用它们;多高呢?高到再进一步会破坏抽象的位置;
- 只有一个实例的类是值得怀疑的;(单件模式除外)
- 只有一个派生类的基类,是值得怀疑的;(可能过度抽象了,避免想得太超前)
- 派生类覆盖了某个子程序,但其中没有任何操作,是值得怀疑的;原因:说明基类的子程序很可能设计的有问题,这个子程序可能应该是包含的关系(可有可无),而不是成员函数(必须有);
- 避免过深的继承:尽量不超过2-3层,所有层数合计的派生类总数不超过7个;
- 尽量使用多态代替大量的类型检查;频繁使用 case 的时候,可能需要考虑使用多态;
- 避免多重继承;
- 四项基本规则
- 如果多个类共享数据,但不共享行为,应该让它们包含某个对象;
- 如果多个类共享行为,不共享数据,应该让它们继承基类,在基类中定义共用的子程序;
- 如果多个类既共享行为,也共享数据,应该让它们继承基类,并在基类中定义共用的数据和子程序;
- 当想让基类控制接口时,使用继承;当想自己控制接口时,使用包含;
- 成员函数和数据成员
- 子程序尽量少;原因:数量越多,出错率越高;
- 一次调用的子程序尽量少;(即低扇入)原因:扇入越高,出错越高;
- 避免间接调用,Demeter 法则;原因:耦合太高,维护修改麻烦;
- 禁止编译器产生不需要的默认方法(将 public 改为 private)目的:禁止调用方代码访问它们;
- 总则:尽量减少类与类之间的合作范围;目的:减少耦合;
- 构造函数
- 如果可能,尽量在所有构造函数中初始化所有数据成员;(防御式编程,避免引入不可控的因素);
- 单件模式下,强制私有化构造函数;原因:避免构造函数被外部调用,导致单件模式失效;此处需配合公开的静态方法接口来实现;
- 优先使用深层拷贝(即 clone,浅层拷贝只复制了指针)原因:减少不可知的相互干扰和检查,大幅降低复杂度;
- 创建类的原因
- 为现实世界建模;
- 对抽象概念建模;例如形状的概念
- 降低复杂度;
- 隔离复杂度;
- 隐藏实现细节;原因:减少对实现细节的关注,可以减少代码间的耦合;
- 控制变动影响的范围;
- 隐藏全局数据;
- 让参数的传递更方便;(如果发现一个参数在多个子程序间传递,可能需要考虑将这个参数和相关子程序重新建一个类)
- 建立中心控制点;
- 为程序变动做好准备;
- 让代码易于重用;
- 方便模块化;
- 方便实现重构;
- 应该避免的类
- 万能类;失去了类的意义;
- 只有数据没有行为的类;可能适合将数据放到其他类中;
- 只有行为没有数据的类;可能适合将行为放到其他类中;
- 包:超越类
- 编程语言的进步,在于我们可以在越来越高的抽象层次上编程;最早是符号,后来是语句,再后来是函数,再后来是类,未来可能是包或模块;
- 包含
- 抽象数据类型
- 高质量的子程序
- 使用子程序的原因
- 降低复杂度
- 提供容易理解的中间抽象;
- 减少重复的代码;
- 方便创建子类;
- 隐藏操作顺序;
- 隐藏指针操作;
- 简化布尔判断;
- 提高可移植性;
- 提高性能;
- 子程序的设计
- 原则:功能上的高内聚性(如果做不到,则应考虑拆这个子程序;尽量让人通过名字,即知道它是干嘛的,让它有自解释性)
- 不理想的内聚
- 顺序上的内聚性:包含特定顺序的操作,操作需要共享数据;
- 通信上的内聚性:只共享了相同的数据,但不同的操作之间没有任何联系;
- 临时的内聚性:需要同时执行然后被放到一起的操作;
- 混乱的内聚性:操作与操作之间完全无关;
- 逻辑上的内聚性:几个操作被放在一起,通过传入控制标志调用其中一个操作,而操作之间却没有关联;
- 好的子程序名字
- 描述所做的所有事情
- 对返回值有所描述;
- 长度合适
- 过程的命名使用动词+宾语
- 优先使用对仗词
1. - 为常用操作制定命名规则;原因:方便团队协作,别人可以快速理解自己的子程序如何使用;
- 避免使用意义模糊的动词;
- 避免使用数字区分命名;
- 子程序的参数
- 按输入、修改、输出来安排参数的顺序;
- 如果几个子程序共用一些参数,让它们的顺序也保持一致;原因:降低学习成本;
- 不会使用的参数,去除掉;
- 状态变量或者错误变量,放在最后;
- 不要将参数当作工作变量,另外新建一个;原因:避免修改原始参数,然后不小心错误引用;
- 在接口中对参数的假定加以说明(定义时就说明,不要等子程序写完再回来补,因为那个时候已经忘了)
- 参数个数不超过7个;
- 参数用对象的部分值,还是整个对象?取决于哪一种更符合接口的抽象层次;
- 如果参数数量很多,可以考虑增加一个函数,用来将形式参数和实际参数映射起来;原因:这样可以避免因参数位置放错,导致的错误;
- 函数的返回值
- 如果函数内部有多条路径,应仔细检查每条路径的返回值;原因:避免返回不想要的结果;
- 不要返回函数内部数据的引用或指针;原因:函数结束后,内部数据销毁;应将返回值保存为类的数据成员,并提供访问器来读取这个数据
- 避免使用宏;
- 尤其是避免使用宏来替代函数;
- 使用子程序的原因
- 防御式编程
- 警惕数据可能非法
- 检查所有外部数据;(如果出现错误应如何处理?是否设置报错规范?将报错信息写在返回值里面,抛出错误?)
- 检查函数参数;
- 断言与错误处理
- 一般二者用其一,如果程序很大很复杂,或者对可靠性要求很高,也可以结合着用;
- 建议
- 断言:
- 用于检查代码中的BUG;
- 用于验证前条件和后条件;
- 错误处理:处理可能发生的错误输入
- 高健壮性:
- 先用断言验证
- 再做错误处理;
- 断言:
- 错误处理技术
- 返回一个中性值;返回上一个正确的值;返回下一个正确的值;返回最接近的合法值;
- 把错误信息记录到日志中;
- 返回错误代码:缺点:需要确保依赖调用方会按错误代码处理;
- 调用统一的错误处理的子程序;(这种方式不错,例如,可以发送一个邮件出来,附上相关信息,并登记日志)
- 显示出错提示;缺点:有可能让界面相关代码变得混乱分散;有可能黑客会利用信息发现漏洞;
- 局部自行处理;缺点:同上;
- 关闭程序;
- 健壮性与正确性:根据场景而定;生命攸关的程度,正确性更重要;普通程序,用户更关心健壮性;
- 异常:当程序不知如何处理时,它会抛出异常,由程序的其他部分进行处理;异常提供了一个让错误不可能被忽视的办法 ,使得错误必须被处理;
- 只有在真正例外的情况下,才抛出异常;如果能够局部处理,则当下处理,不要乱抛;
- 避免在构造函数和构造函数中抛出异常;原因:此时抛异常,会导致资源泄漏;(运行构造函数,发现非法数据怎么办?是否应该在运行构造函数之前进行检查?)
- 在恰当的抽象层次抛出异常;原因:避免泄露程序的实现细节;对于异常的描述,应该合理抽象,不要透露有关的实现细节;
- 在异常消息中,加入所有能想到的关于导致异常出现的信息;原因:方便问题定位;(对内部使用的消息,则异常的消息应该尽量详细)
- 考虑创建一个统一的异常报告办法;例如异常的种类、如何处理、如何格式化异常信息等(嗯,非常有必要,可以提高效率);
- 谨慎考虑是否需要使用异常,因为有时候直接让程序崩溃掉,是更好的做法;
- 隔栏:在数据进入内部之前,先对数据进行检查和清洗,有错误进行处理;之后,内部程序不再检查,假定它们都已经是安全的了;
- 进攻式编程:开发环境中,发生错误的地方,直接终止程序;生产环境中,则使用错误处理包容可能发生的错误,程序不终止;
- 保留多少防御式代码
- 保留检查重要错误的代码;
- 去掉检查细微错误的代码;
- 去掉会导致程序崩溃的代码;
- 保留可以让程序安全崩溃的代码;原因:这种类型的崩溃,不会带来大的损害;
- 保留可以记录错误信息的代码;
- 确认保留的错误提示信息是友好的;
- 警惕数据可能非法
- 伪代码编程
- 原则
- 用接近日常的语言来编写,而不是用编程的语言(可以考虑使用小黄鸭技术,即使用小黄鸭才能听懂的说法);
- 用意图来编写,而不是用编程语言的语法元素;
- 在靠近尽量低的代码层次编写(一开始可以在高的抽象层次,然后逐步深入)
- 好处:方便迭代思路,方便变更,方便形成注释;方便他人理解代码意图;
- 设计子程序
- 检查先决条件:是否必需?目标是否清晰?是否与整体设计匹配?
- 定义要解决的问题:已经有什么?要做什么?返回什么?
- 起个好名字:如果名字很模糊,说明这个子程序可能也是功能模糊的,需要进一步拆解;
- 想想测试:站在测试的角度,想想要如何测试它;(给定正确输入/边界输入/错误输入,检查输出)
- 考虑错误处理:例如错误的输入(是否设计全局的统一错误处理);
- 考虑是否已经有现成的库可以解决:避免重复造轮子;
- 考虑算法和数据类型:如果出现库都无法解决的问题,则应该考虑去查查算法书(先要有一本算法书);
- 性能问题:除非是面向稀缺资源而设计,不然不考虑这个问题;
- 数据类型:如果数据操作是子程序的重点,则应该先把数据类型定义好;
- 编写伪代码:先写头注释,再写逻辑;
- 检查伪代码:先自己复查一下,然后想一下如何向别人解释这些伪代码,再对照一下伪代码,如果可以,可以找人来听听自己的解释,或者帮忙看看伪代码(或者使用传说中的小黄鸭技术);
- 迭代伪代码:优化、分解(直至觉得再写下去是浪费时间)(这一步非常有必要,因为在伪代码上面迭代,要远远比真实的代码容易得多)
- 编写子程序的代码
- 将伪代码加上注释符,变成注释
- 写声明,加上头尾花括号;
- 在每行注释下面填充代码;
- 检查填充的代码是否需要进一步分解(如果段落很大,考虑分解它,单独另建一个子程序);
- 检查代码
- 在脑海中检查:如果很难,表示程序写得太长了,考虑分解改得短一些;(代码是抽象的,更好的思考方式,是在脑海中用图形将它具象它,例如可以使用管道与房间的隐喻)
- 编译代码:仅在自己能够完全自信不会出错的情况下再编译;原因:一旦开始编译,心态便会转变,急着完成它,难以静下心来认真思考;
- 在调试器中逐行执行代码;
- 测试代码:通过测试用例、模拟数据对代码进行测试;
- 消除程序中的错误:如果一段代码漏洞百出,则应考虑重写;原因:出现这种情况,说明很可能没有弄明白这个子程序是要干嘛的;
- 收尾工作
- 检查接口:输入输出参数都参与计算,没有冗余参数;
- 检查质量:只干一件事、干好一件事、松散耦合(高扇入,低扇出)、防御式设计;
- 检查变量:未经定义的变量、未经初始化的对象、初始化后未被使用的对象(最好使用工具来帮忙做这个事情,一般IDE可以);
- 检查语句和逻辑:错误的嵌套、死循环,资源泄漏;
- 检查布局:空白是否正确(建议团队内制定统一的布局风格);
- 检查文档:注释、说明、描述等信息是否正确;
- 去除冗余注释;
- 原则
- 使用变量的一般事项
- 变更定义
- 关闭隐式声明的功能;原因:很容易带来各种不是发觉的错误;
- 所有要用的变量都进行声明(最好使用IDE或者相应的工具帮忙做这项检查);
- 遵循固定的命名原则;原因:避免同一个变量,出现两个命名;
- 利用第三方工具对变量声明进行检查;例如 jsLint;
- 初始化的原则
- 就近原则;
- 声明与初始化同时进行(即声明和定义同时);
- 如果可能,尽量使用 const 或 final ;原因:可以防止变量被重新赋值,减少错误发生的可能性;
- 在类的构造函数中,初始化所有数据成员;
- 特别留意计数器和累加器,例如: i, j, k, sum, total 等,建议使用更具有意义的名字,命名花不了多少时间,但这样做是值得的;
- 如果变量会被循环多次使用,注意是否需要重新初始化;
- 如果编译器能够警告未经初始化的变量,打开它;
- 检查输入参数的合法性(同上章的防御式编程原则)
- 如需使用指针,可以安装内存检查工具来帮忙检查;
- 如需操作内存,在程序开始时,初始化工作内存(可以避免错误);
- 作用域
- 变量尽量局部化:短跨度、短生存
- 原则
- 循环用到的变量,循环开始前再初始化;
- 变量直到使用前,才开始初始化(声明+赋值);减少了变量的生命周期,也就减少了大脑的负担;
- 同一变量相关的语句,尽量放在一起;如有可能,提取成子程序;
- 开始先采用严格的可见性,后续根据需要逐步扩大;原因:由俭入奢易,由奢入俭难(一旦到处引开,修改起来就会变得很困难);
- 持续性
- 变更的生命周期比预期的短;要假设数据已经过期,养成在使用前才开始声明和赋值的好习惯;
- 如有必要,使用断言对变量值进行检查;
- 绑定时间
- 晚的优点是灵活,缺点是增加复杂度,需要在灵活性和复杂度之间取得平衡;
- 从早到晚依次如下:
- 写代码时:使用硬编码赋值;
- 编译时:使用具名常量,使用时引用;
- 加载时:通过从外部读取文件;
- 对象实例化时:通过构造函数赋值;
- 实时:要用到时,再实时去获取;
- 数据类型与控制结构的关系
- 序列型数据,对应顺序语句;
- 选择型数据,对应判断语句;
- 迭代型数据,对应循环语句;(需要反复进行操作的数据,通常保存为窗口中的元素)
- 保持单一用途
- 避免让变量具有隐含的含义,比如一个表示页数的整数变量,即用 -1来表示出错,实际上出错是一种状态,应该使用一个单独的布尔变量;
- 确保使用了所有已声明的变量;原因:没有使用的变量,会为程序埋下隐患,增加出错概率;
- 变更定义
- 变量名的力量
- 注意事项
- 变量名要能准确的代表所描述的事物;
- 长度控制在8-16个字符左右;
- 体现 what ,不体现 how,因为它是事物,不是动作(子程序才是一种动作);
- 对位于全局空间的变量,加上限定词(例如ui_Employee, db_Employee)(如果语言支持,使用 namespace 来划分变量的作用域)
- 对于计算结果的值,事物名放前面,限定词放在后面,例如 applesTotal, applesEverage,而不是 totalApples,原因:名词可以让人第一眼认为它所代表的事物;
- 避免使用 Num,改成 total(总数场景),或 index(下标场景)
- 为特定类型的数据命名
- 循环变量:如果可以,避免使用 i,j,k,特别是有循环嵌套的时候;可以考虑改用高阶函数map, filter等;
- 状态变量:避免使用 flag,因为它啥也没说;改用能准确描述某种事物的状态的命名,例如 reportReady;(当发现自己需要猜测某段代码的含义时,表示需要更改变量的命名了)
- 布尔变量:
- 常用的典型:done, success, found, complete,如果可能,在这些名字前面加上前缀,例如:requestDone, deleteSuccess, recordFound, updateComplete;
- 使用肯定的变量名,而不是用否定的,例如应该避免使用: notFound, notDone,;原因:出现 if not notFound 就尴尬了;
- 枚举变量:给成员加上前缀,区分它们是一伙的,例如 color_red, color_blue, color_yellow;如果编程语言本身自动要求加前缀,则可以不再自己加;
- 具名常量:用能够准确代表事物本身的名字,而不是值本身;
- 制定命名规则
- 好处:低复杂、易交流、低认知、少错误、强关系;
- 使用场景:多人协作、工作交接、他人阅读、大型项目、长期项目
- 非正式的命名规则
- 区分变量名和子程序名:变量名用小写开头,子程序用大写开头;(但这样的话,貌似子程序跟类又容易混淆起来,一点区别:子程序是动词开头,而类是名词)
- 区分类和对象:类用大写字母开头,对象加上更明确的名字,例如:Widget 和 employeeWidget
- 标识全局变量,例如:g_color
- 标识成员变量,例如:m_employee,表示它是类的数据成员;
- 标识具名常量,例如:c_ramda
- 标识枚举类型元素,例如:e_weekday( e 表示 enumerate)
- 如果语言不能保证输入参数不可修改,则使用 const 前缀加以标识输入参数,例如 constDate,这样当在程序中尝试为这个变量进行赋值时,就知道写错了
- 格式化命名:例如采用驼峰或者下划线中的一种(理论上来说,下划线的易读性比驼峰好,尤其是多个单词连在一起时);
- 缩写
- 避免使用缩写;
- 如果必须使用缩写,建议做一份缩写的文档,每个人想要创建某个缩写的时候,就先登记到文档中;原因:由于文档比较麻烦,减少了创建一些不必要的缩写的情况;二是方便将来可以回顾查看;三是如果有人已经创建了某个缩写,可以避免重复;
- 应该避免的名字
- 不要在名字中使用数字;
- 避免使用相似含义的名字,例如 fileNumber 和 fileIndex
- 避免使用含义不同但相似名字的变量,例如 clientRecs 和 clientReps,建议为 clientRecords 和 clientReports
- 不要使用与变量的意义完全无关的听名字,例如用某个人的名字来命名;
- 注意事项
- 基本数据类型
- 原则
- 不要比较不同类型的数据,应先将它们转换为同一种类型;
- 让类型转换显化,例如 x = y + int(z)
- 不要使用神秘的数值,即不要将某个具体的值硬编码到代码中,例如 x > 100,而应该使用具名常量来代替,例如 const max = 100; x > max(原因:数值硬编码很维护,而具名常量则很简单)
- 预防除零错误,凡是使用除法的地方,都要注意这条规则;
- 重视编译器的警告,并消除它们;
- 整数
- 检查整数溢出;
- 检查整数计算的中间结果溢出;
- 检查整数除法,例如 7/10 = 0
- 浮点数
- 避免数量级差距很大的数值进行加减运算;如果需要这么做,则先将它们排序,然后从最小的开始加起;这样虽然不能完全解决精度问题,但可以使用误差降到最小;
- 避免等量判断;原因:浮点数有些时候只是实际值的近似,所以判断相等很难;更好的做法是判断误差控制;比如设置 acceptDiff = 0.0001,然后 if (a - b) < acceptDiff
- 遇到对误差敏感的场景,例如计算金额,考虑换用更高精度的变量类型,例如双精度浮点值,或换用二进制编码的十进制(即 BCD),或者将小数运算,转成整数运算,或者使用语言内置的 Currency 类(货币)(如有的话)
- 字符串
- 避免使用神秘字符,改用具名常量来代替;原因:维护修改比较方便;国际化时翻译比较方便;内存资源紧张时,抽取单独存储比较方便;
- 如需国际化,使用 unicode;
- 在程序内部,统一使用一种编码,仅在输入输出的位置,进行转换;
- 布尔变量
- 可以用它对程序进行说明,例如:if( qty < maxQty || date > earlyDate) 改为:
- var qtyOK = qty < maxQty
- var dateOK = data < earlyDate
- if ( qtyOK && dateOK)
- 这样一来,变量就变得更加清晰和容易理解了;
- 可以用它对程序进行说明,例如:if( qty < maxQty || date > earlyDate) 改为:
- 枚举类型
- 提高可靠性,减少出错,原因:只会在约定范围内取值;
- 方便扩展,只需在里面增加新元素;
- 方便阅读,例如 chooseColor == colore_red, 比 chooseColor == 1 要容易理解得多;
- 可以作为布尔变量的替代方案;原因:当情况超过两种时,布尔变量无法胜任;
- 具名常量
- 避免使用具体数值,避免使用具体字符,统一使用具名常量去替换它们,成为一名剔除具体数值或文字变量的狂热爱好者;
- 如果语言不支持常量,则考虑使用局部作用域的变量来达到相同的效果;
- 数组
- 当超过下标边界访问数组时,容易出现意想不到的错误,因此,如有可能,每次使用数组前,考虑是否可以使用集合、栈、队列等来替代;
- 如果实在不行,养成只对数组进行顺序访问的习惯;或者,如果语言支持,使用迭代器(例如 map, reduce 之类)来顺序访问,避免手工写循环;
- 自定义类型
- 如果可以的话,尽量创建自己的自定义类型,而不是使用预定义类型;原因:隐藏底层实现,增加一层数据抽象,减少信息到处分发,更便于维护和修改,增加可靠性避免出错;
- 原则:避免跟预定义类型冲突;避免修改预定义类型;考虑建一个类,而不是使用 typedef
- 自定义类型的命名,应该以现实世界的事物为导向,而不是以程序底层实现为导向;
- 原则
- 不常见的数据类型
- 结构体(不清楚有哪些语言才支持,看完概念感觉结构很像一个只有属性值的对象)
- 可以用来明确数据关系(比如某些属性属于某些类型的成员)
- 可以更加方便维护(当成员有增减的时候,只需要改动结构体一个地方就可以了)
- 可以简化对数据块的操作,比如原来多个值的多条语句操作,现在变成了只需要一条语句就可以了;
- 可以更方便传递参数(将传递多个参数减少为只传递一个)
- 指针
- 指针存储的是一个内存地址,至于该地址里面的数据如何解读,取决于指针所标识的基类型;
- 指针的使用技巧
- 对指针的操作,封装在子程序或者类里面;原因:避免指针操作分散在程序各处,方便维护和修改;
- 同时声明和定义指针;原因:减少处在声明和定义中间的代码修改了指针;
- 在分配指针的作用域里面,删除指针(对称性操作);原因:出了这个作用域的操作,结果不可预知,很容易出错;
- 使用指针前先进行检查;检查指针指向的变量;使用狗牌来标识头尾,以便检查出已销毁的指针;
- 多用额外的中间变量,来提供代码的清晰度;不要过分追求精简,对于程序来说,提高的性能极其有限,易读性下降带来的损失却很大;
- 删除链表中的指针时,注意顺序正确,避免出现链表断裂;
- 给指针一片自留地,这样它可以优雅的退出。避免溢出时,造成原有数据丢失的尴尬后果;
- 创建一份指针列表,指针销毁后,从列表中消除;使用指针前,则进行检查,确保指针包含在列表中,避免错误的使用(这条建议非常的有用);
- 指针被删除或者释放后,将其至为空值;原因:当出现错误的引用时,能够通过编译器的报警,第一时间发现错误;
- C 语言中的指针
- 显式的指定指针的类型,而不是使用默认类型;原因:因为C语言不关心指针类型,只关心指向正确;因此,如果显式声明,则使用指针时,编译器会进行检查,尽早发现错误;
- 示例:char *pointerName, 或 int *pointerName
- 避免强制类型转换;原因:强制类型转换时,可能导致分配的空间变化,破坏了原有的数据;
- 在内存分配中,使用 sizeof 来确定变量的大小;原因:相当于增加了一层确认变量大小的抽象,由于 sizeof 是编译时动态计算的,这样可移植性很高,而且也不容易出错;
- 显式的指定指针的类型,而不是使用默认类型;原因:因为C语言不关心指针类型,只关心指向正确;因此,如果显式声明,则使用指针时,编译器会进行检查,尽早发现错误;
- 全局数据
- 隐患
- 使得代码不方便重用;
- 破坏了模块化的意义,让代码的复杂度上升
- 容易被无意间修改;
- 存在别名问题(当全局数据被作为子程序的参数进行传递时出现)
- 引入了初始化顺序的要求,增加了复杂度;
- 使用理由
- 模拟具名常量(原因:有些语言不内置支持,如 Python)
- 模拟枚举类型(原因:有些语言不内置支持,如 Python)(python 的 enumerate 函数或者 Enum 类,是否已经可以间接支持?)
- 简化对变量的引用(但是,应优先考虑通过访问器子程序来解决)
- 消除流浪数据(有时会存在一些过路的参数,只在后续的函数中引用)(此时有可能需要考虑是否拆分函数的功能,让其更简单一点)
- 保存全局数值
- 万不得已时
- 变量先设为局部,需要的时候再改为全局,而不是反过来;
- 区分全局数据和类变量,优先使用类变量;
- 优先使用访问器子程序;
- 通过访问器子程序访问全局数据的用法
- 只允许通过访问子程序来获取全局数据;
- 不要所有的全局数据都放在一起,而应该将它们分类一下;
- 访问子程序内部应该建立一个抽象,将各种方法归类一下,避免无意中修改到各个方法的实现细节;
- 访问子程序内部各方法的抽象层次应该一致(原因:这样让别人更加容易理解)
- 用锁定的方式,控制并发访问修改全局数据(如何实现呢?队列?)
- 降低使用全局数据风险的办法
- 建立一个命名规则,让人一眼就看出来某个数据是全局数据;
- 为全局数据建立一份用途的注释清单(好处:对团队中所有成员都有巨大的帮助);
- 不要使用全局数据存储中间结果,只能存储最终结果;(感觉全局数据最好是一个不能变更的常量,这样可以极大的降低使用的风险)
- 避免将数据扔到一个大对象中,然后到处传递;
- 隐患
- 结构体(不清楚有哪些语言才支持,看完概念感觉结构很像一个只有属性值的对象)
- 组织直线型代码
- 有顺序的语句
- 可能的情况下,最好写没有依赖关系的语句;
- 如果语句的执行必须有顺序要求,应该让这种依赖关系变得很明显;办法如下:
- 通过名称来体现,比如第一行语句中,有一个 ‘initial’ 字样;
- 使用参数来实现,例如第二行语句,调用第一行语句的返回结果;
- 引入状态变量,后续的语句通过检查状态变量,来判定是否按顺序正确执行(增加了代码的复杂度,有利有弊)
- 使用注释来实现(最最无奈的一种办法)
- 无顺序的语句
- 让代码能够自上而下的阅读,而不需要让眼睛在整段代码上下来回跳来跳去,这样增加了阅读的负担,让代码变得不容易理解了;
- 相关的语句放在一起,检验标准:通过给代码画框框来检查,如果框框有重叠,说明有些语句流落在外,有必要调整位置;
- 当相关语句放在一起时,就会发现有些部分,很适合组织成另外一个子程序,这样有利于模块化;
- 有顺序的语句
- 条件语句
- If 语句
- if 语句用于处理正常情况,else 用来处理异常情况,而不是反过来(原因:这样让代码更容易阅读和理解,避免阅读过程中思路被各种异常处理打断)(也可以避免出现 if not notFound 的搞笑情况)
- if 后面接一条有意义的语句,而不是放到 else 中(这样可以重点先看有意义的正常情况)
- if 后面永远要有一个 else,即使里面放一条空语句也没有关系;(原因:避免遗漏考虑其他情况)
- 注意等量判断是否使用正确,例如 < 和 <= ,> 和 >=(特别注意一下相等的情况,是否包括在内)
- else 语句也需要进行测试;
- 注意检查 if 和 else 的内容是不是写反了;
- if-then-else 语句
- 可以使用布尔判断来简化语句逻辑(相当于将每一次的判断,抽象成另外一个子程序,在里面执行判断,然后返回布尔值做为结果;这种做法可以大大降低代码的阅读理解难度);
- 出现频率最高的情况,放在语句的最上层;
- 如果可能,使用语句内置的 case 功能来替代这种语句;(case 更加直观易于理解)
- 最后的 else,放入异常处理,用来给自己提示没有考虑到一些出错的情况;
- case 语句
- 顺序
- 如果所有的 case 重要性都相同,那么可以按字母排序,这样比较容易检索;
- 如果重要性不同,重要的在前面;
- 如果发生频率不同,经常发生的在前面;方便别人快速查看常见情况;
- 如果有异常情况,正常情况在前面,异常情况在后面;
- 原则
- 简化每个 case 的操作;如果操作很复杂,则提取成一个子程序;
- 不要刻意制造中间变量,应该使用真实的变量;原因:因为 case 为执行严格的映射,创建的变量不一定符合这个映射,导致出错;如果 case 的数据结构复杂,改用 if-then-else
- 不要越过 case 的末尾(即每个 case 的末尾都有一个 break);
- 不要将 default 用于伪造的默认情况(即把最后一种情况当作默认情况,而非真正的默认情况),原因:这样会导致未来修改不方便,也容易出错;
- 建议将 default 用于错误检查,以便没有找到对应的情况时,尽快报错;
- 顺序
- If 语句
- 控制循环
- 带中途退出的循环
- 将所有的退出条件放在一起,而不是分散在程序各处,不然会给测试带来麻烦,也容易出错;
- 如果所有语言不支持直接中途退出,则使用注释说明下退出的操作意图;
- 循环控制
- 进入循环
- 永远只从头位置进入循环
- 紧挨着循环语句开始的前面位置,进行初始化;原因:未来如果有任何的变更和复制代码,不容易漏了初始化的操作;
- 优先使用 for 循环;如果确实 while 循环更适用的时候,使用 while 循环
- 使用 while(true) 来表示无限循环(可以在中间使用 break 结束);
- 循环体
- 用花括号 {} 将循环体包起来;原因:视觉上对起点和终点一目了然;
- 不要使用空的循环体;原因:容易让人莫名其妙;
- 一个循环只做一件事,避免做多件;如果担心性能问题,写条注释,说明此处可以合并,然后等将来性能出来问题时,再回来合并;
- 循环内务操作(例如 i++)只出现在两个位置,要么循环头,要么循环尾;不要出现在中间;原因:这样可以让代码更清晰;
- 退出循环
- 确保循环一定会退出(或者应该尽量 map 和 filter 之类);
- 确保循环的退出条件看起来很明显;
- 不要为终止循环,胡乱修改循环下标的值;
- 不要在循环结束后,使用循环下标来做其他事情;
- 提前退出循环
- 如果语言支持,使用带标号的 break ,这样可以明确知道是退出哪一层的循环(有嵌套的情况下)
- 如果能够不用 break 和 continue,尽量不要使用;如果一定要用,那么:
- 在循环开头使用 continue
- 不要让多个 break 散布在循环各处,而应集中在一个地方(原因:有利于在一个地方统一思考退出的场景,减轻大脑的思考负担;
- 循环变量
- 如果有嵌套循环,那么应该使用有意义的变量名称作为循环下标;这样也可以避免循环下标用串;
- 用整数或者枚举类型作为循环下标,而不是浮点数;
- 如果可以,让循环下标的作用域局限在其定义的循环体内(原因:更进一步减少串用的风险)
- 循环的长度
- 尽可能短
- 嵌套在3层以内;
- 如果太长,转移部分内容到子程序;
- 如果需要很长,限制只能有单一出口,即最多只有一个 break
- 创建嵌套循环的办法:由内而外,从最内层最简单的一件具体事情开始,然后逐步拓展到外面;
- 进入循环
- 带中途退出的循环
- 不常见的控制结果
- 子程序中有多处返回的使用场景(注:一般情况下不推荐有多个 return,除非是以下是以下两种情况,原因:如果有多个 return,当它们的间距很远时,会导致读后面代码时,忘记了前面的提前退出的可能性)
- 用防卫语句判断错误,提前返回;
- 如果多个 return 可以让代码变得更清晰,那么就使用它;
- 递归
- 递归是一个非常强大的工具,但不要把它用在不合适的地方,因为它也有两个缺点,一是不知道需要需要使用多少内存空间,可能存在栈溢出的风险;二是有可能速度很慢(存在重复计算的可能性,例如用来求解斐波契列);
- 在使用递归之前,先考虑下是否有可能通过栈和循环解决,实在不行的情况下,再考虑使用递归;
- 注意事项
- 限制在一个子程序内使用(即使用的时候,不要去调用另外一个子程序;原因:这样做会极大的增加复杂度,让代码更加不容易被理解)
- 留心栈空间;
- 确保递归可以终止;
- 使用安全计数器(避免出现无穷递归);
- 子程序中有多处返回的使用场景(注:一般情况下不推荐有多个 return,除非是以下是以下两种情况,原因:如果有多个 return,当它们的间距很远时,会导致读后面代码时,忘记了前面的提前退出的可能性)
- 表驱动法
- 表查询在很多情况下可以很好的处理复杂的 if-else 控制链(表驱动法:将各种情况存在表中,可以把它理解成数据库中的表,将各种情况存在一条一条的记录,然后根据需要去查询调用它们;如果场景数量不多,也可以硬编码写在数组里面,缺点是将来变动时,需要改动代码,存在表里面则不用);
- 在一些情况下,表驱动法还可以用来作为复杂的继承结构的替代方案;
- 当将数据存储在外部的时候,如果情况发生变动,甚至可以不改动代码,而只需简单更改一下数据库即可以满足变动的需求;
- 使用表驱动法需要先解决两个问题:
- 如何查询数据:直接访问法、索引访问法、阶梯访问法(或叫分段访问,或折半访问)
- 需要在表里面存一些什么内容,有时候只是数据,有时候是动作,然后需要将动作映射到另外的子程序名称中;
- 索引访问法:除了主数据表外,另外建一张索引表,通过访问索引表,获取主数据表的id,然后再访问主数据表;好处:
- 索引表一般都很小,相对于主数据表,它更加节省空间;
- 索引表的访问,可以做成一个独立的子程序,这样相当于多增加了一层抽象,未来也更加容易维护;
- 索引表的访问,速度一般更快;(注:基本上所有的数据库都支持索引技术,所以只需好好利用就可以了)
- 阶梯访问法:根据数据的分段,以及各段对应的值,写一个子程序,循环的进行判断,当超过某一临界点时,进入那一段,再次判断,如果超过,进入下一段,如果没超过,取值;
- 注意临界点的判断,是 > 还是 >= ,需要考虑清楚;
- 当列表很长时,考虑使用二分(折半)查找,而不是顺序查找,以便能够提高速度
- 将查找方法提成一个子程序;
- 如果对速度要求非常高,则应考虑是否可以使用索引的方法来替代;
- 一般控制问题
- 布尔表达式
- 使用 true 和 false 来做布尔判断,而不要使用 0 或 1,因为它们很模糊,经常容易用错;
- 隐式的比较布尔值,而不是显式,例如,应用 if (a > b), 而不是 if ((a>b) == true)
- 简化复杂的布尔判断的几种方法:
- 拆分复杂的布尔判断,引入更具名称意义的中间变量;
- 将布尔表达式封装成一个布尔函数,这样阅读的人也更容易理解,甚至无需关心函数里面具体是如何实现的;
- 使用表驱动法来替代复杂的 if else 判断;
- 尽量编写肯定形式的表达式
- 将 if 语句中的否定判断,转换成肯定判断,如果是错误处理,则可以通过引入一个中间变量或者封装一个布尔函数,同时做好命名,来达到目的;
- 考虑使用狄摩根定理来简化否定的判断,例如 if ( !Aok || !Bok ),改成 if ( ! (Aok && Bok))
- 尽量多使用括号让整个表达式更加清晰易懂,而不是依赖于语言的求值顺序;
- 对于数值表达式,采用与数轴位置相符的顺序进行书写,能够让整个表达式看起来更加直观易懂,因为它能够直接映射到大脑中的画面;
- 0 可以用于很多的比较场合,但是除了真正处理数值以外,其他场合都不建议使用跟 0 做比较(包括布尔,字符,指针等场合),因为那么样很不直观,不容易一眼看明白是什么意思;
- 空语句
- 由于空语句并不常用,所以有必要突出它;如果可以的话,可以考虑通过一个通用的 doNothing( ) 或者 pass( )子程序来处理;
- 避免出现深层嵌套,方法如下:
- case
- if-then-else
- 抽取子程序
- 对象的多态分派;
- 重复判断一部分条件;
- 引入状态变量
- 使用防卫语句提前退出;
- 使用异常;
- 结构化编程
- 思想:单一入口,单一出口;
- 程序只由三种结构组成,包括:顺序,选择(例如 if-else, case 等),迭代(即循环)
- 非以上三种的其他部分,使用时值得警惕,包括 break, continue, goto, throw-catch, return;
- 控制结构与复杂度
- 由于大脑在同一时间能够记忆的东西是有限的,因此,如果需要处理的东西越少,大脑便能越清晰的思考问题,使它不容易出错;反之,如果东西越多,则大脑越容易发生混乱,从而导致出错率上升;
- 如果可能的话,一段程序中的决策点数量,不要超过5个;
- 布尔表达式
- 软件质量概述
- 质量的外在特性和内在特性:前者是用户关心的(正确、可用、可靠、快速、兼容、精确、健壮),后者是程序员应该考虑的(可移植,可重用、可维护、灵活、可读、可理解、可测试);
- 提高软件质量的方法
- 明确质量目标:如果不明确,可能需求人员预期的质量目标和开发人员实际完成的目标,会存在偏差;(目标示例:程序可读性、减少内存占用、最少代码量、减少计算时间等)
- 明确质量优先级:明确将质量放在第一位,会促使开发人员有意识的调整自己的行为;
- 明确测试的策略:测试不是提高质量的最好办法,它只是一种预防措施,避免出现事故;如果将它当作首要办法,将出现大问题;
- 软件开发指南:将一套成熟的开发流程,实施到日常的开发活动中,有助于提高质量水平,减少各种散乱的作法;
- 非正式检查:开发人员自己检查代码,或找同事帮忙检查;
- 正式检查:闸口式的检查,过了一关,才能进入下一关;它的目标不是要求软件尽量完美,而是评估做到何种程度,算是已经完成预定的首要目标,可以开始下一阶段的活动;
- 好的开发实践
- 对需求变更进行严格的控制:失去控制的需求变更,将来带来灾难;在需求进入开发之前,确保需求是已经被认真思考过的,而不是拍拍脑袋决定出来的;在对需求变更时,为其设置审查的关卡,确保需求的变更,不会轻易进入到开发环节;
- 结果的量化:对质量改善的结果进行量化;无法量化,或者没有量化的质量控制办法,是无效的,因为我们不知道到底那些做法起了作用,以及起了多大的作用,以及无法做出针对性的调整;
- 制作原型:使用原型,可以极大的完美软件的设计,更接近用户的需求,以及更好的可维护性;包括:界面的原型(可用性),算法的原型(性能),数据集的原型(内存);
- 不同方法的效能
- 多种方法组合的效能,比单一方法高;从多个角度观察事物,总会暴露出更多的问题;
- 检查比测试的成本更小;
- 修正缺陷的时间点越早,修正的时间成本越低;原因:因为此时开发人员对代码的印象更深
- 推荐的组合拳
- 对所有的需求、架构及关键部分的设计,进行正式的检查;
- 建模或者创建原型;
- 代码阅读或者检查;包括:个人检查 desk-checking、代码复查 code-review 等
- 执行测试;
- 控制需求
- 越早发现错误,带来的影响越小,而需求错误,将带来一系列严重的后果;因此,对需求的有效性,进行严格的控制,可以减少大量的错误返修成本;
- 软件质量的普通原理
- 在很多时候,很多公司花了大约50%的时间,在调试各种错误,而不是编写代码上;因此,执行严格的质量控制,反而可以提高软件开发的效率和速度,从而降低开发的成本;与传统的“编码-测试-调试”相比,先进的质量控制计划,更加省钱和省时;
- 协同构建
- 协同开发的概要
- 它是质量保证的补充:当人们意识到他们的代码会被检查时,他们就会在潜意识里面,更加认真对待并检查自己的代码,从而减少了错误发生的概率;
- 协同构建有利于知识和经验的传播:它可以在短时间内,将小组内的开发人员,都提高到优秀程序员的水平;
- 集体所有权的好处:单个程序员离开的影响最小化、缺陷可以更快的被修正(谁有空谁上)、多人的检查使得代码的质量更好;
- 在构建前后的其他环节,建议都保持协作的习惯(包括评估、计划、需求、架构、测试、维护等);
- 结对编程
- 关键
- 采用统一的编码规范,原因:避免两个人将时间浪费在对编码风格的争论上;
- 不要在简单的问题上使用结对;如果出现这种情况,更建议两人在白板上画一下思路,然后各自行动;
- 鼓励双方跟上对方的步伐,如果两人差距太大不可弥合,可能需要拆散重组;
- 避免新手组合,两个人至少有一个要有结对的经验;
- 指定一名组长,由其对外联络并对结果负责;
- 不要让结对变成旁观,确保双方都积极主动的参与;
- 定时或不定时进行轮换;
- 确保两个人都可以看到显示器;(可以考虑外接到两个屏幕)
- 避免将关系紧张的人进行结对;
- 好处
- 人在轻微压力下,更容易保持专注的状态;
- 提高代码的质量:代码的可读性和可靠性向团队最优秀水平不断接近;
- 缩短开发的时间:构建的时间增加了10%-20%,但调试的时间减少了80%;
- 传播公司的文化、提高员工的技术、培养员工的归属感;
- 关键
- 正式检查
- 涉及的角色:主持人,评论员,记录员(有可能由评论员兼任),代码作者;
- 步骤
- 计划:主持人定哪些人参加,参加的时间地点,并提前分发材料和核对表,材料需要有行号,以便方便定位;
- 准备:评论员阅读材料,找出其中的错误;如果可以的话,给评论员分派不同的视角或场景(原因:站在不同的角度进行思考,可以发现更多的问题)
- 开会:
- 过程:主持人挑选一名评论员,阐述设计或代码的逻辑,并提出他发现的错误;大家进行讨论,确认这是否为一个错误;确认后,记录员记录下错误的类型和严重级别;讨论结束,进行下一个错误;
- 注意事项:
- 不要在讨论中涉及解决方案;作者享有对错误的接受和处理的最终权利;其他人的责任在于发现更多的可能的潜在问题;
- 会议不要超过2个小时(原因:人的注意力很难连续集中保持2个小时,超过限度后,效率开始大幅下降)
- 报告:会议结束后,主持人写一份总结报告,列出发出的所有缺陷和严重级别(目的:用于改进核对表;用于统计会议花费时间和找出缺陷的数量,可以计算成本和收益)
- 返式:将发现的缺陷,分发给作者进行修改;
- 跟进:可以有3种方法,一是重新来一次正式检查;二是只检查发现缺陷的地方;三是允许作者修改后不再检查;
- 小会:在不超过2个小时的会议结束后,如果有人对讨论解决方案有兴趣,可以另外安排1个小时的会议,用来讨论解决方案;
- 自尊心问题:设计和代码的作者对缺陷享有最终的确认权和处理权,其他人只是提出缺陷的可能性;开会过程中不允许出现批评,建议更多的使用“可能”语句,例如“这里可能有一个缺陷…”
- 其他协同开发实践
- 走查:貌似效果不太好;
- 代码阅读:其实它是正式检查的一部分,仅仅是省略掉了开会讨论的环节;对于异地分布的团队,这个方法更合适一些;
- 协同开发的概要
- 开发者测试
- 开发者测试的用途
- 评估可靠性
- 用于调试
- 制作错误检查表(避免下次犯同样的错误)
- 推荐方法
- 测试需求点:确保需求都已经被实现;
- 测试设计关注点:确保设计都已经被实现;
- 加入基础测试和数据流测试(什么是基础测试?详见下一节);
- 制作检查表,记录历史错误的类型;
- 写代码前,先编写测试用例(原因:先写,或者后写,所需时间没有变,但先编写,迫使更详细的思考和发现需求可能存在的错误;因为错误的需求,测试用例也很难编写;此时估计只能编写黑盒类型的测试用例)(按王垠的意见,很简单明显没错误的部分,可以不用写;没有把握的部分,要写)
- 测试技巧
- 结构化的基础测试
- 确保程序中的每条语句都会被执行一次;如果存在 if 语句,则需要计算有多少条路径,并根据每条路径设计一个测试用例;
- 数据流测试
- 数据状态有3种,分别是变量的已定义、已使用、已销毁;子程序的状态有两种,分别是已进入,已退出;它们的组合中,有8种反常的情况;在开始测试前,先检查一下,是否存在数据流反常;如果有,先纠正过来;
- 检查完之后,再进一步考虑是否已经覆盖所有“已定义-已使用”组合的情况;
- 貌似有专门的检查工具,可以考虑使用
- 等价类划分:去除冗余的测试用例
- 一个好的测试用例,应该可以覆盖输入数据中的很大一个范围段;如果两个测试用例所能提示的错误完全相同,则只需要一个测试用例就够了;
- 猜测错误
- 如果手头有一份过去总结的常见错误核对表,那么可以基于这份表格,对常见的错误进行猜测检查;
- 边界值分析
- 假设存在一个边界值,那么分别用小于、等于、大于这个边界值的数据进行测试,检查结果是否符合预期;
- 使用坏数据测试
- 数据太少
- 数据太多
- 数据错误
- 数据长度错误
- 数据未初始化;
- 使用好数据测试
- 正常的情形
- 最小的正常局面:例如 EXCEL 中只保存一个空的单元格,Word 中只保存一个空格;
- 最大的正常局面:例如 EXCEL 中将所有单元格都填写上数据;
- 与旧数据的兼容性:当使用新的子程序替代旧的子程序的时候进行;
- 使用容易进行手工测试的数据,例如 10000, 比 16549 要好(原因:前者易于手工输入,而它们所能提示的错误并没有区别)
- 结构化的基础测试
- 典型错误
- 80/20 法则
- 错误在程序中并非均匀分布,而是 80% 的错误分布在 20% 的类或者子程序中;(因此,王垠的方法,貌似更有道理了)
- 50 % 的错误存在于 5% 的类中;
- 20% 的类占据了 80% 的开发成本;
- 错误的分类
- 大多数错误的影响范围有限,并且容易修正;
- 大多数错误发生的根源在构建之外,例如缺乏应用领域的知识,频繁变动且矛盾的需求,缺乏有效的沟通和协调;
- 大多数编码错误是由程序员制造的,而由编译器或系统制造的极少;
- 拼写错误是一个常见的错误;
- 每个团队的常见错误类型可能不太一样(做一份自己的核对表很有必要)
- 测试本身的错误
- 测试用例本身也有可能包含错误,尤其是没有认真编写并谨慎对待的情况下;这时会导致花费很多时间在无效的代码错误排查上面;因此,有必要一开始像对待代码一样,认真的编写测试数据,并把单元测试集成到后续的测试中,确保测试数据不是一次性的使用,这样可以增加严肃对待的可能性;(此处又进一步证明了王垠的观点,测试过多,本身也会引入错误,浪费无谓的时间)
- 80/20 法则
- 测试工具
- 脚手架:哑类、伪造函数、哑文件;市面上有各种主流的脚手架工具,使用它们,虽然一开始会花费一点时间,但这是值得的,因为这些工作只需完成一次,未来就可以不断的持续使用,是一件投入产出比很高的事情;
- diff 工具:能够比对预期结果和实际结果的工具,例如 js 里面的 chai.js
- 测试数据生成器:如果可以的话,使用测试数据生成器,这样可以扩大测试用例的数据覆盖范围,发现一些常规少量数据难以发现的错误,并且过程是自动化的(产生相同错误种类的测试数据,只需要一份就可以了,关键可能还在于边界值);
- 覆盖率监测器:通过该工具可以发现现有的测试用例,覆盖了哪些代码,还有哪些没有覆盖,可以帮忙进一步完善测试用例;
- 日志记录器:用来记录日志;
- 符号调试器:它会一行一行的执行代码,然后观察代码产生的值,这样有利于观察整个程序是如何一步一步执行运转的(单步调试,跟踪变量的值),从而暴露出一些之前没有考虑到的问题;同时它也是一个了解所使用语言的好工具,可以更清楚的看到,各种高级语言是如何工具的;
- 系统干扰器:针对会对内存进行操作的场景,包括:内存预填充(用来发现未对变量进行初始化的错误)、内存抖动(用来发现内存引用是使用相对值,而非绝对值)、选择性内存失败(用来测试边界情况,例如内存溢出时程序的处理)、内存访问检查(用来确保所有的指针在程序运行期间都处于正常的工作状态)
- 错误数据库:BUG 跟踪处理工具,例如JIRA、禅道等
- 改善测试过程
- 测试计划:从重要性而言,有必要将测试放在同设计和编码一样重要的位置,并在项目开始之际,就为测试分配时间,提前拟定测试计划;目的:让测试可重复,然后可以进一步完善;
- 自动化测试:很有必要进行,因为它可以让回归测试更加省时间,重要的是,这样我们就可以频繁的使用它们,在每次代码做出一小点修改时,就马上运行测试,将问题在早期及时发现并解决;另外它也为重构提供了一定的安全保障;
- 保留测试记录:
- 用于统计,显示是否项目的质量在朝着更好的方向发展,还是没有什么变化;如果没有变化,则需要采取相应的措施,提高项目开发的质量;
- 除了常规缺陷管理软件所要求记录的字段外,建议增加以下字段:
- 缺陷种类字段,用于统计哪一种类型的错误较频繁出现,并有针对性的改进;
- 缺陷涉及的类名和子程序名:用于统计哪一个类出现的错误数量最多(原因:80/20法则)
- 个人测试记录:用于收集自己觉出现的错误,有利于改进自己的编程习惯,减免后续再发生相同的错误;
- 总结
- 测试数据出现错误的概率,经常比编写代码本身还要高;因此很有必要非常认真审慎的对待测试代码的编写;
- 关于开发者测试的本质,更重要的是在于提高开发编码过程的质量,从而减少在调试环节需要花费的时间;
- 开发者测试的用途
- 调试
- 概述
- 调试是迫不得已采取的手段,提高编码质量本身才是王道;
- 质量、成本、时间,三者在开发过程中并非对立冲突,它们是可以兼得的,关键在于先控制住质量;
- 认真对待调试的好处:
- 进一步理解正在编写的程序:如果它出错了,说明自己对它的某个地方并不理解,存在有知识的盲点;
- 明确错误的类型:总结常见的错误类型,指导自己未来不再犯错;
- 从代码阅读者的角度,分析代码的质量:从外人的眼光,客观的看待自己的代码,更容易找到可以改进的地方;(阅读的过程中,不断问“是什么”“为什么”)
- 审视自己解决问题的办法:有理有据、结构性的思考,不断改进自己调试的方法,而不是胡乱的猜测,有助于更快的定位问题并进行修正;
- 审视自己修正问题的办法:是从根本上系统性的解决问题,还是绷带式解决?
- 总结:调试是一片富饶的土地,它隐藏着让自己进步的种子,要认真好好的对待它;(没错,深有体会)
- 寻找缺陷:
- 科学的方法
- 稳定错误,让它可重复发生;(如果一个错误无法稳定下来,大多数情况,可能跟某个变量没有初始化有关系,或者是空指针)(注:有时候也跟某个变量重复定义有关)
- 分析错误来源:包括收集数据、分析数据、提出假设、证实假设
- 修正缺陷
- 对修正的地方进行测试;
- 排查是否其他地方隐藏类似的错误,如有,逐一修正;
- 寻找缺陷的建议
- 对缺陷的原因进行假设时,考虑尽可能多的数据;原因:考虑越多的数据,就越能够从数据中找到出错的规律,避免浪费无谓的时间;
- 如果当前的测试用例,无法找出错误的根源,则可以尝试调整测试用例,在更大的薄范围内调整参数;
- 对代码进行独立的单元测试;原因:将大程度的测试,分解为对各个子程序的单元测试,更容易找出错误所位;(确保每一部分是否如预期中运行)
- 了解各种调试工具,在需要的时候拿来使用;原因:合适的工具会让某个困难的调试变得非常容易,例子:内存检测工具;
- 当以为已经找到原因时,根据原因,调整测试用例,看错误是否会重现;如果会,则说明问题仍然没有找到;
- 增加一些测试用例(数据和原有用例不同);原因:这样会产生更多的执行结果,有利于进一步定位;
- 排除法:如果某个测试用例没有定位到错误,但它至少可以说明错误不出现在原来所假设的位置;此时可以用笔写下来已尝试并排除掉的点;
- 头脑风暴各种假设(即不要在一种假设上钻牛角尖,而应该先风暴出一大堆各种假设,写下来,然后再过滤;当一种不行时,换另一种;不要在一种假设上钻研太久)
- 缩小嫌疑代码的范围(可以考虑采用二分法,每次排除一半的代码;或者断点,日志输入,跟踪出错的位置)
- 特别关照一下已经产生过错误的类或子程序;原因:有过前科的,总是有很大嫌疑(80/20法则);
- 检查最近修改过的代码;先运行一下老版本,看错误是否存在;如果不在,则使用版本控制(例如 git )比较两个版本,查看那些最近修改过的地方;
- 如果在一个小范围的代码内没有找到错误,可以考虑扩大范围,再重新使用二分法排除;
- 增量式集成;养成好习惯,每次只添加一点点,然后测试它,确保没问题后,然后再添加一点点;不要一下子写太多;(此点深有体会)
- 掏出常见错误核对表;它们是一份宝藏,养兵千日,用兵一时;
- 和其他人讨论;当向别人解释自己的程序时,经常会在不经意,发现自己原来思维的盲点,然后找到错误所在(原因:通过讲述,相当于换了一个角度思考问题;据说这叫小黄鸭方法)
- 去休息一下吧;原因:大脑需要从专注到放松两种状态间切换,才能避免它陷入某个局部点,调用更广阔的信息储备;
- 蛮力测试
- 这是一种常常被忽视的方法,原因在于我们总是有追求捷径的心理。好吧,这是人之常情。但是,需要为这种捷径设置一个时间的上限,比如5分钟,或者10分钟,当超过这个时间时,仍然没有找到错误原因,或许,是考虑使用蛮力测试方法的时候了。因为,重新编写代码可能也不过才花30分钟;(忽然理解了为什么很多人非常讨厌调试别人出错的代码,尤其是这些代码写得很不清晰的时候)
- 蛮力测试方法包括:
- 对崩溃代码的设计和编码进行彻底的检查;
- 抛弃有问题的代码,或者,抛弃整个程序,从头开始设计和编程;
- 编译代码时,生成全部的调试信息;
- 在最苛刻的警告级别中编译代码,不放过任何一个警告信息;
- 全面执行单元测试,并将新的代码隔离起来单独测试;
- 用另一个不同的编译器编译代码;
- 在另一个不同的环境中编译代码;
- 复制用户完整的系统配置信息;
- 将新的代码分成小段进行集成,对每次集成进行完整的测试;
- 语法错误
- 有时候,编译器给出的语法错误的行号并不准确,更有可能出现在所报行号的上下游;
- 当编译器给出几条错误时,第二条开始,经常是不太准确的,所以,先处理第一条,然后重新编译,再根据结果进一步行动;
- 分而治之:将一个大程序拆份成几个小部分,每次去掉一部分,看语法错误是否仍然存在;
- 科学的方法
- 修正缺陷
- 动手之前,务必先理解问题,而不是不懂装懂的开始动手,做法:先使用测试用例对问题进行定位,确保自己已经了解问题所在;
- 动手之前,通过设计测试用例,先验证问题的定位是否正确;假设知道问题可能由几个因素中的一个造成,那么,先通过测试用例排除掉其中不可能的因素;
- 理解代码的设计,即问题所在代码的来龙去脉,而不仅仅是单个问题;
- 如果压力很大,那么应该先休息一下;原因:大脑在放松的状态下,才可以进入发散的模式,调动更多背景知识进行全面的思考;
- 保存一份最初的源代码;原因:这样可以对修改后的代码进行比较,以及出问题时,可以恢复到初始状态;
- 治本,而不是治标;真正从根源上解决问题,而不是针对特例提出修补方案;
- 一次只一个改动;原因:超过一处的改动,会让人不知道是哪个改动真正解决了问题,或者引入了更多的问题,更加让人困惑;
- 反复检查自己的改动,确保与问题相关的方方面面都已经考虑到了;原因:如果考虑不全面,很可能解决一个问题的同时,引入了更多的问题;(因此很有必要了解来龙去脉)
- 增加能暴露问题的测试用例;如果原来的测试用例无法检查出已发生的问题;那么为该问题设计一个测试用例,并加到原来的用例集中,这样未来可以及时避免问题再次发生;
- 搜索类似的错误:问题就像小强,当你发现一只的时候,意味着背后已经有一群;注意:如果想不出来如何查找类似的错误,那么意味着没有真正搞明白这个问题的本质;
- 修改代码时,务必要有一个确定的理由,即确信自己的改动能够产生效果,不要无谓的东试西试,这样不仅会浪费时间,也会打击自己的信心;
- 调试中的心理因素
- 为了提高信息获取效率,我们的大脑总是会不自觉的忽略一些自认为不重要的信息;而很多时候,缺陷就隐藏在这类信息里面;因此,有两种避免这种现象的方法
- 养成良好的编程习惯;原因:这样当错误发生时,错误会显得比较与众不同;
- 给变量或子程序的命名,应该具体,避免使用一些模糊或者容易混淆的写法;
- 为了提高信息获取效率,我们的大脑总是会不自觉的忽略一些自认为不重要的信息;而很多时候,缺陷就隐藏在这类信息里面;因此,有两种避免这种现象的方法
- 调试工具
- 源代码比较工具:检查自己的改动;
- 编译器的警告信息:重视并处理它们;最好把警告提示设置为错误提示,这样可以迫使自己更加慎重的对待它们;在项目组中,使用统一的编译配置文件,避免集成时被警告信息淹没;
- 语法和逻辑检查器:去毛工具 lint
- 性能分析器:可以用来查找程序执行的性能瓶颈;
- 测试框架和脚手架:编写测试用例,通过测试框架进行测试,是定位问题的好办法;另外,学会如何正确使用以及在什么时候使用调试器
- 概述
- 重构
- 重构的基本准则:不断的提高代码质量;若非如此,则应避免假借重构进行胡乱修改;
- 重构的理由
- 代码重复、子程序太长、循环太长或嵌套太深
- 类的内聚性太差(例如搞出了万能类,此时应拆分成多个类)(类是带来状态的一种抽象,它的引用常常是不透明的,因此保持类的简单非常的重要,不然会增加很多复杂度)
- 类接口的抽象层次不一致(克制采用未经深思熟虑的紧急绷带方案的诱惑)(增加一个接口很容易,但使用和维护它却有可能成本很高)
- 子程序的参数列表太长(功能单一的子程序,参数一般都很少;如果参数很多,很可能说明子程序的功能不单一,没抽象好)
- 需要对多个类并行修改(有可能是个问题,也有可能不是,也有可能说明设计存在问题)
- 需要对继承体系多处并行修改
- case 语句需要多处并行修改(说明可能使用继承更加合适)(好奇如何使用继承来代替 case ?会不会是使用多态?)
- 相关的数据项只是放在一起,没有组织到类中;
- 成员函数更多的使用了其他类的功能,而不是自身类的(说明这个函数很可能更适合放在其他类中)
- 过于依赖基本数据类型(例如 Money 更适合封装成一个抽象的数据类型,原因:它是一种人造的事物,本身带有一定的规则,这些规则可以通过抽象数据类型很好的设定和表达,也容易维护);
- 一个类什么事也不做(原因:如果功能已经被其他类取代了,应考虑删除它)
- 一连串传递流浪数据的子程序(可能是问题,也可能不是;原则:反思这些帮忙传递数据的子程序,其接口所表示的抽象是否一致;如果不一致,或许需要重新设计抽象的结构,比如拆分成1父配多子);
- 消除中间人,对接终端;如果某个类,绝大部分代码都在调用其他类,而自己啥功能也没有,应考虑去掉这个做为中间人的类,改成直接调用其他类;
- 某个类与其他类关系过于紧密(即知道的太多了,说明此处可能违反地隐藏信息的原则,宁可多隐藏出错,也不可少隐藏导致紧密耦合)
- 子程序命名不恰当:任何时候发现这个情况,都应马上着手进行修改;
- 数据成员设置为公用:违反了抽象和隐藏信息的原则;应让数据成员私用,然后通过访问子程序进行获取;
- 派生类只使用了基类的部分接口:说明很可能它们应该是合成的关系,而不是继承;
- 注释被用于对复杂难懂的代码进行解释:说明应该重写代码让其简单(不要为拙劣的代码编写文档,而是重写代码)
- 使用了全局变量(应考虑隔离它们,并使用子程序对其进行访问)
- 程序包含了未来可能用到的代码(说明出现了过度设计,应马上删除它们)
- 重构的类型
- 数据级的重构
- 用具名常量替代神秘数值;
- 为变量取有意义能够自解释的命名;
- 引入中间变量:通过有意义的中间变量命名,让表达式更加容易理解;
- 将重复的表达式抽象成函数;
- 用多个单用途的变量,取代一个多用途的变量;原因:多用途的变量很容易出错,还常常让人头大;
- 在局部作用域,尽量使用局部变量,避免修改传入的参数;
- 如果一个基础数据类型拥有功能,则应转化为类;
- 将一组类型码改为类或者枚举类型;
- 如果一组类型码对应的代码片段有不一样的功能,考虑转换化基类和派生类
- 如果数组元素的类型不相同,应考虑转换为对象;
- 用数据类替代传统记录,这样这些记录本身可以自带各种必要的操作,如错误检查,持久化等相关操作;
- 语句级的重构:(1循环 1null 2布尔 3条件)
- 使用 break 或 return 退出循环,而不是引入一个多余的循环控制变量;
- 创建和使用 null 对象,而不是去判断空值;
- 分解布尔表达式,引入中间变量,让布尔表达式的判断目的更加一目了然;
- 将复杂的布尔表达式替换为布尔函数;
- 合并条件语句中重复出现的代码,将它们放到条件的最后一种情况;
- 使用 return 退出条件语句,而不是引入一个多余的条件变量;
- 如果可能,使用多态,替换条件语句中的 case
- 子程序级重构:1独 2换 4对
- 将查询操作从修改操作中独立出来,避免出现某个 getTotal 函数竟然会改变对象状态的情况;
- 将复杂算法替换为简单算法(有可能会牺牲效率,但对于计算机来说,效率是最低优先级的考虑因素,提升的一点点微弱性能根本不值钱)(易读性对于维护更有意义,因为程序员的时间很值钱)
- 将冗长的子程序转换为类,然后在类内部,将子程序拆分为多个成员函数;目的:更加模块化,更加容易理解;
- 子程序是否内联化
- 如果子程序非常简单,一眼看得懂,则内联化;
- 如果不是,则应单独抽取出来,通过有意义的命名,简化阅读代码的理解难度;
- 参数个数多少
- 如果一个参数在子程序内部没有派上用场,删除该参数;
- 如果子程序需要从调用方获取更多的信息,增加参数;
- 合并还是拆分
- 如果两个子程序代码基本相同,只是使用的常量值不同,则应合并,然后将常量做为参数传入;
- 如果一个子程序,根据传入的参数,执行子程序内部完全不同的代码段,则应拆分它,让它变成两个或者多个子程序;原因:避免让子程序成为万能的,而是保持功能单一的;
- 传递成员还是对象
- 如果同一个对象的多个值,被传递给一个子程序,则考虑传递整个对象;
- 如果创建一个对象,并传入一个子程序,仅是因为要使用对象的某个值,则应考虑传入特定数据成员的值,而非整个对象;原因:避免兴师动众,杀鸡用牛刀;
- 类实现的重构(1代3对)
- 用数据初始化,替代虚函数;原因:如果一个虚函数的目的仅是返回某个常量,则可以将这个常量放在数据初始化中进行,不要整些没用的(虚函数)(虚函数的目的,是为了将指针的引用,稳定可靠的指向派生类的方法,而不是偶尔指向基类的方法,通过在方法前面加 virtual 关键字实现)
- 成员函数或成员数据的位置
- 上移:减少派生类中重复的代码;
- 下移:更加特例化,减少基类的冗余;
- 创建对象还是引用对象
- 对象很大很复杂:引用
- 对象很小很简单:创建
- 特殊代码与相似代码
- 相似代码:合并到基类中
- 特殊代码:转移到派生类中
- 类接口的重构
- 去除委托人(大家只和自己的朋友说话,不和朋友的朋友说话)、去除中间人(去中间化是这个时代的主题)、去除闲人(无所事事的类,功能拆分到其他类中,然后删除这个闲人)、去除万能人(如果一个类有不止一个的不相关功能,应拆分为多个类)
- 合成还是继承
- 只用到了其他类的部分接口,用合成;
- 用到了其他类的全部接口,用继承;
- 引入外部的成员函数:想要使用其他类的成员函数,但对方又不开放,怎么办?自己建一个;
- 引入扩展类:想要使用某个类的多个成员函数,有2种办法,
- 合成:建一个新类,然后将原类合成进去;
- 继承:如果使用了原类的所有接口,则使用继承,如果不是,使用合成;
- 对外隐藏变量:永远记得,要永远的隐藏成员变量,只能通过访问器进行访问;
- 如果某个成员变量不能修改,则删除它的 set() 函数,避免误导;
- 如果隐藏某个成员函数,类的接口呈现更好的抽象一致性,那么应该果断的隐藏它;
- 如果派生类没有什么特殊,则合并到基类中,删除派生类;
- 系统级重构
- 对于无法控制的数据,创建明确的索引源;例如前端框架,数据与组件的绑定;
- 单向类与双向类
- 工厂模式与构造函数;
- 抛出异常与处理错误;
- 数据级的重构
- 安全的重构
- 方法
- 保存初始代码
- 小步伐
- 同一时间只做一项重构
- 将要做的事情一件一件罗列出来;重构的时候,先动纸和笔,而不是先动代码;
- 设置停车场:将重构过程中想到的待办事项,先写下来,而不是马上着手进行,先完成当前的事情再说,永远记得,一次只做一件事(不然会把事情搞得很复杂);
- 多用检查点;目的:确保重构代码的执行符合预期;
- 利用编译器警告信息;
- 重新测试
- 增加测试用例
- 检查对代码的修改
- 根据风险高低,选择不同的重构方法;
- 避免:避免用重构替代重写,如果原始代码真的很烂,则应一脚踢开,重新设计和编码;
- 重构时间点:开发阶段的重构,是提升代码质量的最佳时机(原因:此时对代码的记忆最清晰)
- 在增加子程序时重构
- 在增加类时重构
- 在修复缺陷时重构
- 关注易于出错的模块;
- 关注高度复杂的模块;
- 在维护环境中,改善手中正在处理的代码;
- 定义清楚干净代码的标准,然后创建一个接口层,将旧代码与其进行隔离;
- 方法
- 代码调整策略
- 性能概述
- 代码调整目的:通过代码层面的微调,提高效率,满足性能要求;
- 事实上,性能和代码执行速度之间的关系很松散,用户层面关心的性能,并不意味着仅仅在于代码执行速度,还包括操作的便捷性,简单性,有可能需要从优化交互设计入手;
- 提高性能的各项方法,处理的优先级按从高到低排列:
- 程序的需求:由于客户不了解开发成本,有时候会提出不切实际的理想化需求,此时,要给出两种方案的成本差距,会促进其理性的思考;例如所有操作1秒的响应时间和5秒的响应时间,成本差别巨大;
- 程序的设计:某些问题,在A框架下难以处理,但使用 B 框架,处理起来却易如反掌;如果性能或资源很重要,则一开始设计的时候,就应该将其明确提出来,做为整个系统和各个子系统的目标(原因:当目标明确时,程序员在编写代码时,就会依照目标去实现)
- 类和子程序:选择合适的数据类型和算法,会对性能产生很大的影响;
- 同操作系统的交互:有时候性能问题可能出在外部,例如 IO
- 代码编译:换一个更好的编译器,可能使用生成的代码执行起来非常高效;
- 硬件性能:增加硬件性能虽然粗暴,但最方便,也简单有效;
- 代码调整
- 帕累托法则:20%的代码消耗了80%的时间,甚至可能是5%的代码,消耗了95%的时间,找出它们,优化它们;
- 蜜糖和哥斯拉:粘乎乎的代码,哥斯拉般庞大
- 常见低效之源:
- 不必要的输入输出,例如访问磁盘、数据库、网络文件;应尽量优先在内存中处理;
- 内存分页:内存是有分页管理的,如果频繁的在内存分页间切换,会带来很多性能损耗;当然,如果内存很大,则这个差别不太明显;
- 系统调用:调用操作系统的子程序,例如磁盘、键盘、屏幕、打印机、第三方集成软件等;
- 解释型语言:(原因略,不解释)
- 错误:例如正式代码忘了去除调试信息、忘了释放内存、轮询不存在的记录、数据库的表设计失误、数据表没有索引等;
- 常见低效之源:
- 代码调整步骤
- 先编写设计良好的代码,让程序易于理解和修改;
- 如果存在性能问题
- 使用性能分析工具,找出热点
- 思考性能瓶颈是否源于糟糕的设计、数据类型或算法的缺陷,确认是否需要进行代码调整,如果不需要,返回第一步;
- 如果需要,保存一份初始代码;
- 对瓶颈部分进行改写;
- 再次测量改写结果
- 如果没有改进,返回初始状态,重新尝试其他改写方法;
- 重复步骤2
- 性能概述
- 代码调整技术
- 逻辑判断
- 知道答案后立即停止计算;
- 按照出现概率调整判断顺序,越经常出现的场景,放在越前面;
- 用表查询替代复杂的逻辑表达式;
- 使用惰性求值:仅在需要的时候,才进行求值计算;
- 循环
- 将判断外提:如果循环过程中,某个判断结果并不会改变,可以将这个判断外提,减少每次循环的计算量;
- 合并:将对一组对象的多次循环操作,压缩成在单次循环中完成操作;(一开始先分开,因为分开更具易读性;当实在影响性能了,再合并)
- 展开:循环每次处理一个下标,可以考虑展示为每次处理两个或多个下标,这样可以变相减少循环的次数;代价是循环结束的判断变得复杂了;
- 减少循环内部的工作:如果某个计算可以在循环外完成,可以提出来,然后在循环过程中进行引用即可;
- 哨兵值:对于查找循环,可以设置一个不会和数据元素值重复的哨兵值,放在数据末尾,然后开始查找循环;如果循环结果等于哨兵值,则说没有找到(这样可以减少循环内部的判断操作,相当于将判断外提了,只在循环结束的时候,做一次结果判断)(如果判断非常复杂时,此技术适用;如果判断很简单,则提出退出循环也有好处);
- 对于多层嵌套循环,最消耗性能的循环放在嵌套循环的最里层:100+1005 > 5 + 5100,可以减少总的循环次数;
- 消减强度:如果循环内部有高强度的计算,可以考虑替换为低强度的计算,例如将乘法替换为加法;
- 数据
- 整数型代替浮点型:计算机处理整数型的速度要比浮点型快的多;
- 减少数组维度:多维数组的操作更加费时(貌似相当于嵌套循环了,可考虑先将数组压平,处理完以后,再恢复原来的形状)
- 减少数组引用:如果需要多次引用数组某个值,可以将其保存在某个变量中,再通过访问该变量,来减少对数组下标引用的访问次数;
- 索引:关键摘要信息通过索引存储(与数据本身一起,或者单独存储一份,对数据的访问,先通过索引实现,之后只一次性访问磁盘)
- 缓存:经常使用的重复数据,增加缓存机制;代价:会增加程序的复杂性和出错的概率;
- 表达式
- 利用代数恒等式,减少计算次数,例如 not a and not b ,替换为 not (a and b), 原来的三次运算,减少为两次;
- 削弱计算强度:用加法替代乘法,用乘法替代幂乘,用三角恒等式替代三角函数,用移位操作替代乘2或除2,用long或int 代替 long long,用定点数或整型代替浮点型,用单精度代替双精度;
- 编译期初始化:如果有个子程序使用的某个参数是个常量,可以提前计算该常量并进行引用,减少每次调用子程序对该常量的重复计算;
- 小心系统函数:由于操作系统级别的函数的精度非常高,如果不需要这种精度,则减免调用系统函数;
- 使用正确的常量类型:常量的数据类型,与相关的被赋值的变量类型应一致,这样可以避免对二者进行计算时,需要做额外的类型转换计算;如果一开始类型定义正确,则可以减少类型转换工作;
- 提前计算结果:如果结果的值是一个比较小的范围,可以提前计算出结果,在需要的时候进行引用即可;这样可以减少每次引用的计算工作;
- 删除公共子表达式:如果某个表达式重复出现,此时应该引入一个命名良好的变量来替代它;一来避免重复计算,二来代码更加易于理解;
- 用低级语句重写部分代码:
- 例如 python 代码用 c 改写, java 编程用汇编改写等;
- 可以考虑自带翻译功能的编译器,让其将高级语言转换为汇编,然后将汇编提取出来保存使用;
- 逻辑判断
- 程序规模对构建的影响
- 随着团队规模的扩大,交流的路径呈现乘数上升;控制交流规模的方法之一,即是通过文档;
- 项目规模的范围:50%的项目在10人以内;25%的项目在3人以内;
- 小项目的生产率,会比大项目高出 2-3 倍;
- 对于小项目,构建活动的时间,占整个开发时间的65%左右,对于大项目,则要少于50%;原因:随着项目规模的扩大,构建时间呈线性增长,但非构建活动的时间,即呈现非线性增长;导致构建活动的时间比例下降了;非构建活动包括:交流、计划、管理、需求分析、系统功能设计、接口设计和规格说明、架构、集成、消除缺陷、系统测试、文档编写;
- 程序->产品->系统,每一阶,都对应复杂度相应增加一个数量级,开发成本梯度约为 1 -> 3 -> 9;当开发人员用开发程序的经验,来评估产品或系统的开发成本时,会发生3倍甚至更多的误差;
- 管理构建
- 鼓励良好的编码实践
- “如何鼓励良好的编码实践”是管理者要完成的关键问题之一,但这个制定标准的工作,不应由管理者来完成,而建议由一名受人尊敬的专家级架构师来完成(注意:必须确保架构师是受人尊敬的,而不是一名脱离编码实践,完全不了解开发人员在做什么的资深闲杂人士)
- 考虑事项:强制标准并不一定适用于每个团队,有些团队愿意接受,有些不愿意;如果不愿意,可以考虑采用其他更灵活的方式,例如指导原则、建议、最佳实践例子等;
- 鼓励良好实践的方法
- 给项目的每一部分分派两个人,方法有:结对,师徒、buddy-system(伙伴);原因:可以保证一段代码至少有两个人认为它是可以工作的,并且是可读的;
- 代码复查:code review 或者 peer review,原因:确保每行代码至少有2-3个人读过;当开发人员知道自己的代码会被阅读时,会不自觉改变编写方法,让其更加易读,且不容易出错;(即使一开始没有制定标准,如果有代码复查,随着时间的推移,团队成员之间也会不自觉建立起一份关于什么是好代码的实践标准出来)
- 要求代码签名:在代码完成后,高级技术人员需要在代码上进行签名(原理:对事情负起责任的压力)
- 提供良好的代码示例供大家参考:达到目标的关键之一,就是先要明确目标,比起抽象文字,一份代码示例更加让人容易理解;
- 强制代码集体所有权:避免让开发人员认为自己编写的代码是属于自己的;
- 奖励好代码:如果自己无法判断什么是好代码,就千万不要奖励;可以考虑将奖励权交给开发团队自行决定;
- 简单的标准:管理者可以宣称代码的标准是所有代码他能够读懂
- 配置管理(变更控制)
- 变更控制:系统化的定义项目工件和处理变化,使项目一直保持其完整性;
- 需求变更和设计变更控制办法
- 遵循某种系统化的需求变更手续;好处:在执行变更之前,有机会思考一下,如何变更,才是对系统最有利的;
- 成组的处理变更:不要一有变更马上执行,要批量的处理;原因:这样才能从中挑出优先级最高的进行处理,而不是最简单的先处理;
- 评估变更成本:除了构建成本,还需要考虑需求、设计、编码、测试、文档等环节的工作成本,让变更人明白,变更是一项高成本的决策;
- 提防大量的变更请求:如果出现,意味着需求或设计出现了问题,应考虑需求和设计,是否应推倒出来;虽然这些会损失之前开发的代码,但如果需求或设计没有考虑清楚,未来会有更大的损失;
- 成立变更控制委员会:所有变更请求,需要先提交到委员会;委员会负责对需求变更进行筛选,去芜存精;虽然会有点官僚主义的味道,但在需求变更这个环节上,这种官僚主义是有必要的,且是有益的;
- 代码变更:无论任何时候,永远记得使用代码版本控制工具,不管是 svn 还是 git;它会为构建和调试带来帮助;
- 工具版本:将工具也纳入版本控制之中,确保代码的编译环境一致;
- 机器配置:制作统一的开发机器镜像,一来可以减少开发机器配置的时间,二来可以减少因机器配置不同产生的错误;
- 备份计划:定期为代码执行备份,同时测试备份可以用来恢复(注:可以考虑使用云盘,实时备份每次修改)
- 评估构建进度
- 评估的方法
- 建立目标:为什么评估?评估什么内容?只评估构建,还是包括其他环节?只评估工作量,还是包括节假日?评估准确度的要求?乐观与悲观评估的结果差距多大?
- 为评估预留时间并制定计划:避免匆匆忙忙的评估;如果是评估一个大项目,则有必要将“评估”提升到一个小项目的级别来做,并且花时间制定一个评估计划;
- 清楚的说明软件需求:没有明确需求的评估,是无效的;
- 在底层细节进行评估:越接近底层,评估的准确率越高;
- 使用不同的评估方法,并比较结果(有哪些评估方法?评估软件,算法软件(如cocomo),外界评估专家,排练会议等;
- 定期重新评估:越接近项目结束的时间,接近的准确率越高;随着项目进行,定期重新评估,可以及时根据调整相关活动计划;注意:最好一开始就打好预防针,避免对早期评估的准确率抱太高的预期,这样可以为后期的重新评估减少阻力;
- 评估构建的工作量:随着项目规模变大,构建的工作量占整个项目工作量的比例越小;但这个比例在每个公司不尽相同,可以参考公司以前的数据进行估算;
- 进度的影响因素:最响进度的因素很多,其中项目规模是最大的影响因素,其他重要因素还包括产品复杂度、时间限制、存储限制、需求分析能力等;
- 评估与控制:评估很重要,但在评估之后,如何调度和控制资源完成进度更加重要;
- 进度滞后的处理
- 如果可行,增加时间(越接近项目的后期,时间的拖延会越严重,而不是减轻);
- 将功能分为“必须有”、“有了更好”、“可选择”三类,砍掉“可选择”(其实最好一开始考虑使用精益开发的方法,第一版先开发最小可行产品)
- 扩充团队:如果项目中的任务可以分割,可以分得更细并安排给不同的人做,则增加人手有效;如果不行,则增加人手无效,甚至还会进一步拖延进度;
- 评估的方法
- 度量:度量是有必要的,也是可行的;虽然不能保证获得清晰的全景,但有还是比没有好;有一些度量工具,可以选择使用;一开始不要尝试收集所有数据,这样会被数据淹没;而应该先制定目标,提出要解决的问题,然后有针对性的进行度量,收集数据回答问题;基本上所有的项目环节都可以被度量,详细的项目可参考书中的表格;
- 把程度员当人看:
- 由于编程是一件高度抽象的活动,同时也需要充分沟通交流的活动,因此有必要结合 high-tech 和 high-touch ;
- 时间花费:通常情况下,程序员只有1/3的时间花费在编码活动上面,另外还有1/3花在了和编码没有任何有益关系的非技术活动上;
- 性能和质量差异:好的程序员和差的程序员,效率和质量可以差别到一个数量级,但这种差异却跟经验和工作时间没有关系;他们之间的天分和努力的差异,也十分巨大;
- 团队差异:好的程序员倾向于集中在一起,差的也是;80%的贡献来源于20%的贡献者;有必要为聘请10%的最好程序员多支持报酬,这种投资的回报非常可观,同时也不会给原团队拖后腿;
- 信仰问题:编码风格是一个信仰问题,很容易造成紧张的气氛;仅在会影响可读性、代码质量的问题上,提出风格建议,其他情况下,让程序员制定自己的标准即可;甚至可以在编码完成后,使用一些格式化工具,来统一注释风格、缩进风格等,因为这些都无关痛痒;
- 物理环境:安静,不容易被打扰、宽敞的环境,对于抽象的编程活动的效率影响是显著的,非常有必要在这方面进行投入(Joel Sponsky 的公司的办公室设计,或许值得借鉴);
- 管理你的管理者
- 由于非技术出身的管理者随处可见,或者技术出身但已脱离技术10年以上的管理者也比比皆是;技术出色且与时俱进的管理则属凤毛麟角;
- 最佳方案是教育你的管理者,据说可以通过阅读《人性的弱点》一书,来对管理者进行管理,哈哈哈哈……,有道理哦,重点是学完还可以做在很多其他地方,“复用”非常方便;
- 鼓励良好的编码实践
- 集成
- 集成方式的重要性
- 产品不能在最后完成的时候才能运转,而应该是在整个构建过程中一直保证可以运转,这样才能够方便测试,也更早的暴露一些缺陷,进度更加可控,团队更有成就感,客户更加满意;因此,集成的顺序非常重要;个人觉得应该按照最小可用产品的原则,来安排开发的顺序和集成的顺序;
- 集成频率
- 阶段式:非常不好,因为每个单元在开发过程中会存在很多错误的假定、不清晰的接口文档、脆弱的封装等问题,而这些问题只有等到集成时才一下子大爆炸出来,增加了调试难度的数量级;
- 增量式:先构建某个最小的系统功能部件,之后不断往上加一点东西,然后测试,通过后,再加东西,一直反复直到完成(类似滚雪球);好处:进度更透明、士气更高、客户更满意、测试更充分
- 增量集成策略
- 集成的顺序很重要,因集成顺序的安排,直接导致了构建顺序的先后;集成顺序策略有多种,每一种有其优缺点,重要是根据项目的特定需求进行选择;
- 自顶向下集成
- 优点:
- 由于在早期就对系统逻辑进行集成和测试,能够尽早的暴露一些高层概念设计方面的问题;
- 能够尽早的让系统可以工作起来;
- 缺点:
- 需要准备一卡车的 stub,而 stub 难免存在错误;
- 最后集成底层,涉及系统接口,如果存在性能问题,有可能反过来导致顶层的修改,导致增量的作用弱化;
- 总结: 一般很难实现纯粹的自顶向下,更常见的是单个功能模块的自顶向下,最后再集成各个功能模块;
- 优点:
- 自底向上集成
- 优点:
- 较早的发现底层系统接口可能存在的性能问题;
- 缺点
- 使用此方法前,需要先做完高层概念设计,但却到最后才能发现高层概念设计上的问题,但这个时候为时已晚,有可能导致前面很多底层的工作扔掉;
- 总结:纯粹的自底向下也很少见,更常见的是单个功能模块的自底向下;
- 优点:
- 三明治集成
- 先集成顶部的业务对象类,再集成底部的设备接口类和工具类,最后集成中间层的类;
- 优点:自顶向下和自底向上两种方法的折中,现实和实用的做法;
- 风险导向集成
- 顺序同三明治一样,但思考点在于先实现最有挑战,最困难的类,最后再处理轻松的;操作过程中,难免会出现某些类的难度一开始预估不足,这也是正常的;
- 功能导向集成
- 将系统拆解为一个一个独立的功能,然后逐个集成;一开始需要先搭出一个骨架,例如交互式菜单系统,先后再将功能逐个集成到骨架上面;
- 优点:脚手架最少、进度可见、面向对象设计更方便(因为功能一般可以映射为对象)
- 总结:纯粹的功能导向也很难,一般需要先集成某些底层代码,之后才能集成某个功能;
- T型集成
- 功能导向和风险导向的结合;先选择一个最有可能验证设计概念的功能模块,使用风险导向集成完成它,充分暴露潜在的问题,之后再使用功能导向的方法,实现骨架,最后逐一实现其他功能模块;
- 总结:集成类型很多,但更重要的是根据项目具体情况,混合使用以上各种策略,避免僵化;
- 每日构建(daily build)和冒烟测试
- 优点很多,包括可以便于诊断缺陷(上次可用本次不可用),及早发现问题避免问题潜伏积累,最后解决难度倍增;同时也能极大鼓舞团队士气,因为他们每天都可以看一些确实可用的进展;
- 相关事项
- 每日构建:确保是每天,而不是每几天或每周;
- 检查失败的构建:一旦构建失败,应将修复构建视为第一优先级的事项;
- 每天冒烟测试:没有冒烟测试的构建,纯粹是自欺欺人;
- 更新冒烟测试:没有更新的冒烟测试,同样是自欺欺人;
- 每日构建和冒烟测试自动化:无须多言;
- 如果项目很大,安排专人负责每日构建和冒烟测试更新(全职或兼职,视情况需要)
- 每次提交代码都有意义,但别等太久,最少每天一次(迫使开发人员将功能拆分为更小的模块)
- 提交代码前开发人员自己需要先进行冒烟测试;
- 惩罚导致构建失败的人:停止其工作,直至其修复构建;发糖果、捐基金等;
- 在早上发布构建:这样测试人员可以一上班便开始测试,而不是等到夜里;同时有问题也可以及时找到开发人员,夜里则不容易找到人;
- 即使有进度压力,也要坚持每日构建和冒烟测试:表面看上去它耗费时间,事实恰恰相反,它使得项目更快完成;
- 集成方式的重要性
- 编程工具
- 设计工具
- 用一些图形化的符号来表达设计思路,例如:UML,架构方块图、继承体系图、实体关系图、类图等;
- 从本质上看,各种设计工具很像是一堆绘图软件包;相比于纸和笔,它们的好处在于进行修改时,各种图形符号的关系,可以快速自动修正,不须手工逐一更改;
- 源代码工具
- 编辑
- 集成开发环境IDE:花点钱买个最好的IDE是很好的投资;
- 针对多个文件的字符串查找和替换:例如有个地方发现了错误,查找更多文件中是否出现相同的错误;或者,对多个文件中的某个类或子程序改名;
- diff 工具:比较两个文件修改前后的变化;常见的工具如 git
- merge 工具:不同版本的代码进行合并,常见的工具如 git , svn;
- 代码格式化工具:按照统一设定的格式进行美化;例如:统一的缩进,高亮类名和子程序,一致的注释,调整参数列表等;尤其是在处理老代码时,可以快速的让老代码符合编码风格约定;
- 生成接口文档的工具:在编写源代码时,给部分文字打上标记(如 @tag),之后可以用工具将该部分文字提取出来生成文档(常见如 Javadoc);
- 模板:一些需要经常从键盘输入的内容,提取其中的框架,做成模板;在需要使用的地方,通过键盘宏命令快速插入;这样一来可以节省很多输入的时间;二来也可以让团队拥有一致的编码风格;
- 交叉引用工具:用来列出所有变量和子程序,以及使用它们的位置(有一点像书籍附录的脚注索引);
- 类的继承体系生成器:可以用来分析程序的结构,划分程序的模块,将系统分解为软件包或子系统;
- 分析代码质量
- 语法/语义检查器:进行吹毛求疵的检查,例如各种 Lint 工具
- 尺度报告器:很高级的质量报告工具,可以检查子程序的复杂度,统计代码行,数据声明行,注释行,空行等;可以统计对程序的修改,找出哪个部分被频繁修改;可以跟踪缺陷,谁制造了缺陷,谁修复了缺陷等;
- 重构源代码
- 重构器:有些独立的,有些在IDE中集成;可以让重构变得很方便,而且不容易出错;例如提取某段代码生成子程序,重构器可以让这个动作变得很简便;
- 结构改组工具:一般可以运行一遍结构改组工具,看一下计算机的建议,为手工修改提供思路;
- 代码翻译器:可以将一种语言的代码翻译成另外一种语言(前提:源代码写得不错,如果源代码很烂,是翻译出来的代码一样烂,而且让人看不懂)
- 版本控制
- 源代码控制
- 依赖关系控制,类似 UNIX 的 make 工具;
- 文档的版本管理
- 将项目的工具关联在一起,工件包括需求、代码、测试用例等;这样当需求发生变更时,可以找到需要进行修改的代码和测试用例;
- 数据字典
- 用来描述项目中所有重要数据的数据库,即数据库模式 schema;包含每个数据项的名称和描述,也可能包括其使用的注意事项;
- 在大项目中,也用来跟踪成千上百的类定义,避免重复命名,命名冲突等;
- 编辑
- 可执行码工具
- 产生目标码
- 编译器:将源做对转换为可执行码(针对编译型语言)
- 链接器
- 标准链接器:连接多份目标文件,让它们协同工作;这些目标文件可能由多种不同的语言编写;通过使用链接器,减少了手工集中的工作;
- 覆盖链接器:当内存不足时,通过使用覆盖链接器,它可以动态的加载当前需要的文件到内存中,从而实现10个锅9个盖的可持续性运作;
- 构建工具(build)
- 通过构建工具来管理目标文件对源文件的依赖关系,确保目标文件的编译能够保持最新且一致的状态,同时也减少每次编译的工作量(只需编译有改动过的且有依赖关系的文件即可)
- 由于依赖检查比较费时,据说团队通过做好源文件的优化,然后每次构建不检查依赖关系,而是全部重新编译,最终的费时竟然更快;
- 开源库
- 以下类别有很多优秀的开源库,如果遇到此类场景,应该优先使用开源库,而不是自己重新造轮子,这些开源库覆盖的场景包括:容器类、信用卡交易服务、跨平台开发工具、数据压缩工具、数据结构与算法、数据库操作工具、数据文件操控工具、图像工具、许可证管理器、数学运算、网络与互联网通信工具、报表生成器与报表查询、安全与加密工具、电子表格与数据网格工具、文本与拼写工具、语音电话与传真工具;
- 代码生成器
- 很多IDE有集成一些代码生成器,它们主要是面向数据库的应用程序;虽然这类自动生成的代码基本不可读,但它的好处是可以用来快速生成一个粗糙的原型,然后用这个原型是做一些测试并验证想法;如果想法可行,再手工编写代码;如果一开始使用手工编写,可能原型制作要花费数周的时间,但使用代码生成器,则可能1天之内即可完成;
- 安装工具
- 安装程序生成工具;当编写好源代码并转换为目标文件后,可以使用这类安装工具来生成安装程序;
- 预处理器
- 场景:通过预处理器,可以在开发代码和生产代码做不同的配置;比如在开发代码中,某种子程序前面有个内存碎片整理的功能,但生产代码不需要,则可以通过使用宏预处理器,但不同的环境下,开启和关闭相应的功能;
- 有些语言有自带预处理器,有些没有,如果没有,可以考虑使用第三方独立的;
- 调试
- 编译器的警告信息、测试脚手架、diff工具、执行剖测器、追踪监测器、交互式调试器(软件版和硬件版)
- 测试
- 自动化测试框架(JUnit、NUnit、CppUnit)、自动化的测试生成器(这个是啥玩意,貌似很有用的样子)、测试用例的记录和回放工具、符号测试器、系统扰动器、覆盖率监视器、Diff工具、测试脚手架、缺陷注入工具、缺陷跟踪软件
- 代码调整
- 执行剖测器:用来做性能分析,可以发现程序运行的性能瓶颈,然后有针对性的进行调整;
- 汇编代码清单和反汇编:对于计算机来说,最终接收处理的是机器代码,它最近的上一层是汇编代码,编译器会将高级语言翻译成汇编代码,但是这份翻译结果很有可能出人意料;所以,当想要对代码进行性能调整时,去查看这份汇编代码,常常会有意想不到的收获,而且,离发现问题产生的本质最接近;同时,还可以反过来,用一种新的角度认识编译器及其所做的工作;
- 产生目标码
- 工具导向的环境
- 有些开发环境,例如 unix ,天生具备使用小工具的文化氛围,在这种氛围下,了解并擅长使用各式小工具,可以极大的提高生产效率;例如 grep, diff, sort, make, crypt, tar, line, ctags, sed, awk, vi 等;
- 如果开发环境原生不支持上述工具,例如 windows,则可以尝试去寻找类似的工具,一般都可以找到,关键是养成这种习惯;
- 打造你自己的编程工具
- 大多数程序员天生有打造工具提高自己生产效率的习惯,这是一种好事,因为如果有了一个好的工具,将可以在将来重复使用;
- 项目特有的工具:一般来说,中大型项目都会打造一些项目特有的工具,用来提高项目内部的工具效率;
- 脚本:如果日常工具中,老是出现一些重复工具,可以尝试将其写成脚本(也叫批处理命令),然后每次执行脚本即可,一来省时间,二来不出错;
- 工具幻境
- 在过往的历史中,总是有人跳出来说即将消除编程;但几十年过去了,我们确实提高了编程效率,但离消除却依然遥远;究其原因,其实本质在于理解复杂现实世界中的问题,以及告诉计算机如何去处理问题的这个工作,永远需要有人来做;只要计算机无法自动化完成这个工作,那么永远需要有人来做编程的工具;
- 设计工具
- 布局与风格
- 基本原则
- 布局的极端情况:没有任何空白
- 格式化的基本原理:反应逻辑结构优于美观;将重点放在逻辑结构上面,展示逻辑结构的布局,都不会太难看;让好代码更美观,让差代码更丑,优于不管好坏代码都让其变美的技术;
- 人和计算机对程序的解读:人眼倾向于从代码的外观中理解逻辑,而计算机不管外观,只管语法规则;因此,好的布局是为人眼服务的,让外观和逻辑相符,而不是为计算机服务的;
- 在国际象棋中,棋子有意义的布置,高手的记忆能力远远强于新手,在代码中也是一样,有意义的代码布局,会让高手快速的记忆代码;如果代码杂乱无章,则高手和新手的记忆能力相差无几;
- 好的布局风格的特点:始终准确展示逻辑结构、易于阅读、易于修改;
- 布局技术
- 空白:空行、分组、缩进;
- 括号:应该用得比自己感觉需要的更多,例如表达式的求值,增加括号并不会带来任何损失,但给阅读的人极大的减少负担;
- 布局风格
- 纯块结构(块有明确的开始和结束)、模仿纯块结构(用符号模仿明确的开始和结束)、指定边界结构(用符号单起一行且缩进来指定开始和结束的边界,符号缩进位置与块内代码位置一致,块内代码不用两次缩进,原因:会增加复杂度)、行尾布局(这种结构遇到复杂逻辑时可读性大大降低,不推荐)
- 前三种结构在代码可读性上,并没有统计上的显著差别;
- 控制结构的布局
- 开始结束符号:记得缩进,同时避免代码缩进两次;
- 段落之间使用空行
- 复杂的条件表达式,将条件拆分成多行;
- case 语句避免使用行尾风格(原因:当 case 名很长的时候,行尾风格的对齐维护很麻烦)
- 单条语句的布局
- 语句长度:以前由于屏幕比较小,一般控制在80个字符内,现在由于大屏显示器的普及,为了提高可读性,适当增加一些字符也可以接受;
- 用空格使得语句显示清楚,包括应用在:逻辑表达式中、数组引用中、罗列子程序参数中;
- 格式化后续行
- 使后续行变得很明显:比如在行尾放置运算符,显得语句未结束,如果不当修改,也会出现报错;另一种方法是在续行头部放置运算符,由于左边比较容易被眼睛扫视,所以这种方法也不错;
- 紧密关联的元素放在一起:例如数组下标的引用;
- 子程序调用多个参数时,后续行按标准缩进
- 如果参数实在很多,可以考虑让每个参数单独占用一行并缩进,虽然这样会增加很多屏幕面积,但是修改和维护都比较方便,易读性也更好;
- 控制语句折行按标准缩进;
- 赋值语句避免使用等号对齐,原因:当变量名很长时,易读性会下降;
- 赋值语句折行按标准缩进
- 每行仅写一条语句
- 虽然有些理由支持减少语句行数,但从易读性、易维护性、易调试性来说,每行一条语句的优势更加多;
- 在 c++ 中,如果一行语句有副作用,应将其单独成一行;原因:这样才可以显而易见的看出执行的顺序,而不用费力去理解,而且也容易出错,出错也不容易发现;
- 数据声明的布局
- 每行只声明一个数据;原因:易修改、易读、易查找、易定位;
- 变量声明应该尽量接近使用的位置,减少跨度和生存期;
- 合理组织声明顺序:最好按类型进行分组,按字母排序就不要了;
- 在c++中,声明指针变量时,星号 * 应靠近变量名,这样如果一行有多个变量,不会出现仅第一个变量声明成功;但更好的做法是使用指针类型来声明变量;(EmployeeList *employees 改为 EmployeeListPointer employees)
- 注释的布局
- 注释的缩进应与相应的代码的缩进一致;原因:不然会破坏阅读的结构;
- 每段注释用一个空行隔开;原因:由于空行带来了分组的效果,易读性上升;
- 子程序的布局
- 用空行分隔子程序的各个部分,包括头部、数据、常量名声明等;
- 子程序参数使用标准缩进(每个参数单独起一行)
- 类的布局
- 类接口的布局:类成员的顺序如下
- 类的说明和使用方法注释
- 构造函数和析构函数
- public 子程序
- protected 子程序
- private 子程序和数据成员
- 类实现的布局
- 如果编程语言对使用的文件数量没有什么限制的话,最好一个文件中,只放一个类
- 类实现文件的顺序如下
- 描述类所在文件内容的头部注释
- 类数据
- public 子程序
- protected 子程序
- private 子程序
- 如果一个文件中有多个类,应该用多个空行,并用大写的字号将其分隔出来,就像书里面的新起一章一样;
- 文件和程序的布局
- 一个文件应该只有一个类
- 文件名应该与类名相关或一致;
- 文件中的子程序之间使用至少两个空行分隔开
- 将子程序按照字母排列(仅在一种情况下使用:编辑器不能快速查找子程序,不然没必要,太浪费时间,维护也很麻烦)
- 类接口的布局:类成员的顺序如下
- 基本原则
- 自说明代码
- 外部文档
- 外部结构文档通常比编码的层次更高,但比问题定义、需求和架构活动的层次低一些;
- 常见的外部文档类型
- 单元开发文件夹(UDF):提供在其他地方没有说明的设计决策踪迹;单元一般指类,也可指包或组件(貌似这个一般写在类所有文件的头部?)(按照垠神的说法,类最适合用在对数据的抽象,我个人感觉这也符合SICP 第二章的要义;至于添加一些不属于类的方法在类里面,仅为了实现对数据的某种操作,这种思想是要不得的,只会增加复杂性;至此,我终于比较明白本书作者所说的类的子程序的抽象层次要足够高,并且要保持一致性;如果没有保持一次性,很有可能说明混入了一个本不应属于类的一种方法;此时坚持纯粹的完全面向对象的思维,就过头了;类的方法没有返回任何值,只是实现了对象内部数据的修改,这种副作用不知为何,一直让我感到不透明不放心);
- 详细设计文档:低层次的文档,描述类层或子程序层的设计决定,曾考虑过的其他方案,以及采用当前方案的理由;存放的位置可能在UDF中,或单独文档中,或在代码中;
- 思考:如果通过 WIKI 来组织上面的相关文档,或许是一种不错的办法,因为在文档内部,可以加入相关文档的链接,让阅读更加的方便;(现在想想,或许最好的位置应该是离代码最近的位置,如果实在不行,至少可以放个链接)
- 编码风格作文档
- 代码层文档,最详细,而且也最有可能保持实时更新;
- 对于代码层文档,最重要的不是注释,而是代码风格本身,包括有意义的变量名和子程序名、简单的控制结构、具名常量、良好的结构布局等;注释不过是在前面的基础上,添加的小饰品;(好的风格,会让代码实现像自然语言一样的自说明,这是最好的境界;注释应该只是用来对思路的一种抽象,方便快速阅读,而不应该包括实现细节,细节的说明,将由代码的自说明来实现)
- 注释或不注释
- 注释可以在更高的抽象层次上显示代码的意图,因此在后续维护代码时可以提高效率,节省时间;但重复代码本身的注释则是一种浪费时间,另外也容易因为代码的重构而迅速过时,错误的注释比没有注释更加糟糕,因为它会误导人;
- 可行的办法:先用伪代码编程,最后再将伪代码转为注释;
- 高效注释之关键
- 注释的种类
- 重复代码:应避免;
- 解释代码:如果代码需要解释,说明代码写得过于复杂了,建议考虑重构,让其变简单;
- 代码标记:某些部分未完成,需要在发布前进行处理;建议团队统一该种注释的格式,以方便在发布正式版本前进行检查;
- 概述代码:将多行代码的意图用一行注释写出来;
- 意图说明:将一段代码的意图用一行注释写出来;
- 其他非代码信息:例如版本说明、版本号、保密要求等
- 对于完工的代码,只允许出现上面后三种注释;
- 高效注释:
- 如果注释写不出来,很可能是没有完全理解程序本身;而在写代码前,原本应花最多的时间理解程序上面;所以,写不出来很可能是一个警报的信号;
- 避免使用不容易维护的注释风格,例如各种冗余的为了美观的符号;美观是很好,但要让它维护起来不费吹灰之力才好;
- 建议用伪代码编程法减少花在注释上面的时间;
- 将注释集成到开发过程中,不要等项目完成时再来写注释,由于那个时候已经忘了很多代码的细节,需要重新花时间回忆,导致注释效率非常低下;
- 性能不是借口:如果注释会影响性能,只需要在发布正式版时,用格式化工具将注释统一删除掉即可,又快又简单,一举两得;
- 最佳注释量:IBM的研究是平均10行1条注释,但这并不是重点,重点是伪代码编程法,以及注释满足上术上述提到的要求;
- 注释的种类
- 注释技术
- 注释单行
- 不要写跟代码无关的注释,这样只会害死人;
- 行尾注释问题:避免对单行代码使用行尾注释,原因:不好维护,要花费很多时间调格式;不要阅读,眼睛不容易快速定位;经常只是重复代码,没有信息量;
- 使用行尾注释的两个场景:数据声明、标记块结束;
- 注释代码段
- 注释应表明代码的意图:应该在尽量高的层次上去表明意图,写出why(目标),而不是 how(过程);可以想象如果这段代码转换为一个子程序,会如何给这个子程序命名(这个主意很棒,一针见血)
- 代码本身应该具备足够的说明性:可以通过有意义的变量名来达到这一点;
- 用注释为后面的内容做铺垫:这样做的好处,在于将来可以很容易定位想要查找的代码位置;
- 让每条注释都有用:删除没有用的注释,过多的注释并不好;
- 说明非常规做法:如果为了达到某种特定目的,例如性能提升,使用了一种非常规的做法,则可以用注释说明一下,并写出该种做法得到的好处;
- 不要使用缩略语;
- 主次注释的区分:如果有些注释是某条主注释的次级注释,最好的办法是将次级注释对应的部分抽象成一个子程序;这样可以避免出现次级注释,使得所有的注释都位于同一个层次;
- 错误或语言环境的独特点应该添加注释:假如调用某个库函数,发现一个在特定环境下会重现的BUG,则有必要通过注释标明这个BUG,并解释用什么方法绕过它;
- 为使用不良风格编码给出有力理由:避免别人修改代码,以及留下不好印象;
- 不要给投机取巧的代码写注释,除非是在维护别人写好的代码;如果代码出现投机取巧,好的做法应该是重构它;
- 注释数据声明
- 注释数值单位:例如 distance = 1000 // in meters,当然,更好的做法是将单位写到变量名里面,如 distanceInMeters = 1000
- 对数值的允许范围给出注释,例如人民币钞票的面额是 1 到 100 元;
- 注释编码的含义:例如 0 代表直流电,1 代表交流电等(没有枚举类型的情况下);
- 注释对数据输入的限制:例如传入的参数、文件和用户输入等;当然,更好的做法是使用 assert 的,这样可以让程序具备自检查的能力(好奇 assert 是否有必要独立成一个子程序?感觉写在代码里面,由于它不可避免出现在开头位置,会影响阅读,没有第一时间突显主要的内容);
- 如果注释中含有变量名,则应让变量名也出现在注释中,原因:未来如果修改变量名称,通过搜索匹配时,也能够发现并同时修改注释里面的变量名,避免过时;
- 注释全局变量:如果使用了全局变量,则应加以注释,解释使用的原因和目的;另外全局变量的命名最好有突出的规范,例如统一加 “g_” 做为前缀;
- 注释控制结构
- 在if 或 while 开始前进行注释;if/else 注释判断的理由;for or while 循环:注释循环的目的;
- 如果循环非常长,则在循环结束的时候,注释循环结束;目的:为判断循环是否结束提供线索;此时也同时警觉,循环可能需要进行简化了,最好的办法,是简化到可以不写这种结束注释,除非万不得已;
- 注释子程序
- 注释应靠近其说明的位置,避免使用花哨的注释头,最好用1-2句说明完;原因:太多花而不时的东西,会让注释和代码隔得很远;同时杀鸡用牛刀,带来很多工作量,使得人们不敢轻易创建子程序;将来维护起来也很痛苦;
- 在参数声明处进行行尾注释(唯一可以使用行尾注释的例外情况),如果变量名取得好,一眼就知道它是干嘛的,则可以省掉注释(取变量名值得多花点时间,切莫随便);
- 如果有代码文档生成工具(如 Javadoc),则尽量考虑使用,一来有统一的注释位置,二来可以方便的生成文档;
- 如果子程序很长,可以考虑通过注释,区分输入参数和输出参数;原因:这样对于阅读子程序的人,很容易在脑海中勾勒出关键点;
- 对假设进行注释,例如:变量状态的假设(合法或不合法的值,排过序的数组、已经初始化或只包含正常值的数据成员等),当意识到自己正在进行接口的假设时,此时应该将其注释记录下来,原因:未来如果出现错误,可以方便的定位,同时也能够提醒自己必要的时候对假设进行检验;
- 确保注释所有全局变量(最好给全局变量加上 “g_” 的前缀;
- 对子程序的局限性进行注释:例如计算结果的精确度,计算值的允许范围,能够处理的文件大小上限,异外情况可能采取的默认行为等(虽然这些信息在代码里面有,但注释出来可以让人一眼抓住关键,节省时间);(我越来越理解代码是用来读的这句话的意思,因为在多人协同工作的程序中,很多代码可能是要供别人调用的,如果没有清楚的注释,会给别人的调用带来很大的痛苦)
- 说明子程序的全局效果:如果子程序会修改全局数据,务必进行注释说明,描述它对全局数据做了什么(原因:更改全局数据比读取它危险得多)(如果可以的话,我觉得使用另外一个全局变量来保存数据可能更安全);
- 注释所用算法的来源:外部文献的来源,或(自行研发)说明文档的存放位置;
- 用某种规范统一的标记程序的各个部分,例如“/**”表示子程序头;”@param”表示参数;“@version”表示版本,”@throw”表示异常等;可以使用常用的规范(例如 Javadoc),如果没有则考虑自行订立规范
- 注释类、文件和程序
- 类、文件、程序的共同特征是包含了多个子程序,因此它们的注释的重点在于对其所包含的内容提供有意义的概述性说明,比如说明这些子程序的归类原则;
- 标注类的一般原则
- 说明类的设计思路:设计思路有时不容易通过逆向工程获知,提供注释则价值很大,另外注释也可以包括总体设计方法,以及一些曾经考虑过但最后弃用的思路等;
- 说明局限性和用法假设:类似子程序,包括输入输出数据的假设、出错处理的责任划分、全局效果、算法来源等;
- 注释类接口:让其他人只看接口说明即知道如何使用的全部信息,而不需要看类的实现,基本的接口说明包括:参数说明、返回值说明(想起了 opencv 的文档,跟这里的描述很相符,未来可以做为参照);
- 不要在类的接口说明中包含实现细节;
- 注释文件的一般原则
- 在文件头部注释说明该文件的意图和内容:例如如果文件包含多个类,则说明为什么将这些类放在同一个文件中(通常一个类放一个文件,类名和文件名强相关);如果将程序分为多个文件不是出于模块化考虑,则很有必要做出说明,以便他人理解意图和方便查找内容;
- 在大型项目中,有必要在文件头注明作者姓名和联系方式(10人以下的小项目如果实行代码共享所有权,可以不用注释,但大项目模块分工独立,无法实现共享,需要注释);
- 包含版本控制标记:例如 svn 可以通过插入标记自动生成版本信息;
- 如果需要,可以包含法律版权信息等;
- 文件名与内容务必强相关;
- 程序注释用书籍的编排为参考(opencv 的 python turorial 即是一个好的参照)
- 书籍的序:提供整体概要性说明;
- 书籍的目录:提供内容的结构,包括顶层文件、类、子程序等信息,可以是清单的形式,也可以是画成结构图的形式;
- 书籍的章:类
- 书籍的节:子程序声明、数据声明、可执行语句;
- 书籍的附录:交叉引用信息
- 注释单行
- IEEE标准
- 对于代码层以外的说明,据说 IEEE 协会发布的各项标准,是一个很好的信息参考来源,包括软件开发标准、质量保证标准、管理标准等;
- 另外,还有一些书籍对前面这些标准进行整合说明,汇集了各领域顶级专家的经验和智慧结晶,是一个宝库,包括《IEEE 软件工程标准大全》《软件工程标准:用户路线图》等
- 外部文档
- 个人性格
- 研究发现,个人性格对于造就程序员高手需要决定性的意义
- 聪明与谦虚:优秀的程序员能够谦虚的承认自己大脑的局限性,会聪明的使用一些辅助工具,来弥补人类大脑的生理局限,包括:将大问题分解成小问题,进行复查/评审/测试以减少人为错误,将程序写得短小以减少大脑负担,基于问题而不是低层次细节来编程从而减少工作量,通过成熟的规范使用自己的思路从繁琐的编程中解放出来,编写简单容易阅读的代码,方便他人从而减少错误;
- 求知欲
- 对技术事务的求知欲,对于能否成为高手,需要绝对性的重要意义;应将学习当做第一要务;
- 方法
- 在开发过程中,注意自我成长,如果不能成长,应提出抱怨,甚至更换工作;
- 做试验:对于不清晰的模糊问题,应通过写个小程序来进行试验,找出不符合预期的原因,不要写大程序试验,得不偿失(最近使用 numpy 的时候深有体会);
- 阅读他人的问题解决方法:相同问题,一般不是第一次出现,避免重复造轮子;
- 在行动之前,做分析和计划;使用伪代码编程;
- 学习成功项目的开发经验总结:例如人月神话、人件,硝烟中的 Scrum 和 XP;找一些高手编的代码进行阅读,以及渴望了解专家对自己代码的意见;
- 勤于阅读文档,浏览函数库的使用说明;
- 阅读好的书籍杂志
- 同专业人士交往:和同样希望成为高手的人为伍,参加专业的技术交流会议,加入某个用户群,参与网上讨论;
- 向专业开发看齐:编程工作只有15%的时间和计算机打交道,剩下的都是跟人打交道,因此,为人而不是机器编写代码很重要,再怎么强调都不为过;
- 诚实
- 愿意承认自己不知道,乐于承认自己犯下的错误;
- 不忽视编译器的警告,透彻理解自己的代码,而不是满足能够编译运行;
- 提供实际的状况报告,提供实际的进度方案,在上司面前坚持自己的意见;
- 交流与合作:优秀的程序员知道如何与他人融洽的合作和娱乐;明白编码首先是与人的交流,其次才是与计算机的交流;
- 创造力和纪律:二者并不矛盾,在成熟规范内的创造,远比随意创造更有成效,优秀的艺术总是遵守某种形式上的规则,而不是凭空创作;
- 偷懒:编写工具完成烦人的任务,实现一劳永逸的偷懒;
- 其他没有作用的性格因素
- 坚持:钻牛角尖并不能带来更好的结果,当在设定的时间(例如15分钟)内找不到思路时,应马上考虑暂时离开,换个思路,而不是在同一个地方坚持;
- 经验:不能与时俱进的话,经验反而有可能是个累赘;原因:软件技术是迅速变化的,经验与工作效能关系不大
- 疯狂:冷静而清醒,是减少错误的关键,如果把自己搞得很疲惫,反而会犯下大量需要纠正的错误,导致最后的失败;
- 习惯:
- 要在一开始的时候,养成好的习惯;因为一旦坏习惯养成,它就会不自觉的保持下去,导致容易重复出现同样的错误,质量得不到提高(是的,看完本书后,我发现自己还有很多的好习惯需要养成);
- 纠正办法:找到一个新习惯来代替老习惯;例如伪代码编程,编译前检查代码等;
- 软件工艺的话题
- 征服复杂性
- 编程是一项需要应对计算机和现实世界两种复杂度的工作,因此如何降低复杂度,让工作成果的质量和时间都能得到保证,便是编程的重要使命;
- 是否降低复杂度,是衡量程序员成果的最重要依据;
- 精选开发过程
- 对于单人的小项目,软件质量取决于个人能力;对于多个程序员的项目,软件质量取决于组织能力;
- 使用好的开发过程,能够最大程度的保证质量,为它付出时间的投资,将具有巨大的回报;
- 首先为人写程序,其次才是为机器
- 写让人易懂的代码的好处很多,而且,它写起来并不会更慢,重要的是养成良好的习惯;
- 超越一门语言去编程,而不是受限于语言的表层局限性
- 首先根据问题本身寻找解决方案,而不是在语言自身的限制内进行思考;例如:
- 如果所用语言不支持断言,则编写一个自己的 assert() 子程序;
- 即使所用语言支持全局变量和 goto,也要尽量避免使用;
- 如果所有语言不支持枚举类型,则可以制定相关的规范,通过全局变量定义自己的枚举变量或具名常量进行使用;
- 首先根据问题本身寻找解决方案,而不是在语言自身的限制内进行思考;例如:
- 借助规范集中注意力
- 规范可以避免因程序员各自采用不用的细节做法,导致彼此之间的理解困难;
- 规范可以传达重要信息,例如通过添加前缀,一眼即可以识别全局变量、具名常量、变量等;
- 规范可以避免出现危险的错误:例如给复杂的表达式添加括号,一行只写一条语句等;
- 规范可以增加对低层工作的可预见性,例如没有全局变量,便不用思考类和子系统之间可能潜在的联系;
- 规范能够弥补语言的不足之处,例如 python 没有枚举类型和具名常量;
- 基于问题域编程
- 在现实世界的抽象世界中进行思考和表达,而不是在语言的实现细节层次思考,不然会陷入各种小细节,让思路迷失在其中,增加大脑的负担;先思考在不懂代码的情况下,问题如何被解决,之后再考虑如何将解决方案用代码写出来;而不是用代码来思考解决方案;
- 将程序划分为不同层次的抽象,从低到高的抽象
- 操作系统和机器指令
- 编程语言自身的内部实现;
- 低层实现结构:基于编程语言的操作,例如算法、数据结构等
- 低层问题域:对象和服务层;
- 高层问题域:基于上一层的组合,面向最终用户的抽象,某种程度上应让最终用户可以大概看懂;
- 问题域的低层技术:虽然目前并没有系统的结构性方法来实现问题域的抽象,但仍然有一些技术可以辅助实现这个目标,包括:
- 用类来实现有意义的结构
- 使用布尔函数,让复杂的判断变量清晰;
- 有意义的变量命名,例如使用具名常量来描述字符串和文字的意义;
- 引入中间变量保存中间结果;
- 隐藏低层数据类型和实现细节;
- 当心落石
- 由于程序是由人编写的,而人是很容易犯错的,所以需要对程序中各种可能出现错误的地方保持高度的警惕,这些警惕包括:
- 编译器的警告信息;
- 类的成员数量过多,例如有7个以上(说明很可能将过多不属于当前类的操作,混了进来);
- 子程序的判断过多,循环嵌套过深,参数过多等;
- 程序不容易理解;
- 出现的错误次数过多;
- 代码出现重复;
- 未在源头使用避免出错的手段,例如指针释放后置空;
- 子程序难以测试(表明可能与其他子程序过度耦合)
- 由于程序是由人编写的,而人是很容易犯错的,所以需要对程序中各种可能出现错误的地方保持高度的警惕,这些警惕包括:
- 迭代
- 由于现实世界的复杂性和不确定性,在产品开发过程中,需求不断迭代是加深对问题领域的了解的必不可少的过程,因为这种了解需要在实证过程中进行,无法凭空想象;
- 除少需求变化产生的迭代外,开发本身也需要迭代,因为一开始的方案虽然或许可行,但很可能并不是最好的方案,需要在后续过程中不断的调优;
- 评审能够使开发过程少走弯路,它在编码的早期阶段即引入了迭代;对于评审不通过的编码,即需要返工重新编写;
- 分离软件与信仰
- 编程是一种工程技术,这也意味它具备一定的灵活性,同样的问题有不止一种解决方法。在仔细评估各种方法的利弊得失后,可以选择一种比较平衡的方案,并做好备注;避免因为信仰,而刻意丢弃这种灵活性,这样会极大的限制找到最优解;
- 试验:保持开放的思路,多做试验,寻找最优的方法;如果做试验却不能基于实验结果改变思路,则试验只是浪费时间;
- 征服复杂性
- 何处有更多的信息
- 《编程珠玑》
- 《Conceptual Blockbusting: A Guide to Better Ideas》
代码大全
https://ccw1078.github.io/2018/05/09/代码大全/