[关闭]
@king 2015-01-16T10:19:42.000000Z 字数 7422 阅读 3370

《代码整洁之道》读书笔记

编程学习


Me:最近觉得自己的代码可读性差,两三百行的类,每个方法的命名与排序都挺混乱了,正好看到这么一本书就迫不及待地下载下来一读。书中关于并发编程的内容打算以后再看,目前对并发并不熟练,仅能实现最基本的功能,暂时还接受不了高深的技巧。故读书笔记只记了一半,留待后续

5S哲学

  • 整理Seiri,或谓组织(sort,分类、排序……)。搞清楚事物之所在,如恰当地命名
  • 整顿Seiton,或谓整齐(systematize,系统化)。每段代码都该在你希望它所在的地方——如否则重构之
  • 清楚Seiso,或谓清洁(shine,锃亮),清除遗弃的代注释的代码及反映过往或期望的无注释代码
  • 清洁Seiketu, 或谓标准化
  • 修身Shitsuke, 或谓纪律、自律,在裎中贯彻规程

整洁代码

要有代码:代码确然是最终用来表达需求的语言,我们可以创造帮助把需求解析和汇整为正式结构的各种工具,然后永远无法抛弃必要的精确性——代码永存

Bjarne Stroustrup,C++语言发明者:

我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。

整洁的代码力求集中。每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染。

有意义的命名。

检查对象或方法是否想做的事太多,如果对象功能太多,最好切分为两个或多个对象。如果方法功能太多,使用抽取手段(Extract Method)重构,从而得到一个能较为清晰地说明自身功能的方法,以及另外数个说明如何实现这些功能的方法。

所有程序都由极为相似的元素构成,如“在集合中查找某物”,可以把实现手段封装到更抽象的方法或类中。

读与写花费时间的比例超过10:1,写新代码时,我们一直在读旧代码。想轻松写完,先让代码易读。

美国童子军军规:让营地比你来时更干净!


有意义的命名


名副其实:

变量、函数或类的名称应该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。

  1. int d; // 消逝的时间,以日计

名称d什么也没说明,没有引起对时间消逝的感觉,更别说以日计了。应选择指明了计量对象和计量单位的名称

  1. int elapsedTimeInDays;
  2. int daysSinceCreation;
  3. int daysSinceModification;
  4. int fileAgeInDays;

体现本意的名称能让人更容易理解和修改代码。比如开发一种扫雷游戏,在盘面上找出已标记的格子。每个格子用一个数组表示,数组的0下标为其状态,记录是否被标记

  1. public List<int[]> getThem(){
  2. List<int[]> list1 = new ArrayList<int[]>();
  3. for(int[] x : theList)
  4. if (x[0] == 4)
  5. list1.add(x);
  6. return list1;
  7. }

难以看懂上述代码要做什么事,问题在于其代码的模糊度:即上下文在代码中未被明确体现的程度。
在阅读上述代码时需要了解类似以下问题的答案:

  • theList中是什么类型的东西?
  • theList零下标条目的意义是什么?
  • 值4的意义是什么?
  • 我怎么使用返回的列表?

问题的答案应该体现在代码中。如零下标条目是一种状态值,而4表示“已标记”,使用有意义的名称后,改进如下:

  1. public List<int[]> getFlaggedCells(){
  2. List<int[]> flaggedCells = new ArrayList<int[]>();
  3. for(int[] cell : gameBoard)
  4. if (cell[STATUS_VALUE] == FLAGGED)
  5. flaggedCells.add(cell);
  6. return flaggedCells;
  7. }

进而不用数组表示单元格,而是另写一个类代表每个单元格,该类包括一个isFlagged函数,则重构后代码如下:

  1. public List<Cell> getFlaggedCells(){
  2. List<Cell> flaggedCells = new ArrayList<Cell>();
  3. for(Cell cell : gameBoard)
  4. if (cell.isFlagged())
  5. flaggedCells.add(cell);
  6. return flaggedCells;
  7. }

只要简单改一下名称,就能轻易知道发生了什么。


避免误导

别用accountList来指称一组账号,除非它真的是List类型。用accountGroup、bunchOfAccounts甚至accounts都更好。

XYZControllerForEfficientHandlingOfStringsXYZControllerForEfficientStorageOfStrings


做有意义的区分

-名称必须相异,那其意思也应该不同。避免添加数字或废话区分

如果程序员只是为满足编译器或解释器的需要而写代码,就会制造麻烦。若因为同一作用范围内两样不同的东西不能重名而改掉其中一个的名称,光是添加数字系列远远不够。
错误例子:

  1. public static void copyChars(char[] a1, char[] a2){
  2. for(int i = 0; i < a1.length; i++){
  3. a2[i] = a1[i];
  4. }
  5. }

如果参数名改为source和destination,这个就会像样许多。

废话是另一种没意义的区分。Prodeuct,ProductInfo或ProductData,名称不同,意思无区别。
废话都是冗余,Variable一词永远不应出现在变量名中,Table一词永远不应出来在表名中。
再如:

  1. getActiveAccount();
  2. getActiveAccounts();
  3. getActiveAccountInfo();
  4. moneyAmountmoney
  5. customerInfocustomer
  6. accountDataaccount
  7. theMessagemessage

要以读者能鉴别不同之外的方式来区分名称


使用读得出来的名称

-编程是一种社会活动,标识符名称也要能读得出来。

反例:genymdhms


使用可搜索的名称

-单字母名称和数字常量很难在一大篇文字中找出来。长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。名称长短应与其作用域大小相对应

  1. for (int j = 0; j < 34; j++){
  2. s += (t[j]*4)/5;
  3. }

  1. int readDaysPerIdealDay = 4;
  2. final int WORK_DAYS_PER_WEEK = 5;
  3. int sum = 0;
  4. for (int j = 0; j < NUMBER_OF_TASKS; j++){
  5. int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
  6. int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
  7. sum += realTaskWeeks;
  8. }

避免使用编码

人们会很快学会无视前缀或后缀,只看到名称中有意义的部分。代码读得越多,眼中越没有前缀。最终前缀变成了不入眼的废料。


避免思维映射

比如用r代表不包含名和图式的小写字母版url。应当明确命名,编写其他人能理解的代码。


类名

如Customer、Account、AddressParser,避免使用Manager、Processor、Data或Info这样的类名(含义不明确)。


方法名

如postPayment、deletePage或save

  1. String name = employee.getName();
  2. customer.setName("mike");
  3. if (paycheck.isPosted())...

Complex fulcrumPoint = Complex.FromRealNumber(23.0);
通常好于
Complex fulcrumPoint = new Complex(23.0)
可以考虑将相应的构造器设置为private,强制使用这种命名手段


别扮可爱


每个概念对应一个词

例如使用fetch、retrieve和get来给在多个类中的同种方法命名(仅单独使用这几个单词),得耗费大把时间浏览各个文件头及前面的代码。
再如统一使用controller、manager或driver中的一个。


别用双关语

比如用add表示两数相加,那么要写一个把数字加入集合中的方法时,方法名不应为add,而改用insert或append更合适。
应写出易于理解的代码


使用解决方案领域名称


使用源自所涉问题领域的名称


添加有意义的语境

如下代码中,number、verb和pluralModifier这三个变量要遍鉴函数才能推断其含义:

private void printGuessStatistics(char candidate, int count){
String number;
String verb;
String pluralModifier;
if (count == 0){
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "l";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format(
"There %s %s %s%s", verb, number, candidate, pluralModifier);
print(guessMessage);
}

  1. 要分解这个函数,需要创建一个名为GuessStatisticsMessage的类,这样变量在定义上变作了GuessStatisticsMessage的一部分。语境的增强让算法能够通过分解为更小的函数而变得干净利落。
  2. ```java
  3. public class GuessStatisticsMessage{
  4. private String number;
  5. private String verb;
  6. private String pluralModifier;
  7. public String make(char candidate, int count){
  8. createPluralDependentMessageParts(count);
  9. return String.format(
  10. "There %s %s %s%s", verb, number, candidate, pluralModifier);
  11. }
  12. private void createPluralDependentMessageParts(int count){
  13. if (count == 0){
  14. threAreNoLetters();
  15. } else if (count == 1){
  16. thereIsOneLetter();
  17. } else {
  18. threAreManyLetters(count);
  19. }
  20. }
  21. private void thereAreManyLetters(int count){
  22. number = Integer.toString(count);
  23. verb = "are";
  24. pluralModifier = "s";
  25. }
  26. private void thereIsOneLetter() {
  27. number = "l";
  28. verb = "is";
  29. pluralModifier = "";
  30. }
  31. private void thereAreNoLetters(){
  32. number = "no";
  33. verb = "are";
  34. pluralModifier = "s";
  35. }
  36. }
  37. <div class="md-section-divider"></div>

不要添加没用的语境

例如给类名添加工程名缩写的前缀。
对于Address类的实体来说,accountAddress和customerAddress都是不错的名称,不过用在类名上就不太好了。


最后的话

多数时候我们并不记忆类名和方法名,使用IDE对付这些细节,好让自己集中精力于把代码写得像词句篇章、至少像是表和数据结构。

多试试上面这些规则。


函数


短小

该行大抵是一个函数调用语句,块内调用的函数应拥有较具说明性的名称。
这也意味着函数不应该大到足以容纳嵌套结构。所以函数缩进的层级不应多于一层或两层。这样的函数易于阅读和理解。


只做一件事

如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。

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

只做一件事的函数无法被合理地切分为多个区段。


每个函数一个抽象层级

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

向下规则:让代码拥有自顶向下的阅读顺序。每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。

类似这样的程序,像是一系列【要】(原文是To)起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续【要】起头段落:

  • 要容纳设置和分拆步骤,先容纳设置步骤,然后纳入测试页面内容,再纳入分拆步骤
  • 要容纳设置步骤,如果是套件,就纳入套件设置步骤,然后再纳入普通设置步骤
  • 要容纳套件设置步骤,先搜索“SuiteSetup”页面的上级继承关系,再添加一个包括该页面路径的语句
  • 要搜索...

这是保持函数短小、确保只做一件事的要诀。


switch语句

例:

  1. public Money calculatePay(Employee e) throws InvalidEmployeeType{
  2. switch (e.type) {
  3. case COMMISSIONED:
  4. return calculateCommissionedPay(e);
  5. case HOURLY:
  6. return calculateHourlyPay(e);
  7. case SALARIED:
  8. return calculateSalariedPay(e);
  9. default:
  10. throw new InvalidEmployeeType(e.type);
  11. }
  12. }
  13. <div class="md-section-divider"></div>

该函数(包括它调用的其他函数)问题:首先,它太长,当出现新的雇员类型时还会变得更长。其次它明显做了不止一件事。第三,它违反了单一权责原则(Single Responsibility Principle, SRP), 因有好几个修改它的理由。第四,它违反了开放闭合原则(Open Closed Principle, OCP), 因为每当添加新类型时,就必须修改之。不过,最麻烦的是每个调用的函数中都会有类似结构的函数,如:isPayday(Employee e, Date date)deliverPay(Employee e, Money pay)
解决方案是多态,将switch语句埋到抽象工厂底下。calculatePay、isPayday和deliverPay等函数则由Employee接口多态地接受派遣。

  1. public abstract class Employee{
  2. public abstract boolean isPayday();
  3. public abstract Money calculatePay();
  4. public abstract void deliverPay(Money pay);
  5. }
  6. -------------另一个文件中-----------
  7. public interface EmployeeFactory{
  8. public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
  9. }
  10. -------------另一个文件中-----------
  11. public class EmployeeFactoryImpl implements EmployeeFactory{
  12. public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType{
  13. switch (r.type){
  14. case COMMISSIONED:
  15. return new CommissionedEmployee(r);
  16. case HOURLY:
  17. return new HourlyEmployee(r);
  18. case SALARIED:
  19. return new SalariedEmployee(r);
  20. default:
  21. throw new InvalidEmployeeType(r.type);
  22. }
  23. }
  24. }

使用描述性的名称

如includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage


函数参数

最理想的参数数量是零,其次是一,再次是二,尽量避免三。

P55

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注