做一个更好的程序员

一、整洁代码

  • 编程是一件多人合作的事情
  • 随着混乱增加,团队生产力会急剧下降
  • 简单代码规则:
    • 能通过所有测试;
    • 没有重复代码;
    • 体现系统中的全部设计理念;
    • 包括尽量少的实体,比如类、方法、函数等。
  • 时时保持代码整洁,“让营地比你来时更干净。”

二、有意义的命名

  • 选择体现本意的名称,并指明计量对象和计量单位的名称

      # 正例
      int daySinceCreation;
      int elapsedTimeInDays;
    
  • 不使用小写字母l和大写字母O作为变量名,少使用外形相似度高的名称
  • 对于同一作用范围的两样东西应做有意义的区分,不能添加数字系列来命名,也不能通过拼写错误来区分。废话也是没有意义的区分,假如有一个Product类,再有一个ProductInfoProductData类,意思并无区别,这是意义含混的废话。

      # 反例
      getActiveAccount();
      getActiveAccounts();
      getActiveAccountInfo();
      # 程序员怎么知道该调用哪一个函数呢?
    
  • 废话都是冗余。variable一词不应该出现在变量名中,table一词不应该出现在表名中。NameString不会比Name更好,区分名称,应该以读者能鉴别的方式来区分。
  • 使用读得出来的名称,不使用首字母简写,方便讨论时的交流。

      Date genymdhms; // 反例
      Date generationTimestamp; // 正例
    
  • 使用可搜索的名称,对于数字常量来说,应该赋予其便于搜索的名称,即方便查找,也能体现作者的意图。

      # 正例
      final int WORK_DAYS_PER_WEEK = 5;
    
  • 类名和对象名应该是名词或名词短语,例如CustomerWikiPage等,不应该是动词,避免使用ManagerDataInfo这样的类名;方法名应该是动词或动词短语,如postPaymentdeletePage等,属性访问、属性修改和断言应该根据其值命名,并加上前缀getsetis
  • 给每个抽象选一个词,并且一以贯之。例如避免使用fetchretrieveget等在多个类中给同种方法命名,这会让人觉得这几个方法在做不同的事。
  • 添加有意义的语境,对于一个地址信息,将firstNamelastNamestreethouseNumbercitystate等变量放在一起时读者很容易意识到这是个地址,但如果有一个方法中只有一个孤零零的state变量则会造成疑惑,因此可以添加前缀提供语境,例如addrFirstNameaddrLastNameaddrState,更好的解决方法是将其封装为一个Address类。同时也要避免添加无意义的语境,只要短名称足够清楚,就比长名称好,对于邮政地址、MAC地址和Web地址,在类名取名时分为PostalAddressMAC、和URI会更好,这样的名称比在address上加前缀更为精确,而精确正是命名的要点

三、函数

  • 短小,函数的规则就是短小。
  • 只做一件事,判断函数是否做了不只一件事,就是看它是否能再拆出一个函数。做多件事的函数可能带来副作用,造成意想不到的结果。
  • 每个函数中的语句应处于同一抽象层级上,不能将表达基础概念和细节混杂。
  • 令函数参数尽可能的少,便于理解,也便于测试。
  • 如果函数需要多个参数,应考虑将其封装为类。
  • 避免使用输出参数

      public void appendFooter(StringBuffer report) // 反例
    
      report.appendFooter(); // 正例
    
  • 使用异常替代返回错误码
  • try/catch的主体部分抽离出来形成函数,错误处理也是一件独立的事。
  • 重复的代码应该抽象为函数集中到基类中
  • 写出完善的代码不是一开始就按照规则写的,应该是先想什么就写什么,再慢慢打磨推敲

四、注释

  • “别给糟糕的代码加注释——重新写吧。”
  • 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样的多。
  • 必要信息的注释,如与法律有关(版权、著作权声明)的注释。
  • TODO注释,可以提醒自己应该做但因某些原因还没做的事情,但要记得定期查看删除不再需要的TODO注释。
  • 公共API应编写良好的Javadoc。
  • 不写废话的注释。
  • 不保留注释掉的代码,这会给读者带来疑问。

五、格式

  • 垂直方向上的间隔,package声明、import声明、每个函数之间都要用空白行隔开。
  • 实体变量应放在类的顶部。
  • 相关函数应放在一起,若某个函数调用了类中另一个函数,则应将被调用函数放在调用函数的下方,这样可以增强整个模块的可读性。
  • 空格、缩进的使用,应符合团队规范。

六、对象和数据结构

  • 封装、抽象是为了隐藏实现,只暴露抽象接口供外界操作,不能一味的给属性设置gettersetter,这实际上暴露了实现细节,具体应结合实际场景考虑。
  • 过程式代码便于在不改动既有数据结构的前提下添加新函数;面向对象代码便于在不改动既有函数的前提下添加新类。
  • 得墨忒耳律(The Law of Demeter)认为,类C的方法f只应该调用以下对象的方法:
    • C;
    • 由f创建的对象;
    • 作为参数传递给f的对象;
    • 由C的实体变量持有的对象
  • 最为精炼的数据结构是一个只有公共变量、没有函数的类,这种数据结构被称为DTO(Data Transfer Objects),这是一种非常有用的数据结构,常用于应用各层间的信息传递,一般DTO继承Entity类,补充传输过程需要的字段(常用于与数据库通信)。

七、错误处理

  • 创建信息充分的错误消息,并和异常一起传递出去,记录在日志中。
  • 慎重返回null值,这会增加工作量,代码中需要到处充斥着判断对象是否为null的语句,少用返回值为null的函数可以使代码更整洁。
  • 不要传递null值给其他方法。

八、边界

  • 对于引入的第三方代码,可以通过编写测试来理解第三方代码(Apache log4j),这称为学习性测试。当第三方程序包进行修改升级时,原有的学习性测试也能运行查看程序包的行为有没有发生变化。
  • 对于别的团队相关接口未定义时,可以定义一个自己使用的接口,当正式接口定义出来时通过Adapter来跨接,它封装了与API的互动,也提供了一个当API发生变动时唯一需要改动的地方。(没有接触过,还不太懂)

九、单元测试

  • TDD三定律
    • 在编写不能通过的单元测试前,不可编写生产代码。
    • 只可编写刚好无法通过的单元测试,不能编译也算不通过。
    • 只可编写刚好足以通过当前失败测试的生产代码。

    TDD(Test-Driven Development),测试驱动开发,比较激进。

  • 保持测试代码的整洁,测试代码和生产代码一样重要,单元测试的整洁和完善会帮助代码可扩展、可维护、可复用。
  • F.I.R.S.T.准则
    • 快速(Fast),测试代码应该能快速运行。
    • 独立(Independent),测试应该相互独立,每个测试应该能独立运行。
    • 可重复(Repeatable),测试应当在任何环境可以重复通过,生产环境、质检环境甚至是无网络的环境下都能正常运行。
    • 自足验证(Self-Validating),测试应该有布尔值输出,无论成功失败都不应该通过查看日志文件来确认测试是否通过。
    • 及时(Timely),测试应该及时编写,单元测试应该在生产代码编写前写好(或构思好),如果在编写生产代码后编写测试,可能因为某些生产代码本身难以测试而不去设计可测试的代码。

十、类

  • 类的组织应遵循标准的Java约定,类从一组变量列表开始,公共静态变量最先出现,其次是私有静态变量,最后是私有成员变量,一般不会有公有成员变量。
  • 设计类的首要规则就是短小,衡量类的大小可以用其权责来计算;单一权责原则(Single Resonsibility Principle,SRP)认为,类或模块应该有且只有一条加以修改的理由,也即类只应有一个权责。 达到一定规模的系统会包括大量逻辑和复杂性,管理复杂性的首要目标就是加以组织,系统应该由许多短小的类而不是少量巨大的类组成
  • 提高类的内聚性,让类的方法和变量互相依赖、互相结合成一个逻辑整体,即让类的每个成员变量尽可能多的被类的方法所使用。
  • 通过接口和抽象类隔离细节,类应当依赖抽象而不是依赖细节,隔离使得部件之间解耦,编写测试也会更加方便。依赖倒置原则(Dependency Inversion Principle,DIP)认为,细节应当依赖于抽象,抽象不应该依赖于细节,应该面向接口编程而不是面向实现编程,依赖不稳定的具体类会影响高层模块的使用。

十一、系统

  • “软件系统应将起使过程和起使过程之后的运行时逻辑分离开,在起使过程中构建应用对象,也会存在互相缠结的依赖关系。”
  • 当调用者调用被调用者时,如果采用硬编码方式初始化被调用者会导致调用者和被调用者耦合性增加,不利于测试和后期项目的修改和维护。依赖注入(Dependency Injection,DI) 是一种实现分离构造与使用的强大机制。控制反转(Inversion of Control,IoC) 是依赖管理的一种应用手段。Spring 框架提供了最有名的 IoC 容器,用户通过XML文件或注解的方式就可以定义互相关联、依赖的对象,对象的初始化交由容器完成,开发者只需要关注业务逻辑实现即可。
  • “一开始就做对系统”纯属神话,我们应该只去实现当前的业务需求,然后重构,慢慢扩展系统,这就是迭代和增量敏捷的精髓所在。
  • 面向切面编程(Aspect-Oriented Programming,AOP),AOP可以对业务逻辑的各个部分进行分离,降低耦合性,例如将业务逻辑的实现和日志的记录分离开。

十二、迭进

可以通过遵循以下四条规则,使设计变得简单:

  • 运行所有测试。只要系统可测试,就会导向遵循SRP的设计方案,这能够保持类短小且功能单一。另一方面,编写的测试越多,就会越遵循DIP规则,这是因为低耦合的代码会使得编写测试代码更加简单,使得我们会使用依赖注入、接口、抽象等工具去减少耦合。
  • 不可重复。重复代表着额外的工作、额外的风险和不必要的复杂度,要想创建整洁的系统,需要有消除重复的意愿,即使对于短短几行代码也是如此。对于目的不同的两个函数,应考虑对其做共性抽取,将重复代码构建为新方法。如果违反了SRP原则,则还应将新方法分解到另外的类中,这提高了该方法的可见性,有可能团队中其他成员在其他场景中可以复用该方法。将重复的操作放在父类,子类通过重写父类方法并先调用父类方法再进行子类独有的操作,是在项目中最常见的消除重复的手段。
  • 表达程序员的意图。写出自己能理解的代码很容易,但一个软件项目主要成本在于长期的维护,其他维护者可能很难理解或容易误解代码是做什么的,这要求开发者写代码时应清晰的表达自己的意图,这包括添加清晰的注释、选用好名称命名类名和函数名、保持类和函数的短小、采用标准命名法命名采用了某些设计模式的类、编写良好的单元测试等手段。
  • 尽可能少的类和方法。目标是在保持函数和类短小的情况下,保持整个系统的短小精悍,但这条规则是优先级最低的一条,尽管很重要,但测试、消除重复、表达力是更重要的事情。

上述四条规则优先级依次从高到低,其中规则2~规则4实现的方法就是递增式地重构代码,这依靠着规则1,只有足够的测试才能保证在重构代码时不会破坏任何东西,测试消除了重构代码就会破坏原有功能的恐惧。