作者:Robert C. Martin

读后感:

本书从命名、函数、注释、格式、对象和数据结构、错误处理、边界、单元测试、类、系统、迭进、并发编程等各个方面进行规约阐述。随后给出了改进案例。

一. 命名

  1. 要名副其实,不要用魔法值、无意义的参数名。

  2. 要避免误导,不要用一些专有名词,不要用小写的 l、大写的 O 这种看起来和 0、1 很像的容易误导的字符命名。

  3. 要做有意义的区分,比如 product 和 productData、productInfo 就无法做到有意义的区分,要避免。一组命名在一些场景下能做到有意义的区分,在另一些场景下又做不到有意义的区分,只要能做到有意义的区分,不要一概而论的否定。

  4. 使用读得出来的名称,比如缩写的太厉害了,根本无法读出来,这种要避免。如 genymdhms,实际要表达的意思是 generate year month day hour minute second,这个名称读起来就是 gen why em dee aich emm ess。

  5. 使用可搜索的名称。当命名太过简短时,在一个长篇幅的类中,根本无法有效的搜索到它,比如 e。

  6. 类名和对象名应该是名词,方法名应该是动词。

  7. 别耍宝,言到意到、意到言到。

  8. 每个概念对应一个词。比如获取通用 get,不要这里用 get,那里用 fetch。全局统一概念。

  9. 别用双关语,比如在多个类中都有 add 方法,表示将两个值连接或者通过增加来获取新值,而为了保持这种统一,在另一处用 add 表示将元素放到集合中。这个 add 就是双关语。
    PS:好像在项目中,不同来源的三方开源包里就是存在不同的含义,这就无法做到统一了,除非你不用这些三方开源包。

  10. 使用解决方案领域的名称,使用源自所涉领域的名称。

  11. 添加有意义的语境,比如 firstName、lastName、street、houseNumebr、city、state、zipcode。这些合在一起就是一个地址。统一增加前缀以提供语境,还可以和其他地方的类似概念做区分。如 addrFirstName、addrLastName 等。

  12. 不要添加没有意义的语境,假如有一个应用名为“加油站豪华版”(Gas Station Deluxe),在其中的每个类添加 GSD 前缀。那么用 IDE 自动完成功能时,打下 GSD 所有类都能出来。这种命名就不好。

二. 函数

  1. 短小。尽可能的短小。

  2. 只做一件事。要判断函数是否不止做了一件事,就是看是否能在拆出一个函数,该函数不仅只是单纯的重新诠释其实现。

  3. 每个函数一个抽象层级。自顶下下读代码——向下规则。每个函数后面跟着抽象层更低的函数。

  4. swith 语句。书中给的规矩是,如果只出现一次,就用多态对象化解,用工厂模式来创建不同的多态对象。不过这条规矩,我不知道怎么在所有场景中使用。

  5. 使用描述性的名称,不要怕花时间取名字、不要怕长名称。命名方式要保持一致、一脉相承。

  6. 函数参数。使用越少的参数越好。参数越多,函数的含义越负责,测试用例也越复杂。

    1. 一元函数:传入 1 个参数,通常有 2 中情况。1 是你在询问这个参数的情况,返回一个状态;2 是你对这个参数进行操作,转换成一个别的东西。那么取较能区分这 2 中情况的名称。比如 boolean fileExists("MyFile")、InputStream fileOpen("MyFile")。

    2. 标识参数:绝对不要传入 true false 这样的标识参数。标识参数意味着这个函数做了不止一件事情。

    3. 二元函数:有些场景下,用二元函数恰如其分,而且顺序也是固定的,比如经纬度、xy轴等。但对于 2 个参数本身没有关联的场景,要尽量使用一元函数。可以通过一些机制进行转换。比如,可以将某个参数写成是类的成员变量,或者把方法写成是其中一个参数的成员函数,还可以分离出新类在构造器中传入一个参数,另外在写这个方法。

    4. 三元函数:三元函数比二元函数难懂得多。排序、琢磨、忽略的问题都会加倍体现。建议在写三元函数之前一定要想清楚。

    5. 参数对象:如果函数传入二个、三个或者三个以上的参数,就说明其中一些参数需要封装成类了。

    6. 参数列表:String format(String format, Object... args) 实际上是二元函数。

    7. 动词与关键字:函数名由动词加上关键字,有利于记忆参数及其顺序。

  7. 无副作用。

    1. 书中举了个例子,一个 checkPassword 方法,实际上存在时序性的耦合,只有在特定时刻调用才安全。这就是一种副作用。

    2. 还有一种副作用,就是输入参数其实和输出参数的意义相同,如 void appendFooter(StringBuffer report) 。在面向对象编程中,输出参数的大部分需求已经消失了,因为this也有输出函数的意味。所以最好是这样:report.appendFooter()。

    3. 书中建议,应避免使用输出参数。如果函数必须要改变某种状态,就修改所属对象的状态。

  8. 分隔指令与询问,一个函数要么做什么,要么问什么,不要一边做一边问。比如 if (set(attribute, value)),set是做事情,但是if有表明了询问,让人困惑。

  9. 使用异常替代返回码。返回码一方面要求调用方要立即处理错误,另一方面意味着存在一个错误码枚举。前者让函数避免不了深层次的嵌套,后者使系统难以扩展。是用异常处理,能让错误处理从主路径中分离出来。但是 try catch 十分丑陋,它搞乱了代码结构,让错误处理与代码混为一谈。最好是把try catch 代码块的主体部分抽离出来,另外形成函数。函数只做一件事,处理错误就是一件事。

  10. 别重复自己

三. 注释

本书观点,认为良好的代码无需注释,如果需要写注释,说明代码本身不够好,不够清晰。代码的版本发生变化后,注释不一定会跟着变化,取决于维护人员,此时注释就会变成误导信息。

程序员应当负责将注释保持在可维护、有关联、精确的高度。但我更主张把力气用在写清楚代码上,直接保证无需编写注释。

  1. 注释不能美化糟糕的代码

  2. 用代码来阐述,例如:if (employee.isEligibleForFullBenefits())

  3. 好注释

    1. 对意图的解释

    2. 阐释:对一些晦涩的参数或者返回值进行翻译,或者是某个标准库的一部分无法修改代码,阐释就有意义。但风险是阐释本身不正确。

    3. TODO注释

    4. 公共API红的 Javadoc

  4. 坏注释

    1. 喃喃自语,模糊不清,只有自己知道具体含义。

    2. 多余的注释,代码已经很清楚,注释甚至不如代码更清晰。

    3. 误导性注释

    4. 循规式注释:书中观点,所谓每个函数都要有 Javadoc 或每个变量都要有注释的规矩全然是愚蠢可笑的(手动艾特sonar扫描[狗头])

    5. 日志式注释:就好比在Navicat导出ddl的文件中那个头部的日志式注释一样。冗长的记录让模块变得凌乱不堪。

    6. 废话注释:我感觉,通常来自于循规式注释。书中叫我们用整理代码的决心代替创造废话的冲动。

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

    8. 位置标记

    9. 归属和署名:注释在那儿放了一年又一年,越来越不准确,越来越和原作者没什么关系。

    10. 注释掉的代码:其他人不敢删除注释掉的代码。总觉得还有什么用。

    11. 非本地信息,注释描述的是其他位置的信息。

    12. 信息过多,别在注释中添加有趣的历史性话题或无关的细节描述。

    13. 不明显的联系,注释里描述的内容和代码在一些细节上无法关联上,甚至注释本身还需要一个注释描述才能搞清楚。

    14. 函数头,短函数不需要太多描述。

    15. 非公共代码中的 Javadoc,虽然 Javadoc 对公共 API 非常有用,但对于不打算作公共用途的代码就令人厌恶了。

看得出来,Bob 大叔真的很不喜欢注释。他认为代码本身就是文章,要什么注释,要不了一点。

四. 格式

  1. 垂直格式

    1. 文件不要太大,短文件通常比长文件更易于理解。

    2. 垂直方向上的区隔,方法与方法之间空一行,可读性更强。从上往下读,目光总是落在空白行后的一行。

    3. 垂直方向上的靠近,概念紧密的代码要靠近。

    4. 垂直距离,关系密切的概念应该互相靠近,如果没有特别的理由,不要将关系密切的概念分置于不同的文件中。

      1. 变量声明要靠近其被使用的地方。因为函数很短,本地变量应该在函数的顶部出现。偶尔,在较长的函数中,变量也可能在某个代码块的顶部。

      2. 实体变量应该在类的顶部声明。

      3. 相关函数,若某个函数调用了另外一个,就应该把它们放到一起,并且调用者应尽可能放在被调用者上面。

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

  2. 横向格式

    1. 横向字符短些好,尽量不超过80,最多不要超过120。这是Bob大叔的习惯和建议。

    2. 水平方向上的区隔与靠近,用空格把相关性较弱的事物隔开。我现在已经养成了这个习惯,甚至有点强迫症。符号和代码之间空格隔开,英文字母和汉字之间、数字和汉字之间要空格隔开。确实美观。

    3. 缩进,IDE中都自动了。唯一可以说到说到的是,有些人 2 个字符缩进,有些人 4 个字符缩进。我喜欢 4 个字符,据大佬说,2 个字符缩进更显专业。我看Bob大叔也是 2 个字符缩进。

五. 对象和数据结构

  1. 数据抽象:类中的属性使用private隐藏,暴露出getter方法。实际上,这是暴露抽象接口,以抽象形态表述数据。要以最好的方式呈现某个对象包含的数据,而不是乱加取值器和赋值器。

  2. 数据、对象的反对称性:

    对象把数据隐藏于抽象之后,暴露操作数据的函数;数据结构暴露其数据,没有提供有意义的函数。二者是对立的,差异貌似微小,实则有深远的含义。看下面 2 段代码:

    public class Square implements Shape {
            private Point topLeft;
            private double side;
            
            public double area() {
                return side * side;
            }
        }
        
        public class Rectangle implements Shape {
            private Point topLeft;
            private double height;
            private double width;
            
            public double area() {
                return height * width;
            }
        }
        
        public class Circle implements Shape {
            private Point center;
            private double radius;
            public final double PI = 3.141592654589793;
            
            public double area() {
                return PI * radius * radius;
            }
        }
    public class Square {
            public Point topLeft;
            public double side;
        }
        
        public class Rectangle {
            public Point topLeft;
            public double height;
            public double width;
        }
        
        public class Circle {
            public Point center;
            public double radius;
        }
        
        public class Geometry {
            public final double PI = 3.141592653589793;
            
            public double area(Object shape) throws NoSuchShapeException {
                if (shape instanceof Square) {
                    Square s = (Square) shape;
                    return s.side * s.side;
                } else if (shape instanceof Rectangle) {
                    Rectangle r = (Rectangle) shape;
                    return r.height * r.width;
                } else if (shape instanceof Circle) {
                    Circle c = (Circle) shape;
                    return PI * c.radius * c.radius;
                }
                throw new NoSuchShapeException();
            }
        }

在面向对象编程中,添加新函数比较简单;在过程式代码中,添加新数据类型比较方便。二者各有优势。在需要添加新数据类型的时候,面向对象比较合适;如果要添加新函数而不是数据类型的时候,过程式代码比较合适。

  1. 德墨忒尔律(The Law of Demeter, LoD):只与朋友交谈,不与陌生人说话.

    1. order.getCustomer().getAddress().getCity().getName(); 在这段链式调用中,对调用者order来说,city、name都是陌生人。

    2. 对象应该隐藏其内部结构。

    3. 链式调用本身不是罪,罪的是 让外部知道你的内部结构

      1. 如果你不希望外界知道内部结构,那应该增加封装。

      2. 如果内部结构就是你的 API 部分,那链式调用是合理的。

      3. 德墨忒尔律的目的不是禁止链式调用,而是防止暴露“内部结构”。

  2. 数据传送对象DTO(Data Transfer Objects):数据传送对象,是一个只有公共变量、没有函数的类。是最为精炼的数据结构。这种结构非常有用。

  3. 总结

    1. 对象适合封装行为;数据结构适合暴露数据。两者没有绝对优劣,只是在不同场景下适用。

    2. 使用对象,会得到“多态”。
      使用数据结构,则更容易做“过程式代码”和“算法性逻辑”。

    3. 对象与数据结构不要混用,否则会变成最糟糕的设计。

    4. 业务中常见的类的定位:

      类型

      常见内容

      本质

      Clean Code 分类

      DTO

      字段 + getter/setter

      携带数据

      数据结构

      VO

      字段 + getter/setter

      展示数据

      数据结构

      BO(国内普遍)

      字段袋子 + 少量工具方法

      业务内部数据

      数据结构

      BO(DDD/规范)

      行为 + 不变式

      领域对象

      对象

      Service

      业务行为、流程协调

      封装行为

      对象

六. 错误处理

  1. 使用异常而非返回码

  2. 在编写可能抛出异常的代码时,先写出 try-catch-finally 语句。先写单元测试方法,驱动创建异常处理代码。

  3. 依调用者需要定义异常类,封装三方开源 API 成打包类,自定义异常类。

  4. 定义常规流程,某些异常的结果在业务上是正常业务,所以要对其进行定义,而不是抛异常。使用特例模式,创建一个类或配置一个对象,用来处理特例。

  5. 别返回 null 值,这样的话到处都要处理 null,代码很脏。

  6. 别传递 null 值,系统内部要保持干净,null 校验要在系统外部,不要让 null 进入到系统内部。

    1. 核心层不需要校验,要保持干净。同一领域内部应当互相信任,不返回 null。要么抛异常,要么返回特例。

    2. 边界层需要校验,所以最重要的是判断什么是边界。

      1. 构造器是一个天然的边界,需要校验。

      2. 外部进入内部就是边界,比如 Controller。

      3. 从底层进入业务层,也是边界,比如 Repository。

      4. 不可靠开发者写的模块,也是外部,是边界,需要校验。

      5. 对于工具类,如果是团队内部约定好不传 null 的工具类,属于领域内,不用校验。如果是不可行工具类,则需要校验。

    3. 总而言之,目的是不让 null 在业务内部乱飞。

七. 边界

  1. 学习第三方代码,用学习型测试这个方法。编写测试代码来理解第三方代码。

  2. 使用第三方代码,不要在系统中传递第三方API,而是进行封装,确定边界。直接的传递第三方API,比如Map,Map在任何地方都可能被修改,会增加耦合性,未来变更需求是很痛苦。用业务类来封装Map,作为业务自己的数据结构。如下代码:

    public class SensorRegistry {
        private Map<String, Sensor> sensors = new HashMap<>();
    
        public Sensor getByName(String name) {
            return sensors.get(name);
        }
    
        public void register(Sensor sensor) {
            sensors.put(sensor.getName(), sensor);
        }
    }
  3. 使用未知代码,用Adapter模式。Adapter封装了与未知代码API的互动,这里是当API发生变动时唯一需要改动的地方,这样就能得到一个整洁的边界。

八. 单元测试

  1. TDD 三定律:

    1. 定律一 在编写不能通过的单元测试前,不可编写生产代码。

    2. 定律二 只可编写刚好无法通过的单元测试,不能编译也算不通过。

    3. 定律三 只可编写刚好足以通过当前失败测试的生产代码。

  2. TDD 三定律解析:

    1. 先写一个会 失败 的测试

    2. 一次只关注一个测试

    3. 只写刚好足以通过测试的生产代码(最小实现)

  3. 举例说明:

    1. 写测试(失败):

      @Test
      void counterStartsAtZero_andIncrementIncreases() {
          Counter c = new Counter();
          assertEquals(0, c.value());
          c.increment();
          assertEquals(1, c.value());
      }
    2. 写最小生产代码(让测试通过):

      public class Counter {
          private int v = 0;
          public int value() { return v; }
          public void increment() { v++; }
      }
    3. 重构(如果发现需要抽象或添加并发控制,再重构)

  4. TDD 定律导致测试代码非常多,因此一定要保证整洁,否则会成为巨大的债务。

  5. 测试编写规范,要满足构造-操作-检验模式,given-when-then。

  6. 测试代码的标准和生产代码不同,要尽可能整洁。

  7. 每个测试一个断言:测试中的断言要尽可能少。

  8. 每个测试一个概念:只测试一个概念。

  9. F.I.R.S.T

    1. 快速(Fast),测试应该足够快。

    2. 独立(Independent),测试应该相互独立。

    3. 可重复(Repeatable),测试应当可在任何环境中重复通过。

    4. 自足验证(Self-Validating),测试应该有布尔值输出。

    5. 及时(Timely),测试应及时编写。

九. 类

  1. 类的组织:

    1. 类的组织从一组变量开始

      1. 公共静态常量,最先出现。

      2. 私有静态变量,其次出现。

      3. 私有实体变量,最后出现。

      4. 公共变量,很少会有。

    2. 公共函数应跟在变量列表之后,公共函数调用的私有工具函数紧随其后。这符合自顶向下原则,就像读报纸文章。

  2. 类应该短小:衡量方法在于计算权责

    如果类名中包括含义模糊的词,比如“Proccessor”或“Manager”或“Super”,这种现象往往说明有不恰当的权责聚集情况存在。

    1. 单一权责原则(SRP):该原则认为,类或者模块有且只有一条加以修改的理由。系统应该由许多短小的类而不是少量巨大的类组成。

    2. 内聚:内聚性高,意味着类中的方法和变量相互依赖、互相结合成一个逻辑整体。

      1. 保持内聚性就会得到许多短小的类。一个有许多变量的大函数,将其一小部分拆解成单独的函数。函数有很多参数,将这些参数提升为类的实体变量,完全无需传递任何变量。但这意味着内聚性丧失。于是我们将其拆成单独的类,以保持内聚性。

    3. 开放-闭合原则:类应该对扩展开放,对修改关闭。通过子类化的手段,可以让类对新功能是开放的,而且可以同时不触及其他类。

    4. 依赖倒置原则:类应当依赖于抽象,而不是依赖于具体细节。比如,把一个功能的接口(抽象)而不是实现作为类的实例变量。

十. 系统

  1. 将系统的构造和使用分开。

    软件系统应将启始过程和启始过程之后的运行时逻辑分开,在启始过程中构建应用对象,也会存在互相缠结的依赖关系。

    1. 构造过程,比如延迟初始化,需要构造的全部细节。在使用时不应当关注这些。即:

    2. 分解 main:将系统全部构造过程搬迁到 main 或者称之为 main 的模块中。

    3. 工厂:有时应用程序也要负责确定何时创建对象,可以使用抽象工厂模式让应用这记性控制何时创建对象。

    4. 依赖注入(Dependcy Injection, DI)、控制反转(Inversion of Control, IoC)。

  2. 扩容

    与物理系统相比软件系统比较独特。它们的架构都可以递增式的增长,只要我们持续将关注面恰当地切分。

    1. 切分关注面:EJB1和EJB2的例子,定义接口、实现的Entity Bean,业务逻辑和应用容器紧密耦合。隔离单元测试困难,面向对象变成本身也被侵蚀。出现 DTO 很常见,会拥有很多同样数据的冗余类型。

    2. 横贯式关注面,使用 AOP。

  3. Java 代理:ProxyHandler

  4. 纯 Java AOP 框架:Spring AOP

十一. 迭进

本章主要是介绍了简单设计原则。

  1. 原则1:运行所有测试。紧耦合的代码难以测试,测试越多,就约会遵循DIP之类的设计原则。

  2. 原则2~4:重构

    1. 原则2:不可重复。尽最大努力的消除重复,即便只是短短几行。当消除重复后,往往会违反一些原则如 SRP,那么就要调整重复代码隔离出来的方法到新的类中,以持续遵循原则。
      模板方法模式是一种移除高层级重复的通用技巧。

    2. 原则3:表达力。写出易于阅读理解的代码,用好名称,使用设计模式的命名采用标准命名法来表达;编写表达性好的测试;勇于尝试,当写出能工作的代码后,要下功夫调整代码而不是立即进入下一个工作内容。

    3. 原则4:尽可能少的类和方法。不拘泥于一些教条,如某个编码标准称应当为每个类创建接口等。本原则弱于以上3个原则。

十二. 并发编程

  1. 为什么要并发

    1. 并发是一种解耦策略。是目的(做什么)和时机(何时)的解耦。

    2. 解耦目的和时机能很明显地改进应用程序的吞吐量和结构。

  2. 并发的挑战:程序的执行路径有非常多种

  3. 并发防御原则:

    1. 单一全责原则:方法/类/组件应当只有一个修改的理由。并发设计自身足够复杂到成为修改的理由,所以也该从其他代码中分离出来。

    2. 推论:限制数据作用域。对共享数据使用临界区,保护临界区。手段之一是 synchronized。

    3. 推论:使用数据复本。避免使用共享数据,使用数据复本。

    4. 推论:线程应尽可能地独立。

  4. 了解Java库

    1. java.util.concurrent

    2. java.util.concurent.atomic

    3. java.util.concurent.locks

  5. 了解执行模型

    1. 生产者-消费者模型。一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。

    2. 读者-作者模型。当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。增加吞吐量,会导致线程饥饿和过时信息的累积。更新会影响吞吐量。协调读者线程,不去读作者线程正在更新的信息(反之亦然),这是一种辛苦的平衡工作。作者线程倾向于长期锁定许多读者线程,从而导致吞吐量问题。

    3. 宴席哲学家。想象一下,一群哲学家环坐在圆桌盘。每个哲学家的左手边放了一把叉子。桌面中央摆着一大碗意大利面。哲学家们思考良久,直到肚子饿了。每个人都要拿起叉子吃饭。但除非手上有两把叉子,否则就没法进食。如果左边和右边的哲学家已经取用了一把叉子,中间这位就得等到别人吃完、放回叉子。每位哲学家吃完后,就将两把叉子放回桌面,知道肚子再饿
      用线程代替哲学家,用资源代替叉子,就变成了许多企业级应用中进程竞争资源的情形。

  6. 警惕同步方法之间的依赖

    public synchronized boolean isAvailable() {
        return available;
    }
    
    public synchronized void use() {
        if (!available) {
            throw new IllegalStateException();
        }
        available = false;
    }

    调用方:

    if (resource.isAvailable()) {
        resource.use();
    }

这里隐含依赖:isAvailable()use() 必须是原子操作,但它们是两个同步方法,不是一个同步块

public synchronized boolean tryUse() {
    if (!available) {
        return false;
    }
    available = false;
    return true;
}
  1. 保持同步区域微小。尽可能减小同步区域。

  2. 很难编写正确的关闭代码。考虑一个被指示关闭的系统。父线程告知全体子线程放弃任务并结束。如果其中两个子线程正以生产者/消费者模型操作会怎样?假设生产者线程从父线程处接收到信号,并迅速关闭。消费者线程可能还在等待生产者线程发来消息,于是就被锁定在无法接收到关闭信号的状态中。它会死等生产者线程,永不结束,从而导致父线程也无法结束。
    尽早考虑关闭问题。

  3. 测试线程代码

    1. 将伪失败看作可能的线程问题

    2. 先使非线程代码工作。不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。

    3. 编写可插拔的线程代码。

    4. 编写可调整的线程代码。

    5. 运行多于处理器数量的线程。

    6. 在不同平台上运行。不同操作系统有着不同线程策略,尽早并经常地在所有目标平台上运行线程代码。

    7. 装置试错代码。线程中的缺陷之所以不频繁、偶发、难以发现,是因为在几千个穿过脆弱区域的可能路径中,只有少数路径会真的导致失败。经过会导致失败的路径的可能性惊人的低,所以,侦测与调式非常之难。
      装置代码,增加对 Object.wait()、Object.sleep()、Object.yield()、Object.priority() 等方法的调用,改变代码执行顺序。
      有两种装置代码的方法:硬编码、自动化。

    8. 硬编码。手工向代码中插入 wait()、sleep()、yield() 和 priority() 的调用。这种方法有许多毛病:

      1. 得手工找到合适的地方来插入方法调用。

      2. 不必要地在产品环境中留下这类代码,将拖慢代码执行速度。

      3. 可能找不到缺陷。

    9. 自动化。可以使用 Aspect-Oriented Framework、CGLIB 或 ASM 这类的工具通过编程来装置代码。

十三. 坏味道和启发

1. 注释

  1. 不恰当的信息:注释只应该描述有关代码和设计的技术性信息。

  2. 废弃的注释

  3. 冗余注释

  4. 糟糕的注释:注释要写就要写好。字斟句酌,使用正确的语法和拼写。别闲扯、画蛇添足,保持简洁。

  5. 注释掉的代码:删除

2. 环境

  1. 需要多步才能实现的构建

  2. 需要多不才能做到的测试

3. 函数

  1. 过多的参数

  2. 输出参数:如果函数非要修改什么东西的状态不可,就修改它所在的对象的状态。

  3. 标识参数:布尔值参数大声宣告函数做了不止一件事情,他们令人迷惑,应该消灭掉。

  4. 死函数:永不被调用的方法应该丢弃。

4. 一般性问题

  1. 一个源文件中存在多种语言

  2. 明显的行为未被实现:遵循“最小惊异原则”。函数或类应该实现其他程序员有理由期待的行为。比如,考虑将一个日期名称翻译为表示该日期的枚举的函数。

  3. 不正确的边界行为:追索没中边界条件,并编写测试。

  4. 忽视安全

  5. 重复:遵循 DRY(Don't Repeat Yourself)原则。每次看到重复代码,都代表遗漏了抽象。重复的代码可能成为子程序或者干脆是另一个类。

    1. 较隐蔽的形态是在不同模块中不断重复出现、检测同一组条件的 switch/icase 或 if/esle 链,可以用多态替代之。

    2. 更隐蔽的形态是采用类似算法但具体代码行不同的模块。这也是一种重复,可以使用模板方法模式或策略模式来修正。

    3. 多数设计模式都是消除重复的有名手段。考德范式(Codd Normal Forms)是消除数据库规划中的重复的策略。OO 自身也是组织模块和消除重复的策略。结构化编程也是。

  6. 在错误的抽象层级上的代码

    1. 创建分离较高层级一般性概念与较低层级细节概念的抽象模型。所有较低层级概念放在派生类中,所有较高层级概念放在基类中。比如,只与细节有关的常量、变量或工具函数不应该出现在基类中。基类应该对这些东西一无所知。

  7. 基类依赖于派生类

  8. 信息过多

  9. 死代码

  10. 垂直分隔:变量和函数应该在靠近被使用的地方定义。

  11. 前后不一致:不同的地方命名方式相同。

  12. 混淆视听

  13. 人为耦合:人为耦合是指两个没有直接目的之间的模块的耦合。其根源是将变量、常量或函数不恰当地放在临时方便的位置。这种漫不经心的偷懒行为。

  14. 特性依恋:类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。当方法通过某个其他对象的访问器和修改器来操作该对象内部数据,则它就依恋于该对象所属类的范围。

  15. 选择算子参数:即 Boolean 参数

  16. 晦涩的意图

  17. 位置错误的权责

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

  19. 使用解释性变量:解释性变量多比少好。

  20. 函数名称应该表达其行为。

  21. 理解算法

  22. 把逻辑依赖改为物理依赖:如果某个模块依赖于另一个模块,依赖就该是物理上的而不是逻辑上的。

  23. 用多态替代 if/Else 或 Switch/Case

  24. 遵循标准约定

  25. 用命名常量代替魔术数

  26. 准确:在代码中做决定时,确认自己足够准确。明确自己为何要这么做,如果遇到异常情况如何处理。

  27. 结构甚于约定

  28. 封装条件:如果没有 if 或 while 语句的上下文,布尔逻辑就难以理解。应该把解释了条件意图的函数抽离出来。

    1. 例如: if (shouldBeDeleted(timer)) 要好于 if (timer.hasExpired() && !timer.isRecurrent())

  29. 避免否定性条件

  30. 函数只该做一件事

  31. 掩蔽时序耦合:常常有必要使用时序耦合,但你不应该掩蔽它。排序函数参数,好让它们被调用时的次序显而易见。

  32. 别随意

  33. 封装边界条件

  34. 函数应该只在一个抽象层级上

  35. 在较高层级放置可配置数据

  36. 避免传递浏览

5. Java

  1. 通过使用通配符避免过长的导入清单

  2. 不要继承常量:不要用继承常量,应该用静态导入。

  3. 常量 vs 枚举:枚举更好

6. 名称

  1. 采用描述性名称

  2. 名称应该与抽象层级相符

  3. 尽可能使用标准命名法

  4. 无歧义的名称

  5. 为较大作用范围选用较长的名称

  6. 避免编码

  7. 名称应该说明副作用

7. 测试

  1. 测试不足

  2. 使用覆盖率工具

  3. 别略过小测试

  4. 被忽略的测试就是对不确定事务的疑问

  5. 测试边界条件

  6. 全面测试相近的缺陷

  7. 测试失败的模式有启发性

  8. 测试覆盖率的模式有启发性

  9. 测试应该快速

十四. 并发编程II