重构改善既有代码的设计-代码的坏味道(中)

续上一篇文章链接:
!(https://www.jianshu.com/p/251d1b796259?v=1673184489236)[重构改善既有代码的设计-代码的坏味道(上)]

我们继续整理代码中的“坏味道”

1.9 Primitive Obsession (基本类型偏执)

大多数编程环境都有两种数据:结构类型允许你将数据组织成有意义的形式,基本类型则是构成结构类型的积木块。

对象的一个极大的价值在于:他们模糊了横亘于基本数据类型和体积较大的类之间的界限,你可以轻松编写出一些与语言内置(基本)类型没有区别的小型类。例如java就以基本类型表示数值,而以类表示字符串和日期。一般对象技术的新手都不爱在小任务上使用小对象-就像结合数值和币种的money类=》 由一个起始值和结束值组成的range类。
你应该多运用Replace Data Value with Object(以对象取代数据值175)手法,将原本独立存在的数值抽象成对象,走出传统的编程技术,尽量使用面向对象编程的思想。

一句话总结:少用基本类型表示数值,根据实际情况使用面向对象的方式,将数值重构成类,去使用。

注(Replace Data Value with Object,例举:开发初期,一开始使用一个字符串来表示电话号码,后面发现电话号码需要验证格式,抽取区号等等特殊行为,如果这类的数据只有一两个,你还可以把相关函数放进数据项所属的对象里,但是重复代码与依赖情结的坏味道就出来了。)。

代码示例:
public class OrderBefore {

    private String customer;

    public OrderBefore(String customer) {
        this.customer = customer;
    }

    public String getCustomer() {
        return customer;
    }

    public void setCustomer(String customer) {
        this.customer = customer;
    }

    // 可能的用法
    private static long countOrders(List<OrderBefore> beforeList, String customer) {
        long count = beforeList.stream().filter(o -> {
            return customer.equals(o.getCustomer());
        }).count();
        return count;
    }
}

重构后:

public class Customer {

    private final String name;

    public Customer(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class OrderAfter {
    // 与原有使用的数值相关的调用都改成类的方式
    private Customer customer;

    public OrderAfter(String customer) {
        this.customer = new Customer(customer);
    }

    public String getCustomer() {
        return customer.getName();
    }

    public void setCustomer(String customer) {
        this.customer = new Customer(customer);
    }

    // 可能的用法,改版java8
    private static long countOrders(List<OrderBefore> beforeList, String customer) {
        return beforeList.stream().filter(o-> customer.equals(o.getCustomer())).count();
    }
}

实现建议:

  1. 如果想要替换的数值是类型码,而不影响行为。 则可以运用Replace Type Code with Class(218以类取代类型码,指数值中有一个数值类型码,但它不影响类的行为,可以以一个新的类替换该数值类型码。)

举例如下代码:

重构之前:
package com.code.refactor.test.com.code.refactor.replacetypecodewithclass.before;

/**
 * 类:人
 * 血型:OABC
 * 类型码code表示血型
 *
 *
 *  * 定义: 类之中有一个不影响类的行为的数值类型码,code
 *  * 做法: 1。为类型码建立一个类(1.该类需要一个记录类型码的字段,get函数,使用一组静态变量保存允许被创建的实例,并以一个静态的函数返回原本类型码的实例)
 *  *        2。修改源类的实现,让他使用新建的类。(维持原先以类型码为基础的函数接口,但改变静态字段,以新建的类产生代码,然后修改类型码相关函数,让他们也从新建的类中获取类型码)
 *  *          3。编译,测试
 *  *          4。对原类中每一个使用类型码的函数一一对应建立一个函数,让新函数使用新建的类
 *  *          5。逐一修改源类用户,让他们使用新接口。
 *  *          6。每修改一个用户编译并测试。
 *  *          7。删除使用类型码的旧接口,并删除保存旧类型码的静态变量。
 *  *          8。编译测试。
 *  *
 *
 */
public class Person {

    public static final int O = 0;
    public static final int A = 1;
    public static final int B = 2;
    public static final int C = 3;

    private int bloodGroup;

    public Person(int bloodGroup) {
        this.bloodGroup = bloodGroup;
    }

    public int getBloodGroup() {
        return bloodGroup;
    }

    public void setBloodGroup(int bloodGroup) {
        this.bloodGroup = bloodGroup;
    }
}

/*
 *  * 做法: 1。为类型码建立一个类(1.该类需要一个记录类型码的字段,get函数,使用一组静态变量保存允许被创建的实例,并以一个静态的函数返回原本类型码的实例)
 *  *        2。修改源类的实现,让他使用新建的类。(维持原先以类型码为基础的函数接口,但改变静态字段,以新建的类产生代码,然后修改类型码相关函数,让他们也从新建的类中获取类型码)
 *  *          3。编译,测试
 *  *          4。对原类中每一个使用类型码的函数一一对应建立一个函数,让新函数使用新建的类
 *  *          5。逐一修改源类用户,让他们使用新接口。
 *  *          6。每修改一个用户编译并测试。
 *  *          7。删除使用类型码的旧接口,并删除保存旧类型码的静态变量。
 *  *          8。编译测试。
 *  *
 *
 */


重构之后:

package com.code.refactor.test.com.code.refactor.replacetypecodewithclass.after;

import java.util.Arrays;
import java.util.List;

/**
 * 第一步 1。为类型码建立一个类(1.该类需要一个记录类型码的字段,get函数,使用一组静态变量保存允许被创建的实例,并以一个静态的函数返回原本类型码的实例)
 */
public class BloodGroup {

    public static final BloodGroup O = new BloodGroup(0);
    public static final BloodGroup A = new BloodGroup(1);
    public static final BloodGroup B = new BloodGroup(2);
    public static final BloodGroup C = new BloodGroup(3);

    private static final List<BloodGroup> values = Arrays.asList(O, A, B, C);

    private final int code;

    public BloodGroup(int code) {
        this.code = code;
    }

    public int getCode() {
        return code;
    }

    public static BloodGroup code(int argCode) {
        return values.get(argCode);
    }
}

package com.code.refactor.test.com.code.refactor.replacetypecodewithclass.after;

/**
 * 2。修改源类的实现,让他使用新建的类。(
 *    维持原先以类型码为基础的函数接口,
 *    但改变静态字段,以新建的类产生代码,然后修改类型码相关函数,让他们也从新建的类中获取类型码)
 */
public class PersonAfter {

    // 最后一步:删除
//    public static final int O = BloodGroup.O.getCode();
//    public static final int A = BloodGroup.A.getCode();
//    public static final int B = BloodGroup.B.getCode();
//    public static final int C = BloodGroup.C.getCode();

      // 最后一步:删除
      //private int bloodGroup;

    // 最后一步:删除
//    public PersonAfter(int bloodGroup) {
//        this.bloodGroup = BloodGroup.code(bloodGroup);
//    }

    // 最后一步:删除
//    public void setBloodGroup(int bloodGroup) {
//        this.bloodGroup = BloodGroup.code(bloodGroup);
//    }


    private BloodGroup bloodGroup;

    //1.修改方法名,改为getBloodGroupCode
//    public int getBloodGroup() {
//        return bloodGroup.getCode();
//    }

    /** 1.rename method  */
    public int getBloodGroupCode() {
        return bloodGroup.getCode();
    }

    /** 2.新增一个取值函数获取BloodGroup */
    public BloodGroup getBloodGroup() {
        return bloodGroup;
    }

    /**
     * 3。建立新的构造器和设值函数
     * @param bloodGroup
     */
    public PersonAfter(BloodGroup bloodGroup) {
        this.bloodGroup = bloodGroup;
    }

    public void setBloodGroup(BloodGroup bloodGroup) {
        this.bloodGroup = bloodGroup;
    }
}

public class Test01 {
    public static void main(String[] args) {
        // 原先的调用方式
        Person person = new Person(Person.A);
        int bloodGroup = person.getBloodGroup();
        person.setBloodGroup(Person.B);

        //改变成如下:
        PersonAfter personAfter = new PersonAfter(BloodGroup.A);
        BloodGroup bloodGroup1 = personAfter.getBloodGroup();
        personAfter.setBloodGroup(BloodGroup.C);

        // 确保每个都修改完成后,开始删除原本使用整数类型的那些,取值函数、构造函数、静态变量,设值函数。
    }
}

使用Replace Type Code with Class手法之前,应该要考虑类型码的其他替换方式,只有当类型码是纯粹数据(不会在switch语句中引起行为变化时)你才可以以类取代它(后续的java版本可以支持)。

为什么要这样重构呢??降低代码复杂度,可读性,可拓展性。

  1. 如果有与类型码相关的条件表达式,则可以运用Replace Type Code With SubClass(213)或者Replace Type Code With State/Strategy(227)。这部分内容比较复杂,后面附链接详细讲解。

1.10 Switch Statements(Switch 惊怵现身)

面向对象编程的一个明显的特征就是少用swith(或case)语句。
书中指出:使用switch语法的问题,会发现同样的switch语句会分布在很多不同的地方,导致要进行修改的时候,就需要修改很多地方,大多数时候,我们应该养成一看到switch语句就考虑以多态来替换它的习惯。

做法:

但是通常,基于我自己的做法是将switch语句使用提炼方法的方式提炼到一个独立的函数中,这一步基本都会这样实现(下意识)。接着再使用移动函数的手法将它搬移到需要多态性的那个类里。

最后你需要确认,是否要使用Replace Type Code With SubClass(223以子类取代类型码,如果有一个不可变的类型码,它会影响类的行为,可以以子类替代这个类型码)或者Replace Code With State/Stragy(277),一旦这样完成以后,你就可以运用replace Conditional With Ploymorphism(255)

关于该坏味道后续可以查看该文章整理出来的实现:

1.11 Parallel Inheritance Hierarchies(平行继承体系)

每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。如果你发现某个继承体系的类名前缀和另一个继承体系的类名前缀完全相同,便是闻到了这种坏味道。

出现平行继承体系的弊端
1.不利于后期的维护,每当向下添加子类时需同步增加平行体系的子类。
2.代码阅读容易混淆。
3.一条平行的中的一个错误会影响整个体系。

优化方式

1.让一个继承体系的实例引用另一个继承体系的实例。
2.运用 Move Method (搬移函数)和Move Field (搬移字段)将引用端的继承体系取消。

场景这个概念讲解的有点抽象,我列举了一个场景示例如下:


//从种类方面考虑
//原有两条体系   猫 -> 宠物猫 -> 短腿猫
    //              狗 -> 宠物狗 -> 短腿狗
class Cat{
   public String name;
   public void eat(){ 
       System.out.println(name+"正在吃"); 
   }
   public void sleep(){
       System.out.println(name+"正在睡");
   }
   public void catBark(){
    System.out.println(name+"喵喵喵");
   }
}

class Dog{
   public  String name;
   public void eat(){ 
       System.out.println(name+"正在吃"); 
   }
   public void sleep(){
       System.out.println(name+"正在睡");
   }
   public  void dogBark(){
    System.out.println(name+"汪汪汪");
   }
}
//继承猫类
class PetCat extends Cat{
   public void shower(){
    System.out.println(name+"洗澡");
   }
}
//继承狗类
class PetDog extends Dog{
   public void shower(){
    System.out.println(name+"洗澡");
   }
}

//继承宠物猫类
class ShortLeggedCat extends PatCat{
   public void jump(){
    System.out.println(name+"跳不起");
   }
}
//继承宠物狗类
class ShortLeggedDog extends PatDog{
   public void run(){
    System.out.println(name+"肚子贴地跑");
   }
}

这里会发现猫狗的子类继承体系类似,于是我们可以把猫体系移入狗体系(搬移函数),并根据实际情况更改类名(注意并不是将猫类和狗类去继承一个父类)
改善:

class Animal{
   public String name;
   public void eat(){ 
       System.out.println(name+"正在吃"); 
   }
   public void sleep(){
       System.out.println(name+"正在睡");
   }
   public void dogBark(){
        System.out.println(name+"汪汪汪");
    }
   public void catBark(){
        System.out.println(name+"喵喵喵");
    }
}

class Pet extends Animal{
    public void shower(){
        System.out.println(name+"洗澡");
    }
}

class ShortLeggedPet extends Pat{
    private void run(){
        System.out.println(name+"肚子贴地跑");
    }
    private void jump(){
        System.out.println(name+"跳不起");
    }
}

1.12 冗余类

你创建的每一个类,都得有人去理解它, 维护它,这些工作都是花钱的. 如果一个类的所得不值其身价, 他就应该消失. 项目中经常会出现这样的情况:

起因:
1.某个类原本对得起自己的身价,但重构使它身形缩水,不再做那么多工作;
2.开发者事前规划了某些变化,并添加一个类来应付这些变化,但变化实际上没有发生。

如果这些子类没有做足够的工作, 试试 Collapse Hierarchy(344折叠继承体系,如果超类与子类之间没有太大的区别,将他们合为一体。) , 对几乎没用的组件, 你应该以 Inline Class(内联类) 对付它们。

1.13 Speculative Generality (夸夸其谈未来性)

这个令我们十分敏感的坏味道,命名者是Brian Foote。当有人说“噢,我想我 们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。那么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,那就值得那么做;如果用不到,就不值得。用不上的装置只会挡你的路,所以,把它搬开吧。

处理:

如果你的某个抽象类其实没有太大作用,请运用Collapse Hierarchy (344折叠继承体系)不必要的委托可运用Inline Class (154)除掉。如果函数的某些参数未被用上,可对它实施 Remove Parameter(277)。如果函数名称带有多余的抽象意味,应该对它实施Remme Method(273),让它现实一些。如果函数或类的唯一用户是测试用例,这就飘出了坏味道Speculative Generality。

如果你发现这样的函数或类,请把它们连同其测试用例一并删掉。但如果它们的用途是帮助测试用例检测正当功能,当然必须刀下留人。

1.14 Temporary Field (令人迷惑的暂时字段)

有时你会看见,其内某个变量仅为某种特定情况而设,这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变量。在变量未被使用的情况下猜测当初其设置目的,会让你发疯的。

通常情况下,临时变量是为了需要大量输入的算法创建的。程序员将这些字段创建在类中,而没有将它们放在方法的入参中。这些字段就只在这个算法中使用到了,在剩下的时间,都是处于未被使用的状态。

Primitive Obsession (基本类型偏执)临时字段的解决方式跟文章开篇说的这种味道处理方式大同小异,都是可以使用Extract class(149 提炼成一个类的方式)的重构手法去处理。把所有相关的代码都放进这个新的类中。

同时也推荐可以使用Introduce Null Object(260 )在变量不合法的情况下,创建一个null 对象,从而避免写出条件表达式。

重构后的好处:代码更加清楚且有条理。

1.15 Message Chains (过度耦合的消息链)

如果你看到用户向一个对象索求(request)另一个对象,然后再向后者索求另一个对象,然后再索求另一个对象……这就是Message Chains。实际代码中你看到的可能是一长串getThis()或一长串临时变量。采取这种方式,意味客户将与查找过程中的导航结构(structure of the navigation)紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。

这时候你应该使用Hide Delegate(157隐藏「委托关系」)。你可以在Message Chains的不同位置进行这种重构手法。理论上你可以重构Message Chains上的任何一个对象,但这么做往往会把所有中介对象(intermediate object )都变成Middle Man。

通常更好的选择是:先观察Message Chains最终得到的对象是用来干什么的,看看能否以 Extract Method(提炼函数) 把使用该对象的代码提炼到一个独立函数中,再运用Move Method(搬移函数) 把这个函数推入Message Chains。如果这条链上的某个对象有多位客户打算航行此航线的剩余部分,就加一个函数来做这件事。

有些人把任何函数链(method chain。译注:就是Message Chains;面向对象领域中所谓「发送消息」就是「调用函数」)都视为坏东西,我们不这样想: 记住:

具体问题具体分析,先了解下Hide Delegate(157隐藏「委托关系」)这种手法

代码示例:
public class Person
{
    Department department;

    public Department getDepartment()
    {
        return department;
    }

    public Person getManager()
    {
        return department.getManager();
    }

    public void setDepartment( Department arg )
    {
        department = arg;
    }
}

public class Department
{
    private String chargeCode;

    private Person manager;

    public Department( Person person )
    {
        manager = person;
    }

    public Person getManager()
    {
        return manager;
    }
}

修改后:
public class ClientTest {
    public static void main(String[] args) {
        //如果客户希望直到某人的经理是谁,他必须先取得 Department 对象:
        Person person = new Person();
        Person manager = person.getDepartment().getManager();

        //这样的编码就是对客户揭露了 Department 的工作原理,于是客户直到 Department 用以追踪 “经理” 这条信息。
        // 如果对客户隐藏 Department,可以减少耦合。为了这一目的,我们在 Person 中建立一个简单的委托函数:

        //现在,修改 Person 的所有客户,让它们改用新函数:
        person.getManager();
        //只要完成了对 Department 所有函数的委托关系,并相应修改了 Person 的所有客户,就可以移除 Person 中访问函数 getDepartment () 了
    }
}

1.16 Middle Man(中间人)

在上一种坏味道中也提到了这个词。

一般来说,对象的基本特征之一就是封装,包括(1. 对外部隐藏内部属性。2.对外部隐藏内部实现细节)。封装往往伴随着前面距离的委托。一旦过度使用委托,多次委托,那么就会产生很多Middle Man(中间人,例举一件事需要通过多个人,但是最终你只需要通过最后一个人来实现即可,不需要知道中间的细节),这是指导致过度使用。

也许你看到一个类接口有一半的函数都委托给其他类,这样就是过度使用。,这是应该使用remove middle man(160),直接和真正对象打交道。

如果这样“不干实事”中间人的函数只有几个,可以运用InlineMethod(117)把它们放进调用端。 如果这些中间人还有其他行为,则可以运用replace Delegation with Inheritance(355继承替代委托类)把它变成实责对象的子类,这样你既可以拓展对象的行为,又不必负担那么多的委托动作。

1.17 Inappropriate Intimacy(狎昵关系)

有时你会发现两个类过于亲密,花费太多时间去探究彼此的private 成分属性,对于类,我们希望他们保持独立性。

就像古代恋人一样,过分的狎昵必须拆散,我们可以采用Move Method(142)和Move Field(146)帮它们划清界限,从而减少这种行为。

待续。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容