Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2014-01-19) CleanCode读书笔记

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

正在看《代码整洁之道 Clean code》,本文将持续更新,记录学习中的疑问和重点。

书中第1章1.1到1.3.3写得太好了

太有共鸣了,简直就是对我当前项目情况的写实。一直在想当前项目的情况是不是特例,看到这里才发现,这种事果然是不断重复的。

短短的三页,没经历过的人看着可能没感觉,一翻就过了,只有经历过的人才知道文字下的血泪。。。

书中多处提到了一个项目FitNesse,它是干什么的,有人用吗?

官网在这里:http://fitnesse.org

介绍:The fully integrated standalone wiki and acceptance testing framework

公司内网一个很好的关于测试类型、工具、实践的PPT,里面把FitNesse归于BDD工具,跟[Cucumber]http://cukes.info/分在一起

好资料值得一看:https://my.thoughtworks.com/docs/DOC-19567

SO上一个关于它的问题:http://stackoverflow.com/questions/598863/why-fit-fitnesse

大意是为了非程序员或非技术人士写测试用的。不过我大略扫了一眼FitNesse的文档,感觉让他们学习使用也不会是太简单的事。

另外根据在公司论坛里的搜索结果,发现似乎用得不多哦。

P22 关于“接口和实现”中给名字加前后缀的问题

如果接口和实现必须选一个来编码的话,我宁可选择实现。ShapeFactoryImpl甚至是丑陋的CShapeFactory,都比对接口名称编码来得好。

问题:CShapeFactory前面的C,代码什么意思?是什么缩写?

回答:[不知道,欢迎留言]

P23 类名

避免使用Manager, Processor, Data或Info这样的类名

我能说现在项目里全用上了吗?另外,不光有Processor,还有PreProcessor、PostProcessor,硬生生的以时间划分的方式,把代码逻辑切成了三部分。每次想搜点代码,都要在脑袋里想一遍这几个Processor到底有什么不同。类名完全没有体现出实际的功能。

为什么不动手改呢?因为这些代码属于缺少测试的遗留代码,虽然我们已经多次靠加班把一些最难以忍受的地方改了,但是对于这种相对来说危害小一点问题,还是忍忍算了。

P24 每个概念对应一个词

目前项目中,经常有多个词对应同一个概念。比如一个在系统中作为id的值,有的地方叫uuid,有的地方叫guid,有的地方叫systemId。比如目录,有的地方叫directory,有的地方叫folder。再比如本地用于存放临时文件的目录,有的地方叫作dataDir,有的地方叫作processingDir。

想把这些东西统一起来还真不容易。比如,团队里有的人坚持自己的命名方式,有的人坚持在命名方面每个人有自己的自由,有的人宣称自己无所谓所以一会儿用这个一会儿用那个。再比如,我们使用的第三方类库中,也出现不同的用词(folder/directory)。所以到最后就没人提了,反正大家都习惯了。

P27 不要添加没用的语境

文中说有一个Gas Station Deluxe的应用,在其中每个类前都加上GSD前缀。我们项目中,客户就曾经提议,把每个类的前面都加上XYZ(化名)的前缀,被我们强烈否决。

P3 勒布朗法则:Later equals never

有时候我们发现了问题,但手头上还有任务,就会说“我晚点再改”,十有八九就不会改,所以久而久之,这句话就等同于“我不想改”。所以现在一旦有人这么说,我们就会说这句法则“Later equals never”,不过对于客户来说,只能算玩笑。

“晚点再改”,到底改还是不改,还是要看个人。我目前的做法是,简单的问题随手改掉,麻烦一点的记在便利贴上,等卡做完后再改。不过还是经常发现自己面前积了好多张已经变色的便利贴。

P12 童子军军规:让营地比你来时更干净

作为本年度获得“无底线团队”称号的团队,我们只能用这句话来提醒其他的同事了。“重构”之类差不多是奢望,毕竟在一个由多公司员工组成的团队中,有不少成员可能没听说过这个词。


P33 一个函数只做一件事

这是一句听起来和说起来都很简单、很有道理的话,但看自己或别人的代码,一不小心就看到长长的函数,里面一口气把所有的事情做完。这种函数一般都会有一个模棱两可的名字,比如run/handle,然后把所有的逻辑放在一起,顶多抽几个被反复调用的工具方法出来。有的时候,还会添加空行和注释,把代码分成几块,帮助阅读。我相信随便打开一个项目,都能看到不少这样的函数。

两种方法判断一个函数是否做了太多的事情:

  1. 使用以TO开头的一句话描述这个函数的功能,看是否能够轻松的描述清楚
  2. 看能不能再拆出一个函数,这个函数不只是单纯的重新诠释一遍它的实现

P34 每个函数一个抽象层级

这句话非常有用,和上一句一起,解答了我的一个很大的疑惑。

在客户现场,我给新同事做了一个简单的重构培训,把项目中的一个长长的函数,重构成多个小函数,每个才几行甚至一两行。虽然新同事觉得现在的代码读起来比以前清楚了,但他们有一个担心:函数太多了,有时候很难清楚地看到它们之间的关系,不知道从哪儿入手。而写成一个长函数的话,只需要从上到下读完,就知道函数的功能了。如果把握分解函数的度呢?

我对这个度实际上也不太清楚。之前在分解函数时,更多的是一种感觉。比如正在看一个函数,想了解它的作用,突然看到几行代码做了非常琐碎的事情,有种不耐烦的感觉。我不想知道它们是怎么做的,只想知道它们做了什么,这时就会顺手抽一个方法出去,并起一个合适的名字。

有时候写完代码看回头看时,又发现刚才抽出去的方法过于抽象,就会把它们inline回来,再参照前后,考虑是否把它们抽成更多的方法。

实际上这个过程,就是在努力的让一个函数里的代码,保持在同一个抽象层次。就像讲故事要娓娓道来,不能突然一句话一笔带过,让人没听懂怎么回事,也不能突然就找到一件小事啰嗦讲个不停,让人听得着急。所以有时候会把一些只有一两行的代码抽成一个函数,就是因为它们虽然短,但过于细节,跟前后的代码不在同一个抽象层次,这时就算抽出很多只有一两行的函数,也是值得的。

说到这里,想起来我曾经在看play1的代码时,看到它有很多很长的方法,由于写得还比较清楚,所以当时并没有觉得应该再抽出更多的方法来。之前和别人讨论,我还用这个例子来说明,只要代码用心写,长点也可以很清楚。今天又找到了那些代码,发现又看不懂了,如果不一行行的仔细读下去,很难看懂它的主要逻辑。于是试着把它们重构一下,结果发现虽然方法多了,但逻辑的确比以前清楚多了。本想把相关代码全部重构好,发上来作个对比,但发现自己的重构功力还是不足,当需要对代码结构进行大的修改时,很难做到“不破坏编译”,于是打算等完全看完CleanCode和重构这两本书后再做。

P88 对象和数据结构

这一章触动相当大,值得好好思考。一般我们在代码里,很少使用public的fields,而是先定义一个private的field,再添加对应的getter/setter。很多的书和文章都告诉我们应该避免使用public字段,久而久之人们就视其为洪水猛兽。

这种全部使用public字段定义出来的类,可视为一种数据结构,而不是对象。它跟我们通常所说的对象式的代码,正好是对立的。对立的意思是说,有一些事,使用数据结构很方便,但用对象式很麻烦;另一些事,可能正好相反。到底选择哪种实现方式,关键是要在这套系统中,我们是添加新类型的情况多,还是添加新行为的情况多。如果是前者,用对象更好;如果是后者,用数据结构更好。并没有说,因为java是面向对象的,我们就应该在任何情况都使用对象的方式。

关于这一点,在coursera的《programming language》课中,也反复强调,甚至有一节,专门选用了一个题目,分别使用函数式加数据结构的方式,和面向对象的方式来实现。从中可以深切体会到,某些修改对于数据结构相当容易,另一些修改对于对象来说更加容易。

书中这句话是一个很好的总结:老练的程序员知道,一切都是对象只是一个传说。有时候你真的想要在简单数据结构上做一些过程式的操作。

不过结合到现实情况,大多数时候我们用java做的项目,都是企业应用或者网站之类,这类项目通常更适合使用对象式。而且平时使用的很多库,如spring/hibernate等,都会使用字节码增强,这种增强通常都能在方法层面上做,所以getter/setter是必要的。所以我们很少有机会使用public字段的数据结构。

只是需要注意,我们不用,不是说明它就是邪恶的,只是我们的项目类型不合适。

P91 火车失事

我们经常会在代码中写这样的长长的调用:

String path = ctx.getOptions().getScratchDir().getAbsolutePath();


一直以来都觉得这没什么问题,直到看到这一章,才知道这是一种不好的调用。因为我需要知道`ctx`的内部实现,才能实现我的功能,这是一种耦合。相对应有一个叫“得墨忒耳律”来解释这个问题,相应的页面为[http://en.wikipedia.org/wiki/Law_of_Demeter](http://en.wikipedia.org/wiki/Law_of_Demeter),里面详细解释了定义、优势与劣势。我觉得里面有一句话很好:“你命令你的狗走路,而不是它的腿”。

但书中也提到,如果我们调用的是一堆数据结构,那么是没有问题的。因为数据结构就是要暴露实现,所以下面的代码反而是没问题的:

String path = ctx.options.scratchDir.absolutePath;

前提是它们都是数据结构,只有public字段,而没有方法。但是在Java中,这样的情况很少见,就算是数据结构,也会被包装成一个个javabean,由方法调用,让人搞不清它到底是数据结构还是对象。

对于这样的代码,文中建议结合上下文,搞清楚那个path到底是谁在用怎么用,然后把这串代码移到相应的类里去,暴露出一个方法来。

我对这种代码还是很有疑惑。正如wiki里所说,它的劣势在于会产生很多wrapper方法,所以如果这样的串很长很多,我可能需要在很多类中建立很多wrapper方法,感觉会很乱。还有一种情况,前面说如果都是数据结构,那这样是没问题的,但如果我想提供一个方法(如getFullName()),仅仅是把几个字段(如firstName, lastName)组合一下返回过去,这到底还数据结构还是对象呢?那我还应不应该用长串调用?

所以如果这个火车不长,我还是可以接受的,或者看看有没有更好的方法避免它们,但不会建立一大堆没有意义的wrapper来绕开。

如wiki最后所说,这个律普通是用来衡量代码话味道的一种标准,而不是一种解决方法。

P92 混杂

这里提到了一些类,一半是数据结构,一半是对象。既把一些私有值直接或通过getter暴露给外部,让外部处理,又提供了一些方法隐藏内部实现。这种做法是两边都不讨好的做法,让以后的维护变得困难。

这样的代码应该很常见,特别是当我们习惯于无理由添加很多getter把private field暴露出去的时候。希望能意识到这个问题,在平时写代码时注意。

P93 DTO

DTO是数据传送对象,我们一般是写成一个java bean,一个private field对应一对getter/setter。按书中的说法,这种情况我们其实可以直接使用public字段。写成java bean的形式,更多是会让某些OO纯化论者感觉舒服一些,并没有更多的好处。

P94 Active Record

它的定义是:是一种特殊的DTO,除了拥有可公共访问的数据结构,通常还会有如save, find这样的方法。它们一般是对数据库表或其他数据源的直接翻译。

我们应该把它们当作数据结构来用,而不要往它里面塞入业务方法。如果需要的话,可以新建一些对象提供这些方法,在内部使用它们。

P99 如果我们调用的第三方方法抛出很多种异常

经常会遇到这样的情况,调用一个第三方库提供的方法,它里面抛出了三四种受控异常。通常我们会在自己的业务代码中,使用多个catch来分别捕获分别处理,代码就会看起来很乱,真正有价值的业务代码被淹没在大片的错误处理之中。在多数情况下,对于这多个异常的处理方式都是相似或者相同的。

对于这种情况,书中推荐的做法是,自己定义一个类去包装那个第三方类,提供等价的方法把各异常捕获后,统一抛出一个自定义的异常,在自己的代码中只处理它即可。这样做可以让我们的代码看起来很干净,同时又能将第三方实现隔离开。

不过在实际中,对于一些不太重要的系统,我们往往直接catch那个Exception类,一了百了,然后在最外层的某个地方,再统一处理一下。虽然感觉有点草率,但相对于“分别处理错误”而增加的代码和复杂度相比,这样可能还会减少一些出错的可能性。毕竟见过太多程序员连正常的业务功能都很难实现好,更别说错误处理这么有技术含量的事情了。而且所谓的“出错处理”大多数情况只是记下日志而已。

P98 关于可控异常和不可控异常之争

“辩论已经结束!”

使用可控异常是一种不好的做法,因为当我们在底层代码的方法签名中增加了一个可控异常时,所有调用了这个方法的高层代码都需要修改,然后重新构建、发布,哪怕它们自身所关注的任何东西都没改动过。这种方式破坏了封装。

同时几乎其它所有语言都不支持可控异常,但并不阻碍使用它们写出健壮的代码。所以可控异常不是必须的,而且是不好的。

只有当我们编写一套关键代码库,里面的每一个异常都必须小心处理时,可控异常才有用,因为它会强迫提醒你处理这些异常。但对于大多数一般的应用开发,它的依赖成本要高于收益。

这一节解答了我多年的疑惑,因为这个争论持续了很多年。特别是当年我在topcoder上做项目时,也看到相关的讨论,当时人们的结论是它有很必要,因为就算是不可控的异常,我们也必须一一小心处理,这样的话,使用可控异常更保险一些。

但现在我认为小心处理是必要的,但是可控异常却不是必要的。因为我们会有单元测试去保证我们对异常的处理是正确的。

comments powered by Disqus