美文网首页Java
《代码整洁之道》读书笔记

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

作者: 高稷 | 来源:发表于2018-01-02 09:02 被阅读70次

    序,前言

    1. 重视代码
      facebookcode review作为重点KPI考核,并采用连坐制。
      code wins argument
      当两人为一个问题争执不下时,不妨以最快的速度用代码把想法写出来,事实胜于雄辩。

    2. 代码是负债不是资产
      代码越多,维护所要付出的成本就越高。
      如果代码结构越好,做了越多单元测试,代码质量越好、越小、耦合越松,那么添加新代码付出的成本就越少。

    3. 本书观点:代码质量与整洁度成正比

    一,整洁代码

    • 糟糕混乱的代码给项目带来的负面影响
      略过。

    • 什么是整洁代码
      关键字: 只做好一件事,简单直接,意图明确,单元测试,尽量少的依赖关系,看起来像是专门解决那个问题而存在。

    • 破窗理论和童子军军规
      第一扇窗被打破,但是没有人管,接下来会有更多的窗被打破,烂代码不去管它只会越来越烂。
      当你离开一个地方时,要让它比你来之前更干净。

    • 代码要易读
      我们编写代码时,读和写花费的时间比例超过10:1,要想代码易写,首先做到易读。

    • 面向对象设计的原则(SOLID)
      单一职责原则
      开闭原则
      里氏代换原则
      接口隔离原则
      依赖倒置原则

    二,有意义的命名

    1. 名副其实:为变量或函数起一个能反映其含义的名字并不容易,但起名字花的时间是值得的,好的命名能让读者快速理解代码,减少维护成本。这和TDD四原则里的“揭示意图”是一致的。

    2. 作者详细介绍了命名需要注意的问题,

      naming.PNG
      结合我们的项目,印象比较深的是“每个概念对应一个词”
      项目代码中通常会有一些特定职责的类,xxxHandler, xxxManager, xxxProcessor, xxxBuilder, xxxHelper... 这样的类名含义模糊,令人费解,而这些类往往在做类似的事情。

    三,函数的原则

    1. 短小
      if、else、while语句内的代码块应该只有一行,该行大抵是一个函数调用语句。
      这样的函数不仅能保持短小,而且调用的函数具有说明性的名称,从而增加了文档上的价值。
      所以函数的缩进不能多于两层。

    2. 只做一件事
      写函数是为了把大一些的概念(换言之,函数名称)拆分为另一抽象层上的一系列步骤。
      判断函数是否不止做了一件事,还有一个办法就是看是否还能再拆出一个函数。

    3. 每个函数一个抽象层级
      要让代码有自顶向下的阅读顺序,向下规则:每个函数后面跟着下一抽象层级的函数。

    4. Switch语句
      问题:太长,做了不止一件事,违反单一职责原则,违反开闭原则,到处存在类似结构的函数。
      解决方案:把switch语句放在工厂类,使用接口多态的接受派遣。

    5. 使用描述性的名称
      函数越短小,功能越集中,越便于取个好名字。

    6. 函数参数

    • 参数越少越好,参数超过三个时,排序、琢磨、忽略的问题都会加倍体现。
    • 一元函数的普遍形式:A.操作参数,转换,返回。B.传入event事件。
    • 参数过多,最好先封装成类再传入。
    • 避免使用标识参数,传入true/false,明显违反“只做一件事”的原则。
    • 避免使用输出参数,如果要修改对象的状态,要调用对象自己的函数:
      appendFooter(report);
      应该改成
      report.appendFooter();
    1. 分隔指令与询问
    public boolean set(String attribute, String value);
    

    该函数设置某个属性的值,如果设置成功返回true,如果不存在这个属性返回false。
    就会导致以下语句出现:

    if (set("userName", "Leo")) {...}
    

    应该改成

    if (attributeExists("userName")) {
      set("userName", "Leo")
    } 
    
    1. 使用异常替代返回错误码
      从指令式函数返回Error Code,轻微违反了指令与询问分隔的原则,而且导致更深层次的嵌套结构。使用异常,错误处理代码就能从主路径代码中分离出来,得到简化。
    if (deletePage(page) == E_OK) {
      if (registry.deleteReference(page.name) == E_OK) {
        if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
          logger.log("page deleted");
        } else {
          logger.log("configKey not deleted"); 
        }
      } else {
        logger.log("deleteReference from registry failed"); 
      }
    } else {
      logger.log("delete failed");
      return E_ERROR;
    }
    

    改成

    try {
      deletePage(page);
      registry.deleteReference(page.name);
      configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
      logger.log(e.getMessage());
    }
    

    try/catch代码块违反了只做一件事的原则,应该把try和catch代码块主体部分抽出来,另外形成函数

    public void delete(Page page) {
      try {
        deletePageAndAllReferrence(page);
      } catch (Exception e) {
        logError(e);
      }
    }
    
    private void deletePageAndAllReferrence(Page page) {
        deletePage(page);
        registry.deleteReference(page.name);
        configKeys.deleteKey(page.name.makeKey());
    }
    
    private void logError(Exception e) {
      logger.log(e.getMessage());
    }
    
    1. 别重复自己

    2. 结构化编程
      只要函数保持短小,循环偶尔出现return, break, continue没有问题,避免使用goto

    3. 函数修改的策略
      对于冗长复杂的函数,先加单元测试覆盖每行丑陋的代码,然后分解函数、修改名称、消除重复,同时保持单元测试通过。

    小结
    函数是动词,类是名称,编程艺术是语言设计的艺术。大师级程序员把系统当成故事来讲,而不是当作程序来写。

    四、注释

    当我们无法用代码表达意图时才使用注释。尽量避免使用注释,用代码表达意图。

    五、格式

    1. 格式的目的
    • 代码格式关乎沟通。
    • 代码风格和可读性仍会影响到可维护性和扩展性。
    1. 垂直格式
    • 短文件通常比长文件易于理解,尽量短小而精悍。
    • 概念间垂直方向上的区隔,不同的思路段落之间用空白行隔开,例如单元测试中的Given、When、Then用空白行隔开。
    • 垂直方向上,关系密切的概念应该相互靠近。
    • 垂直顺序,被调用的函数应该放在执行调用的函数下面。
    1. 横向格式
    • 水平方向的区隔和靠近,赋值语句=号左右加空格,函数参数之间加空格,运算符优先级高的× / ÷左右不加空格,优先级低的+/-左右加空格。
    • 水平对齐。
    • 缩进表示层次。
    • 空范围。
    • 格式范例:


      code format sample.PNG

    六、对象和数据结构

    1. 数据抽象
      隐藏实现关乎抽象!类并不简单的用getter、setter将其变量推向外间,
      而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。
      以抽象形态表述数据。
    //具象点
    public class Point {
      public double x;
      public double y;  
    }
    
    //抽象点
    public interface Point {
      double getX();
      double getY();
      void setCartesian(double x, double y);
      double getR();
      double getTheta();
      void setPolar(double r, double theta);
    }
    
    //具象机动车
    public interface Vehicle {
      double getFuelTankCapacityInGallons();
      double getGallonsOfGasoline();
    }
    
    //抽象机动车
    public interface Vehicle {
      double getPercentFuelRemaining();
    }
    
    1. 数据、对象的反对称性
    • 对象把数据隐藏于抽象之后,暴露操作数据的函数。
      数据结构暴露其数据,没有提供有意义的函数。

    • 对象与数据结构之间的二分原理:
      过程式代码(使用数据结构)便于在不改动数据结构的前提下添加新函数,
      面向对象代码便于在不改动既有函数的前提下添加新类。
      反过来说,
      过程式代码难以添加新数据结构,因为必须修改所有函数,
      面向对象代码难以添加新函数,因为必须修改所有类。

      过程式代码.PNG
      面向对象多态代码.PNG
    1. 德墨忒尔定律(迪米特法则,Law Of Demeter)
      也叫做“最少了解原理”,模块不应该了解它所操作对象的内部情形。
      C类的函数f()只能调用以下对象的方法:
    • C类的对象
    • f()创建的对象
    • 通过参数传入的对象
    • C类的实体变量对象

    另一种解释:只暴露应该暴露的接口方法,只依赖需要依赖的对象

    law of demeter sample.PNG
    System应该只暴露close()的接口方法,而不该暴露close()内部的细节,
    Person应该只依赖Container(硬件设备容器)的接口,而不该直接依赖System(操作系统)。
    这样做也符合依赖倒置原则,也就是面向接口编程

    火车失事
    下列代码应该切分成三行。

    final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
    

    是否违反德墨忒尔定律,取决于ctxt、options、scratchDir、absolutePath是对象还是数据结构。
    如果是对象,应该隐藏内部结构。
    如果是数据结构,则需要暴露内部结构,不算违反德墨忒尔定律。

    混杂
    尽量避免混合结构,一半是对象,一半是数据结构,既有执行操作的函数,又有getter/setter。同时增加了添加函数和添加数据结构的难度。

    隐藏结构
    经查,发现上述代码获取outputDir是为了根据路径得到BufferedOutputStream,创建文件,

    String outFile = outputDir + "/" +className.replace('.', '/') + ".class";
    FileOutputStream fout = new FileOutputStream(outFile);
    BufferedOutputStream bos = new BufferedOutputStream(fout);
    

    ctxt应该仅仅暴露获取BufferedOutputStream的接口方法,隐藏具体实现。

    BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
    
    1. 数据传送对象
      DTO是只有公共变量(包括私有变量+公共getter/setter)、没有函数的类,是最精炼的数据结构。
      Active Record是一种特殊的DTO形式,同时也会拥有save、find方法,通常是对数据库或其他数据源的之间翻译(就是我们项目中的Domain Object,一个类对应数据库一张表)。Active Record往往被塞进业务规则方法,导致数据结构和对象的混杂体。
      应该把Active Record当成数据结构,另外创建包含业务规则、隐藏内部数据的独立对象。

    小结

    • 数据结构暴露数据,没有明显行为。
      过程式代码操作数据结构,添加新的函数无需修改数据结构,但添加新的数据结构需要修改所有函数。
    • 对象暴露行为,隐藏数据。
      面向对象式代码操作对象,添加新的类无需修改既有函数,但添加新的函数需要修改所有类。
      不应对任何一种抱有成见,根据具体情况使用。

    七、错误处理

    对错误的处理很有必要,也很重要,要保证出现错误时程序仍能正常运行。
    但不能因此让代码逻辑变得混乱。

    1. 使用异常而非返回码
      使用异常进行错误处理,能把实现部分和错误处理分离,以免错误处理影响实现部分的逻辑。

    2. 先写Try-Catch-Finally

    3. 使用不可控异常
      我的理解:可控异常违反开闭原则,修改内层方法抛出一个可控异常,外层方法都必须修改捕获这个异常,导致从内到外的修改链。
      但是使用java编写文件处理、反射的程序时,不可避免的需要捕获可控异常。
      自定义的异常都应该继承RuntimeException(不可控异常)。

    4. 给出异常发生的环境说明

    5. 依调用者需要定义异常类
      定义异常,要考虑它们如何被捕获。
      对于第三方API抛出各种不同异常的情况,可以打包API抛出通用的异常类型,简化调用时的代码。
      直接在调用API的地方捕获异常:

      api exception1.PNG
      打包调用API,简化调用代码:
      api exception2.PNG
    6. 定义常规流程
      采取特例模式(SPECIAL CASE PATTERN),创建一个类或配置一个对象,用来处理特例。客户代码就不用应付异常行为了。

    7. 不要返回或传递null值
      方法返回null,不如抛出异常或返回特例对象,否则会有NullPointerException的隐患。

    小结
    将错误处理和主要逻辑隔离,就能写出整洁而强壮的代码。

    八、边界

    我们不可避免的需要使用第三方或者其他团队开发的组件,整合到我们自己的代码中,这章主要讲如何保持软件边界整洁。

    1. 使用第三方代码
    • Map的接口功能非常丰富,接收者不要删除其中的映射
    • Map的接口一旦修改,许多地方代码需要修改
    • 不要把Map(或其他在边界上的接口)在系统中传递,否则也要保留在类中,避免从公共API返回边界接口,或把边界接口作为参数传递给公共API。
    1. 通过编写学习型测试来理解第三方代码

    2. 使用尚不存在的代码

      使用尚不存在的代码.PNG
      通信控制器依赖于Transmitter API,但API尚未定义且不受我们控制。
      先定义适合通信控制器使用的接口Transmitter,一旦提供了Transmitter API,就编写Transmitter Adapter来跨接。适配器封装了与API的互动,并且如果API发生变动,只需要修改适配器。

    小结
    边界上会发生我们不可控的改动,要避免我们的代码过多依赖第三方代码的细节。
    使用Sensor类封装第三方接口的返回结果,或使用Adapter模式将第三方接口转换为我们需要的接口。
    使用Adapter模式不仅能将不兼容的接口改写成兼容的接口,还能对第三方接口重新封装来避免边界变化对系统的影响。

    九、单元测试

    1. TDD三定律
    • 在编写不能通过的单元测试前,不能编写生产代码
    • 只能编写刚好不能通过的单元测试,不能编译也算不通过
    • 只能编写刚好足以通过当前失败测试的生产代码
      我对上述定律的理解,首先必须先写单元测试再写实现,同时在写单元测试和实现时必须保持小步前进。由于真实项目的业务逻辑往往很复杂,一个story如何拆分tasking,需要在小步和项目进度之间做权衡,也取决于对TDD和重构掌握的熟练程度。
    1. 整洁的测试
      测试代码最重要的是可读性,明确、简洁、有足够的表达力。

    2. 一个测试一个断言
      作者认为单个断言是个好的准则,一个测试方法的断言数量要尽量少。
      但太过强调单个断言,会导致given和when部分有很多重复代码。
      我自己TDD的体会,一个测试方法对应一个test-case,如果测试用例拆得足够小,测试方法中的断言自然就会少,这和作者提到的每个测试一个概念应该是一致的。

    3. F.I.R.S.T
      测试还应遵守以下5条规则。

    • 快速(fast) 测试应该能快速运行,太慢了你就不会频繁的运行,就不会尽早发现问题。
    • 独立(independent) 测试应该相互独立,某个测试不应该为下个测试设定条件。当测试相互依赖,一个没通过导致一连串的测试失败,使问题诊断变的困难。
    • 可重复(repeatable) 测试应该可以在任何环境中重复通过。
    • 自足验证(self-validating) 测试应该有布尔值输出,无论通过或失败,不应该是查看日志文件去确认
    • 及时(timely) 单元测试应该恰好在使其通过的生产代码之前编写。

    小结
    关于单元测试的内容还有很多,这一章主要还是强调保持整洁的测试

    十、类

    1. 类的组织
      公共静态常量 - 私有静态变量 - 私有实体变量 - 公共方法 - 私有方法,保证自顶向下的阅读顺序。

    2. 类应该短小
      如何判断一个类是否太长,主要看类是否承担了多个职责
      单一职责原则是OO最容易理解和遵循的原则,通常也是被违反得最多的原则。类或模块应有且只有一条加以修改的理由。系统应该有许多短小的类而不是巨大的类组成,每个小类封装一个职责。

    3. 内聚
      如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。内聚性高,意味着类中的方法和变量相互依赖,相互结合成一个逻辑整体。
      保持内聚性就能得到短小的类,一旦发现类失去内聚性,就拆分它!当某些实体变量只被少数方法使用,就应该拆分出一个类。

    4. 为了修改而组织
      通过多态将一个大类中的细节隔离,等同于把修改隔离,符合开闭原则
      让调用方依赖接口而不依赖细节,符合依赖倒置原则
      隔离细节,更利于单元测试。

    十一、系统

    将系统的构造和使用分开:构造和使用是不一样的过程。

    1. 工厂
      使用抽象工厂模式,将构造的细节隔离于应用程序之外。

    2. 依赖注入(DI/IOC)
      在依赖管理情景中,对象不应该负责实例化对自身的依赖,反之,它应该将这份权责移交给其他有权利的机制,从而实现控制的反转。

    3. 扩容
      “一开始就做对的系统”纯属神话。
      反之,我们应该只实现今天的用户的需求。
      然后重构,明天再扩容系统,实现新用户的需求。

    4. 面向切面编程(AOP)
      AOP中,被称为方面(aspect)的模块构造指明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。这种说明是用某种简洁的声明(Attribute)或编程机制来实现的。

    小结
    这一章的概念和描述比较多,例子不多,看完并没有很深的体会。
    但是工厂模式、依赖注入、AOP这些在项目中都有应用,关于Spring AOP可以参考《Spring实战》和这篇文章Spring之AOP由浅入深

    十二、迭进

    1. 简单设计规则1 运行所有测试
      紧耦合的代码难以编写测试。同样编写测试越多,就会越遵循DIP之类的原则,使用依赖注入,接口和抽象等工具尽可能减少耦合。如此一来设计就会有长足进步。遵循有关编写测试并持续运行测试的、明确的规则,系统就会更贴近OO低耦合度、高内聚的目标。

    2. 简单设计规则2 重构
      在重构过程中,可以应用有关优秀软件设计的一切知识,提升内聚性,降低耦合度。换句话说:消除重复,保证表达力,尽可能的减少类和方法的数量。

    3. 不可重复
      重复代表着额外的工作、额外的风险和额外不必要的复杂度。重复有多种表现。雷同的代码行是一种。不但是从代码行的角度,也要从功能上消除重复。

    4. 揭示程序员意图

    十三、并发编程

    1. 为什么要并发
      并发是一种解耦策略,它帮助我们把做什么(目的)和何时(时机)做分解开。
      在单线程应用中,目的与时机紧密耦合。
      而解耦目的与时机能明显地改进应用程序的吞吐量和结构。
      从结构的角度看,应用程序看起来更像是许多台协同工作的计算机,而不是一个大循环。
      单线程程序许多时间花在等待Web套接字I/O结束上面。

    2. 迷思与误解

    • 并发总能改进性能:并发有时能改进性能,但只在多个线程或处理器之间能分享大量等待时间的时候管用。

    • 并发编程无需修改设计:并发算法的设计可能与单线程系统的设计极不相同,目的与时机的解耦往往对系统结构产生巨大影响。

    • 在采用Web和EJB容器时,理解并发问题不重要:最好了解容器在做什么,如何应付并发更新、死锁等问题。

    • 并发会在性能和编写额外代码上增加一些开销

    • 正确的并发是复杂的,即使对于简单的问题也是如此。

    • 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,而未被当做真的缺陷看待。

    • 并发常常需要对设计策略的根本性修改

    平时工作中并发编程涉及得很少,读起来体会不深,转载一篇java并发编程相关的文章,以后继续学习
    关于Java并发编程的总结和思考

    相关文章

      网友评论

        本文标题:《代码整洁之道》读书笔记

        本文链接:https://www.haomeiwen.com/subject/rddawxtx.html