代码之丑

1. 命名

不好的命名方法

  • 不精准的命名

    • 命名过于宽泛,不能精准描述

    • 命名要能够描述出这段代码在做的事情。

  • 用技术术语命名

    • 是一种基于实现细节的命名方式

    • 如:xxxMap、xxxSet

命名遵循原则

  • 🔥描述意图,而非细节。

    • 比如,StartTranslation

  • 面向接口编程,接口是稳定的,实现是易变的

  • 技术名词的出现,往往就代表着它缺少了一个应有的模型

    • 一个技术类的项目中,这些技术术语其实就是它的业务语言。

    • 但对于业务项目,这个说法就必须重新审视了

  • 用业务语言写代码

    • 建立团队的词汇表

总结

  • 好的命名,是体现业务含义的命名

2. 乱用英语

常见的命名规则

  • 类名是一个名词,表示一个对象

  • 方法名则是一个动词,或者是动宾短语,表示一个动作

几种乱用英语的点

  • 违反语法规则的命名;

  • 不准确的英语词汇;

  • 英语单词的拼写错误。

如何从实践层面上更好地规避

  • 制定代码规范,比如,类名要用名词,函数名要用动词或动宾短语;

  • 要建立团队的词汇表;

  • 要经常进行代码评审。

3. 重复代码

重复代码

  • 复制粘贴的代码

  • 重复的结构

  • if 和 else 代码块中的语句高度类似

消灭重复原则

  • Don’t Repeat Yourself

  • 每一处都有单一、明确、权威的描述

实践

  • 在一个职业的队伍里,谁发现谁修改

  • 可以用设计模式和工具框架去解决

4. 长函数

🔥一个好的程序员面对代码库时要有不同尺度的观察能力,看设计时,要能够高屋建瓴,看代码时,要能细致入微。

长函数的产生

  • 以性能为由;

    • 性能优化不应该是写代码的第一考量

    • 部分优化交给编译器

    • 可维护性比性能优化要优先考虑

  • 平铺直叙;

  • 一次加一点。

消灭长函数的原则

  • 定义好长函数的标准:如最长20行

  • 做好『分离关键点』

  • 坚守『童子军军规』

    • 让营地比你来时更干净

重构手法

  • 提取函数

一句话

  • 把函数写短,越短越好

5. 大类

大类的产生

  • 职责不单一

  • 字段未分组

所谓的将大类拆解成小类,本质上在做的工作是一个设计工作

软件设计原则

  • 单一职责

实践

  • 对象健身操

  • 聚合层扮演的角色其实一个防腐层,它本身的职责就是和请求应答去做一一应对。一般来说,这样类行为很单一,主要的职责就是数据转换。对于这种类,大一点是可以的,因为它不会对业务造成什么影响。重点在于,这个类里没有业务。

  • 重点是,简单的聚合并不是一个好办法,而是需要谨慎地设计通信协议,这才是保证类比较小的根本办法。

一句话

  • 把类写小,越小越好。

6. 长参数列表

参数数量多导致的长参数

  • 变化频率相同

    • 将参数列表封装成一个类

    • 一个模型的封装应该是以行为为基础的

  • 多个变化频率

    • 封装成多个类

  • 静态不变

    • 成为软件结构的一部分

  • 方法:动静分离

    • 分离关注点

      • 动数据(bookId)和静数据(httpClient 和 processor),是不同的关注点,应该分离开来

    • 静态不变的数据完全可以成为这个函数所在类的一个字段,而只将每次变动的东西作为参数传递

标记(flag)参数导致的长参数

  • 标记(flag)存在的形式

    • 布尔值

    • 枚举值

    • 直接的字符串或者整数

  • 根据标记参数的不同拆分成多个函数

  • 重构的手法

    • 移除标记参数(Remove Flag Argument)

一句话

  • 减小参数列表,越小越好。

7. 滥用控制语句

嵌套的代码

  • 重构手法

    • 以卫语句取代嵌套的条件表达式(Replace Nested Conditional with Guard Clauses)

重复的 switch(Repeated Switch)

  • 重构的手法

    • 以多态取代条件表达式(Relace Conditional with Polymorphism)

衡量代码复杂度常用的标准

  • 圈复杂度(Cyclomatic complexity,简称 CC)

    • 圈复杂度越高,代码越复杂,理解和维护的成本就越高。

    • 工具:Checkstyle

8. 缺乏封装

过长的消息链(Message Chains)

  • 火车残骸(Train Wreck)

    • 链式调用不一定都是火车残骸。比如 builder 模式,每次调用返回的都是自身,不牵涉到其他对象,不违反迪米特法则。

    • 火车残骸的代码和流畅(Fluent)代码的重要差别是,火车残骸描述的具体怎么做,而流畅(Fluent)代码描述的是做什么,是声明式的。

  • 重构手法

    • 隐藏委托关系(Hide Delegate)

  • 指导原则

    • 迪米特法则(Law of Demeter)

      • 按照迪米特法则这样写代码,可能让代码里有太多简单封装的方法?不过,这也是单独解决这一个坏味道可能带来的结果。这种代码的出现,根本的问题是缺乏对封装的理解,而一个好的封装是需要基于行为的,所以,如果把视角再提升一个角度,我们应该考虑的问题是类应该提供哪些行为,而非简简单单地把数据换一种形式呈现出来。

以基本类型为模型的坏味道称为基本类型偏执(Primitive Obsession)

  • 重构手法

    • 以对象取代基本类型(Replace Primitive with Object)

    • 也就是提供一个模型代替原来的基本类型

封装之所以有难度,主要在于它是一个构建模型的过程

  • 作为这个类的使用者,你并不需要知道这个类到底是怎么实现的

  • 有任何业务上的调整,都会发生在类的内部,只要保证接口行为不变,就不会影响到其它的代码。

9. 可变的数据

可变数据最直白的体现就是各种 setter

  • 问题

    • 相比于读数据,修改是一个更危险的操作

    • 比可变的数据更可怕的是,不可控的变化

    • 缺乏封装再加上不可控的变化,setter 几乎是排名第一的坏味道

  • 重构手法

    • 移除设值函数(Remove Setting Method)

在实践中,完全消除可变数据是很有挑战的

  • 一个实际的做法是,区分类的性质。

    • 值对象就要设计成不变类

      • 所有的字段只在构造函数中初始化;

      • 所有的方法都是纯函数;

      • 如果需要有改变,返回一个新的对象,而不是修改已有字段。

    • 实体类则要限制数据变化

函数式编程的本质

  • 对于赋值进行了约束

    • 很多编程语言都引入了值类型,而让变量成为次优选项

全局数据

  • 一个与数据相关的坏味道

一句话

  • 限制可变的数据

10. 变量声明与赋值分离

变量的初始化

  • 问题

    • 先声明后赋值

    • 变量初始化与业务处理混在一起

  • 基本原则

    • 变量一次性完成初始化

  • 尽可能使用不变的量

集合初始化

  • 问题

    • 先声明后添加元素

声明式代码

  • 区别

    • 命令式的代码

      • 告诉你 “怎么做” 的代码

      • 声明一个集合,然后添加一个元素,再添加一个元素

    • 声明式的代码

      • 告诉你 “做什么” 的代码

      • 要一个包含了这两个元素的集合

    • 总结

      • 声明式的代码体现的意图,是更高层面的抽象,把意图和实现分开,从某种意义上来说,也是一种分离关注点

      • 用声明式的标准来看代码,是一个发现代码坏味道的重要参考

一句话

  • 一次性完成变量的初始化

11. 依赖混乱

缺少防腐层

  • 让请求对象传导到业务代码中,造成了业务与外部接口的耦合

  • 通过防腐层将外部系统和核心业务隔离开来

依赖倒置原则

  • 高层模块不应依赖于低层模块,二者应依赖于抽象。

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

  • 抽象不应依赖于细节,细节应依赖于抽象。

  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

其他

  • 习惯于用业务术语去表达,而不仅仅是技术术语。封装是为了概念,而不单纯是为了封装。

ArchUnit

  • 把架构层面的检查做成了单元测试

一句话

  • 代码应该向着稳定的方向依赖

12. 不一致的代码

命名中的不一致

  • 类似含义的代码应该有一致的名字

  • 一旦出现了不一致的名字,通常都表示不同的含义

方案中的不一致

  • 应对同一个问题出现了多个解决方案

  • 一般是随时间演化造成的

    • 随着时间流逝,有新的解决方案了,而原有解决方案虽存在的各种问题,但已深入人心

代码中的不一致

  • 不要测私有方法

  • 🔥很多程序员纠结的技术问题,其实是一个软件设计问题,不要通过奇技淫巧去解决一个本来不应该被解决的问题。

应对策略

  • 团队统一编码风格

  • 团队统一编码解决方案

一句话

  • 保持代码在各个层面上的一致性

13. 落后的代码风格

Java8 Optional

  • 避免引发空指针问题

  • 示例: author?.name

函数式编程

  • 循环语句是在描述实现细节,而列表转换的写法是在描述做什么,二者的抽象层次不同。

  • 列表转换的本身就完全变成了一个声明,这样的写法才是能发挥出列表转换价值的写法。

编程规则

  • 声明式编程

一句话

  • 不断学习 “新” 的代码风格,不断改善自己的代码