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

it2023-07-17  73

第一章  整洁代码

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