代码的坏味道——为什么建议使用模型来替换枚举?
Posted tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代码的坏味道——为什么建议使用模型来替换枚举?相关的知识,希望对你有一定的参考价值。
为什么建议使用对象来替换枚举?
在设计模型时,我们经常会使用枚举来定义类型,比如说,一个员工类 Employee,他有职级,比如P6/P7。顺着这个思路,设计一个 Level 类型的枚举:
class Employee
private String name;
/**
* 薪水
*/
private int salary;
/**
* 工龄
*/
private int workAge;
/**
* 职级
*/
private Level level;
enum Level
P6, P7;
假设哪天悲催的打工人毕业了,需要计算赔偿金,简单算法赔偿金=工资*工龄
class EmployeeService
public int calculateIndemnity(int employeeId)
Employee employee=getEmployeeById(employeeId);
return employee.workAge * employee.salary;
后来,随着这块业务逻辑的演进,其实公司是家具备人文关怀的好公司,再原有基础上,按照职级再额外补发一定的金额:
public int calculateIndemnity(int employeeId)
Employee employee = getEmployeeById(employeeId);
switch (employee.level)
case P6:
return employee.workAge * employee.salary + 10000;
break;
case P7:
return employee.workAge * employee.salary + 20000;
break;
default:
throw new UnsupportedOperationException("");
当然,这段逻辑可能被重复定义,有可能散落在各个Service。
这里就出现了「代码的坏味道」
新的枚举值出现怎么办?
显然,添加一个新的枚举值是非常痛苦的,特别通过 switch 来控制流程,需要每一处都修改枚举,这也不符合开闭原则。而且,即使不修改,默认的防御性手段也会让那个新的枚举值将会抛出一个异常。
为什么会出现这种问题?
是因为我们定义的枚举是简单类型,无状态。
这个时候,需要用重新去审视模型,这也是为什么 DDD 是用来解决「大泥球」代码的利器。
一种好的实现方式是枚举升级为枚举类,通过设计「值对象」来重新建模员工等级:
abstract class EmployeeLevel
public static final EmployeeLevel P_6 = new P6EmployeeLevel(6, "资深开发");
public static final EmployeeLevel P_7 = new P7EmployeeLevel(7, "技术专家");
private int levle;
private String desc;
public EmployeeLevel(int levle, String desc)
this.levle = levle;
this.desc = desc;
abstract int bouns();
class P6EmployeeLevel extends EmployeeLevel
public P6EmployeeLevel(int level, String desc)
super(level, desc);
@Override
int bouns()
return 10000;
static class P7EmployeeLevel extends EmployeeLevel
public P7EmployeeLevel(int level, String desc)
super(level, desc);
@Override
int bouns()
return 20000;
你看,这里叫「EmployeeLevel」,不是原先的「Level」,这名字可不是瞎取的。
这里,我把 EmployeeLevel 视为值类型,因为:
● 不可变的
● 不具备唯一性
通过升级之后的模型,可以把员工视为一个领域实体 Employee:
class Employee
private String name;
/**
* 薪水
*/
private int salary;
/**
* 工龄
*/
private int workAge;
/**
* 职级
*/
private EmployeeLevel employeeLevel;
public int calculateIndemnity()
return this.workAge * this.salary + employeeLevel.bouns();
可以看到,计算赔偿金已经完全内聚到 Employee 实体中,我们设计领域实体的一个准则是:必须是稳定的,要符合高内聚,同时对扩展是开放的,对修改是关闭的。你看,哪天 P8 被裁了,calculateIndemnity 是一致的算法。
当然,并不是强求你把所有的枚举都替换成类模型来定义,这不是绝对的。还是要按照具体的业务逻辑来处理。
代码的坏味道与重构技术
一、前言
本文大部分内容、图片来自Martin Flower的《Refactoring》一书以及refactoringguru网站(一个很棒的网站),之前在博客发表过,这次属于整理后重新发表,以重温经典。
二、什么是代码的坏味道
通过发霉腐坏的气味隐喻那些在设计上别扭、理解上费劲、维护上困难的问题代码。
代码的各种坏味道
三、什么是重构
重构是指在不改变代码外部行为的前提下,使代码变得设计简单、干净整洁的方法。
四、什么是整洁代码
整洁代码设计简单、干净精简、易理解、可测通、好维护。
整洁代码的特征
五、关于技术债务
重构可以消除技术债务,但重构之前要避免产生技术债务。
技术债务产生的原因
六、重构的时机
重构遵循“事不过三”原则,代码呼唤你重构的时候,就立即开始吧。
重构的时机
七、重构的守则
重构需要:目标导向、做好防护、循规蹈矩、小步前进、稳扎稳打。
重构守则
八、重构的手法
1. 整理
a. 删除 Delete(无用注释、废弃代码)
b. 移动 Move(上下左右,代码片段、方法、类、包)
c. 重命名 Rename(包、类、方法、变量等)
2. 变换
a. 封装 Encapsulate (变量 -> 成员变量)
b. 变量改为方法 Replace Temp with Query(缩短方法)
c. 表达式改为变量 Introduce Explaining Variable(易懂)
d. 抽取 Extract(变量 -> 参 数,变量 -> 常量,代码片段 -> 方法, 参数 -> 类)
e. 内联 Inline (方法 -> 表达式,变量 -> 表达式)
3. 易构
a. 提取类 Extract Class、Extract SubClass
b. 提升 Pull Up
c. 下放 Push Down
d. 代理 Extract Delegat
九、消除各种坏味道
9.1 重复代码
重复代码是首当其冲的坏味道。
消除重复
特征
1.重复的表达式
2.不同的算法做相同的事
3.代码相似
目标
相同表达式只在一个类的一个方法出现,供其他方法调用
情况 | 处理方式 |
---|---|
同一个类的两个函数有相同表达式 | 重复代码提取为方法 |
兄弟类含有相同的表达式 | ① 重复代码提取为方法 ② 提升方法到父类 |
不相干的类含有相同代码 | 提取为独立类供调用 |
9.2 过长函数
代码越长越难以理解。
如果函数有个好名字,就可以省去关注它的时间。
重构过长函数
特征
1.代码前面有注释
2.代码中有条件表达式
3.代码中有循环
目标
每个函数只做一件事
函数要职责单一、命名准确
情况 | 处理方式 |
---|---|
将步骤块、分支块分别提取为具备独立职责的方法 |
9.3 过大的类
一个类应该是一个清楚的抽象,处理一些明确的职责,只响应一种变化。
在不同的环境扮演不同角色的类,使用接口就是好主意。
重构过大的类
特征
1.一个类中有太多实例变量
2.一个类中有太多代码
目标
每个类负责一组具有内在的相互关联的任务
情况 | 处理方式 |
---|---|
部分字段之间相关性高 | 相关的字段和方法提取到新类 |
某些字段和方法只被某些实例用到 | 这些字段和方法都下放到子类中 |
9.4 过多参数
太长的参数列导致难以理解、不易使用、被迫修改。
方法需要的参数,多数应当从类中获得。
过长的参数列表
特征
1.参数列表过长
2.参数列表变化频繁
目标
只需要传给函数足够的、让其可以从中获取自己需要的东西就行了
情况 | 处理方式 |
---|---|
方法可以通过其他方式获取该参数 | 让参数接受者自行获取该参数 |
同一对象的若干属性作为参数 | 在不使依赖恶化的前提下,使用整个对象作为参数 |
被调用函数使用了另一个对象的很多属性 | 将方法移动到该对象中 |
某些数据缺乏归属对象 | 首先创建对象 |
9.5 变化集中
在理想境地下,外界变化与类应该是一对一关系。
重新封装变化
特征
一个类受多种变化的影响
目标
针对某一外界变化的所有修改,只应发生在单一类中,而这个类中所有的内容都应反映此变化
情况 | 处理方式 |
---|---|
类经常因为不同的原因在不同的方向上发生变化 | 将特定原因造成的所有变化提取为一个新类 |
9.6 修改发散
需要修改的代码散布四处,就难以找到、容易忘记。
散弹式修改
特征
一种变化引发多个类的修改
目标
针对某一外界变化的所有修改,只应发生在单一类中,而这个类中所有的内容都应反映此变化
情况 | 处理方式 |
---|---|
某种变化需要在许多不同的类中做出小修改 | 把所有需要修改的代码放进同一个类中 |
9.7 错误依恋
面向对象的精髓:将数据和相关行为封装在一起、将总是一起变化的东西放在一起。
你知道的太多了
特征
一个函数使用其他类属性比使用自身类属性还要多
目标
将数据和对数据的操作行为包装在一起
情况 | 处理方式 |
---|---|
某个函数从另一个对象调用了几乎半打的取值函数 | 将依恋代码提取为单独方法,移动到另一对象 |
9.8 数据泥团
数据项就像小孩子,喜欢成群结队地待在一块儿。
总是结队出现的数据项,应放进属于自己的对象中。
重新组织
特征
同时使用的相关数据并未以类的方式组织
1.两个类中相同的字段
2.许多函数中相同的参数
目标
总是绑在一起的数据应该拥有属于它们自己的对象
情况 | 处理方式 |
---|---|
先将字段提取为类,再缩减函数签名中的参数 |
9.9 基本类型
对象技术的新手通常不愿意在小任务上运用小对象。
类型码终究是数值,无法强校验,可读性差。
使用真正的面向对象编程
特征
过多使用基本类型
目标
将单独存在的数据值转换为对象
情况 | 处理方式 |
---|---|
总是被放在一起的基本类型字段 | 提取类 |
参数列中有基本类型 | 提取参数对象 |
数组中容纳了不同的对象,需要从数组中挑选数据 | 用对象取代数组 |
基本数据是类型码 | 使用类替换类型码 |
带条件表达式的类型码 | 使用继承类替换类型码 |
9.10 重复分支
面向对象一个最明显的特征就是:少用Switch语句。
Switch语句的问题在于到处重复、可读性差。
重复的分支结构
特征
相同的switch、case语句散布于不同地方
目标
避免到处做相同的修改
情况 | 处理方式 |
---|---|
根据类型码进行选择的switch | 使用多态替代switch |
单一函数中有switch | 使用显式的方法取代参数 |
9.11 平行继承
平行继承体系是散弹式修改的一种。
需要同步修改的继承体系
特征
1.为某个类增加子类时,必须为另一个类增加子类
2.某个继承体系类名前缀和另一个继承体系类名前缀相同
目标
避免到处做相同的修改
情况 | 处理方式 |
---|---|
一个继承体系中的实例引用另一个继承体系中的实例,然后迁移成员,合并为一个继承体系 |
9.12 无所事事
如果一个类不值其身价,就消除它。
无所事事的类
特征
类无所事事
目标
情况 | 处理方式 |
---|---|
父类和子类无太大差别 | 将它们合为一体 |
某个类没有做太多事情 | 将这个类所有成员移到另一个类中,然后删除它 |
9.13 夸夸其谈
杜绝以“未来”为幌子、挡箭牌的过度设计。
为未来设计的无用代码
特征
过度设计
为未来预留
目标
避免过度设计、超前设计
情况 | 处理方式 |
---|---|
某个抽象类没有太大作用 | 将父子类合并 |
不必要的委托 | 将这个类所有成员移到另一个类中,删除它 |
函数的某些参数未用上 | 移除参数 |
函数名称带有多余的抽象意味 | 重命名函数名 |
函数只被测试方法调用 | 连同测试代码一并删除 |
9.14 临时字段
猜测未使用变量当初的设置目的,会让人发疯
字段变化规律不同
特征
1.某个实例字段仅为某种情况而设
2.某些实例字段仅为某个函数的复杂算法少传参数而设
目标
封装变化,确保每一个类只因一种原因变化
情况 | 处理方式 |
---|---|
提取单独的类,封装相关代码 |
9.15 长串委托
一旦消息链结构发生变化,就不得不修改调用方。
每个对象都应该尽可能少了解系统的其他部分。
消息链
特征
一长串的getThis或临时变量
目标
消除耦合
情况 | 处理方式 |
---|---|
客户类通过一个委托类来取得另一个对象 | 隐藏委托 |
9.16 莫名中介
封装需要委托,但是不要过度委托。
无用的中间人
特征
某个类接口有大量的函数都委托给其他类,过度使用委托
目标
情况 | 处理方式 |
---|---|
有一半的函数 | 移除中间人 |
少数几个函数 | 直接调用 |
中间人还有其他行为 | 让委托类继承受托类 |
9.17 窥探隐私
过分的相互了解就是一种耦合。
狎昵关系
特征
某个类需要了解另一个类的私有成员
目标
重新封装
情况 | 处理方式 |
---|---|
子类过分了解超类 | 将继承改为委托,把子类从继承体系移出 |
类之间双向关联 | 去掉不必要的关联 |
类之间有共同点 | 提取新类 |
9.18 异曲同工
签名不同的功能相同类
特征
两个函数做同一件事,但是签名不同
目标
合并、消除重复
情况 | 处理方式 |
---|---|
合并 |
9.19问题组件
特征
类库函数构造得不够好,又不能修改它们
目标
情况 | 处理方式 |
---|---|
想修改一两个函数 | 在调用类增加函数 |
想添加一大堆额外行为 | 使用子类或包装类 |
9.20 幼稚类型
成熟的对象应承担职责,包含对其数据操作的方法。
特征
某个类除了字段,就是字段访问器、设置器
目标
封装
情况 | 处理方式 |
---|---|
1.用访问器取代public字段 2.恰当封装集合3.移除不需要的设置器4.搬移对访问器、设置器调用方法到此类5.隐藏访问器、设置器 |
9.21 爱心泛滥
不合适的继承
特征
派生类仅使用了基类很少一部分成员函数
目标
封装
情况 | 处理方式 |
---|---|
子类拒绝继承超类接口 | 使用委托替代继承 |
9.22 注释泛滥
注释可以记述将来、标记风险、解释来由。
要杜绝喃喃自语的注释。
消除注释泛滥问题
特征
一段代码有着长长的注释
目标
情况 | 处理方式 |
---|---|
根据情况消除各种坏味道 |
结束语
代码如水,顺势而为,成为编码高手后,就很容易看出代码的“势”,很容易感知代码的“坏味道”,重构多了,重构的手法也会纯熟很多。
我们可以多学习和应用设计模式,多读优秀的代码,经常练习重构,一定会成为个中高手。
时代在进步,目前很多IDE都有内置了代码重构的功能,甚至还有一些检测代码坏味道的工具或插件可以使用,重构代码变得越来越方便和安全了。
谨以本篇献给热爱编码的同学们,加油!!!
原文地址
以上是关于代码的坏味道——为什么建议使用模型来替换枚举?的主要内容,如果未能解决你的问题,请参考以下文章