代码之丑¶
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
函数式编程¶
循环语句是在描述实现细节,而列表转换的写法是在描述做什么,二者的抽象层次不同。
列表转换的本身就完全变成了一个声明,这样的写法才是能发挥出列表转换价值的写法。
编程规则¶
声明式编程
一句话¶
不断学习 “新” 的代码风格,不断改善自己的代码