0%

代码整洁之道——代码编写原则

整洁代码的意义

随着脏代码增加,团队生产力持续下降趋向于零。当生产力下降时,管理层就只有增加更多忍受到项目中,期望提升生产力。可是新人并不熟悉系统的设计,搞不清楚什么样的修改符合设计意图,什么样的修改违背设计意图。于是他们制造更多的混乱,生产力向零的那端不断下降。
整洁代码意义

命名原则

  1. 核心原则:采用有意义的命名,使变量名具有自解释性。
  2. 使用有意义命名
    • 使用有意义的单词作为变量名,避免使用A、b、c、D这种表意不明的名称。
    • 使用相关业务领域方案中的名称进行命名。
    • 即使是循环体中的i(for循环中的下标变量),如果涉及到的范围大,循环体比较大,也应该给i一个更有意义的命名。
    • 使用意义明确的命名,如方法genymdhms(生成年月日时分秒)实际希望表达的意义是生成时间戳,可以使用genTimestamp代替。
    • 使用正式的书面用语。
    • 使用可搜索的名称,比如用到常量5的地方,如果希望表达一周有五天这个含义,可以抽取一个常量WORK_DAYS_PER_WEEK=5。
    • 可以添加有意义的语境。
    • 减少无意义的词汇:比如NameString,使用Name作为变量名即可。
  3. 避免使用有误导意义的名称
    • 不要使用专有名词,如UNIX平台中的hp、aix、sco是有特指意义的。
    • 不要用有特殊意义的词汇,比如只有当变量本身是一个List时候才能用List做结尾,如accountList(参照IDEA的自动命名,只有其本身是List时会加List后缀)。
    • 业务领域名词前后使用一致,如”榜单”,如果使用rank表意榜单,那么整个项目中都应该使用rank,不要同时使用rank、board等类似意义的单词。
    • 不要使用具有双关意义的命名。
    • 注意区分以下字符:
      • 小写字母l和数字1
      • 大写字母O和数字0
    • 不同用途尽量体现差异,如不要使用getActiveAccount()getActiveAccountInfo()getActiveAccounts()作为不同场景下获取账号的函数命名,而应该在函数名中明确区分这些场景。
  4. 避免编码命名方式(多是C语言对名称长度有限制时留下的习惯)
    • 避免匈牙利语标记法,即避免使用标记变量类型、用途的前后缀。
    • 避免成员前缀,如m_等。
    • 接口和实现,接口不需要加前缀,如IXxFactory可以直接命名为xXFactory。
  5. 单词使用
    • 类名应使用名次或名词短语。
    • 方法名应使用动词或动词短语。

注释原则

首先需要明确一点:注释用于补充解释无法自解释的的代码,如果代码能做到自解释是无需注释的。注释并不能美化糟糕的代码,我们应该尽量用代码来阐述其本身的意义。

  • 好的注释:
    1. 法律信息,如©copyright
    2. 提供信息的注释
      • 解释某个抽象方法的返回值,把一些难懂的参数返回值注释成可读的形式
      • 解释某段代码(如for循环)的用途
      • 警示:提示某些代码的重要性或需要特别注意的点,提示方法某些看起来不合理地方的重要性。
      • TODO:提示代码中遗留的问题。TODO注释应解释本次为什么无法实现,将来应该是怎样。同时需要定期查看,删除不再需要的TODO。
  • 坏的注释
    1. 多余的注释
      • 没有意义,比如只有作者自己能懂的注释、单纯为了标记代码中某个位置的注释、注释掉的代码。
      • 不能比代码提供更多的信息的注释,如无关的细节描述、用来描述函数功能的注释(描述函数功能可以为函数起一个好名字)。
      • 不如代码精确,反而可能提供错误的信息的注释。
      • 括号后面的注释。这种注释在嵌套层数深的长函数会有意义,但这种长函数应该拆分成更易读的小函数。
      • 不要写系统级信息(如http使用8080端口)
    2. 误导性注释
      • 功能性的误导,如果读者更信赖注释,读了注释之后不再读代码,反而会得到错误的信息。
      • 对象指代误导,注释里对象指代要清楚。
      • 对应关系误导,注释只描述离它最近的代码
    3. 循规式注释
      • 要求每个函数都有Javadoc一样的说明性注释是没必要的。尤其是非公共代码中的Javadoc,Javadoc对公共API非常有用,但是不打算公开对代码没必要写这种注释。
      • 记录每次代码修改日志的注释是没必要的
      • 代码归属与署名,首先代码的编写者可以通过git看到。同时随着代码被修改,署名会越来越不准确。

代码格式

团队代码规则永远高于个人喜好,下边会介绍一些比较通用的格式规则。

  • 垂直格式
    1. 整体规划
      • 单个文件应在200行左右,最长不超过500行
      • 最顶部给出高层次概念和算法,细节往下逐次展开。
      • 整体名称简单一目了然, 由许多篇文章组成,多数短小精悍
      • 实体变量应该在类的顶部声明
      • 变量声明应尽可能靠近其使用位置
      • 本地变量应该在函数的顶部出现
    2. 垂直距离上规划
      • 概念相关的代码应该放在一起,相关性越强距离越短。
      • 调用者尽可能放在被调用者上边(python无法实现)。
      • 同一函数中也可以有垂直方向上的区隔,每组代码行展示一条完整的思路,这些代码组之间用空白行隔开。
  • 横向格式
    1. 整体规划
      • 单行代码尽量小于80个字符,不要超过120个字符
    2. 水平距离规划(自己定义的水平分隔基本用不上,因为代码格式化时候就都消失了(比如idea里command + option + L))
      • 赋值操作左右添加空格
      • 函数名和左括号之间不加空格
      • 参数间一一隔开
      • 缩进:通过缩进模式,看代码行左侧就可以知道自己在什么范围中工作,从而快速跳过与当前情形无关的范围。即使是小的if、while循环或小函数也不要违反缩进规则写在一行。
    3. 禁止用空范围代码
      • while(blabla);
      • for(blabla);

对象和数据结构

  1. 对象和数据结构概念对比
    • 对象把数据隐藏于抽象之后,暴露操作数据的函数。
    • 数据结构暴露其数据,不提供有意义的函数。
    • 函数式编程便于在不改动既有数据结构的前提下增加新函数;面向对象编程便于在不改动既有函数的前提下添加新类。
    • 函数式编程难以添加新数据结构,因为必须修改所有函数;面向对象编程难以添加新函数,因为必须修改改所有类。
  2. 数据抽象
    • 抽象层的作用是屏蔽用户无需感知的实现逻辑,使用户可以直接操作相关数据。
    • 隐藏实现并非只是在变量之间放上一个函数层,单纯的增加getter、setter是最坏的方式。
  3. 得墨忒耳定律:模块不应了解他所操作对象的内部情形,主要有以下三条原则:
    • 火车失事:一连串的调用,看起来就像是一列火车,应该避免。如final String outputDir = ctxt.getOptions().getScratchir().getAbsolutePath();,这种问题一般源于过深的对象层级,可以将获取每一层级的对象拆分出来。
    • 混杂:访问对象属性,有执行函数(封装了部分逻辑的数据访问函数),有公共变量,也有公共变量的getter和setter。
    • 隐藏结构:应该将访问底层数据结构封装到执行函数中,从而隐藏内部结构,防止当前函数浏览他不需要知道的对象。
  4. 数据传输对象——DTO(Data Transfer Object)
    • 只有公共变量,没有函数的类,多用于与数据库或套接字传输消息场景。
    • IMPORTANT: 不要向DTO中添加业务逻辑。

函数原则

  1. 功能设计
    • 函数应该只做一件事
      • 函数要么做一件事,要么回答一个问题,也就是说函数应该修改某对象的状态,或者是返回对象有关的信息,两者不应该混在一起。
    • 函数应该只涉及自己抽象层级的业务逻辑,便于自顶向下读代码的阅读习惯
      - 以返回HTML页面为例,每子自函数组装一个模块,最外层函数组装整个页面。
    • 结构化编程,每个函数应该有一个入口一个出口
      • 这个规则对于短小的函数帮助不大,但对于大函数应该严格遵守。
  2. 编写原则
    • 减少重复,相同功能的代码抽成函数进行统一维护。
    • 函数应该尽量短小,不要超过20行,每行不超过150个字符。
      • switch语句应该隐藏在factory模式中。
    • 代码块和缩进,函数的缩进层尽量在两层以内,更易于阅读和理解。
    • 减少函数入参
      • 最理想的是没有参数的函数,其次是一个参数,再次是两个参数,应该尽量避免三个参数的函数。有足够特殊的理由才能使用三个以上的参数。
      • 超过三个参数说明其中一些参数需要封装成类比如圆心的横纵坐标应该抽象为一个Point(点)类。
  3. 命名原则
    • 使用描述性函数名,增加相关功能或相关语境,尽量准确的表明函数功能。
    • 函数中某些功能只有特定情况下才会用到,应该在函数名中有所体现,从而保证函数名和功能完全一致,避免在错误的地方使用函数造成副作用。
  4. 异常处理方式
    • 使用抛出异常的方式处理,不要使用返回错误码的方式。
    • 抽离try-catch代码块,try-catch处理异常作为单独的功能应该抽离成一个函数,主函数中不应有处理异常流程。

类的原则

  1. 短小
    • 我们通过计算权责(提供的函数功能)来计算一个类的大小,构造一个短小的类应当满足单一权责原则,即类或模块应该有且只有一条加以修改的理由。
    • 系统应由许多短小的类组成而不是几个功能复杂的类。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
  2. 高内聚性
    • 类的名称应该描述其权责,大概25个单词,如果无法精确命名说明类的权责过多
    • 类应该只有少量实体变量
    • 保证类中函数的短小及参数列表短小
    • 保持内聚性就会得到许多短小的类,重构方式如下:
      1. 一个拥有许多变量的大函数
      2. 拆分成单独的函数,其中使用的共同的变量,需要在多个函数中分别声明
      3. 将这些变量提升为类的实体变量
      4. 类丧失了内聚性,越来越多的实体变量只为其中几个函数使用
      5. 拆分类
  3. 面向修改
    多数系统在构建过程中会不断修改,每一处修改都让我们冒着系统其它部分不能正常工作的风险。在整洁的系统中,我们对类加以阻止以降低修改的风险。
    • 每个类都只负责一种权责,将每个类中的代码都变得极为简单,降低理解成本,同时降低函数对其他函数造成的破坏风险。
    • 隔离修改
  4. 类的组织方式,自顶向下结构如下:
    • 变量列表
      • 公共静态常量
      • 私有静态变量
      • 私有实体变量
      • 公共变量(应该很少)
    • 公共函数
      • 被某个公共函数调用的私有函数应该紧随气候

异常处理

  1. 代码编写

    • 异常处理是单独的一件事,不应混杂在业务代码中。
    • 先写try-catch-finally语句块,后续可以用测试驱动方法构建剩余的代码逻辑。
    • null值
      • 不要返回null值,返回null值会大大增加其他代码的判断逻辑,可以返回空集合or空对象
      • 不要传递null值,除非API要求传递null值,否则应该尽可能避免。
    • 封装第三方代码库,减少在代码各个位置对第三方库抛出异常的捕获,从而减小耦合度,便于切换到其他库。
  2. 处理方式
    • 定义异常处理流程
      • 常规异常应该在各个需要的地方抛出,在代码顶端定义一个处理一统一处理。
      • 需要业务处理的异常可以封装成特例特例对象进行返回,在业务代码中处理,从而减少上层逻辑对异常的判断负担。
    • 给出异常发生的环境说明,抛出的每个异常都应该提供足够的环境说明,以便判断异常的来源和处所。stack track可以得出异常的来源,同时应该打印message,表明出现异常的原因。
    • 使用不可控异常(运行时异常)。Java中的可控异常并非高稳定性软件所必须,同时它破坏了开闭原则,如果底层新抛出一种异常类型,则整个函数调用路径中都要增加向上一层抛出这个异常。
    • 使用异常处理方式,不要使用异常返回码的方式。

并发编程

对象是过程的抽象,线程是调度的抽象。并发是一种解耦策略,他把我们做什么(目的)和何时做(时机)分开

  1. 防御原则
    • 单一权责原则。分离并发相关代码与其他代码。
    • 限制数据作用域。缩小synchronized保护使用共享对象的临界区,synchronized保护临界区越大成本越高。
    • 使用数据复本。每个线程使用独立的副本,避免共享数据,从而避免多线程间交互问题。
    • 线程应尽可能的独立。尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集,减少线程见交互。
  2. 代码编写
    • 相关Java库,java.util.concurrent。
    • 减少同步方法之间的依赖,避免使用一个共享对象的多个方法。
    • 保持同步区域微小,锁会带来延迟和额外开销。
    • 尽早考虑线程关闭问题,死锁等问题会比想象中难得多。
    • 使用多余处理器数量的线程,同时使用可调整的线程数量代码
  3. 相关测试
    • 编写涉及到多线程模块的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误,不要因为后来测试通过忽略之前的失败,这样的伪失败可能是线程问题。
    • 先使非线程代码可工作