《代码整洁之道》

  1. 有意义的命名
  2. 函数
  3. 注释
  4. 格式
  5. 对象和数据结构
  6. 错误处理
  7. 边界
  8. 单元测试
  9. 类
  10. 系统
  11. 迭进
  12. 并发编程
  13. 味道与启发
    1. 注释
    2. 环境
    3. 函数
    4. 一般性问题
  14. Java
  15. 名称
  16. 测试

如何写出整洁代码是一名优秀程序员的必修课。

整洁的代码有助于项目提高迭代质量,减轻历史包袱;同时也能让别人更易看懂代码,促进团队协作分工。

《代码整洁之道》一书里边有不少观点值得采纳。本文摘录书本主要内容,以备后忘。详细建议阅读原著。

有意义的命名

  • 命名应当名副其实,能准确体现对象的含义;
  • 避免误导性命名。比如:不要使用”l”、”1” 以及其他专有名称;
  • 命名应该做有意义的区分。比如:ProductInfo 、ProductData和Product没区别,moneyAmount和money没区别;
  • 使用可以读出来的命名。比如:使用generationTimestamp,而不是genymdhms;
  • 使用可以搜索的命名。比如:”e” 或者魔法数在代码中难以搜索;
  • 避免使用编码(包括匈牙利标记法、成员前缀或接口实现标记等等)。比如:IShapeFactory无论是作为子类或者其实现,其首字母的标记”I”都是累赘的,应该使用ShapeFactory;
  • 类名或者对象名应该是名词,如:Customer、WikiPage、Account、AddressParser。避免使用含义不确切的名词:Manager、Processor、Data、Info等,更不应使用动词;
  • 方法名应该是动词或者动词短语;
  • 添加有意义的语境。比如:firstName、secondName、street、houseNumber等零散字段,可以增加前缀”addr-“作为语境,明确暗示它们构成一个地址;更好的办法是封装为Address类;
  • 不要添加无用的语境。比如开发一个名为GSD的系统,不应该给所有的类都加上GSD前缀;

函数

  • 函数第一规则:短小
  • 函数的缩进层次要控制的两层之内;
  • 函数应该做一件事,做好这件事,只做一件事;
  • 使用抽象工厂类隐藏switch语句;
  • 使用描述性命名,长而具有描述性的命名,要比短而令人费解的命名要好;
  • 函数参数越少越好,当参数数量需要两个、三个或三个以上的时候,就说明其中一些参数可能应该封装为类;
  • 标志参数丑陋不堪,不应该向函数传入布尔值,而是拆分为两个方法。比如:setStatus(Boolean start) 应该拆分为 start() 和 stop();
  • 分隔询问和指令。比如if (set(“username”, “unclebob”)… 应该拆分为:
        if (attributeExists("username") {
            setAttribute("username", "unclebob");`
            ...
        }
    
  • 使用异常替代返回错误码;
  • try/catch代码块会搞乱代码结构,应该将try和catch代码块的主体部分抽离出来形成独立函数。比如:
        try {
            deletePageAndAllReferences(page);
        } catch (Exception e) {
            logError(e);
        }
    
  • 避免重复的代码;

注释

  • 恰当的注释是用来弥补我们用代码表达意图时所遭遇的失败。如果发现自己需要写注释,先复盘下是否可以通过代码表达而不是注释;

  • 注释不能美化糟糕的代码。与其花时间编写解释糟糕代码的注释,还不如花时间清理下糟糕的代码;

  • 通过良好的代码来阐述逻辑。比如:

    • 良好代码:
        if (employee.isEligibleForFullBenefits())
    
    • 而不要采用糟糕注释:
        // check to see if the employee is eligible for full benefits
        if ((employee.flags & HOURLY_FLAG) && employee.age > 65))
    
  • 好注释

    • 法律信息
    • 提供信息的注释
    • 对意图的解释
    • 阐释
    • 警示
    • TODO注释
    • 放大
    • 公共API的Javadoc
  • 坏注释

    • 喃喃自语
    • 多余的注释
    • 误导性注释
    • 循规式注释
    • 日志式注释
    • 废话式注释
    • 注释掉的代码
    • HTML注释
    • 非公共API的Javadoc

格式

  • 单个文件控制在恰当的长度(比如200~500行);
  • 源文件应该像报纸一样。名称应该简单且一目了然。名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。
  • 源文件内部在垂直方向上,不同概念之间应该有间隔。比如:import块、import static块、变量定义、方法区等等,它们之间应该有空行间隔;
  • 紧密相关的代码应该相互靠近。比如:
    • 不必在变量之间插入无用的注释;
    • 变量声明应该尽可能靠近其实用的位置;
    • 循环中的控制变量应该尽量在循环语句中声明;
    • 被调用的函数应该在调用函数的下方;
    • 概念相关的代码应该放在一起;
  • 每行代码长度控制在120个字符以内;
  • 代码行中恰当使用空格。比如:逗号后加一个空格;
  • 统一的缩进风格;

对象和数据结构

  • 数据抽象以避免曝露数据细节,以抽象形态表述数据;
  • 过程式代码便于在不改动既有数据机构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类;
  • 反之亦然,过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类;

错误处理

  • 使用异常而非返回码;
  • 不要在方法中返回null值,而是抛出异常或返回特例对象;
  • 不要向方法传递null值;

边界

单元测试

  • 不要遵循TDD三定律(所谓的编写生产代码前先编写单元测试);
  • 保持测试代码的整洁,抽象隐藏测试准备细节,突出测试逻辑;
  • 单元测试中优先考虑简介,再考虑执行效率;
  • 每个测试有且只有一个断言;

类

  • 类应该保持短小;
  • 类应该遵守单一权责原则(SRP);
  • 类内部应该保持高内聚(保持每个方法都操作内部的一个或者多个变量);

系统

  • 系统的构造和使用分开的办法:

    • 分离构造过程,创建系统所需的对象,并传递给应用程序,应用程序只管使用;
    • 在应用程序中使用抽象工厂方法来隐藏构建细节;
    • 使用依赖注入来分离构造和使用;
  • Java AOP三种机制:

    • Java代理,常用的字节码操作库有:CGLIB、ASM、Javassist;
    • 纯Java AOP框架;
    • AspectJ;

迭进

  • 运行所有测试;
  • 不可重复;
  • 调整代码,确保具有良好的表达力;
  • 尽可能少的类和方法;

并发编程

  • 对象是过程的抽象,线程是调度的抽象;
  • 并发防御性原则
    • 单一权责原则;
    • 限制数据作用域;
    • 使用数据副本;
    • 线程应尽可能独立;
  • 了解Java并发安全库;
  • 了解并发执行模型;
  • 保持同步区域微小;

味道与启发

注释

  • 不恰当的信息
  • 废弃的注释
  • 冗余的注释
  • 糟糕的注释
  • 注释掉的代码

环境

  • 需要多步才能实现的构建
  • 需要多步才能做到的测试

函数

  • 过多的参数
  • 输出参数
  • 标志参数(布尔值参数)
  • 不被调用的死函数

一般性问题

  • 一个源文件存在多种语言
  • 明显的行为未被实现
  • 不正确的边界行为
  • 忽视安全(防御性编程)
  • 重复代码
  • 在错误的抽象层级上的代码
  • 基类依赖于派生类
  • 信息过多(类中方法过多、函数变量过多等等)
  • 不执行的代码
  • 垂直距离过大(变量、函数应该在靠近使用的地方定义)
  • 前后不一致(不同地方的相似概念应该用相似一致的命名,如方法名processVerificationRequest和processDeletionRequest)
  • 混淆视听的代码(如:没有实现的默认构造函数、没用的变量、无调用的函数、无信息量的注释)
  • 人为耦合。(不相互依赖的东西不应该耦合)
  • 特性依恋。(类的方法只应对所属类中的变量和函数感兴趣,不应该垂青其他类中的变量和函数)
  • 选择算子参数。选择算子可能是boolean、枚举、整数等形式,通常可以拆分为多个小函数
  • 晦涩的意图,包括联排表达式、匈牙利语标记法、魔术数等
  • 位置错误的权责
  • 不恰当的静态方法。如果的确需要静态方法,那么请确保没机会打算让它有多态行为
  • 使用解释性变量
  • 函数名称应该表达其行为
  • 理解算法
  • 把逻辑依赖改为物理依赖。(比如某些写死的静态变量,改为从实体的get方法获取)
  • 用多态替代If/Else或Switch/Case
  • 团队成员应遵循标准约定
  • 用命名常量替代魔术数
  • 封装布尔条件
  • 避免否定性布尔条件
  • 函数只做一件事
  • 掩蔽时序耦合
  • 封装边界条件(比如level+1、level-1)
  • 函数应该只在一个抽象层级
  • 在较高层级放置可配置数据

Java

  • 通过使用通配符避免过长的导入清单(import)
  • 不要继承常量
  • 优先使用枚举而不是常量

名称

  • 采用描述性名称
  • 名称应该与抽象层级相符
  • 尽可能使用标准命名法
  • 无歧义的名称
  • 作用范围较大的名称选用长名称(范围小的可以用短名称,比如循环变量i)
  • 避免编码式命名(比如匈牙利语标记法等)
  • 名称应该说明副作用(比如createOrReturnOos优于getOos)

测试

  • 测试不足
  • 使用覆盖率工具包
  • 别略过小测试
  • 被忽略的测试就是对不确定事物的疑问
  • 测试边界条件
  • 全面测试相近的缺陷
  • 测试失败的模式有启发性
  • 测试覆盖率的模式有启发性
  • 测试应该快速

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 duval1024@gmail.com

赏

×

喜欢就点赞,疼爱就打赏