代码整洁之道-读书笔记(至第九章)
--第三章 函数
函数只做一件事情,要尽量短小
编写函数毕竟是为了把大一些的概念拆分为另一个抽象层上的一系列步骤
要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆除一个函数,该函数不仅只是单纯地重新诠释其实现(需要改变抽象层级)
每个函数一个抽象层级
遵循自顶向下的编写规则:程序就像是一些列TO起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续TO起头段落
switch语句,尽量确保每个switch都埋藏在较低的抽象层级,而且永远不重复
函数要尽量使用描述性的名称,描述函数所做的事情
沃德原则:如果每个例程都让你感到深合己意,那就是整洁代码
函数越短小、功能越集中,就越便于取个好名字
函数参数:最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数),有足够特殊的理由才能使用三个以上参数(多参数函数)
普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧
例子:public void appendFooter(StringBuffer report) 调整成
report.appendFooter() 是不是更好理解
分隔指令与询问:函数要么做什么事,要么回答什么事,但二者不可兼得。函数应该修改某对象的状态,或是返回该对象的有关信息。如setXXX对应设置对象属性,不可有返回参数,isXXX应该返回boolean类型的参数
使用异常替代返回的错误码,即使用try/catch,但是错误处理就是一件事情,只做一件事情
例子:
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
logger.log("page delete");
} else {
logger.log("configKeys not delete");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERROR;
}
调整为:
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey();
} catch(Exception e) {
logger.log(e.getMessage());
}
进一步模块化:
public void delete(page) {
try {
deletePageAndAllReferences(page);
} catch(Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey();
}
private void logError(e) {
logger.log(e.getMessage());
}
不要重复自己:重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建的。可参考代码清单3.1和代码清单3.7
如何写出完美的函数:写代码和写文章很想,先想写什么就写什么,然后再打磨它,初稿也许粗陋无需,就需要斟酌推敲,直至达到你心目中的样子。并不是从一开始就按照规则写函数,需要打磨,需要分解、修改名称、消除重复,有时还拆散类,同事保持测试通过,最后遵循函数编写的规则,组装好这些函数。
小结:每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设计来描述这个系统的。函数是语言的动词,类是名词,大师级程序员把系统当做故事来讲,而不是当做程序来写,真正的目标在于讲述系统的故事,而编写的函数必须干净利落地拼装在一起,形成一种精确而清晰的语言,帮助我们讲故事。参考代码清单3-7
--第四章 注释
注释不能美化糟糕的代码,尽管有时也需要注释,我们也该多花心思尽量减少注释量
用代码来阐述,很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可
TODO 是一种程序员认为应该做,但由于某些原因目前还没做的工作。可能是要提醒某个不必要的特性,或者要求他人注意某个问题,无论目的如何,它都不是在系统中留下糟糕的代码的借口
JAVADOC 如果你再编写公共API,就该为它编写良好的Javadoc
应该避免 喃喃自语 多余的注释 误导性注释 循规式注释 日志式注释 废话注释
范例 参考对比 代码清单4-7 与 代码清单4-8
--第六章 对象和数据结构
数据抽象 隐藏实现并非只是在变量之间放上一个函数层那么简单。隐藏实现关乎抽象,类并不简单地用取值器和赋值器将其变量推向外间,而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体
例子:
具象机动车
public interface Vehicle {
double getFuleTankCapacityInGallons();
double getGallonsOfGasoline();
}
抽象机动车
public interface Vehicle {
double getPercentFuelRemaining();
}
数据、对象的反对称性 对象把数据隐藏于抽象之后,暴露操作数据的函数;数据结构暴露其数据,没有提供有意义的函数。
对象与数据结构之间存在二分原理:过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。(例子见6.2 数据、对象的反对称性 代码清单6-5 6-6)
迪米特法则 模块不应了解它所操作对象的内部细节。意味着对象不应通过存取器暴露其内部结构,因为这样更像是暴露而非隐藏其内部结构。
例子:类C的方法f只应该调用以下对象的方法:
C
由f创建的对象
作为参数传递给f的对象
由C的实体变量持有的对象
当出现对象需要直接暴露数据的时候,可以去探究要直接暴露数据的目的,清楚目的后,通过模块化隐藏掉细节,暴露操作数据的方法
对象暴露行为,隐藏数据,便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。
数据结构暴露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向既有函数添加新数据结构。
在任何系统中,我们有时会希望能够灵活地添加新数据类型,所以更喜欢在这部分使用对象。另一些时候,我们希望能灵活地添加新行为,这时我们更喜欢使用数据类型和过程。优秀的软件开发者不带成见地了解这种情形,并依据手边工作的性质选择其中一种手段。
数据结构指的就是数据的载体,暴露数据,而几乎没有有意义的行为的贫血类。最常见的应用在分布式服务,以wcf,webservice,reset之类的分布式服务中不可或缺的数据传输对象(DTO)模式,DTO(Request/Response)就是一个很典型的数据载体,只存在简单的get,set属性,并且更倾向于作为值对象存在。而对象则刚好相反作为面向对象的产物,必须封装隐藏数据,而暴露出行为接口,DDD中领域模型倾向于对象不仅在数据更多暴露行为操作自己或者关联状态。
数据结构和对象之间看是细微的差别却导致了不同的本质区别:使用数据结构的代码便于在不改动现在数据结构的前提下添加新的行为(函数),面向对象代码则便于不改动现有函数的前提下添加新的类。换句话说就是数据结构难以添加新的的数据类型,因为需要改动所有函数,面向对象的代码则难以添加新的函数,因为需要修改所有的类。在任何一个复杂的系统都会同时存在数据结构和对象,我们需要判断的是我们需要的是需要添加的新的数据类型还是新的行为函数。
隐藏作为面向对象主要特性中的最重要特性,封装隐藏是面向对象中最重要的特性,一个好的面向对象代码肯定是对对象的内部细节做到很好的隐藏封装,封装过后才有是多态,委派之类的。一个好的面向对象的代码一定是具有很好的隐藏封装,易于测试,不稳定因素往往集中在一处很小或者固定的位置,不稳定因素的变更不会导致更大面积的修改扩散。
对象的隐藏要求:方法不应和任何调用方法返回的对象操作,换句话之和朋友说话,不和陌生人说话(迪米特法则,或被译为最小知识原则),比如:ctxt.getOptions().getSearchDir().getAbsolutePath(),就是迪米特法则的反例模式。
--第七章 错误处理
使用异常而非返回错误码 会使代码变得整洁,使错误处理与算法隔离开,在遇到错误时,最好抛出一个异常
先写Try-catch-finally语句 最好先写出该语句,这能帮你定义代码的用户应该期待什么,无论try代码块中执行的代码出什么错都一样
使用不可控异常 使用可控异常会违反开闭原则,在中低层的修改,会涉及到高层的签名,所以如果在编写一套关键代码库,则可控异常有时会很有用:必须捕获异常,但对于一般应用的开发,其依赖成本要高于收益
依调用者需要定义异常类 我们在应用程序中定义异常类时,最重要的考虑应该是它们是如何被捕获的。但是在做异常分类的时候可能会包含一堆重复的异常处理代码(例子:7.5)
可以通过 打包API(做一层封装) 的方式,确保其异常类型,从而简化代码。(参考7.5)
定义常规流程 在业务逻辑和错误处理代码之间设置好了隔阂,这样把错误检测推到了程序的边缘地带(打包API的方式,在代码顶端定义处理器来应付任何失败的运算),但也存在特例(参考7.6)
使用特例模式,创建一个或者配置一个对象,用来处理特例;在接口中使用多态的方式进入到特例中,这样外部程序不需要调整,只要调整内部行为即可(参考7.6)
别返回NULL值 不要返回NULL值,如果实例化对象失败,建议也要创建默认实例。因为程序中如果没有判断null值,就会奔溃,即使对null值处理也会造成代码不整洁,可使用特例模式避免NULL值返回
别传递NULL值 除非API要求你向他传递null值,否则就要尽可能避免传递null值
整洁的代码是可读的,但也要强固,两者并不冲突,如果将错误隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码
第九章 单元测试
TDD三大定律
1.在编写不能通过的单元测试前不可编写生产代码;
2.只可编写刚好通过得单元测试,不能编译也算不通过;
3.只可编写刚好足以通过当前失败测试的生产代码
保持测试整洁 测试代码和生产代码一样重要。它需要被思考、被设计和被照料。它该项生产代码一般保持整洁。
整洁的测试 可读性、可读性和可读性,以尽可能少的文字表达大量内容,明确、简洁,还有足够的表达力。
构造-操作-检验(BUILD-OPERATE-CHECK)测试代码三个步骤
F.I.R.S.T 整洁的测试遵循以下5条规则:
1.快速(Fast) 测试应该能快速运行,频繁运行测试,就能今早发现问题
2.独立(Independent)测试应该相互独立。某个测试不应为下一个测试设定条件,可以单独运行每个测试,及以任务顺序运行测试。
3.可重复(Repeatable)测试应该可在任务环境中重复通过。
4.自足验证(Self-Validating)测试应该有布尔值输出。不应该手工对比量不同文件来确认测试是否通过。
5.及时(Timely)测试应该及时编写。单元测试应该恰好在使其通过的生产代码之前编写,如果在编写生产代码之后编写测试,你会发现生产代码难以测试。