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

第一章  整洁代码

    我们都曾经说过有朝一日再回头清理代码。当然,在那些日子里,我们都没听过勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)。
    程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。
    混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法---做得快的唯一方法---就是始终尽可能保持代码整洁。    
     破窗理论 。窗户破损了的建筑让人觉得似乎无人照管。于是别人也再不关心。他们放任窗户继续破损。最终自己也参加破坏活动,在外墙上涂鸦,任垃圾堆积。一扇破损的窗户开辟了大厦走向倾颓的道路。        
     整洁的代码只做好一件事 。( 每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染
     读与写花费时间的比例超过10:1 。写新代码时,我们一直在读旧代码。 既然比例如此之高,我们就想让读的过程变得轻松,即便那会使得编写过程更难。没可能光写不读,所以使之易读实际也使之易写。
    不读周边代码的话就没法写代码。编写代码的难度,取决于读周边代码的难度。要想干得快、要想早点做完、要想轻松写代码,先让代码易读吧。
    美国童子军一条简单的军规,应用到我们的专业领域: 让营地比你来时更干净。
 
  • 我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。
      -- Bjarne Stroustrup,C++语言发明者,C++ Programming Language(中译版《C++程序设计语言》)一书作者。
  • 整洁的代码应可由作者之外的开发者阅读和增补。它应当有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达。
     -- "老大"Dave Thomas,OTI公司创始人,Eclipse战略教父。
  • 整洁代码就是作者着力照料的代码。有人曾花时间让它保持简单有序。他们适当地关注到了细节。他们 在意 过。
  • 简单代码,依其重要顺序:
         1. 能通过所有测试;
         2.  没有重复代码
         3.  提高表达力
         4. 提早构建简单抽象 ;    
         5.  体现系统中的全部设计理念;
         6. 包括尽量少的实体,比如类、方法、函数等。
  • 如果每个例程都让你感到 深合己意 ,那就是整洁代码。如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。
  • 漂亮的代码让编程语言像是 专为解决那个问题 而存在!
    
    心得:
  • 考虑项目进度与需求,从经理的角度看待问题
 

第二章  有意义的命名

     命名规则:
      1.名副其实
  • 变量、函数或类的名称应该已经答复了所有的大问题。
  • 只要简单改一下名称,就能轻易知道发生了什么。
int d; //消逝的时间,以日计
修改为:
int elapsedTimeInDays;
  • 无法第一时间理解代码的问题不在于代码的简洁度,而是在于代码的模糊度:即上下文在代码中未被明确体现的程度。
  • 选个好名字需要花时间,但省下的时间比花掉的多。注意命名,一旦有好的命名,就换掉旧的。
     2.避免误导
  • 程序员必须应当避免使用与本意相悖的词。
  • 专有名称、关键字(如list)对程序员有特殊意义。尽量避免命名。
     例:别用accountList来指称一组账号,除非它真的是List类型。
     如果包纳账号的容器并非真是个List,就会引起错误的判断。所以,用accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。避免使用有歧义的字母与数字。
 
  • 提防使用不同之处较小(不明显)的名称。
  • 以同样的方式拼写出同样的概念才是信息。拼写前后不一致就是误导。
      3.做有意义的区分
  • 如果名称必须相异,其意思也应该不同。
  • 命名要有实质意义上的区分。
  • 以数字系列命名(a1、a2,……aN)纯属误导,完全没有提供正确信息;没有提供导向作者意图的线索。
不规范示例:
Public static void copyChars(char a1[],char a2[]){
    for(int i=0;i<a1.length;i++){
        a2[i]=a1[i];
    } 
}
  • 要区分名称,就要以 读者能鉴别不同之处的方式来区分。
  • 废话是另一种没意义的区分。
例:假设你有一个Product类。如果还有一个ProductInfo或ProductData类,它们的名称虽然不同,意思却无区别。Info和Data就像a、an和the一样,是意义含混的废话。
  • 废话都是冗余。
例:Variable一词永远不应当出现在变量名中。Table一词永远不应当出现在表名中。NameString会比Name好吗?难道Name会是一个浮点数不成?
 
      4. 使用读得出来的名称
  • 单词能读得出来。
      5. 使用可搜索的名称
  • 长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。
  • 单个字母或者数字常量是很难在一大堆文章中找出来。
  • 窃以为单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。
for (int j=0; j<34; j++) {  
  s += (t[j]*4)/5;  
}

修改为:

int realDaysPerIdealDay = 4;  
const int WORK_DAYS_PER_WEEK = 5;  
int sum = 0;  
for (int j=0; j < NUMBER_OF_TASKS; j++) {  
  int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;  
  int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);  
  sum += realTaskWeeks;  
}

     6.避免使用编码

  • 把类型或作用域编进名称里面,徒然增加了解码的负担。带编码的名称通常也不便发音,容易打错。
  • 人们会很快学会无视前缀(或后缀),只看到名称中有意义的部分
      7. 避免思维映射
  • 不应当让读者在脑中把你的名称翻译为他们熟知的名称。
  • 读者必须在脑中将变量名映射为真实概念。
  • 明确是王道,编写其他人能理解的代码
      8.类名
  • 类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account和AddressParser。
  • 避免使用Manager、Processor、Data或Info这样的类名。
  • 类名不应当是动词。
      9.方法名
  • 方法名应当是动词或动词短语,如postPayment、deletePage或save。
  • 属性访问器、修改器和断言应该根据其值命名,并依Javabean标准 加上get、set和is前缀。
      10.别扮可爱
  • 名称解释够准确无误;别用花哨的名字。
  • 言到意到。意到言到。
     11.每个概念对应一个词
  • 给每个抽象概念对应一个词,并且一以贯之。
  • 切记同时使用多个词对应一个概念(例:切记同时出现controller、manager、driver等)
     12.别用双关语
  • 避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。
      13. 使用解决方案领域名称
  • 只有程序员才会读你的代码。所以,尽管用那些计算机科学(Computer Science,CS)术语、算法名、模式名、数学术语。
     14. 使用源自所涉问题领域的名称
  • 如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。
  • 优秀的程序员和设计师,其工作之一就是分离 解决方案领域 问题领域 的概念。
  • 与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。
     15. 添加有意义的语境
  • 你需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。
  • 如果没这么做,给名称添加前缀 就是最后一招了。
      16. 不要添加没用的语境
  • 只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。
  • 正确是命名的要点。
  • 设若有一个名为"加油站豪华版"(Gas Station Deluxe)的应用,在其中给每个类添加GSD前缀就不是什么好点子。
     17.最后的话
  • 取好名字最难的地方在于需要良好的描述技巧和共有文化背景。
 
    心得:
  • 通过看代码的名称,就能知道代码具体要干什么
  • 状态量(STATUS_VALUE, FLAGGED)大写
 
第三章  函数
     1.短小
  • 函数的第一规则是要短小。第二条规则是还要更短小。
      2.只做一件事
  • 函数应该做一件事。做好这件事。只做这一件事。
     3.每个函数一个抽象层级
  • 自顶向下读代码:向下规则
  • 程序就像是一系列TO起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续TO起头段落。
     4.使用描述性的名称
  • 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。
     5.函数参数
  • 最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。
  • 有足够特殊的理由才能用三个以上参数(多参数函数)-所以无论如何也不要这么做。
  • 如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
Circle makeCircle(double x, double y, double radius);  
Circle makeCircle(Point center, double radius);

     6.使用异常替代返回错误码

  • Try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。这意味着如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
public void delete(Page page) {  
   try {  
     deletePageAndAllReferences(page);  
   }  
   catch (Exception e) {  
     logError(e);  
   }  
}  
  • 使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署。
     7.如何写出这样的函数
  • 写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。
  • 我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。 然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。 最后,遵循本章列出的规则,我组装好这些函数。 我并不从一开始就按照规则写函数。我想没人做得到。
  • 不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。
 
第四章  注释
  • 优秀的代码不需要注释,本身就能说明问题。
  • 唯一真正好的注释是想办法不去写注释。
  • 好的注释比不上干净的代码。
// Check to see if the employee is eligible for full benefits
if (( employee.flags & HOURLY_FLAG ) && ( employee.age > 65 ))
修改后:
if ( employee.isEligibleForFullBenefits() )
  • 很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可,用代码解释大部分的意图。
  • 如果决定写注释,就要花必要的时间确保写出最好的注释。
 
第五章  格式
  • 选用一套管理代码格式的简单规则,然后贯彻这些规则
  • 源文件要像报纸那样。名称应当简单且一目了然,名称本身应该足够告诉我们是否在正确的模块中,源文件最顶部应该给出最高层次概念和算法,细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。
  • 紧密相关的代码应该相互靠近。
  • 变量声明应该尽可能靠近其使用位置。
  • 实体变量应该在类的顶部声明。
  • 相关函数,应该把它们放到一起,并且调用函数应该尽可能放在被调用函数上面。
  • 概念相关的代码应该放到一起,相关性越强,彼此之间的距离就该越短,相关性可能来自执行相似操作的一组函数。
  • 根据运算符优先级格式化代码:
  • 优先级高的不加空格,优先级低的添加空格。
private static double determinant(double a, double b, double c) {
    return b*b - 4*a*c
}
  • 保持循环体为空时的格式缩进。
 
第六章  对象和数据结构
  • 过程式代码(使用数据结构的代码)便于在不该懂既有数据结构的前提下添加新函数;面向对象代码便于在不改动既有函数的前提下添加新类。
  • 过程式代码难以添加新数据结构,因为必须修改所有函数;面向对象代码难以添加新函数,因为必须修改所有类。
  • 得墨忒耳律:模块不应该了解它所操作对象的内部情形。
  • 类C的方法f只应该调用以下对象的方法:
    • C;
    • 由f创建的对象;
    • 作为参数传递给f的对象;
    • 由C的实体变量持有的对象。
    • 方法不应调用由任何函数返回的对象的方法。即:只和朋友谈话,不与陌生人谈话。
 
第七章  错误处理
  • 如果错误处理搞乱了代码逻辑,就是错误的做法。
  • 遇到错误时,最好抛出一个异常。
  • 尝试编写强行抛出异常的测试,再往处理器中添加行为,使之满足测试要求。结果就是要先构造try代码块的事务范围,维护该范围的事务特征。
  • 在应用程序中定义异常类时,最重要考虑的时它们 如何被捕获
  • 特例模式:创建一个类或配置一个对象,用来处理特例。客户代码就不用应付异常行为了,异常行为被封装到特例对象中。
  • 别返回null值。更别传递null值。
List<Employee> employees = getEmployees();
if(employees != null) {                     // employees可能为null
    for(Employee e : employees) {
        totalPay += e.getPay();
    }
}
修改后:
public List<Employee> getEmployees(){
    .....
    return Collections.emptyList(); // Java collection.empty() 返回一个预定义不可变列表,避免NullPointException出现
}

List<Employee> employees = getEmployees();
for(Employee e : employees) {
    totalPay += e.getPay();
}
  • 如果在调用第三方API中可能返回null值的方法,可以用新方法打包返回null值的方法,在新方法中抛出异常或返回特例对象。
 
第八章  边界
  • 将外来的代码干净利落的整合进自己的代码中。
  • 如果使用类似Map这样的边界接口,就把它保留在类或近亲类中。避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API。
Map sensors = new HashMap();
Sensors s = (Sensor)sensors.get(sensorId);       // 强制类型转换
修改为:
Map<Sensor> sensors = new HashMap<Sensor>();     // 泛型
Sensor s = sensor.get(sensorId);
修改为:
public class Sensors{
    private Map sensors = new HashMap();

    public Sensor getById(String id){
        return (Sensor) sensors.get(id);         // 转换与类型管理在Sensors类内部处理
    }
}
  • 边界上的代码需要清晰的分割和定义了期望的测试。
 
第九章  单元测试
 
  • TDD三定律
  1. 在编写不能通过的单元测试前,不可编写生产代码。
  2. 只可编写刚好无法通过的单元测试,不能编译也算不通过。
  3. 只可编写刚好足以通过当前失败测试的生产代码。
  • 测试代码和生产代码一样重要。它需要被思考、被设计和被照料,该像生产代码一般保持整洁。
  • 整洁的测试三要素:可读性、可读性、可读性。
  • 每个测试函数只测试一个概念。
     整洁的测试遵循5条规则:
  1. 快速,测试应该够快。
  2. 独立,测试应该相互独立。应该可以单独运行每个测试,以及任何顺序运行测试。
  3. 可重复,测试应当可在任何环境中重复通过。应该能够在生产环境、测试环境中运行测试。
  4. 自足验证,测试应该有布尔值输出。
  5. 及时,测试应及时编写,单元测试应该恰好在使其通过的生产代码之前编写。
 
第十章  类
  • 类应该短小;
  • 类的名称应该描述其权责(方法的数量);
  • 类长短的标准:能否为某个类命以精确的名称;(包括含义模糊的词:processor、manager、super)
  • 单一权责原则:类或模块应该有且只有一条加以修改的理由。
  • 系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
保持内聚性就会得到许多短小的类:
  • 当把含有许多变量的大函数拆解成单独的函数,要拆解的代码使用了该函数声明中的多个变量,是否必须将这多个变量都作为参数传到新函数中?
       完全没必要!只要将多个变量提升为类的实体变量,完全无需传递任何变量就能拆解代码,应该很容易将函数拆分成为小块。
  • 通过降低连接度,类就遵循了依赖倒置原则:即类应当依赖于抽象(接口)而不是依赖于具体细节。
 
第十一章  系统
  • 软件系统应将启动过程和启动过程之后的运行时逻辑分离开,在启动过程中构建应用对象,也会存在相互缠结的依赖关系。
  • 将构造与使用分开的方法之一就将全面构造过程搬迁到main或main的模块中。
  • 使用抽象工厂模式让应用自行控制并创建对象,但构造的细节却隔离于应用程序代码之外。
  • 依赖注入,控制反转实现分流构造与使用。
 
第十二章  跌进
  • 跌进设计:
  • 运行所有测试;
  • 不可重复;
  • 表达了程序员的意图;
  • 尽可能减少类和方法的数量。
  • 以上规则按其重要程度排列。
  • 测试消除了对清理代码就会破坏代码的恐惧。
  • 应用简单设计后三条规则:
    • 消除重复、保证表达力、尽可能减少类和方法的数量。
 
第十三章  并发编程
  • 并发会在性能和编写额外代码上增加一些开销;
  • 正确的并发是复杂的,即便对于简单的问题也是如此;
  • 并发缺陷并非总能重视,所以常被看作偶发事件而忽略,未被当作真的缺陷看待;
  • 并发常常需要对设计策略的根本性修改。
  • 并发防御原则
    • 单一权责原则:建议:分离并发相关代码与其他代码。
    • 限制数据作用域:(防止多线程共享对象的同一字段互相干扰,synchronized保护一块使用共享对象的临界区)谨记数据封装;严格限制对可能被共享的数据的访问。
    • 使用数据复本:避免共享数据的方法之一就是开始就避免共享数据。
    • 线程尽可能独立:尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集(不与其他线程共享数据)。
  • 建议:检读可用的类。对于Java掌握java.util.concurrent, java.util.concurrent.atomic 和 java.util.concuttent.locks。
  • 执行模型:学习这些基础算法,理解其解决方案。
    • 生产者-消费者模型;
    • 读者-作者模型;
    • 宴席哲学家模型;
  • 测试线程代码:
    • 编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。
 
    建议:
  • 将伪失败看作可能的线程问题;-- 不要将系统错误归咎于偶发事件。
  • 先是非线程代码可工作;-- 不要同事追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。
  • 编写可插拔的线程代码;-- 编写可在数个配置环境下运行的线程代码。
  • 编写可调整的线程代码;-- 在不同配置环境下检测系统性能,允许线程数量可调整与线程变动,允许线程根据吞吐量和系统使用率自我调整。
  • 运行多于处理器数量的线程;-- 防止频繁切换任务导致死锁。
  • 在不同的平台上运行;-- 今早并经常的在所有目标平台上允许线程代码。
  • 调整代码并强迫错误发生;-- 捕捉线程中罕见的错误,采用硬编码与自动化的方法装置代码,改变代码执行顺序。
  • 如果花点时间装置代码,能极大的提升发现错误代码的机会。
  • 只要采用了整洁的做法,做对的可能性就有翻天覆地的提高。
 
第十四章  逐步改进
  • 要编写整洁代码,必须先写肮脏代码,然后清理它。
  • 避免程序在改进时无法恢复,采用测试驱动开发的规程,保证系统始终能运行。
  • 解决之道就说保持代码持续整洁和简单,用不让腐坏有机会开始。
 
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值