Skip to content

Latest commit

 

History

History
275 lines (171 loc) · 8.42 KB

代码的坏味道.md

File metadata and controls

275 lines (171 loc) · 8.42 KB

​Bad smell

靠代码行数来衡量开发进度,就像是凭重量来衡量飞机制造的进度。——比尔·盖茨

1. 可变的数据

?> 形态

  • 暴露的细节
  • 可变的数据
  • 全局数据

?> 原则

  • 限制变化,谨慎生成setter方法,对于一些用于序列化传输的对象,可以使用setter,但是其他场景下的对象则需要严格限制可变的数据。
  • 尽可能编写不可变类
  • 区分类的本质,实体对象要限制数据变化,而值对象就要设计成不变类
  • 使用函数式编程

?> 重构

移除设值函数

2. 滥用控制语句

?> 形态

  • 嵌套的代码
  • else语句
  • 重复的swtich
  • 循环的语句

?> 原则

  • 嵌套的代码首先要对代码进行关注点分离(可以参考以下函数模块的长函数),使用卫语句取代嵌套的条件表达式,遵循函数至多有一层缩进规则
  • 不要使用else关键字,在if模块尽早return
  • 使用多态或者策略模式来取代条件表达式,如消灭重复的swtich
  • 遵循开闭原则

3. 缺少精准的命名

?> 形态

我以前也写过比较多缺少精准命名的代码,而且还特别喜欢以下两类:

public void processDepartment(List<Department> departments) {
}

public void payHandler(PayReuqest payRequest) {
}

processDepartment函数的主要目的是与钉钉的部门信息保持同步,payHandler主要是负责订单支付,以前看这两个函数,我觉得还是挺合适的,但是过了一段时间,回头看这两个函数,却不能单从函数名看出这个函数的意图是什么,反而必须要通过阅读代码,才能知晓。因此,以上的两个函数的命名太宽泛了。

这是一类典型的命名问题,从表面上看,名字是有含义的,但实际上,它并不能有效地反映这段代码的含义。从沟通的角度来讲,这不是一个有效的沟通,而需要耗费大量的时间和精力,才能理解它。

?> 原则

当有这些词出现在代码里,应该要改用更精准的词去替代。

宽泛的词:datainfoflagprocesshandlebuildmaintainmanagemodify

?> 重构

根据以下原则进行命名改善:

  • 命名要能够描述这段代码在做的事情
  • 一个好的名字应该描述意图,而非细节

根据以上两个原则,我们将以上函数命名修改为:

public void syncDepartment(List<Department> departments) {
}

public void pay(PayReuqest payRequest) {
}

syncDepartment表示同步部门信息,那么该函数做的事情就是将部门信息同步到本地。pay表示支付,那么该函数主要就是负责支付功能。

4. 用技术用语命名

?> 形态

用技术用语命名,是很多程序员的习惯,这种代码到处可见,同时,大部分人都不认为这是一个问题。上代码:

List<Integer> userIdList = new LinkedList<>();

以上的变量之所以命名为userIdList,那是因为使用了List集合类,类比,还有xxxMap,xxxSet等。如果后面userIdList的数据类型修改为Map,然后没有修改变量,那么这个变量的命名就是毫无相关了。这类命名都是基于实现的细节来命名。

?> 原则

  • 在一个技术类的项目中,这些技术语其实就是它的业务语言。但对于业务项目,这个说法就必须重新审视了。
  • 命名中出现技术用语,往往它是缺少了一个模型。

?> 重构

修改这类命名,我们需要遵循面向接口编程,因为接口是稳定的,实现是可变的。上代码:

List<Integer> userIds = new LinkedList<>();

5. 乱用英语

?> 形态

  • 违反语法规则

类型、函数名不满足语法规则,一般来说,类名是一个名词,表示一个对象,而方法名则是一个动词,或者是动宾短语,表示一个动作。

  • 不准确的英语词汇

使用不准确的英语来命名,在这种情况下,最好的解决方案还是建立一个业务词汇表,千万不要臆想。

?> 原则

  • 编写符合英语语法规则的代码
  • 通过代码审查发现问题

6. 重复代码

?> 形态

出现重复代码,不外乎以下几种原因:

  • 复制粘贴代码
  • 结构重复的代码
  • ifelse代码块中的语句高度类似

?> 原则

  • 每一处知识都必须有单一、明确、权威地表述
  • 不要被动词上的差异迷惑,结构重复,也是重复代码

7. 长函数

?> 形态

在一些代码检查插件上,超过80行的代码则认为是长函数。总之,对函数长度容忍度高,是导致长函数产生的关键点。

长函数产生的主要原因,如下:

  • 以性能为由:减少调用函数进出栈的性能损耗
  • 平铺直叙:主要是把多个业务处理流程放在一个函数实现或者把不同层面的细节放到一个函数里实现
  • 一次加一点:每次都往函数里加一些代码,这种无意识的累积,虽然每个人都有正当的理由,但是最终的结果很糟糕

?> 原则

  • 定义好函数长度的标准
  • 做好“分离关注点”
  • 坚守“让营地比你来时更干净”(童子军军规)规则

?> 重构

  • 分离关注点,提取函数
  • 把函数写短,越短越好

8. 长参数列表

?> 原则

消除长参数:

  • 参数数量多导致的长参数
    • 变化频率相同,则封装成一个类
    • 变化频率不同:
      • 静态不变的,可以成为软件结构的一部分(如,成为类的成员变量)
      • 多个变化频率的,可以封装成几个类
  • 标记参数导致的长函数
    • 根据标记参数,将函数拆分成多个函数

?> 重构

  • 将参数列表封装成对象
  • 移除标记参数
  • 减少参数列表,越小越好

9. 缺乏封装

?> 形态

  • 过长的消息链
  • 基本类型偏执,这种设计往往缺少了一个模型

?> 原则

  • 遵循迪米特法则
  • 封装所有的基本类型和字符串
  • 使用一流的集合:任何包含集合的类中,不应包含其他成员变量。这样集合的各种行为就有了明确的依附物,这些行为包含各种过滤器,针对每个元素的特殊规则、多个集合的处理(拼接、交集等等)

?> 重构

  • 使用隐藏委托关系解决过长的消息链
  • 以对象取代基本类型
  • 构建模型,封装散落的代码

10. 大类

?> 形态

  • 职责不单一
  • 字段未分组

?> 原则

  • 遵循单一职责原则,将大类拆分为小类,本质上是在做设计工作
  • 将可变的同一种性质的字段进行分组。
  • 极致的追求:每个类不超过2个字段
  • 把类写好,越小越好

11. 变量的声明与赋值分离

?> 原则

变量要一次性完成初始化,对于集合的声明和赋值,可以使用guava提供的库。

  • 在声明前面加上final,用不可变的限制约束代码
  • 用声明式的方式进行集合的初始化,guava库是个不错的选择
  • 一次性完成变量的初始化

12. 依赖混乱

?> 形态

在重构大类或者设计模型时,为了避免同时面对所有的细节,需要对类进行拆分,分解成一个又一个的小模块,但随之而来的问题就是,需要把这些拆分出来的模块按照一定的规则重新组装在一起,这就是依赖的缘起。

坏味道呈现形态:

  • 缺少防腐层,业务与外部接口耦合
  • 业务代码中出现具体的实现类

?> 原则

  • 引入防腐层,将业务与内部接口隔离
  • 引入模型,将业务与具体实现隔离
  • 遵循依赖倒置原则
    • 高层模块不应依赖于底层模块,二者都应依赖于抽象
    • 抽象不应依赖于细节,细节应依赖于抽象
  • 代码应该向着稳定的方向依赖

13. 不一致代码

?> 形态

一个团队,代码保持一致性是非常重要的一件事,因为不一致会造成认知上的负担。

坏味道呈现形态:

  • 命名中的不一致
  • 方案中的不一致
  • 代码中的不一致

?> 原则

  • 团队统一编码方案
  • 提取函数,将不同层次的内容放入不同函数中,这样也可以避免单元测试要测试私有方法的情况。
  • 保持代码在各个层面的一致性

14. 落后的代码风格

?> 要点

  • 引入Optional可以减少由于程序员的忽略而引发空指针异常
  • 学习Java8的函数式编程,基础要懂map、filter、reduce这些操作

?> 原则

  • 声明式编程
  • 写短小的函数,不要在lambda中写过多的代码
  • 不断学习“新”的代码风格,不断改善自己的代码