代码整洁之道笔记
代码整洁之道
整洁代码
要有代码
记住,代码确然是我们最终用来表达需求的那种语言.我们可以创造各种与需求接近的语言.我们可以创造帮助把需求解析和汇整为正式结构的各种工具.然而,我们永远无法抛弃必要的精确性–所以代码永存.
什么是整洁代码
- 我喜欢优雅和高效的代码,代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来,整洁的代码只做好一件事.
- 整洁的代码简单直接,整洁的代码如同优美的散文.整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句
- 整洁的代码应当可由作者之外的开发者阅读和增补.它应有单元测试和验收测试.它使用有意义的命名.它只提供一种而非多种做一件事的途径.它只有尽量少的依赖关系,而且要明确地定义和提供清晰,尽量少的API.代码应通过其字面表达含义,因为不同的语言导致并非所有必须信息均可通过代码自身清晰表达.
- 我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本性的.整洁的代码总是看起来像是某位特别在意它的人写的.几乎没有改进的余地.作者的代码什么都想到了,如果你企图改进它,它总会回到原点,赞叹某人留给你的代码–全心投入某人留下的代码.
- 近年来,我开始研究贝克的简单代码规则,差不多也都琢磨透了.简单代码,依其重要顺序:
- 能通过所有测试
- 没有重复代码
- 体现系统中的全部设计理念
- 包括尽量少的实体,比如类,方法,函数等
- 如果每个例程都让你感到深和己意,那就是整洁代码,如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码.
思想流派(整洁代码派宗旨)
- 武术家从不认同所谓最好的武术,也不认同所谓绝招.武术大师们常常创建自己的流派,聚徒而授.
- 弟子们沉浸于创始人的授业.他们全心师从某位师傅,排斥其他师傅.弟子有所成就后,可以转投另一位师傅,扩展自己的知识和技能.有些弟子最终百炼成钢,创出新招数,开宗立派.
- 任何门派都并非绝对正确.
- 可以把本书看作是对象导师整洁代码派的说明.
童子军军规
让营地比你来时更干净
如果每次签入时,代码都比签出时干净,那么代码就不会腐坏.
小结
艺术书并不保证你读过之后能成为艺术家,只能告诉你其他艺术家用过的工具,技术和思维过程.本书同样也不担保你成为好程序员.它不担保能给你”代码感”.它能做的,只是展示好程序员的思维过程,还有他们使用的技巧,技术和工具.
有意义的命名
名副其实
- 变量,函数或类名称应该已经答复了所有的大问题,如果名称需要注释来补充,那就不算名副其实
- 代码的模糊度:上下文在代码中未被明确体现的程度
避免误导
- 程序员必须避免留下掩藏代码本意的错误信息.应当避免使用与本意相悖的词.
- 别用accountList来指称一组帐号,除非它真的是List类型.
- 提防使用不同之处较小的名称.
- 以同样的方式拼写出同样的概念才是信息.拼写前后不一致就是误导
- 误导性名称真正可怕的例子,是用小写字母l和大写字母O作为变量名
做有意义的区分
- 如果程序员只是为满足编译器或解释器的需要而写代码,就会制造麻烦.
- 光是添加数字系列或是废话远远不够,即便这足以让编译器满意.
- 以数字系列命名是依义命名的对立面.这样的名称纯属误导.
- 废话是另一种没意义的区分.Info,Data就像a,the一样是意义含混的废话.
- 废话都是冗余
使用读得出来的名称
人类长于记忆和使用单词.大脑的相当一部分就是用来容纳和处理单词的.
使用可搜索的名称
单字母名称和数字常量有个问题,就是很难在一大片文字中找出来.
避免使用编码
把类型或作用域编进名称里面,徒然增加了解码的负担.
避免思维映射
不应当让读者在脑中把你的名称翻译为他们熟知的名称.这种问题经常出现在选择是使用问题领域术语还是解决方案领域术语时.
类名
类名和对象名应该是名词或名词短语
方法名
方法名应当是动词或动词短语
别扮可爱
如果名字太耍宝,那就只有同作者一般有幽默感的人才能记得住.
每个概念对应一个词
给每个抽象概念选一个词,并且一以贯之.
别用双关语
避免将同一单词用于不同的目的.同一术语用于不同概念,基本上就是双关语了.
使用解决方案领域名称
记住,只有程序员才会读你的代码.所以,尽管用那些计算机科学术语,算法名,模式名,数学术语吧.
使用源自所涉问题领域的名称
如果不能用程序员熟悉的术语来给手头工作命名,就采用所涉问题领域而来的名称
添加有意义的语境
很少有名称是能自说明的—-多数都不能.反之,你需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境.如果没这么做,给名称添加前缀就是最后一招了.
不要添加没用的语境
只要短名称足够清楚,就要比长名称好.别给名称添加不必要的语境.
函数
短小
函数的第一规则是要短小.第二规则还要更短小.
代码块和缩进
if语句,else语句,while语句等,其中的代码块应该只有一行.该行大抵应该是一个函数调用语句.
只做一件事
- 函数应该只做一件事.做好这件事.只做这一件事
- 如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事.
- 要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现.
每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上.
自顶而下读代码:向下规则
我们想要让代码拥有自顶向下的阅读顺序.我们想要让每个函数后面都跟着位于下一个抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层向下阅读了.
switch语句
写出只做一件事的switch语句很难.switch语句天生要做N件事.不幸我们总无法避开switch语句,不过还是能够通过多态确保每个switch都埋藏在较低的抽象层级.
使用描述性的名称
- 长而具有描述性的名称要比短而令人费解的名称好.
- 长而具有描述性的名称要比描述性的长注释好.
- 选择描述性的名称能理清你关于模块的设计思路,并帮你改进之.
- 命名方式要保持一致.使用与模块名一脉相承的短语,名词和动词给函数命名.
函数参数
最理想的参数数量是零,其次是一,再次是二,避免使用三
参数列表
可变参数可以视为List对象看待,有可变参数的函数可能是一元,二元,三元的.
动词与关键字
函数和参数应当形成一种非常良好的动词/名词对形式
无副作用
副作用是一种谎言.函数承诺只做一件事,但还是会做其他被藏起来的事.有时,它会对自己类中的变量做出未能预期的改动.有时,它会把变量搞成向函数传递参数或是系统全局变量.无论那种情况,都具有破坏性,会导致古怪的时序性耦合及顺序依赖.
输出参数
避免使用输出参数.如果函数必须要修改某种状态,就修改所属对象的状态吧.
分隔指令与询问
1 | if (set("username", "unclebob")) |
使用异常替代返回错误代码
从指令式函数返回错误码轻微违反了指令与询问分隔的规则.它鼓励了在if语句判断中把指令当作表达式使用.这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构.如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化.
抽离try/catch代码块
try/catch代码块丑陋不堪.它们搞乱了代码结构,把错误处理与正常流程混为一谈.最好把try和catch代码块的主体部分抽离出来,另外形成函数.
错误处理就是一件事
函数应该只做一件事.错误处理就是一件事.因此,错误处理的函数不该做其他事.这意味着如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容.
Error.java依赖磁铁
使用异常替代错误代码,新异常就可以从异常类派生出来,无需重新编译或重新部署.
别重复自己
重复可能是软件中一切邪恶的根源.许多原则与实践规则都是为控制与消除重复而创建.自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复.
结构化编程
只要函数保持短小,偶尔出现return,break或continue语句没有坏处,甚至比单入单出原则更具有表达力.另外一方面,goto只在大函数中才有道理,所以应该尽量避免使用.
如何写出这样的函数
注释
什么也比不上放置良好的注释来得有用.什么也不会比乱七八糟的注释更有本事搞乱一个模块.什么也不会比陈旧,提供错误信息的注释更有破坏性.
- 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败.
- 如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达.
- 注释存在的时间越久,就离其所描述的代码越远,越来越变得全然错误.原因很简单:程序员不能坚持维护注释.
- 程序员应当负责将注释保持在可维护,有关联,精确的高度.
- 不准确的注释要比没注释坏得多.
- 真实只在一处地方有:代码.
注释不能美化糟糕的代码
带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多.与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码.
用代码来阐述
只要想上那么几秒钟,就能用代码j解释你的大部分意图.很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可.
好注释
法律信息
有时,公司代码规范要求编写与法律有关的注释.这类注释不应是合同或法典.只要有可能,就指向一份标准许可或其他外部文档,而不要把所有条款放到注释中.
提供信息的注释
这类注释有时管用,但更好的方式是尽量利用函数名称传达信息.
对意图的解释
有时,注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图.
阐释
有时,注释把某些晦涩难明的参数或返回值的意义翻译为某种可读的形式,也会是有用的.通常,更好的方法是尽量让参数或返回值自身就足够清楚;但如果参数或返回值是某个标准库的一部分,或是你不能修改的代码,帮助阐释其含义的代码就会有用.
警示
有时,用于警告其他程序员会出现某种后果的注释也是有用的.
TODO注释
有时,有理由用//TODO形式在源代码中放置要做的工作列表.
放大
注释可以用来放大某种看来不合理之物的重要性.
公共API中的Javadoc
没有什么比被良好描述的公共API更有用和令人满意的了.标准Java库中的Javadoc就是一例.
坏注释
大多数注释都属此类.通常,坏注释都是糟糕的代码的支撑或借口,或者对错误决策的修正,基本上等于程序员自说自话.
喃喃自语
如果只是因为你觉得或者因为过程需要就添加注释,那就是无谓之举.如果你决定写注释,就要花必要的时间确保写出最好的注释.
多余的注释
它并不能比代码本身提供更多的信息.它没有证明代码的意义,也没有给出代码的意图或逻辑.读它并不比读代码更容易.
误导性注释
有时,尽管初衷可嘉,程序员还是会写出不够精确的注释.
循规式注释
所谓每个函数都要有Javadoc或者每个变量都要有注释的规矩全然是愚蠢可笑的.这类注释徒然让代码变得散乱,满口胡言,令人迷惑不解.
日志式注释
很久以前,在模块开始处创建并维护这些记录还算有道理.那时,我们还没有源代码控制系统可用.如今,这种冗长的记录只会让模块变得凌乱不堪,应当全部删除.
废话注释
这类注释废话连篇,我们都学会了视而不见.读代码时,眼光不会停留在它们上面.最终,当代码修改之后,这类注释就变作了谎言一堆.
可怕的废话
Javadoc也可能是废话.它们只是源自某种提供文档的不当愿望的废话注释.
能用函数或变量时就别用注释
位置标记
如果标记栏不多,就会显而易见.所以尽量少用标记栏,只在特别有价值的时候用.
括号后面的注释
尽管这对于含有深度嵌套的结构的长函数可能有意义,但只会给我们更原意编写的短小,封装的函数带来混乱.如果你发现自己想标记右括号,其实应该做的是缩短函数.
归属与署名
源代码控制系统非常善于记住是谁在何时添加了什么.没必要用那些小小的签名搞脏代码.
注释掉的代码
直接把代码注释掉是讨厌的做法.注释掉的代码堆积在一起,就像破酒瓶底的渣滓一般.
HTML注释
源代码注释中的HTML标记是一种厌物.
非本地信息
如果你一定要写注释,请确保它描述了离它最近的代码.别在本地注释的上下文环境中给出系统级别的信息.
信息过多
别在注释中添加有趣的历史性话题或者无关的细节描述.
不明显的联系
注释及其描述的代码之间的联系应该显而易见.如果你不麻烦要写注释,至少让读者能看着注释和代码,并且理解注释所谈何物.
函数头
短函数不需要太多描述.只为做一件事的短函数选个好名字,通常要比写函数头注释要好.
非公共代码中的Javadoc
虽然Javadoc对于公共API非常有用,但对于不打算作公共用途的代码就令人厌恶了.
格式
当有人查看底层代码实现时,我们希望他们为整洁,一致及所感到的对细节的关注而震惊.我们希望他们高高扬起眉毛,一路看下去.我们希望他们感受到那些为之劳作的专业人士们.但若他们看到的只是一堆像是由酒醉的水手写出的鬼画符,那他们多半会得出结论,认为项目其他任何部分也同样对细节漠不关心.
你应该保持良好的代码格式.你应该选用一套管理代码格式的简单规则,然后贯彻这些规则.如果你在团队中工作,则团队应该一致同意采用一套简单的格式规则,所有成员都要遵从.使用能帮你应用这些格式规则的自动化工具会很有帮助.
格式的目的
先明确一下,代码格式很重要.代码格式不可忽略,必须严肃对待.代码格式关乎沟通,而沟通是专业开发者的头等大事.
垂直格式
这意味着有可能用大多数为200行,最长500行的单个文件构造的出色系统.尽管这并非不可违背的原则,也应该乐于接受,短文件通常比长文件易于理解.
向报纸学习
源文件也要像报纸文章那样.名称应当简单且一目了然.名称本身应该足够告诉我们是否在正确的模块中.源文件最顶部应该给出高层次概念和算法.细节应该往下渐次展开,直至找到源文件中最底层的函数和细节.
概念间垂直方向上的区隔
几乎所有的代码都是从上往下读,从左往右读.每行展现一个表达式或一个子句,每组代码行展示一条完整的思路.这些思路用空白行区隔开来.
垂直方向上的靠近
如果说k空白行隔开了概念,靠近的代码行则暗示了它们之间的紧密关系.所以紧密相关的代码应该互相靠近.
垂直距离
- 关系密切的概念应该互相靠近.显然,这条规则并不适用于分布在不同文件中的概念.除非有很好的理由,否则就不要把关系密切的概念放到不同的文件中.
- 对于那些关系密切,放置于同一源文件中的概念,它们之间的区隔应该成为对相互的易懂度有多重要的衡量标准.应避免迫使读者在源文件和类中跳来跳去.
- 变量声明:变量声明应尽可能靠近其使用位置.因为函数很短,本地变量应该在函数的顶部出现.
- 循环中的控制变量应该总是在循环语句中声明.
- 偶尔,在较长的函数中,变量也可能在某个代码顶部,或在循环之前声明.
- 实体变量应该在类的顶部声明.这应该不会增加变量的垂直距离,因为在设计良好的类中,它们如果不是被类的所有方法也是被大多数方法所用.
- 相关函数:若某个函数调用了另外一个,就应该把它们放到一起,而且调用者应该尽可能放在被调用者上面.
- 概念相关:概念相关的代码应该放到一起.相关性越强,彼此之间的距离就越短.
垂直顺序
一般而言,我们想自上向下展示函数调用依赖顺序.也就是说,被调用的函数应该放在执行调用的函数下面.这样就建立了一种自顶向下贯穿源代码模块的良好信息流.
横向格式
这说明,应该尽力保持代码行短小.死守80个字符的上限有点僵化,而且我也并不反对代码行长度达到100个字符或120个字符.再多的话,大抵就是肆意妄为了.
水平方向上的区隔与靠近
我们使用空格字符将彼此紧密相关的事物连接到一起,也用空格字符把相关性较弱的事物分开.
- 在赋值操作符周围加上空格字符,以此达到强调的目的.
- 不在函数名和左括号之间加空格.
- 把函数调用括号中的参数一一隔开,强调逗号.
水平对齐
这种对齐方式没什么用.对齐,像是在强调不重要的东西,把我的目光从真正的意义上拉开.
缩进
源文件是一种继承结构,而不是一种大纲结构.其中的信息涉及整个文件,文件中每个类,类中的方法,方法中的代码块,也涉及代码块中的代码块.这种继承结构中的每一层级都圈出一个范围,名称可以在其中声明,而声明和执行语句也可以在其中解释.
程序员相当依赖这种缩进模式.没有缩进的话,程序就会变得无法阅读.
空范围
把分号放到另一行再加以缩进,否则就很难看到它.
1 | while (dis.read(buf, 0, readBufferSize) != -1) |
团队规则
一组开发者应当认同一种格式风格,每个成员都应该采用那种风格.我们想要让软件拥有一以贯之的风格.我们不想它显得是由一大票意见相左的个人所写成.
好的软件系统是由一系列读起来不错的代码文件组成.它们需要拥有一致和顺畅的风格.读者要能确信,他们在一个源文件中看到的格式风格在其他文件中也是同样的用法.绝对不要用各种不同的风格来编写源代码,这样会增加其复杂度.
鲍勃大叔的格式规则
1 | public class CodeAnalyzer implements JavaFileAnalysis { |
对象和数据结构
将变量设置为私有(private)有一个理由:我们不想其他人依赖这些变量.我们还想在心血来潮时能自由修改其类型或实现.那么,为什么还是有那么多程序员给对象自动添加赋值器和取值器,将私有变量公之于众,如同它们根本就是公共变量一般呢?
数据抽象
隐藏实现并非只是在变量之间放上一个函数层那么简单.隐藏实现关于抽象!类并不简单地用取值器和赋值器将其变量推向外间,而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体.
我们不愿意暴露数据细节,更原意以抽象形态表达数据.这并不只是用接口和/或赋值器,取值器就万事大吉.要以最好的方式呈现某个对象包含的数据,需要做严肃的思考.
数据,对象的反对称性
对象和数据结构之间的二分原理:
- 过程式代码便于在不改动既有数据结构的前提下添加新函数.面向对象代码便于在不改动既有函数的前提下添加新类.
- 过程式代码难以添加新数据结构,因为必须修改所有函数,面向对象代码难以添加新函数,因为必须修改所有类.
在任何一个复杂系统中,都会有需要添加新数据类型而不是新函数的时候.这时,对象和面向对象就比较合适.另一方面,也会有想要添加新函数而不是数据类型的时候.在这种情况下,过程式代码和数据结构更合适.
The Raw of Demeter
模块不应了解它所操作对象的内部情形.对象隐藏数据,暴露操作.这意味着对象不应通过存取器暴露其内部结构,因为这样更像是暴露而非隐藏其内部结构.
更准确地说,得墨忒耳律认为,类C的方法f只应该调用以下对象的方法:
- C
- 由f创建的对象
- 作为参数传递给f的对象
- 由C的实体变量持有的对象
火车失事
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
这类代码常被称作火车失事,这类连串的调用通常被认为是肮脏的风格,应该避免.
混杂
这种混淆有时会不幸导致混合结构,一半是对象,一半是数据结构.此类混杂增加了添加新函数的难度,也增加了添加新数据结构的难度,两面不讨好.
隐藏结构
BufferOutPutStream bos = ctxt.createScratchFileStram(classFileName);
直接让ctxt对象来创建流,隐藏了其内部结构,防止当前函数因浏览不该知道的对象而违反得墨忒耳律.
数据传送对象
最为精炼的数据结构,是一个只有公共变量,没有函数的类.这种数据结构有时被称为数据传送对象,或DTO.
小结
对象暴露行为,隐藏数据.便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为.数据结构暴露数据,没有明显的行为.便于向既有数据结构添加新行为,同时也难以向既有函数添加新数据结构.
错误处理
错误处理只不过是编程时必须要做的事之一.输入可能出现异常,设备可能失效.简言之,可能会出错,当错误发生时,程序员就有责任确保代码照常工作.
使用异常而非返回码
返回码的问题在于,它们搞乱了调用者代码.调用者必须在调用之后即刻检查错误.不幸的是,这个步骤很容易被遗忘.所以,遇到错误时,最好抛出一个异常.调用代码很整洁,其逻辑不会被错误处理搞乱.
先写Try-Catch-Finally语句
在某种意义上,try代码块就像是事务.catch代码块将程序维持在一种持续状态,无论try代码块中发生了什么均如此.所以,在编写可能抛出异常的代码时,最好先写try-catch-finally语句.
使用不可控异常
可控异常的代价就是违反开放/闭合原则.如果你在方法中抛出可控异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常处之间的每个方法签名中声明该异常. 这意味着对软件中较低层级的修改,都将波及较高层次的签名.修改好的模块必须重新构建,发布,即便它们自身所关注的任何东西都没改动过.
给出异常发生的环境说明
你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所.
依调用者需要定义异常类
当我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获.
定义常规流程
特例模式:创建一个类或配置一个对象,用来处理特例.用户就不用对付异常行为了.异常行为被封装到特例对象中.
别返回null值
返回null值,基本上是在给自己增加工作量,也是在给调用者添乱.只要有一处没检查null值,应用程序就会失控.如果你在调用某个第三方API中可能返回null值的方法,可以考虑用新方法打包这个方法,在新方法中抛出异常或返回特例对象.
别传递null值
在方法中返回null值是糟糕的做法,但将null值传递给其他方法就更糟糕了.除非API要求你向它传递null值,否则就要尽可能避免传递null值.
边界
使用第三方代码
在接口提供者和使用者之间,存在与生俱来的张力.第三方程序包和框架提供者追求普适性,这样就能在多个环境中工作,吸引广泛的用户.而使用者则想要集中满足特定需求的接口.这种张力会导致系统边界上出现问题.
浏览和学习边界
不要在生产代码中试验新东西,而是编写测试来遍览和理解第三方代码.Jim Newkirk把这叫做学习性测试.在学习性测试中,我们如在应用中那样调用第三方代码.我们基本上是在通过核对试验来检测自己对那个API的理解程度.测试聚焦于我们想从API得到的东西.
学习性测试的好处不只是免费
- 学习性测试毫无成本.
- 学习性测试是一种精确试验,帮助我们增进对API的理解.
- 学习性测试不光免费,还在投资上有正面的回报.
- 学习性测试确保第三方程序包按照我们想要的方式工作.
- 无论你是否需要通过学习性测试来学习,总要有一系列与生产代码中调试方式一致的输出测试来支持整洁的边界.
使用尚不存在的代码
还有另一种边界,那种将已知和未知分隔开的世界.在代码中总有许多地方是我们的知识未及之处.有时,边界那边就是未知的.有时,我们并不往边界那边看过去.
编写我们想得到的接口,好处之一是它在我们控制之下.这有助于保持客户代码更可读,且集中于它该完成的工作.
整洁的边界
边界上会发生有趣的事.改动是其中之一.有良好的软件设计,无需巨大投入和重写即可进行修改.在使用我们控制不了的代码时,必须加倍小心保护投资,确保未来的修改不至于代价太大.
边界上的代码需要清晰的分割和定义了期望的测试.应该避免我们的代码过多地了解三方代码中的特定信息.依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它控制.
单元测试
TDD三定律
- 定律一: 在编写不能通过的单元测试前,不可编写生产代码.
- 定律二: 只可编写刚好无法通过的单元测试,不能编译也不算通过.
- 定律三: 只可编写刚好足以通过当前失败测试的生产代码.
保持测试整洁
测试代码和生产代码一样重要.它需要被思考,被设计和被照料.它该像生产代码一般保持整洁.
测试带来的一切好处:
如果测试不能保持整洁,你就会失去它们.正是单元测试让你的代码可扩展,可维护,可复用.
覆盖了生产代码的自动化单元测试程序组尽可能地保持设计和架构的整洁.测试带来了一切好处,因为测试使改动变得可能.
如果测试不干净,你改动自己代码的能力就有所牵制,而你也会开始失去改进代码结构的能力.
整洁的测试
测试三要素:可读性,可读性和可读性.
构造-操作-检验模式:
- 构造测试数据
- 操作测试数据
- 检验操作是否得到期望的结果
面向特定领域的测试语言
打造一套包装API的函数的工具代码,帮助程序员编写自己的测试,也可以帮助后来者阅读测试.
双重标准
有些事你大概永远不会在生产环境中做,而在测试环境中做却完全没问题.通常这关乎内存或cpu效率的问题,不过却永远不会与整洁有关.
每个测试一个断言
我认为,单断言是个好准则.我通常都会创建支持这条准则的特定领域测试语言.不过,我也不害怕在单个测试中放入一个以上断言.我认为,最好的说法是单个测试中的断言数量应该最小化.
每个测试一个概念: 更好一些的规则是每个测试函数中只测试一个概念.
F.I.R.S.T.
- 快速(Fast): 测试应该够快.测试运行缓慢,你就不会想要频繁地运行它.
- 独立(Independent): 测试应该相互独立.某个测试不应为下一个测试设定条件.
- 可重复(Repeatable): 测试应当可在任何环境中重复通过.
- 自足验证(Self-Validating): 测试应该有布尔值输出.无论是通过或是失败.
- 及时(Timely): 测试应及时编写.单元测试应该恰好在使其通过的生产代码之前编写.
类
类的组织
遵循标准的Java约定,类应该从一组变量开始.如果有公共静态变量,应该先出现.然后是私有静态变量,以及私有实体变量.
公共函数应跟在变量列表之后.
封装: 我们喜欢保持变量和工具函数的私有性,有时,我们也需要用到受护(protected)变量或工具函数,好让测试可以访问到.
类应该短小
类的第一条规则是类应该短小.对于类我们计算权责.类的名称应当描述其权责.命名正是帮助判断类的长度的第一个手段.如果无法为某个类命以精确的名称,这个类大概就太长了.类名越含混,该类越有可能拥有过多权责.
单一权责原则
单一权责原则认为,类或模块应有且只有一条加以修改的理由.该原则既给出了权责的定义,又是关于类的长度的指导方针.类应只有一个权责–只有一条修改的理由.
内聚
类应该只有少量实体变量.类中的每个方法都应该操作一个或多个这种变量.通常而言,方法操作的变量越多,就越粘聚到类上.如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性.
保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加.出现这种情况时,往往意味着至少有一个类要从大类中挣扎出来.你应当尝试将这些变量和方法拆分到两个或多个类中,让新的类更为内聚.
保持内聚性就会得到许多短小的类
将大函数拆为许多小函数,往往也是将类拆分为多个小类的时机.程序会更加有组织,也会拥有更为透明的结构.
为了修改而组织
对于多数系统,修改将一直持续.每处修改都让我们冒着系统其他部分不能如期望般工作的风险.在整洁的系统中,我们对类加以组织,以降低修改的风险.
我们希望将系统打造成在添加或修改特性时尽可能少惹麻烦的架子.在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性.
隔离修改: 部件之间的解耦代表着系统中的元素互相隔离得很好.隔离也让对系统每个元素的理解变得更加容易.
通过降低连接度,我们的类就遵循了另一条类设计原则,依赖倒置原则.本质而言,DIP认为类应当依赖于抽象而不是依赖于具体细节.
系统
如何建造一个城市
城市能运转,因为它演化出恰当的抽象等级和模块,好让个人和他们所管理的”组件”即便在不了解全局时也能有效地运转.
尽管软件团队往往也是这样组织起来,但他们所致力的工作却常常没有同样的关注面切分及抽象层级.
将系统的构造与使用分开
软件系统应将起始过程和起始过程之后的运行时逻辑分开,在起始过程中构建应用对象,也会存在互相缠结的依赖关系.
如果我们勤于打造有着良好格式并且强固的系统,就不该让这类就手小技巧破坏模块组织性.对象构造的启始和设置过程也不例外.应当将这个过程从正常的运行时逻辑中分离出来,确保拥有解决主要依赖问题的全局性一贯策略.
分解mian
将构造与使用分开的方法之一是将全部构造过程搬迁到mian或被称之为main的模块中,设计系统的其余部分时,假设所有对象都已正确构造和设置.
工厂
依赖注入
有一种强大的机制可以实现分离构造与使用,那就是依赖注入(Dependency Injection, DI),控制反转(Inversion of Control, IoC)在依赖管理中的一种应用手段.控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循了单一权责原则.在依赖管理情景中,对象不应该负责实体化对自身的依赖.反之,它应当将这份权责移交给其他”有权利”的机制,从而实现控制的反转.因为初始设置是一种全局问题,这种授权机制通常要么是mian例程,要么是有特定目的的容器.
扩容
“一开始就做对系统”纯属神话.反之,我们应该只去实现今天的用户故事,然后重构,明天再扩展系统,实现新的用户故事.这是迭代和增量敏捷的精髓所在.测试驱动开发,重构以及它们打造出的整洁代码,在代码层面保证了这个过程的实现.
横贯式关注面: 原则上,你可以从模块,封装的角度推理持久化策略.但在实践上,你却不得不将实现了持久化策略的代码铺展到许多对象中.我们用术语”横贯式关注面”来形容这类情况.同样,持久化框架和领域逻辑,孤立地看也可以是模块划的,问题在于横贯这些领域的情形.
在AOP中,被称为方面(aspect)的模块构造指明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景.
Java代理
Java代理适用于简单的情况,例如在单独的对象或类中包装方法调用.代码量和复杂度是代理的两大弱点,创建整洁代码变得困难.另外,代理也没有提供在系统范围内指定执行点的机制,而那正是真正的AOP解决方案所必须的.
纯Java AOP框架
使用描述性配置文件或API,你把需要的应用程序构架组合起来,包括持久化,事务,安全,缓存,恢复等横贯性问题.在许多情况下,你实际上只是指定Spring或Jboss类库,框架以对用户透明的方式处理使用Java代理或字节代码库的机制.这些声明驱动了依赖注入(DI)容器,DI容器再实体化主要对象,并按需将对象连接起来.
AspectJ的方面
通过方面来实现关注面切分的功能最全的工具是AspectJ语言,一种提供”一流的”将方面作为模块构造处理支持的Java扩展.在80%~90%用到方面特性的情况下.SpringAOP和JBossAOP提供的纯Java实现手段足够使用.然而,AspectJ却提供了一套用以切分关注面的丰富而强有力的工具.AspectJ的弱势在于,需要采用几种新工具,学习新语言构造和使用方式.
测试驱动系统架构
最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现.不同的领域之间用最不具有侵害性的方面或类方面工具整合起来.这种架构能测试驱动,就像代码一样.
优化决策
拥有模块化关注面的POJO系统提供的敏捷能力,允许我们基于最新的知识做出优化的,时机刚好的决策.决策的复杂性也降低了.
明智使用添加了可论证价值的标准
有了标准,就更易复用想法和组件,雇用拥有相关经验的人才,封装好点子,以及将组件连接起来.不过,创立标准的过程有时却漫长到行业等不及的程度,有些标准没能与它要服务的采用者的真实需求相结合.
系统需要领域特定语言
- DSL是一种单独的小型脚本语言或以标准语言写就的API
- DSL填平了领域概念和实现领域概念的代码之间的”壕沟”
- DSL在有效使用时能提升代码惯用法和设计模式之上的抽象层次.
- 领域特定语言允许所有抽象层级和应用程序中的所有领域,,从高级策略到底层细节,使用POJO来表达.
小结
- 系统也应该是整洁的.侵害性架构会湮灭领域逻辑,冲击敏捷能力.
- 在所有的抽象层级上,意图都应该清晰可辩.
- 无论是设计系统或单独的模块,别忘了使用大概可工作的最简单方案.
迭进
通过迭进设计达到整洁目的
Kent Beck简单设计的四条规则:
- 据Kent所述,只要遵循一下规则,设计就能变得”简单”
- 运行所有测试:
- 不可重复:
- 表达了程序员的意图:
- 尽可能减少类和方法的数量:
- 以上规则按其重要程度排列.
简单设计规则1:运行所有测试
全面测试并持续通过所有测试的系统,就是可测试的系统.看似浅显,但却重要.不可测试的系统同样不可验证.不可验证的系统,绝不应部署.
遵循有关编写测试并持续运行测试的简单,明确的规则,系统就会更贴近OO低耦合度,高内聚的目标.编写测试引致更好的设计.
简单设计规则2~4:重构
在重构过程中,可以应用有关优秀软件设计的一切知识.提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等.这也是应用简单设计后三条规则的地方:消除重复,保证表达力,尽可能减少类和方法的数量.
不可重复
重复是拥有良好设计系统的大敌.它代表着额外的工作,额外的风险和额外且不必要的复杂度.重复有多种表现.极其雷同的代码行当然是重复.类似的代码往往可以调整得更相似,这样就能更容易地进行重构.重复也有实现上的重复等其他一些形态.
表达力
软件项目的主要成本在于长期维护.为了在修改时尽量降低出现缺陷的可能性,很有必要理解系统是做什么的.当系统变得越来越复杂,开发者就需要越来越多的时间来理解它,而且也极有可能误解.所以,代码应当清晰地表达其作者的意图.作者把代码写得越清晰,其他人花在理解代码上的时间也就越少,从而减少缺陷,缩减维护成本.
- 可以通过选用好名称来表达.
- 可以通过保持函数和类尺寸短小来表达.
- 可以通过采用标准命名法来表达.
- 编写良好的单元测试也具有表达性.
- 做到有表达力的最重要方式却是尝试.
尽可能少的类和方法
- 即便是消除重复,代码表达力和SRP等最基础的概念也会被过度使用.
- 类和方法的数量太多,有时是由毫无意义的教条主义导致的.
- 我们的目标是在保持函数和类短小的同时,保持整个系统短小精悍.
并发编程
为什么要并发
并发是一种解耦策略.它帮助我们把做什么(目的) 和 何时(时机) 做分解开.解耦目的 与时机 能明显地改进应用程序的吞吐量和结构.从结构的角度来看,应用程序看起来更像是许多台协同工作的计算机,而不是一个大循环.系统因此会更易于被理解,给出了许多切分关注面的有力手段.
结构并非采用并发的唯一动机.有些系统对响应时间和吞吐量有要求,需要手工编写并发解决方案.
迷思与误解:
- 并发总能改进性能: 并发有时 能改进性能,但只在多个线程或处理器之间能分享大量等待时间的时候管用.
- 编写并发程序无需修改设计: 事实上,并发算法的设计有可能与单线程系统的设计极不相同.目的与时机的解耦往往对系统结构产生巨大影响.
- 在采用web或EJB容器的时候,理解并发问题并不重要: 并发更新,死锁问题
中肯说法:
- 并发会在性能和编写额外代码上增加一些开销.
- 正确的并发是复杂的, 即便对于简单的问题也是如此
- 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待
- 并发常常需要对设计策略的根本性修改
并发防御原则
单一权责原则
- 并发相关代码有自己的开发, 修改和调优生命周期;
- 开发相关代码有自己要对付的挑战,和非并发相关代码不同, 而且往往更为困难;
- 即便没有周边应用程序增加的负担,写得不好的并发代码可能的出错方式数量也已经足具挑战性.
建议: 分离并发相关代码和其他代码
推论:限制数据作用域
两个线程修改共享对象的同一字段时,可能互相干扰,导致未预期的行为.解决方案之一是采用synchronized关键字在代码中保护一块使用共享对象的临界区.
建议: 谨记数据封装;严格限制对可能被共享的数据的访问.
推论:使用数据复本
避免数据共享的好方法之一就是一开始就避免共享数据.在某些情形下,有可能复制对象并以只读方式对待.在另外的情况下,有可能复制对象,从多个线程收集所有复本的结果,并在单个线程中合并这些结果.
推论:线程应尽可能独立
让每个线程在自己的世界中存在,不与其他线程共享数据.每个线程处理一个客户端请求,从不共享的源头接纳所有请求数据,存储为本地变量.
建议: 尝试将数据分解到可被独立线程操作的独立子集.
了解执行模型
- 生产者-消费者模型
- 读者-作者模型
- 宴席哲学家
警惕同步方法之间的依赖
建议: 避免使用一个共享对象的多个方法.
必须使用一个共享对象:
- 基于客户端的锁定 – 客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了最后一个方法的代码.
- 基于服务端的锁定 – 在服务端内创建锁定服务端的方法,调用所有方法,然后解锁.让客户端代码调用新方法.
- 适配服务端 – 创建执行锁定的中间层.这是一种基于服务端的锁定的例子,但不修改原始服务端代码.
保持同步区域微小
锁是昂贵的,因为它们带来了延迟和额外的开销.另一方面,临界区应该被保护起来.所以,应该尽可能少地设计临界区.
建议: 尽可能减少同步区域.
很难编写正确的关闭代码
建议: 尽早考虑关闭问题,尽早令其工作正常.
测试线程代码
建议: 编写有潜力曝露问题的测试,在不同的编程配置,系统配置和负载条件下频繁运行.如果测试失败,跟踪错误.别因为后来测试通过了后来的运行就忽略失败.
- 将伪失败看作可能的线程问题;
- 先使非线程代码可工作;
- 编写可插拔的线程代码;
- 编写可调整的线程代码;
- 运行多于处理器数量的线程;
- 在不同平台上运行;
- 调整代码并强迫错误发生;