代码重构(一)——总结代码的坏味道

《重构-改善既有代码的设计》一书在第三章指出了22种代码的坏味道,下面是我对这些坏味道的总结,分享给大家,还请指正。

1. Duplicated Code(重复的代码)

重复的代码分三种类型:
- 位于同一个类:提炼成新方法进行调用
- 位于不同的子类:提炼成方法放进父类
- 位于完全不相干的类:提炼出一个新的类,将重复代码放进新的类中

我们看下面代码,有一个汽车类,要在控制台打印奔驰和宝马的详细信息

代码示例1:

public class Car{
    // 奔驰
    public void printBenz(String brand, String model, Integer price, double power) {
        // 打印基本信息
        System.out.println("品牌" + brand);
        System.out.println("型号:" + model);
        System.out.println("动力:" + power);
        System.out.println("价格:" + price);

        // 计算税费
        double salePrice = price;
        if (price > 200000) {
            salePrice = price * 0.98;
        }
        if (power <= 1.6) {
            System.out.println(salePrice * 0.05);
        } else {
            System.out.println(salePrice * 0.1);
        }
    }

    // 宝马
    public void printBmw(String brand, String model, Integer price, double power) {
        // 打印基本信息
        System.out.println("品牌" + brand);
        System.out.println("型号:" + model);
        System.out.println("动力:" + power);
        System.out.println("价格:" + price);

        // 计算税费
        double salePrice = price;
        if (price > 200000) {
            salePrice = price * 0.98;
        }
        if (power <= 1.6) {
            System.out.println(salePrice * 0.05);
        } else {
            System.out.println(salePrice * 0.1);
        }
    }
}

很明显,两个方法的逻辑基本一致,我们可以用Extract Method(提炼函数)的重构手法进行修改,也就是把重复的代码提炼出一个单独的方法,以便复用,提炼后的代码如下:

代码示例2:

public class Car{
    // 奔驰
    public void printBenz(String brand, String model, Integer price, double power) {
        printBasicInfo(brand, model, price, power);
        getTax(power, price);
    }

    // 宝马
    public void printBmw(String brand, String model, Integer price, double power) {
        printBasicInfo(brand, model, price, power);
        getTax(power, price);
    }

    // 提炼打印基本信息方法
    private void printBasicInfo(String brand, String model, Integer price, double power) {
        System.out.println("品牌" + brand);
        System.out.println("型号:" + model);
        System.out.println("动力:" + power);
        System.out.println("价格:" + price);
    }

    // 提炼计算税费的方法
    private double getTax(double power, Integer price){
        double salePrice = price;
        if (price > 200000) {
            salePrice = price * 0.98;
        }

        if (power <= 1.6) {
            return salePrice * 0.05;
        } else {
            return salePrice * 0.1;
        }
    }
}

优化了重复代码是我们的方法变的简洁,简洁之后我们就很容易发现另一种坏味道。

2. Long Parameter List(过长参数列)

比如打印基本信息的方法printBasicInfo,我们每次要传四个参数,如果汽车增加一个属性,入参要增加参数,这样需要改动多出代码,这时可以用Introduce Parameter Object(引入参数对象)进行重构,创建一个汽车对象在方法之间进行传递,见代码示例3。

3. Data Clumps(数据泥团)

数据泥团指的是经常一起出现的数据,比如示例二中每个方法的参数几乎相同,处理方式与过长参数列的处理方式相同,用Introduce Parameter Object(引入参数对象)将参数封装成对象。

代码示例3:

    // 奔驰
    public void printBenz(CarEntity carEntity) {
        printBasicInfo(carEntity);
        // 计算税费
        getTax(carEntity);
    }

    // 宝马
    public void printBmw(CarEntity carEntity) {
        printBasicInfo(carEntity);
        getTax(carEntity);
    }

    private void printBasicInfo(CarEntity carEntity) {
        System.out.println("品牌" + carEntity.getBrand());
        System.out.println("型号:" + carEntity.getModel());
        System.out.println("动力:" + carEntity.getPower());
        System.out.println("价格:" + carEntity.getPrice());
    }
    // 计算税费
    private double getTax(CarEntity carEntity) {
        // 打折后价格
        double salePrice = carEntity.getPrice();
        if (carEntity.getPrice() > 200000) {
            salePrice = carEntity.getPrice() * 0.98;
        }

        if (carEntity.getPower() <= 1.6) {
            return salePrice * 0.05;
        } else {
            return salePrice * 0.1;
        }
    }

重构后的好处很明显,除了参数列变短,就算汽车有新的属性进行扩展,也不需要修改参数列,这样更便于我们对代码的维护。

4. Long Method(过长函数)

对于过长的方法,我们可以用Extract Method(提炼函数)进行重构。但是如果方法内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。

接下来仔细看计算税费的方法,逻辑是这样的,用优惠后的价格乘以税率就是需要缴的税,而价格超过200000的会打98折,而税率是根据排量的大小计算,1.6L排量下的为5%,1.6以上税率是10%

虽然只有几行代码,却不能一目了然的使人理解,主要是因为出现了临时变量,这时我们可以用一种重构手法——Replace Temp with Query(以查询取代临时变量)把此方法重构成如下方式。

代码示例4:

    private double getTax(CarEntity carEntity) {
        return getSalePrice(carEntity) * getTaxRate(carEntity);
    }
    // 计算打折后价格
    private double getSalePrice(CarEntity carEntity) {
        if (carEntity.getPrice() > 200000) {
            return carEntity.getPrice() * 0.98;
        }
        return carEntity.getPrice();
    }
    // 计算税率
    private double getTaxRate(CarEntity carEntity) {
        if (carEntity.getPower() <= 1.6) {
            return 0.05;
        }
        return 0.1;
    }

分别把计算优惠后的价格和计算税率的步骤提炼出单独的方法,这样其他人看起来很容易理解。

5. Large Class(过大类)

对于过大的类,可以用Extract Class(提炼类)把不同的业务抽象到其他类中;如果一个类中的某些特性只能被一部分实例使用到, 可以用Extract Subclass(提炼子类)的方法, 将只能由一部分实例用到的特性转移到子类中,上面的汽车类Car.java就可以提炼出两个子类:

类图

代码示例5:

//Benz
public class Benz extends BaseCar {
    public void printBenz(CarEntity carEntity) {
        printBasicInfo(carEntity);
        getTax(carEntity);
    }
}
// BMW
public class Bmw extends BaseCar {
    public void printBmw(CarEntity carEntity) {
        printBasicInfo(carEntity);
        getTax(carEntity);
    }
}
// 父类
public class BaseCar {
    public void printBasicInfo(CarEntity carEntity) {

    }
    public double getTax(CarEntity carEntity) {

    }
}

这样我们把公用的提炼到了父类中,并且使子类的方法可以灵活扩展,又消除了一种坏味道。

6. Divergent Change(发散式变化)

多个业务发生变化时,修改的都是同一个类,说明此类承担的职责过多,可以运用Extract Class(提炼类)根据业务提炼到不同的类中。
举个例子,比如一个电商系统,有一个商品类,商品类中有计算价格和查询库存等方法:
这里写图片描述
如果计算价格和查询库存的业务都发生变化,我们都要修改Product类,这就是发散式变化。可以把价格和库存的方法都提炼出来,变成如下方式,这样价格和库存的业务互不影响,任凭价格的逻辑怎么改,我们都直接修改ProducePrice类,不会影响到库存的代码。
这里写图片描述

7. Shotgun Surgery(霰弹式修改)

一旦有业务修改,需要修改程序的多处,这种坏味道可以用Move Method(搬移函数)Move Field(搬移值域)把相同业务的代码放进同一个类。

还是商品计算价格的例子,假设现在有很多种类的商品:促销商品,团购商品,秒杀商品,每种商品类中都有计算价格的方法,如下图:

这里写图片描述

这时修改计算价格的逻辑我们需要修改三处代码,这就有了霰弹式修改的坏味道,可以把计算价格的方法提炼到一个类中。

8. Feature Envy(依恋情结)

当一个类的函数为了计算经常调用另一个类的一大堆的函数时就表示出现了依恋情节。将此部分出现依恋情节的代码提炼成函数放到另一个类里面

9. Primitive Obsession(基本型别偏执)

使用对象把基本类型封装起来,下面是一个订单类,包含用户名、用户性别、订单价格、订单id等信息。利用Replace Data Value with Object(以对象取代数据值)把用户相关信息提炼成一个单独的Custom类,再在订单类中引用Custom对象。

这里写图片描述

代码示例6:

// 订单
public class Order {
    private String customName;
    private String customSex;

    private Integer orderId;
    private Integer price;
}


----------


// 把custom相关字段封装起来,在Order中引用Custom对象
public class Custom {
    private String name;
    private String address;
}

// 订单
public class Order {
    private Custom custom;
    private Integer orderId;
    private Integer price;
}

10. Switch Statements(switch惊悚现身)

switch语句的问题在于重复,如果要为它添加一个新的子句,你必须找到所有switch语句并修改它们,这种情况我们可以引用工厂 + 策略模式。用工厂把重复的switch提炼到一起构建成一个工厂类,策略模式把switch分支中执行的动作提炼成单独的类。

还拿汽车的例子来说,查询不同品牌汽车的价格,需要不同的逻辑,直接写出代码就是这样的:

代码示例7:

    public void getPrice(String type){
        if ("Benz".equals(type)) {
            System.out.println("奔驰车价格");
        } else if ("BMW".equals(type)) {
            System.out.println("宝马车价格");
        } else if ("audi".equals(type)) {
            System.out.println("奥迪车价格");
        }
    }

业务不复杂的时候上面这段代码很简单,那如果又加了一个方法,查询不同品牌汽车的剩余库存呢,我们还需要写一段相同的if else:

代码示例8:

    public void getStock(String type){
        if ("Benz".equals(type)) {
            System.out.println("奔驰车的剩余库存。。。");
        } else if ("BMW".equals(type)) {
            System.out.println("宝马车的剩余库存。。。");
        } else if ("audi".equals(type)) {
            System.out.println("奥迪车的剩余库存。。。");
        }
    }

现在,我们加一个汽车的类型,那么需要把两段代码的if else同时加一个分支,这时候不仅仅出现了第一个坏味道,重复的代码,还伴随着霰弹式修改。

下面我们就用工厂+策略模式重构上面代码,看类图,把不同品牌的车抽象出了不同的类,用工厂根据类型帮我们创建不同的汽车类。
这里写图片描述

代码示例9:

// 工厂类
public class CarFactory {
    public CarService instance(String type){
        if ("Benz".equals(type)) {
            return new BenzService();
        } else if ("BMW".equals(type)) {
            return new BWMService();
        } else if ("audi".equals(type)) {
            return AudiService();
        }
    }
}

// 不同品牌的汽车接口,定义汽车的所有方法
public interface CarService {
    Integer getPrice();
}

// 奔驰车实现类
public class BenzService implements CarService {
    @Override
    public Integer price() {
        // 返回奔驰车的价格
        return 1000000;
    }
}

// 宝马车实现类
public class BMWService implements CarService {
    @Override
    public Integer price() {
        // 返回宝马车的价格
        return 2000000;
    }
}

....

// 客户端根据品牌取价格,无需再用if else
public static void main(String[] args) {
    String type = "Benz";
    CarFactory factory = new CarFactory();
    Integer price = factory.instance(type).getPrice();
    System.out.println(price);
}

11.Parallel Inheritance Hierarchies(平行继承体系)

每当你为某个类增加一个子类时,必须为另外一个类增加子类,那么就有问题。这种情况我们让一个继承体系的实例去引用另一个继承体系。

12. Lazy Class(冗赘类)

如果一个类没什么价值,应该让他消失。比如父子类之间的差别不大,就可以合并成一个。

这里写图片描述

13. Speculative Generality(夸夸其谈未来性)

不要去考虑未来将有可能发生的事,如果用不到就不值得。

14. Temporary Field(令人迷惑的暂时值域)

某个实例变量仅为某种特定情况而设,这样的代码就会不易理解,可以提炼新的对象以适应此种特殊情况。

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

对象1调对象2,对象2调对象3。。。这就形成了一条消息链,下面的例子,客户端要知道Department类的manager是谁,要通过Person找到,也就是person.getDepartment().getManger();

这里写图片描述

可以使用Hide Delegate(隐藏「委托关系」)讲关系变成如下形式:
这里写图片描述
这里我们把getManager()的方法委托给了Person类,这样我们直接person.getManger()就能得到结果。

16. Middle Man(中间转手人)

万事必有反,如果中间人承受的委托过多,就会造成真正负责的类不做事,这种情况要移除中间人,也就是把Message Chains(过度耦合的消息链)的处理方式反过来。

17. Inappropriate Intimacy(狎昵关系)

说的是两个classes过于亲密,一个典型的例子是双向关联(bidirectional associations),比如刚刚订单的例子,一个订单对应一个客户,一个客户又可以对应多个订单,这就形成了双向关联,如下图。

这里写图片描述

这两个类会过于依赖,如果order作废会对custom造成影响,这种情况我们发现,Custom类并不需要包含订单信息,也就可以改为单向关联来减少耦合,如下图。

这里写图片描述

18. Alternative Classes with Different Interfaces(异曲同工的类)

是指两个类做的差不多的事,需要根据不同的业务把方法搬移到不同的类中。

19. Incomplete Library Class(不完美的程序库类)

当你用别人的类库但不能满足需求时,需要扩展可以试试Introduce Local Extension(引入本地扩展)进行重构,假设我们有一个Date类,我们在不能修改源码并且需要扩展的时候,可以建立一个子类继承Date类,在子类中进行扩展。
这里写图片描述

20. Data Class(纯稚的数据类)

简单的说就是把字段用get、set方法封装起来,把私有变量隐藏好。

21. Refused Bequest(被拒绝的遗贈)

指的是一个子类,不需要父类中的过多方法,这样我们可以为这个子类创建一个兄弟类,把父类中不需要的方法下移到兄弟类中去。

22. Comments(过多的注释)

并不是说写注释不好,而是当你写一段很长的注释来说明代码逻辑的时候,说明这段代码真的很糟糕,你就要考虑重构了。
引用作者的一句话“当你感觉需要撰写注释,请先尝试重构,试着让所有注释都变得多余。”

总结

根据我目前的理解,重构的目的是让代码便于维护和理解,而遵循的原则是封装和解耦,封装使代码可复用,解耦使代码逻辑清晰,维护性变高。

在迭代迅速的互联网产品开发中,消除代码的坏味道,才能达到“需求万变,即刻上线”的目标。

  • 5
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值