原英文可链接地址:http://wenku.baidu.com/view/df129425a32d7375a4178043.html
冯兴来(K083) 杨国珂(K081) (译)
2013-6-26
clean code(整洁代码)
整洁代码的缘由
若团队中的每个人能很容易地读懂代码的话,那么此代码是整洁的。其可理解能力源自可读性,可变性,延展性和可维持性。大多事情在没有累积到一定量的技术负荷时,一项项目的进展需历经很长一段时间。
一项项目一开始就写整洁代码是一次投资,这次投资会根据软件产品的生命周期尽可能地保持更新代价恒定,因此,当书写整洁代码时(灰色行),起初的更新代价会比那快而又垃圾的程序(黑色行)稍微高级,但很快就会得不偿失。特别铭记软件代码维护期间所花的大笔费用,不整洁代码会造成技术负荷!这种技术负荷随着时间的推移而增加。导致技术负荷还有其他原因,如糟糕的处理和文件的缺失,但不洁的代码是一个主要主要因素。因此,变化反应的能力会降低(红色行)。
错误无法隐藏于整洁代码
当改变现存代码时,许多软件缺陷就会被引入。其背后的原因是开发商不能完全掌握改变代码时产生的影响。通过编写尽可能容易理解的代码,整洁代码会缩小软件缺陷的风险。
原则
不严的结合+
当至少有一个模块使用了其它的模块时,则两种类,组件或模块将会耦合。有关它们的术语了解越少,其结合性能就越松散。
仅能够松散地耦合到其环境中的组件比那比强结合的组件更容易改变或者替换。
高凝聚性能+
凝聚指的是一个模块内部各成分之间相关联程度的度量。
类中的一个组件、类的方法和字段应该应具有较高的凝聚力。高凝聚性能在类和组件中能生成较为更简单,更易理解的代码框架和设计。
局域性变化+
若软件系统须保持,延长和更新很长一段时间,保持局域性变化降低了有关的成本和降低了风险。保持局域变化意味着有设计变化不交叉的边界。
易于删除+
我们通常是通过添加,延展或改变功能来构建软件。然而,移除元件很重要,这样可以保持整体设计尽可能的简单。若存在代码过于复杂化,移除或用一个或多个简单的块替代是必要的。
恶习
刚性–
软件更改是不容易的。一个微小的变动就会引来后续一连串的变化。
脆弱性–
软件很多地方遭到破坏,取决于一个单一的变化。
静态性 –
不能重复使用其他项目中的代码,因为会存在一定的风险和付出更大。
粘度设计–
做快捷和引进技术负荷更能够事半功倍。
粘度环境–
创建,测试和其他任务需要消耗很长时间。为此,任何人都无法正确执行这些活动,技术负荷也如此。
不必要的复杂化–
次设计会包含当前还没使用的成分。增添的复杂化使代码更难理解。因此,延展和变更代码将会投入更多作为。
不必要的重复性–
代码中包含大量重复的代码:精确的代码重复或设计重复(即以不同的方式做同样的事情)。重复更改一段代码更昂贵,而且更易于出错,因为须多处变化,随之一处的风险无法改动。
透明度–
代码是很难理解的。因此,由于不懂得其负面效应,任何变化都需要花费额外的时间来重新设计代码,并更可能导致缺陷。
类的设计
单一职责原则(SRP)+
一个类应该有一个,且只有一个理由去改变。
开闭原则(OCP)+
在不修改它的情况下,你能够有扩展一个类的行为。
衍生替换原则(LSP)+
驱动类必须替代为其基类。
反向依赖原则(DIP)+
依赖于抽象,而不是结合。
界面分离原则(ISP)+
为特定客户量身打造细粒般的接口。
类应尽量小+
较小的类是比较容易理解的。类应不超过约100行的代码。否则,它是很难识别出类是如何运行的,它可能不仅仅是一个单一的作业。
凝聚性封装
释放再用等效原理(RREP)+
重用的颗粒就是释放该颗粒。
通用终止原则(CCP)+
一起改变的类被封装在一起。
共同重用原则(CRP)+
一起使用的类被封装在一起
封装结合
无环依赖原则(ADP)+
包的依赖关系图必须无周期性。
稳定依赖原则(SDP)+
取决于稳定的方向。
稳定抽象原则(SAP)+
抽象随稳定而增加
常规
依据标准公约+
编码,建筑,设计指南(检查工具)
保持简单而又不乏愚蠢(KISS)+
简单的总是更好。尽可能地降低复杂性。
童子军规则+
在你发现它时,要保持营地清洁。
根本原因分析+
不停地寻找问题的根本原因。否则,你会再而三地犯错误。
源文件中的多元化语言–
C#,Java中,JAVASCRIPT,XML,HTML,XAML,英语,德语...
环境
项目建设只需要一步+
核实然后建立一个单一的命令。
执行测试只需要一步+
运行所有的单元测试,只需一个单一的命令。
源控制系统+
通常使用源控制系统。
持续集成+
保证完整性与持续集成
安全优先–
不要忽视警告,错误,异常处理 – 这类时常会出现在你面前。
依懒关系注入
源于运行时间的分离结构+
运行时,在完全分离结构阶段,有助于简化运行时的行为。
设计
较高水平的配置数据+
一个如默认或配置恒定的值,具有抽象化预计的高水平,不要与个低级别的功能相提并论。作为一个参数揭露它,从高层次的功能上调用低级别的功能。
不具随意性+
要能解释你构建的代码,并且拥有各原因来确保代码的结构是可传达。如果结构出现任意性,别人就会有权去改变它。
精确性+
在你代码做出变动前,确保你能用得恰当。知道你为什么用它,你将如何处理任何错误。
常规结构+
按照约定的结构完善设计决策。命名规范要求要好,但当他们处于劣势时会强制遵从。
趋于多态性if / else或Switch/Case+
“一种切换”:对于一个给定类型的选择有可能是不超过一个switch语句。switch语句的使用情况,必须在系统其余部分的地方创建关于switch语句的多态对象。
对称/类比+
精致的对称设计(例如 Load –Save)和遵循类比的设计(例如.NET框架中相同的设计)
独立的多线程代码+
不要和其余的代码混合使用来处理多线程问题。应分为不同的类别。
误置职责–
代码随意放置。
抽象错误级别的代码–
处于抽象错误级别的功能,如一个PercentageFull物业中一个通用的ISTACK<T>。
非定义状态数据–
专业数据并不属于实例状态,但用于保存临时数据。使用局部变量或提取一类抽象执行的动作。
可配置性–
防止可配置性,因为没有人能决定它应该如何构建。否则,这将出现过于复杂且不稳定的系统。
微层–
不要在上面添加功能,但整体需要简化。
依懒性
实体逻辑相依性+
如果一个模块依赖于另一个,那应该是实体依赖,而不只是逻辑。不需要提出假设。
单身/服务定位–
使用依赖注入。单身隐藏的依赖关系。
基类取决于它们的派生类–
基类可以与任何派生类一起存在。
信息过多 –
减少接口以缩小耦合度
特征排斥–
类的方法应该是用在属于该类的类变量和函数中产生作用的,而不是作用于其他类别的变量和函数。当一个方法使用一些其他的对象来操作该对象内的数据的访问器和存取器时,那么它排斥其他对象类。它希望它里面的其他类以便可以直接访问变量操纵。
人工耦合–
彼此毫无依赖不应该被人为地加上。
隐性临时耦合 –
例如,如果说一些方法调用的顺序非常重要的话,那么应确保他们不能被处于错误次序。
可传递导览–
迪米特库阿法则,写的代码乏味枯燥。
一个模块只需要知道它的直接依赖
命名
选择描述性/明确的名称+
名称必须能够反映变量,字段,属性所代表的含义。名称必须是精确的。
选择适当的抽象级别名称+
选择合适的名称,反映出你所使用类或方法的抽象性
功能抽象化后命名接口+
一个接口的名称应该来自其使用的客户端,如istream。
名称类别后,他们如何实现他们的接口+
类的名称应该反映它是如何通过如记忆流的IStream的接口(s)体现其功能。
命名之后他们做些什么+
方法的命名应该说明做了什么,而不是它是如何做。
为长作用域使用长文件名+
领域->参数->局部变量->循环变量->长->短
名称效应+
名称必须反映整个功能。
在可能的情况下进行标准命名+
有标准规范的时不要捏造属于自己的语言。
编码名称
没有前缀,没有类型/范围信息+
可理解性
一致性+
如果你想以一种特定的方式来做某些事情,那么可以统一的方式做同一类似的事情: 相同的变量名称对应相同的概念,相应的概念对应相同的命名模式。
使用解释性变量+
使用区域变量来定义算法名称
封装边界的条件+
边界的条件是很难跟踪到的,可将它们放在一个地方进行处理,如下一步=该步+1
相对于原始数据类型选择专用值对象+
使用专用的基本类型取代像字符串,整数这样的原始类型,如,用绝对路径代替字符串。
差评–
评论不添加任何值(冗余代码),像格式不正确,不正确的语法和拼写。
模糊的意图–
过于密集的算法会失去所用的表现形式。
明显的行为方式不能执行–
违反了“最小奇异原则”,你所期望的就是你想得到的。
隐藏的逻辑依赖–
一个调用方法的正确与否取决于同一个类的其它东西,如调用删除时,能删除,返回真,否则返回假。
方法
一题多解式+
循环,异常处理,...封装在子方法
方法应该下降1个抽象级别+
声明在同一个语句中的方法都应该写在同一个抽象层中,它应该是低于描述功能函数的名称。
太多参数式的方法–
参数较少最好。其功能可能被外包到一个含有字段信息的专用的类中
OUT/ REF参数的方法–
防止使用。返回复杂对象体现所有值,再分成不同种方法。如果你的方法必须改变事物的状态时,它被称为改变对象的状态。
选择器/标志符–
全局整数符(布尔标志)分割成几个独立的方法,这些独立方法可以从客户端调用无标志的方法。
不适当静态 –
静态方法应该是一个引证方法
源代码结构
纵向分离+
变量和方法应接被用来定义在他们关系密切的地方。局部变量应该声明略高于他们的第一次使用,并有一个小的垂直范围。
嵌套+
嵌套代码应比非嵌套的代码更具体,或能够处理较小可能性的情况。
命名空间功能的代码结构+
保持所有属于相同的特性。不要使用命名空间通信层。一个特性可能会调用其他功能,商业功能可能会调用类似记录的核心功能。
条件语句
封装式条件+
if(this.ShouldBeDeleted(定时器))优选于if(&&timer.HasExpired!timer.IsRecurrent)。
积极条件+
积极的条件比消极的条件语句容易阅读。
垃圾东西
无用的注释及代码 –
删除未使用的东西。您可以在版本控制系统中找到他们。
杂乱–
不是无效的代码但不添加任何功能。
不适当的信息–
注释比不同的系统信息要好:产品积压,源代码控制。仅使用代码注释的技术说明
可维护性杀手
复制–
消除重复。对 “不要重复自己”(DRY)原则的侵犯。
幻数/字符串–
当有意义的命名不能来自本身的价值时,将幻数和字符串替换命名常量,给他们一个有意义的名字。
枚举(持续或特定行为)–
持续使用的参考代码,而非枚举式的。他们特定的行为使用多态,而非枚举。
异常处理
捕捉特定的异常+
捕获异常要尽可能具体。捕获的异常,可以以一种有意义的方式反应。
有意义方式反应下的捕捉+
捕获异常时,你可以以一种有意义的方式反应。否则,让其他人调用堆栈中的反应它。
使用异常,而非返回代码或空+
在特殊情况下,当你的方法不能完成其工作时,须摒弃异常。不接受或返回null。不要返回错误代码。
故障快速切换+
在发现一个错误特例后,应尽早摒弃异常。通过异常的堆栈跟踪,这有助于查明确切位置的问题
为控制流使用异常 –
控制流使用异常:有坏的表现,难理解,导致较难处理的真实异常。
误吞的异常–
只有当留下捕获块后在特殊情况下完全解决异常,其可以被吞噬。否则,系统留在不一致的状态。
难以更新代码到整洁代码的过渡
总会有一个正在运行的系统+
小步骤式变更系统,从一个运行状态到另一个运行状态。
1)识别功能+
识别代码中现有的功能,并根据有关它们未来的发展(变化的可能性和风险)再来优先考虑他们。
2)引入可测性的边界接口+
重构的界限,你的系统接口,以便可以模拟环境测试的双精度(膺品,嘲笑,存根,模拟器)。
3)写功能验收测试+
覆盖功能验收测试,以构建一个安全网络。
4)识别组件+
基于一种功能,确定所使用的组件提供的功能。根据未来发展的相关性(可能性和风险的变化)的优先次序来排列组件。
5)重构组件之间的接口+
重构(或采用)之间的接口组件,以便每个组件都可以在隔离的环境中测试。
6)写组件的验收测试+
凭借组件功能的验收测试来囊括其功能。
7)决定每个组件:重构,流程再造,保留+
决定是否要重构,再造或保留它的每个组件。
8A)重构组件+
内重新设计类组件和重构一步步骤(见重构形式)。为每一个最新设计的类添加单元测试。
8B)重新设计组件+
使用ATDD和TDD(见清洁ATDD/TDD)的重新实现组件。
8C)保留组件+
对于一个组件,如果预计未来只有少数的变化,并且该组件过去很少有缺陷,不妨可以考虑保留它。
模式重构
调解分歧——类似代码一体化
逐级变更代码直至相同。
隔离变更+
首先,隔离的代码从剩余部分进行重构。然后重构。最后,撤消隔离。
数据迁移+
通过从一个表示到另一个移动临时性重复数据结构。
临时并行实现+
通过引入临时并行的算法实现来“重构”。后一个调用者切换等。当不再需要时,删除旧的解决方案。
非包装区的组件+
采用了内部组件边界,且把一切不必要的内部边界外推到组件接口和内部边界之间的非包装区。重构组件接口与内部边界相匹配并消除非包装区。
如何学会整洁代码
结对编程 +
在一个单一的工作站,若有两个开发人员一起解决问题。那么一个是驱动器,另一个是导航器。该驱动器是负责编写代码。导航器是负责按照编码准则来维持解决方案与架构,并着眼于下一步如何进行(例如写下一步的测试)。想问题的思路以及解决问题的方式得到激发。
评论提交+
开发人员与并同开发人员通过所有代码变更前的提交(或推动)到版本控制系统的变化。同行开发人员针对整洁代码的指引和设计对代码进行检查。
编码道场+
一组开发人员聚集于编码道场,培养他们的技能。两个开发人员结对编程以解决问题(套路)。其余的人员一起观察。10分钟后,该组旋转建立一个新的队。但只有当所有的测试都是新的时,观察员才可以评论当前的解决方案。
参考书目
整洁代码:由罗伯特·马丁敏捷软件大师手册
自动测试的种类
ATDD——验收测试驱动开发+
先指定一个功能测试,然后实现
TDD——测试驱动开发+
红色——绿色——重构。测试一点代码——编写一点代码。
DDT——缺陷驱动测试+
写一个单元测试,再现缺陷——修复代码——测试成功——缺陷不再返回。
POUTing——普通的单元测试
Aka测试后,编写单元测试,检查现有的代码。你不能,也可能不想要测试驱动所有的。使用POUT增加健全性。用于在测试驱动开发后添加额外测试(例如,环绕边界值的测试)
可测试性设计
构造函数——简单
对象必须是可治愈的。否则,简单和快速的测试是不可能的。
构造函数——生命周期(使用寿命)
通过依赖关系和配置/参数的构造函数,有一个使用寿命等于或超过创建的对象。为其他值使用方法或属性。
在系统边界的抽象层
在系统边界(数据库、文件系统、网络服务、COM接口……)使用抽象层,通过使用mock对象简化单元测试。
结构
AAA准备——执行——断言
结构总是由AAA测试。切勿混用这三个模块。
测试程序集(.net)
为每个生产程序集创建测试程序集,并将其命名为生产程序集 +“.test”。
测试命名空间
将测试放在相同的命名空间作为其关联法
单元测试方法显示全部真相
单元测试方法显示测试所需的所有部件。不要使用安装(设置)方法或基类对关联法或依赖项执行操作。
只对基础设施设置/清除仅基础设施的安装/拆卸
只对你的单元测试所需要的基础设施使用设置/清除方法。请勿用于其他任何测试。
测试方法命名
名称反映了什么测试, 例如:FeatureWhenScenarioThenBehaviour
单一方案中每个测试
一个测试只检查一种方案
资源文件
测试和资源都在一起:FooTest.cs,FooTest.resx
命名
SUT测试变量命名
给被测试系统下的变量总是相同的名称 (例如: testee或 sut)。明确标识 SUT,抗重构。
结果值命名
给变量持有的测试方法的结果总是相同的名称(例如,result)。
匿名变量
总是对保持无趣的参数测试方法的变量使用相同的名称(例如anonymousText)
不要假设
理解算法
只是工作是不够的,确保你明白为什么它工作。
在边界的错误的行为
始终单元测试边界。不要假设行为
伪造(存根,假装,间谍,模拟……)
从环境隔离
用假货来模拟测试对象的所有依赖关系。
伪造框架
对假货使用动态的伪造框架,显示在不同的测试方案的不同行为(小行为重用)。
手工编写的假货
使用手工编写的假货当他们可以用在一些测试和当在这些方案中他们只很少的改变行为时(行为重用)。
混合存根与期望宣言
当使用mock对象时,确保你遵循AAA(准备,执行,断言)。不要混合设置存根(使测试对象可以运行)的期望(测试对象应该做的)在相同的代码块。
检查假货代替测试对象
测试不检查测试对象,但通过假货返回值。通常情况下,是由于过多的使用假货。
过多的假用法
如果您的测试需要大量的mock对象或模拟设置,然后考虑将测试对象分为若干类别,或在测试对象和它的依赖项之间提供一个额外的抽象。
单元测试的原则
快
为了经常执行单元测试必须快速。快意味着远小于秒。
独立
清除故障发生的位置。测试之间没有相关性(随机顺序)。
可重复性
没有假设初始状态,什么都没有留下,外部服务没有依赖项可能不可用(数据库、文件系统…)。
自我验证
不需要手动测试解释或干预。红色或绿色!
及时
测试被写入在恰当的时间(TDD,DDT,POUTing)。
单元测试的缺陷
测试没有测试任何东西
通过测试,乍一看是有效的,但没有测试测试对象。
测试需要过多的设置
一个测试,需要几十行代码来设置其环境。这种噪声使得它很难看到什么是真正测试。
对多个方案太大的测试/断言
这是一个有效的测试,但是,过大。原因可能是,这个测试检查多个功能或测试对象做多件事情(违反单一职责原则)。
检查内部
一个测试,直接(反射)访问测试对象的内部(私有/受保护的成员)。这是重构杀手。
测试仅在开发人员的计算机上运行
一个测试,是依赖于开发环境和其他地方失败的测试。使用持续集成,尽快赶上他们。
超过必要的测试检查
一个测试是专项的检查,当有些东西改变时,测试是不必要的,则测试失败。尤其是当假货参与或检查无序集合中的项目顺序时。
不相关的信息
了解测试时,测试包含的信息是不相关的。
健谈的测试
用文本填补了控制台的测试——对某些东西可能使用一次手动检查
测试吞咽异常
捕获异常的一个测试并让测试通过。
测试不属于主机测试夹具
测试一个比在夹具的所有其他测试者完全不同的测试。
过时的测试
检查在该系统中不再需要的东西的测试。因为它仍被引用,甚至可能妨碍清理生产代码。
隐藏的测试功能
测试功能隐藏在设置方法,基类或辅助类。只通过查看测试方法测试应该清楚别的没有初始化或断言的地方。
臃肿的建设
建设的依赖项和参数用于调用测试对象使得测试几乎不可读。提取的辅助方法,可重复使用。
不清楚失败原因
拆分测试或使用断言消息。
条件测试逻辑
测试不应该有任何条件测试逻辑,因为它是难以阅读。
在生产代码的测试逻辑
测试依赖于在生产代码中的特殊逻辑。
不稳定的测试
有时通过,有时候失败由于左越过或环境。
TDD原则
一个测试检查的一个特征
一个测试检查测试对象的一个特点。这意味着,它测试此功能中所包括的所有东西,但不多。这可能包括多个测试对象的调用。通过这种方式,测试作为测试对象的用法示例和文档。
微小的步骤
制定小步骤。在编写要求生产代码前在测试中只添加少量代码。然后重复,每一步只添加一个断言。
保持简单的测试
每当一个测试变得复杂,检查测试对象是否可以分割成若干类(单一职责原则)。
相比于行为验证更多的应用状态验证
只有如果没有状态来验证使用行为验证。
测试领域特定语言
使用DSLs测试来简化阅读测试:辅助方法,类。
TDD过程的缺陷
将代码覆盖率用作目标
使用代码覆盖率寻找到失踪的测试,但不使用它作为驱动工具。否则,结果可能是测试的代码覆盖率,但不是确定性增加。
在最后的 ~ 10 分钟没有绿色栏
小步骤获得尽可能快和频繁的反馈。
不在编写生产代码之前运行测试
只有如果测试失败,则需要新的代码。另外,如果测试出人意料的是,不存在,失败,然后请确保测试是正确。
对于重构没有花足够的时间
重构是一种对未来的投资。可读性,可变性和可扩展性会偿还。
跳过一些太容易的测试
不要假设,检查它。如果它是很容易的,那么测试就更容易了。
跳过一些太难的测试
使其更简单,否则缺陷会隐藏在那里,可维护性将受到影响。
组织周围方法的测试,而不是执行
这些测试是脆性和重构杀手。测试完整的“迷你”的用例的方式反映在现实世界中将如何使用该功能。不要独立的测试setter和getter,测试它们在使用中的情况。
红色栏模式
单步测试
选择一个你有有信心可以实现最大化学习效应的测试 (例如,影响设计)。
部分测试
编写一个测试,它不完全检测所需的行为,但使你更进一步的接近它。然后用下面的扩展测试。
扩展测试
扩展现有的测试,以更好地匹配真实世界的场景。
另一个测试
如果你想到新的测试,然后将他们写在任务列表上,并不会丢失当前测试的重点。
学习测试
针对外部元件的测试,以确保他们的行为符合预期。
绿色栏模式
假设(直到成功)
返回一个常数来获得的第一个测试运行。然后重构。
三角定位一驱动抽象
编写具有至少两组样本数据的测试。抽象的执行这些数据。
明显的实现
如果实现是很明显的就实现,测试运行。如果不是,那么退后一步,只是测试运行和后重构。
一对多一驱动收集操作
首先,对单个元素实施操作。然后,逐步为多个元素实施操作。
验收测试驱动开发
使用验收测试来驱动你的TDD测试
检查验收测试所需的功能。让它们引导你的TDD。
用户功能测试
验收测试是一个为完整的用户功能从顶部到底部提供商业价值的测试。
自动化ATDD
对回归测试和可执行的规范使用自动化的验收测试驱动开发。
组件的验收测试
为单个组件或子系统编写验收测试,使这些部分可以自由组合而不失测试覆盖率。
模拟系统边界
模拟系统的边界,如用户界面,数据库,文件系统和外部服务,来加快您的验收测试,并能够检查异常情况(例如,一个完整的硬盘)。使用系统测试检查的边界。
验收测试spree
不要为每一种可能性编写验收测试。只为真实方案编写验收测试。特殊和理论的情况下,用单元测试可以更容易覆盖。
持续集成
预提交检查
运行所有的单元测试和验收测试覆盖当前工作代码在提交到源代码库之前。
后提交检查
在持续集成服务器上运行每个提交到版本控制系统的所有的单元测试和验收测试。
传递失败的集成到整个团队
每当一个阶段持续集成服务器上发生故障时,通知整个团队为了尽快解决阻塞情况。
构建分段
将完整的持续集成工作流程拆分成各个阶段,以减少反馈时间。
自动构建测试系统的安装程序
在测试系统上尽可能经常为测试软件自动生成安装程序(用于手动测试,或真实的硬件测试)。
连续部署
为每次提交或手动请求安装系统到测试环境。部署到生产环境中也是自动的。
测试金字塔
完