《clean code》读书笔记

来源:互联网 发布:为啥mac叫热辣的塔希提 编辑:程序博客网 时间:2024/05/19 13:25

《clean code》读书笔记

http://macrochen.javaeye.com/blog/812355

文章分类:IT生活



距离第一次看鲍勃大叔的"敏捷开发实践与模式"那本书已经有好多年了, 与那本书相比, 这本书相对来说更强调细节, 如果前一本书强调从大的方面, 比如从设计上, 从方法学上如何写出好的程序, 那么这一本书则是来强调从类的结构, 方法的布局, 变量的命名上阐述如何写出好的代码. 这本书基本上可以和kent back的"实现模式"那本书相提并论, 不过在有些理论上让我们从其他的方面看到了为什么, 如何写出好的代码. 另外这本书也让我明白了很多长期以来一些让我很纠结的原则, 比如一个方法只应该做一件事, 那么一个函数里面包含多个步骤, 每个步骤都做一件事, 那么多个步骤加在了一起岂不是做了多件事么? 而鲍勃大叔很好的解释了这个问题: 处于一个方法中同一抽象层级的多件事可以认为是一件事. 只有一个方法中同时处理了处于不同抽象层次的事物才违反了这个原则.


下面是一些笔记

有意义的命名 

如果每次check in时, 代码都比check out时干净, 那么代码就不会腐坏

选个好名字是要花时间的, 但是省下来的时间比花掉的多. 一旦发现又更好的名称, 就换掉旧的.

变量, 函数或类的名称应该已经回答了所有的大的问题

如果名称需要注释来补充, 那就不算是名副其实.

好代码的演变:
第一个版本:

 

Java代码 
  1. List<int[]> theList;  
  2.   
  3.   
  4. public List<int[]> getThem() {  
  5.   
  6.   
  7.     List<int[]> list1 = new ArrayList<int[]>();  
  8.   
  9.   
  10.     for (int[] x : theList) {  
  11.   
  12.   
  13.         if (x[0] == 4) {  
  14.   
  15.   
  16.             list1.add(x);  
  17.   
  18.   
  19.         }  
  20.   
  21.   
  22.     }  
  23.   
  24.   
  25.     return list1;  
  26.   
  27.   
  28. }  
 



第二个版本:

Java代码 
  1. private static final int FLAGGED = 4;  
  2.   
  3.   
  4. private static final int STATUS_VALUE = 0;  
  5.   
  6.   
  7. List<int[]> gameBoard;  
  8.   
  9.   
  10. public List<int[]> getFlaggedCells() {  
  11.   
  12.   
  13.     List<int[]> flaggedCells = new ArrayList<int[]>();  
  14.   
  15.   
  16.     for (int[] cell : gameBoard) {  
  17.   
  18.   
  19.         if (cell[STATUS_VALUE] == FLAGGED) {  
  20.   
  21.   
  22.             flaggedCells.add(cell);  
  23.   
  24.   
  25.         }  
  26.   
  27.   
  28.     }  
  29.   
  30.   
  31.     return flaggedCells;  
  32.   
  33.   
  34. }  
 



最终的版本

Java代码 
  1. class Cell {  
  2.   
  3.   
  4.     int status;  
  5.   
  6.   
  7.     private static final int FLAGGED = 4;  
  8.   
  9.   
  10.   
  11.   
  12.   
  13.     boolean isFlagged() {  
  14.   
  15.   
  16.         return status == FLAGGED;  
  17.   
  18.   
  19.     }  
  20.   
  21.   
  22. }  
  23.   
  24.   
  25.   
  26.   
  27.   
  28. List<Cell> gameBoard;  
  29.   
  30.   
  31.   
  32.   
  33.   
  34. public List<Cell> getFlaggedCells() {  
  35.   
  36.   
  37.     List<Cell> flaggedCells = new ArrayList<Cell>();  
  38.   
  39.   
  40.     for (Cell cell : gameBoard) {  
  41.   
  42.   
  43.         if (cell.isFlagged()) {  
  44.   
  45.   
  46.             flaggedCells.add(cell);  
  47.   
  48.   
  49.         }  
  50.   
  51.   
  52.     }  
  53.   
  54.   
  55.     return flaggedCells;  
  56.   
  57.   
  58. }  
 



别用accountList来指称一组账号, 除非它真的是List类型, 用accountGroup或bunchOfAccounts, 甚至直接是accounts更好一些.

提防使用不同之处较小的名称. 避免混淆

废话是另外一种没有意义的区分, 假设你有一个Product类, 如果还有一个ProductInfo或者ProductData类, 那么它们的名称虽然不同, 意思却无区别. info和data就像a, an, the一样, 是意义含混的废话.

使用读得出来的名称.

单字母名称和数字常量有一个问题, 就是很难在一大篇文字中找出来.

单字母名称仅用于短方法中的本地变量, 名字长短应与其作用域大小相对应

类名和对象名应该是名词或者名词短语, 类名不应是动词.

重载构造器时, 使用描述了阐述的静态工厂方法名.

如果不能用程序员熟悉的术语给手头的工作命名, 就应该采用从涉及问题领域的名称. 至少负责维护代码的程序员就能去请教领域专家了.

添加有意义的语境, 可以添加前缀addrFirstname, addrLastName, addrState等, 以此提供语境, 至少, 读者会明白这些变量是某个更大结构的一部分. 当然更好的方案是创建名为Address的类, 这样, 即使是编译器也会知道这些变量隶属于某个更大的概念了.

不要添加没用的语境, 只要短名称足够清楚, 就要比长名称好.

取好名字最难的地方在于需要良好的描述技巧和共有的文化背景, 与其说这是一种技术还不如说是一种艺术(修辞学).

函数 

函数只应该做一件事. 如果函数只是做了该函数名下同一抽象层上的步骤, 则函数还是只做了一件事, 编写函数毕竟是为了把大一些的代码拆分为另一抽象层上的一系列步骤

如何判断一个函数是做了一件事还是多件事, 可以用简洁的TO(为了..., 要...)起头来描述这个函数

要判断函数是否不止做了一件事, 还有一个办法,就是看是否能再拆出一个函数, 该函数不仅只是单纯地重新诠释其实现.

要确保函数只做一件事, 函数中的语句就要在同一抽象层级上.

一般情况下, 我们需要在每个函数后面跟着下一抽象层级的函数, 这样做阅读起来就比较有顺序和层次.

程序就像是一系列TO(为了..., 要...)起头的段落, 每一段都描述当前抽象层级, 并引用位于下一抽象层级的后续TO起头段落.

对于switch语句, 我的原则是如果只是出现一次, 用于创建多态对象, 而且隐藏在某个继承关系中, 在系统其它部分看不到, 就还能容忍, 当然也要就事论事, 有时候我也会部分或者全部违反这条原则.

函数越短小, 功能越集中, 就越便于取个好名字

别害怕长名称, 长而且具有描述性的名称, 要比短而令人费解的名称好, 长而具有描述性的名称, 要比描述性的长注释好.

命名方式要保持一致, 使用与模块名一脉相承的短语, 名词和动词给函数命名.

最理想的参数数量是0, 其次是1, 然后是2, 应尽量避免3, 3个以上的除非有特殊的理由.

参数与函数名处于不同的抽象层级, 它要求你了解目前并不特别重要的细节.

从测试的角度来看, 参数甚至更叫人为难.

输出参数比输入参数还要更敢于理解.

如果函数要对输入参数进行转换操作, 转换结果就该体现为返回值.

标识参数丑陋不堪, 向参数传入布尔值简直就是骇人听闻的做法. 这样做方法签名立刻变得复杂起来, 大声宣布本函数不止做了一件事, 如果表示为true会这样做, 表示为false会那样做.

如果函数看起来需要两个, 三个或三个以上的参数, 就说明其中一些参数应该封装为类了.

函数和参数应该形成一种非常良好的动名词形式, 比如assertEqual改成assertExpectEqualsActual会好很多, 虽然名称长了些, 但是更明确.

普遍而言, 应该尽量避免使用输出参数, 如果函数必须要修改某种状态, 就修改所属对象的状态吧.
比如这样的方法:private void appendFooter(StringBuffer report)
可以改为:report.appendFooter()

函数要么做什么事, 要么回答什么事, 但二者不可得兼(split query and command)

如果使用异常替代返回错误码, 错误处理代码就能从主路径代码中分离出来, 得到简化.
比如这样的代码

Java代码 
  1. if (deletePage(page) == E_OK){  
  2.   
  3.   
  4. if (deleteReference(page.getName()) == E_OK){  
  5.   
  6.   
  7. if (deleteKey(page.getName().getKey())){  
  8.   
  9.   
  10. doSomething...  
  11.   
  12.   
  13. }else{  
  14.   
  15.   
  16. log...  
  17.   
  18.   
  19. }  
  20.   
  21.   
  22. }else{  
  23.   
  24.   
  25. log...  
  26.   
  27.   
  28. }  
  29.   
  30.   
  31. }else{  
  32.   
  33.   
  34. log...  
  35.   
  36.   
  37. }  
 


更清晰的代码应该是这样:

Java代码 
  1. try{  
  2.   
  3.   
  4. deletePage(page)  
  5.   
  6.   
  7. deleteReference(page.getName())  
  8.   
  9.   
  10. deleteKey(page.getName().getKey())  
  11.   
  12.   
  13. }catch(Exception e){  
  14.   
  15.   
  16. log...  
  17.   
  18.   
  19. }  
 


try/catch代码丑陋不堪, 他们搞乱了代码结构, 把错误处理与正常流程混为一谈, 最好把try和catch代码块的主题部分抽离出来, 另外形成函数.

函数应该只做一件事, 错误处理就是一件事, 因此, 处理错误的函数不该做其他事.

使用异常替代错误码, 新异常就可以从异常类派生出来, 无需重新编译或重新部署.

重复可能是软件中一切邪恶的根源.

我们赞成结构化编程的目标和规范, 但对于小函数, 这些规则助益不大, 只有在大函数中, 这些规则才会有明显的好处.

只要函数保持短小, 偶尔出现的return, break或者continue语句并没有坏处, 甚至更具有表现力.

我并不从一开始就按照规则写函数, 我想没人能做到.

大师级程序员把系统当作故事来讲, 而不是当作程序来写.

注释 
注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败.

如果你发现自己需要写注释, 再想想看是否有办法翻盘, 用代码来表达, 每次用代码来表达你就该夸奖一下自己. 每次写注释, 你应该感受到你在表达能力上的失败.

写注释的常见动机之一是糟糕的代码的存在.

与其花时间编写解释你搞出的糟糕的代码的注释, 不如花时间清洁那堆糟糕的代码.

唯一真正好的注释是你想办法不去写的注释.

用于警告其他程序员会出现某种后果的注释是有用的.

TODO是一种程序员认为应该做, 但由于某些原因目前还没做的工作

如果你在编写公共的API, 就该为他编写良好的javadoc

坏注释都是糟糕代码的支撑或接口

如果你决定写注释, 就要花必要的时间确保写出最好的注释.

所谓每个函数都要有javadoc或者每个变量都要有注释的规矩全然是愚蠢可笑的.

能用函数或变量时, 就别用注释.

短函数不需要太多的描述. 为只做一件事的短函数选个好名字, 通常要比写函数头注释要好.

格式 
紧密相关的代码应该互相靠近

相关函数, 如果某个函数调用了另外一个, 就应该把它们放在一起, 而且调用者应该尽量放在被调用者上面. 这样, 程序就有了自然的顺序. 若坚定的遵循这条约定, 读者就能够确定函数声明总会在其调用后很快出现.

概念相关. 概念相关的代码应该放在一起. 相关性越强, 彼此之间的距离就该越短.

代码的组织上, 应该像报纸一样, 最重要的概念先出来, 尽量以包含最少细节的方式表述它们, 底层细节最后出来.

尽量保持代码行短小, 不过死守80个字符有些僵化.

团队规则. 我们想要软件拥有一以贯之的风格. 我们不想让它想的是由一大票意见向左的个人所写成.

对象和数据结构 
过程式代码便于在不改动既有数据结构的前提下添加新函数; 面向对象代码便于在不改动既有函数的前提下添加新类. 换言之. 过程式代码难以添加新数据结构, 因为必须修改所有函数, 面向对象代码难以添加新函数, 因为必须修改所有类. // 没看懂:(

对于面向对象较难的事, 对于过程式代码却较容易, 反之亦然.

方法不应调用由任何函数返回的对象的方法

Active Record是一种特殊的DTO, 它们是拥有javabean式的数据结构, 同时拥有类似save和find这样的方法, 它是一种数据库表或其他数据源的直接翻译

对象暴露行为, 隐藏数据.

错误处理 
错误处理很重要, 但如果他搞乱了代码逻辑, 就是错误的做法.

别返回null值, 别传null值, 在大多数编程语言中, 没有良好的方法能对付由于调用者意外传入的null值, 事已至此, 恰当的做法是禁止传入null值, 这样你在编程的时候, 就会时时记住参数列表中的null值意味着出问题了.

如果将错误处理隔离看待, 独立于主要逻辑之外, 就能写出强固而整洁的代码, 而做到这一步, 我们就能够单独处理它, 也能极大的提升了代码的可维护性.

单元测试 
单元测试三要素: 可读性, 可读性和可读性.

好的测试一般遵从构造-操作-检验(build-operate-check)模式, 每个测试都清晰分为三个环节, 第一个环节构造测试数据, 第二个环节操作测试数据, 第三个环节检验操作是否得到期望的结果.

单个测试中的断言数量应该最小化

 
公共函数应该跟在变量列表之后, 我们喜欢把由某个公共函数调用的是有工具函数紧随在该公共函数后面. 这符合了自顶向下原则, 让程序读起来就像一片报纸文章.

类名应该描述其职责, 实际上命名正是判断类的长度的一个手段, 如果无法为某个类命名以精确的名称, 这个类大概就太长. 类名越含混, 该类越有可能拥有过多的权责.

我们也应该能够用大概25个单词简要描述一个类, 且不用"if, and, or, but"等词汇.

类和模块应该有且只有一条加以修改的理由.(单一职责原则SRP)

类应该只有少量的实体变量, 类中的每个方法都应该操作一个或多个这种变量. 通常而言, 方法操作的变量越多, 就越粘聚到类上, 如果一个类中的每个变量都被每个方法所使用, 则该类具有很大的内聚性.

一般来说, 创建这种极大化类聚类是既不可取也不可能的; 另一方面, 我们希望内聚性保持在较高位置. 内聚性高, 意味着类中的方法和变量互相依赖, 互相结合成一个逻辑体.

当类丧失了内聚性, 就拆分它.

将大函数拆分为许多小函数, 往往也是将类拆分为多个小类的时机.

为了修改而组织, 将修改隔离.

系统 
将系统的构造与使用分开

迭进 
软件项目的主要成本在于长期维护. 为了在修改时尽量降低出现缺陷的可能性, 很有必要理解系统是做什么的, 当系统变得越来越复杂, 开发者就需要越来越多的时间来理解它, 而且也极有可能误解. 所以, 代码应当清晰地表达其作者的意图.

逐步改进 
编程是一种技艺胜于科学的东西. 要编写整洁代码, 必须先写肮脏代码, 然后再清理它.

写出好文章是一个逐步改进的过程.

毁坏程序的最好方法之一就是以改进之名大动其结构,  有些程序永远不能从这种所谓的"改进"中恢复过来.

放进拿出是重构过程中常见的事. 小步和保持测试通过, 意味着你会不断移动各种东西, 重构有点像是解魔方. 需要经过许多小步骤, 才能达到较大目标, 每一部都是下一步的基础.

总结 
如果注释描述的是某种充分自我描述了的东西, 那么注释就是多余的.
注释应该谈及代码自身没提到的东西.
如果要编写一条注释, 就花时间保证写出最好的注释.

只与细节相关的常量, 变量或工具函数不应该在基类中出现, 基类应该对这些东西一无所知.
如果看到基类提到派生类名称, 就可能发现了问题. 通常来说, 基类对派生类应该一无所知.

每个选择算子参数将多个函数绑定到了一起, 选择算子参数只是一种避免把大函数切分为多个小函数的偷懒做法. 选择算子不一定是boolean类型, 可能是枚举, 整数或任何用于选择函数行为的参数, 使用多个函数, 通常优于向单个函数传递某些代码来选择函数行为.

通常应该倾向于选用非静态方法, 如果有疑问, 就是用非静态函数, 如果的确需要静态函数, 确保没机会打算让它有多态行为.

如果你必须查看函数的实现或者文档才知道它是做什么, 就应该换个更好的函数名, 或者重新安排功能代码, 放到有较好名称的函数中.

在你认为自己完成了某个函数之前, 确认自己理解了它是如何工作的. 通过全部测试还不够好. 你必须知道解决方案是正确. 而获得这种知识和理解的最好途径, 往往是重构函数, 得到某种整洁而足够具有表达力, 清楚呈现如何工作的东西.

SECONDS_PRE_DAY是个好名字

魔术字不仅仅是说数字, 它泛指任何不能自我描述的符号.

Java代码 
  1. if (shouldBeDelete(timer))  
 


要好于:

Java代码 
  1. if (timer.hasExpired() && !timer.isRecurrent())  
 



否定式要比肯定式难明白一些, 所以尽量将条件表达式表示为肯定方式.

常常有必要使用时序耦合, 但你不应该掩饰它.

Java代码 
  1. public void dive(String reason){  
  2.   
  3.   
  4. saturateGradient();  
  5.   
  6.   
  7. reticulateSplines();  
  8.   
  9.   
  10. diveForMoog(reason);  
  11.   
  12.   
  13. }  
 


更好的写法是:

Java代码 
  1. public void dive(String reason){  
  2.   
  3.   
  4. Gradient gradient = saturateGradient();  
  5.   
  6.   
  7. List<Spline> splines = reticulateSplines(gradient);  
  8.   
  9.   
  10. diveForMoog(splines, reason);  
  11.   
  12.   
  13. }  
 


这样就通过创建顺序队列暴露了时序耦合. 每个函数都产生出下一个函数所需的结果, 这样一来就没理由不按顺序调用了.

拆分不同抽象层级是重构的最重要功能之一, 也是最难做的一个. 

通常我们不想让某个模块了解太多其协作者的信息, 更具体的说, 如果A与B协作, B与C协作, 我们不想让使用A的模块了解C的信息.

现在的enum已经可以放心使用了, 别再用那个public static final int 老花招了, 那样做就int的意义就丧失了. 而enum则不然, 因为它们隶属于有名称的枚举. 而且仔细研究enum的语法, 它可以拥有方法和字段, 从而成为能比int提供更多表达力和灵活性的强力工具.

不要取具体实现的名字; 取反映类或函数抽象层级的名称, 这样做不容易, 人们擅长于混杂抽象层级, 每次浏览代码, 你总会发现有些变量的名称层级太低. 你应该层级为之改名.
比如一个Modem接口:

Java代码 
  1. public interface Modem{  
  2.   
  3.   
  4. ...  
  5.   
  6.   
  7. String getConnectionPhoneNumber();  
  8.   
  9.   
  10. }  
 


因为有些Modem可能不是采用电话拨号的,  比如宽带, 这个方法不具有抽象性, 因此需要重命名, 比如采用getConnectionLocator

名称长度与作用范围的广泛度相关. 对于较小的作用范围, 可以用较短的名称, 而对于较大作用范围就该用较长的名称.

名称应该说明其副作用.
比如这样的方法:

Java代码 
  1. public ObjectOutputStream getOos(){  
  2.   
  3.   
  4. if (m_oos==null){  
  5.   
  6.   
  7. m_oos = new ObjectOutputStream(m_stocket.getOutputStream());  
  8.   
  9.   
  10. }  
  11.   
  12.   
  13. return m_oos;  
  14.   
  15.   
  16. }  
 


这里不只是获取一个oos, 如果oos不存在, 还会创建一个. 所以更好的名称是: createOrReturnOos

缺陷倾向于扎堆, 在某个函数中发现一个缺陷时, 最好全面测试那个函数, 你可能发现缺陷不止一个.

现代处理器拥有一种通常称为比较交换(Compare and Swap, CAS)的操作, 这种操作类似于数据库中的乐观锁定, 而其同步版本则类似于保守锁定.

关键字synchronized总是要求上锁, 即便第二个线程并不更新同一个值时也是如此. 尽管这种固有锁的性能一直在提升, 但代价依然昂贵.
非上锁版本假定多个线程通常并不频繁改同一个值, 导致问题产生. 他高效地侦测这种情形是否发生, 并不断滴尝试, 直至更新成功, 这种侦测行为总比上锁来的划算, 在争用激烈的情况下也是如此.

当某个方法试图更新一个共享变量, CAS操作就会验证要赋值的变量是否保有上一次的已知值, 如果是, 就修改变量值, 如果不是, 则不会碰变量, 因为另一个线程正在试图更新变量值. 要更新数据的方法(通过CAS操作)查看是否修改并持续尝试.

Java代码 
  1. int variableBeingSet  
  2.   
  3.   
  4. void simulateNonBlockingSet(int newValue){  
  5.   
  6.   
  7. int currentValue;  
  8.   
  9.   
  10. do{  
  11.   
  12.   
  13. currentValue = variableBeingSet;  
  14.   
  15.   
  16. }while(currentValue != compareAndSwap(currentValue, newValue));  
  17.   
  18.   
  19. }  
  20.   
  21.   
  22.   
  23.   
  24.   
  25. int synchronized compareAndSwap(int currentValue, int newValue){  
  26.   
  27.   
  28. if (variableBeingSet == currentValue){  
  29.   
  30.   
  31. variableBeingSet = newValue;  
  32.   
  33.   
  34. return currentValue;  
  35.   
  36.   
  37. }  
  38.   
  39.   
  40. return variableBeingSet;  
  41.   
  42.   
  43. }  
 



getNextOrNull这个方法名不错.

 

原创粉丝点击