代码的坏味道——为什么建议使用模型来替换枚举?

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都有内置了代码重构的功能,甚至还有一些检测代码坏味道的工具或插件可以使用,重构代码变得越来越方便和安全了。

谨以本篇献给热爱编码的同学们,加油!!!

原文地址

本文转载于:https://www.toutiao.com/i6961294514677907999

以上是关于代码的坏味道——为什么建议使用模型来替换枚举?的主要内容,如果未能解决你的问题,请参考以下文章

重构—改善既有代码的设计3——代码的坏味道

重构的素养

学习重构-代码的坏味道

代码的坏味道

我的重构识别代码的坏味道

Bad Smell (代码的坏味道)

(c)2006-2024 SYSTEM All Rights Reserved IT常识