重构——清除代码中的坏味道

1,320 阅读23分钟

重构.png

一、何谓重构

1.基本概念

  • “重构”这个词既可以用作名词也可以用作动词

    • 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本
    • 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构

2.虚假的重构

  • 如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的事不是重构

3.重构与性能优化

  • 相似之处:两者都需要修改代码,并且两者都不会改变程序的整体功能
  • 差别:重构是为了让代码“更容易理解,更易于修改”;性能优化只关心让程序运行的更快

二、为何重构

  • 重构改进软件的设计

完成同样一件事,设计欠佳的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事,因此改进设计的一个重要方向就是消除重复代码。这将使未来可能的程序修改动作容易很多

  • 重构使软件更容易理解

编程的核心在于“准确说出我想要的”。但除了计算机外,源码还有其他读者:几个月后另一位尝试读懂代码并做修改的程序员,尤其是这位读者可能还是开发者自己

  • 重构帮助找到bug

重构可以让我们深入理解代码的所作所为,帮助我们更有效的写出健壮的代码

我不是一个特别好的程序员,我只是一个有着一些特别好的习惯的还不错的程序员。

——Kent Beck

  • 重构提高编程速度

软件的内部质量很重要,需要添加新功能时,内部质量良好的软件让我可以很容易找到在哪里修改、如何修改。良好的模块划分使我只需要理解代码库的一小部分,就可以做出修改

三、何时重构

第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。(事不过三)

——Don Roberts

不值得重构的情况:

  • 丑陋的代码隐藏于API下
  • 重写比重构还容易

四、重构步骤

五、一个例子

0.原代码

  • 这个方法的用途是消费者接收订单消息,将其转换为VO类
    public static TradeVO toTradeVO(int messageType, OrderInfo orderInfo, PayInfo payInfo, List<RefundInfo> refundInfos, Boolean isFlush) {

        if (Objects.isNull(orderInfo) || Objects.isNull(payInfo)) {
            return null;
        }
        
        TradeVO tradeVO = new TradeVO();
        
        // 基本信息
        tradeVO.setPoiId(payInfo.getPoiId());
        tradeVO.setOrderId(payInfo.getOrderId());
        tradeVO.setUserId(orderInfo.getUserId());
        tradeVO.setUserType(orderInfo.getUserType());
        
        // 金额相关信息
        tradeVO.setOrderAmount(payInfo.getTotalAmount());
        tradeVO.setPayAmount(payInfo.getPayAmount());
        
        // 补贴相关信息
        tradeVO.setDiscountAmount(orderInfo.getDiscountAmount());
        tradeVO.setReceiptTime(payInfo.getPayTime());

        relateTradeVO(tradeVO, messageType, payInfo, refundInfos, isFlush);

        if (orderInfo.isCardBill()) {
            tradeVO.setPayAmount(payInfo.getPayAmount() - orderInfo.getCardAmount());
            tradeVO.setOrderAmount(payInfo.getTotalAmount() - orderInfo.getCardAmount());
            tradeVO.setCardFee(0);
        }
        
        // 填充订单类型
        tradeVO.setOrderType(orderInfo.getOrderType());
        if (orderInfo.getOrderType() == OrderTypeEnum.DISCOUTN.getType())) {
            tradeVO.setOrderType(OrderTypeEnum.ALL_PAY.getType());
        }

        if (isSecondHalfSingle(orderInfo.getOrderType(), orderInfo.getPreferentials())) {
            tradeVO.setOrderType(OrderTypeEnum.SECOND_HALF_SINGLE.getType());
        }
      
        fillMarketingType(orderInfo, tradeVO);
        
        // XXX 50行代码填充其他信息

    }

1.单元测试

    //单品订单-已完成
	@Test
    public void toTradeVO_SINGLE_COMPLETE() {
        // XXX
    }

	//折扣订单-已完成
    @Test
    public void toTradeVO_DISCOUNT_COMPLETE() {
        // XXX
    }

	//满减订单-已完成
    @Test
    public void toTradeVO_DEDUCTION_COMPLETE() {
        // XXX
    }

	//单品订单-退款
    @Test
    public void toTradeVO_SINGLE_REFUND() {
        // XXX
    }

	//折扣订单-退款
    @Test
    public void toTradeVO_DISCOUNT_REFUND() {
        // XXX
    }

	//满减订单-退款
    @Test
    public void toTradeVO_DEDUCTION_REFUND() {
        // XXX
    }

2.分解函数

1)过长函数

  • 首先应该注意到的问题是,这个方法行数过多: 阿里开发规范中规定,一个方法中代码行数不宜超过80行
  • 同时不满足CleanCode中一个很重要的原则,同一个函数中的方法要处于同一层级: 其中 fillMarketingType 和其他set方法就明显不是同一个层级,应该对set方法进行封装
  • 具体执行时,有一个小技巧,找代码中的注释:如果代码前方有一行注释,就是在提醒你——可以将这段代码替换成一个函数
    public static TradeVO toTradeVO(int messageType, OrderInfo orderInfo, PayInfo payInfo, List<RefundInfo> refundInfos, Boolean flush) {
        if (Objects.isNull(orderInfo) || Objects.isNull(payInfo)) {
            return null;
        }

        TradeVO tradeVO = new TradeVO();

        fillBaseInfo(orderInfo, payInfo, tradeVO);

        fillAmountInfo(orderInfo, payInfo, tradeVO);

        fillReceiptInfo(orderInfo, payInfo, tradeVO);

        relateTradeVO(tradeVO, messageType, payInfo, refundInfos, flush);

        fillOrderType(orderInfo, tradeVO);

        fillMarketingType(orderInfo, tradeVO);

        // XXX 其他fill方法

        return searchTradeVO;
    }

2)过长参数列表-合并对象

  • 方法签名中比较明显的问题是:方法参数过多了,不宜超过4个

    • 其中orderInfo、payInfo、refundInfos都是订单数据,都是从Mafka消息体中解析出来的,通常一起使用,因此可以合并为一个TO类
    public static TradeVO toTradeVO(int messageType, OrderInfoTO orderInfoTO, Boolean flush) {
        if (Objects.isNull(orderInfoTO.getOrderInfo()) || Objects.isNull(orderInfoTO.getPayInfo())) {
            return null;
        }
        OrderInfo orderInfo = orderInfoTO.getOrderInfo();
        PayInfo payInfo = orderInfoTO.getPayInfo();
        List<RefundInfo> refundInfos = orderInfoTO.getRefundInfos();

        TradeVO tradeVO = new TradeVO();

        fillBaseInfo(orderInfo, payInfo, tradeVO);

        fillAmountInfo(orderInfo, payInfo, tradeVO);

        fillReceiptInfo(orderInfo, payInfo, tradeVO);

        relateTradeVO(tradeVO, messageType, payInfo, refundInfos, flush);

        fillOrderType(orderInfo, tradeVO);

        fillMarketingType(orderInfo, tradeVO);

        // XXX 其他fill方法

        return searchTradeVO;
    }

3)过长参数列表-区分函数行为的标记

  • 方法中的布尔值参数,坏味道有点刺鼻了:这是一种“打补丁”行为,对代码的伤害很大

    • 把通过布尔值来区分的方法拆解为2个方法,一个处理布尔值为true的场景,一个处理为false的场景
  • relateTradeVO的拆分:原有的relateTradeVO方法,是根据布尔值作了个if判断,分别赋了不同的值,我们把它也拆成两个方法就好了

	//布尔值为true
	public static TradeVO toTradeVOFlush(int messageType, OrderInfoTO orderInfoTO) {
        if (Objects.isNull(orderInfoTO.getOrderInfo()) || Objects.isNull(orderInfoTO.getPayInfo())) {
            return null;
        }
        OrderInfo orderInfo = orderInfoTO.getOrderInfo();
        PayInfo payInfo = orderInfoTO.getPayInfo();
        List<RefundInfo> refundInfos = orderInfoTO.getRefundInfos();

        TradeVO tradeVO = new TradeVO();

        fillBaseInfo(orderInfo, payInfo, tradeVO);

        fillAmountInfo(orderInfo, payInfo, tradeVO);

        fillReceiptInfo(orderInfo, payInfo, tradeVO);

        relateTradeVOFlush(tradeVO, messageType, payInfo, refundInfos);

        fillOrderType(orderInfo, tradeVO);

        fillMarketingType(orderInfo, tradeVO);

        // XXX 其他fill方法

        return searchTradeVO;
    }

	//布尔值为false
    public static TradeVO toTradeVONoFlush(int messageType, OrderInfoTO orderInfoTO) {
        if (Objects.isNull(orderInfoTO.getOrderInfo()) || Objects.isNull(orderInfoTO.getPayInfo())) {
            return null;
        }
        OrderInfo orderInfo = orderInfoTO.getOrderInfo();
        PayInfo payInfo = orderInfoTO.getPayInfo();
        List<RefundInfo> refundInfos = orderInfoTO.getRefundInfos();

        TradeVO tradeVO = new TradeVO();

        fillBaseInfo(orderInfo, payInfo, tradeVO);

        fillAmountInfo(orderInfo, payInfo, tradeVO);

        fillReceiptInfo(orderInfo, payInfo, tradeVO);

        relateTradeVONoFlush(tradeVO, messageType, payInfo, refundInfos);

        fillOrderType(orderInfo, tradeVO);

        fillMarketingType(orderInfo, tradeVO);

        // XXX 其他fill方法

        return searchTradeVO;
    }

4)重复代码

  • 经过了上一步的重构,代码出现了新的坏味道:大量的重复代码

    • 将代码中重复的部分提炼出来
	public static TradeVO toTradeVO(int messageType, OrderInfoTO orderInfoTO) {
        if (Objects.isNull(orderInfoTO.getOrderInfo()) || Objects.isNull(orderInfoTO.getPayInfo())) {
            return null;
        }
        OrderInfo orderInfo = orderInfoTO.getOrderInfo();
        PayInfo payInfo = orderInfoTO.getPayInfo();
        List<RefundInfo> refundInfos = orderInfoTO.getRefundInfos();

        TradeVO tradeVO = new TradeVO();

        fillBaseInfo(orderInfo, payInfo, tradeVO);

        fillAmountInfo(orderInfo, payInfo, tradeVO);

        fillReceiptInfo(orderInfo, payInfo, tradeVO);

        fillOrderType(orderInfo, tradeVO);

        fillMarketingType(orderInfo, tradeVO);

        // XXX 其他fill方法

        return searchTradeVO;
    }

	//布尔值为true
	public static TradeVO toTradeVOFlush(int messageType, OrderInfoTO orderInfoTO) {
        TradeVO tradeVO = toTradeVO(messageType, orderInfoTO);
        relateTradeVOFlush(tradeVO, messageType, orderInfoTO);
        return tradeVO;
    }

	//布尔值为false
	public static TradeVO toTradeVONoFlush(int messageType, OrderInfoTO orderInfoTO) {
        TradeVO tradeVO = toTradeVO(messageType, orderInfoTO);
        relateTradeVONoFlush(tradeVO, messageType, orderInfoTO);
        return tradeVO;
    }

5)神秘命名

  • 整洁代码中最重要的一环就是好的名字:不常用的单词常常让人摸不到头脑

    • relateTradeVO这个方法命名就很奇怪,实际做的事情是往VO类中填充数据,不如把relate替换为fill
  • 提炼函数后,代码的命名也需要改动:函数的意义改变了

    • toTradeVO这个方法实际上也变成了一个往VO类中填充数据的方法,因此我们把VO类的初始化提炼出来,把to替换为fill
	//布尔值为true
	public static TradeVO toTradeVOFlush(int messageType, OrderInfoTO orderInfoTO) {
        TradeVO tradeVO = new TradeVO();
        fillTradeVO(tradeVO, orderInfoTO);
        fillTradeVOFlush(tradeVO, messageType, orderInfoTO);
        return tradeVO;
    }

	//布尔值为false
	public static TradeVO toTradeVONoFlush(int messageType, OrderInfoTO orderInfoTO) {
        TradeVO tradeVO = new TradeVO();
        fillTradeVO(tradeVO, orderInfoTO);
        fillTradeVONoFlush(tradeVO, messageType, orderInfoTO);
        return tradeVO;
    }

	public static void fillTradeVO(int messageType, OrderInfoTO orderInfoTO) {
        if (Objects.isNull(orderInfoTO.getOrderInfo()) || Objects.isNull(orderInfoTO.getPayInfo())) {
            return null;
        }
        OrderInfo orderInfo = orderInfoTO.getOrderInfo();
        PayInfo payInfo = orderInfoTO.getPayInfo();
        List<RefundInfo> refundInfos = orderInfoTO.getRefundInfos();
        
        fillBaseInfo(orderInfo, payInfo, tradeVO);

        fillAmountInfo(orderInfo, payInfo, tradeVO);

        fillReceiptInfo(orderInfo, payInfo, tradeVO);

        fillOrderType(orderInfo, tradeVO);

        fillMarketingType(orderInfo, tradeVO);

        // XXX 其他fill方法
    }

3.拆分阶段

  • 拆分阶段的目的:提高方法的复用性

  • 具体做法:引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中

  • 最简单的拆分逻辑:就是把一大段行为分成顺序执行的两个阶段,如 数据处理阶段+数据展示阶段

  • 下面这个方法中

    • 正面:前面两行是正面例子,将方法拆分为两个阶段,第一阶段做数据处理,第二阶段把DTO拼装成VO类展示
    • 反面:第三行实际上应该合并入第一行,做的事情也是数据处理
    public TradeVO geTradeVO(String orderId) {
        OrderDTO dto = getOrderDTOByOrderIdO(orderId);// 第一阶段,将数据处理为DTO类
        TradeVO tradeVO = TradeVOConverter.toTradeVO(dto);// 第二阶段,将数据拼装为VO类
        fillRemitAmount(Collections.singletonList(tradeVO));// 坏味道,应该合并入第一阶段
        return tradeVO;
    }

4.引入多态

  • 引入多态的目的:提升方法的可读性和扩展性

  • 下面的VO类,可以根据messageType拆分为多个VO,这样我们把公共的逻辑放到父类中,对应 初始化订单、支付成功订单、退款订单 的逻辑放在子类中

  • 注意样例方法如果引入多态,实际上是一种过度设计

    • 如果函数本身并不复杂,引入多态反而会让系统更难理解和维护

六、代码的坏味道

1.神秘命名

定义

整洁代码中最重要的一环就是好的名字(包括函数、模块、变量和类)

原因

很多人不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间

解决

改名可能是最常用的重构手法

Demo

//重构前 方法命名过于简单 
public PoiDTO getEntity(Long id)
//重构后
public PoiDTO getPoiInfo(Long id)

2.重复代码

定义

在一个以上的地点有相同的代码结构

原因

一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改

解决

  • 同一个类的两个函数含有相同的表达式——「提炼函数」
  • 重复代码只是相似而不是完全相同——先 「移动语句」重组代码顺序,把相似的部分放在一起以便提炼
  • 重复代码位于一个超类的不同子类——「代码上移」来避免在两个子类之间互相调用

Demo

// 重构前 重复代码
public UserVO getUserInfo(UserDto user, CardDto card) {
     // XXX
     if (card != null) {
        vo.setCardId(card.getCardId());
        vo.setCardAmount(card.getAmount());
     }
     // XXX
}

public List<UserVO> batchGetUserInfo(UserDto user, CardDto card) {
     // XXX
     if (card != null) {
        vo.setCardId(card.getCardId());
        vo.setCardAmount(card.getAmount());
     }
     // XXX
}

// 重构后
public UserVO getUserInfo(UserDto user, CardDto card) {
     // XXX
     fillCardInfo(vo, card);
     // XXX
}

public List<UserVO> batchGetUserInfo(UserDto user, CardDto card) {
     // XXX
     fillCardInfo(vo, card);
     // XXX
}

// 把重复的代码单独抽出为一个方法
private void fillCardInfo(UserVO vo, CardDto card) {
    if (card == null) {
       return;
    }
    vo.setCardId(card.getCardId());
    vo.setCardAmount(card.getAmount());
}

3.过长函数

定义

函数的行数过长

原因

据我们的经验,活得最长、最好的程序,其中的函数都比较短。因为函数越长,就越难理解

一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名

解决

  • 「提炼函数」

如何确定该提炼哪一段代码:

  • 寻找注释

    • 它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名
  • 条件表达式
    • 对于庞大的switch语句,其中的每个分支都应该通过提炼函数变成独立的函数调用
  • 循环

    • 应该将循环和循环内的代码提炼到一个独立的函数中
    • 如果发现提炼出的循环很难命名,可能是因为其中做了几件不同的事,需要拆分循环

Demo

// 重构前
public static List<Pair<Integer, Integer>> changeList(List<Pair<Integer, Integer>> list) {
  //校验
  if (list == null || list.size() > 20) {
    return null;
  }
  //对元素做操作
  for (int i = 0; i < list.size(); i++) {
    Pair<Integer, Integer> pair = list.get(i);
    if (pair.getRight() > 10) {
      pair.setValue(10);
    }
  }
  //对集合中的元素排序
  list = list
    .stream()
    .sorted(Comparator.comparing(Pair::getLeft))
    .collect(Collectors.toList());
  return list;
}  

// 重构后
public static List<Pair<Integer, Integer>> changeList(List<Pair<Integer, Integer>> list) {
  if (!check(list)) {
    return null;
  }
  operation(list);
  return sort(list);
}

private boolean check(List<Pair<Integer, Integer>> list) {
  if (list == null || list.size() > 20) {
     return false;
  }
  return true;
}

private void operation(List<Pair<Integer, Integer>> list) {
  for (int i = 0; i < list.size(); i++) {
    Pair<Integer, Integer> pair = list.get(i);
    if (pair.getRight() > 10) {
      pair.setValue(10);
    }
  }
}

private List<Pair<Integer, Integer>> list sort(List<Pair<Integer, Integer>> list) {
  return list
    .stream()
    .sorted(Comparator.comparing(Pair::getLeft))
    .collect(Collectors.toList());
}

4.过长参数列表

定义

一个函数的入参过多

原因

过长的参数列表本身也经常令人迷惑

解决

  • 如果可以向某个参数发起查询而获得另一个参数的值——「以查询取代参数」去掉这第二个参数
  • 从现有数据结构中抽出很多数据项——「保持对象完整」,直接传入原来的数据结构
  • 有几项4参数总是出现——「引入参数对象」将其合并成一个对象
  • 某个参数被用作区分函数行为的标记——「移除标记参数」根据标记参数拆分为两个或多个函数

Demo

// 重构前
public SearchVO getSearchVO(double latitude, double longitude, int cityId, List<Integer> topPoiId);
// 重构后
public SearchVO getSearchVO(SearchReq searchReq);

5.全局数据

定义

公开全局可修改的变量

原因

全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改

解决

  • 「封装变量」,可以看见修改它的地方,并开始控制对它的访问

Demo

// 重构前
public String templateId;




// 重构后
private String templateId;

public void getTemplateId() {
    return templateId;
}

public void setTemplateId(String templateId) {
    this.templateId = templateId;
}

6.可变数据

定义

被初始化后可以修改的数据

原因

对数据的修改经常导致出乎意料的结果和难以发现的bug

解决

  • 「封装变量」来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进
  • 一个变量在不同时候被用于存储不同的东西——「拆分变量」将其拆分为各自不同用途的变量
  • 如果可变数据能在其他地方计算出来——「以查询取代派生变量」即可

Demo

// 重构前
public String templateId;



// 重构后
public final String checkTemplateId;
public final String sendTemplateId;

7.发散式变化

定义

某个模块因为不同的原因在不同的方向上发生变化

原因

一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改

解决

  • 如果发生变化的两个方向自然地形成了先后次序——「拆分阶段」将两者分开,两者通过一个清晰的数据结构进行沟通
  • 如果两个方向之间有更多的来回调用,就应该先创建适当的模块——「搬移函数」把处理逻辑分开
  • 如果函数内部混合了两类处理逻辑——先「提炼函数」将其分开,再做搬移
  • 如果模块是以类的形式定义的——「提炼类」来做拆分

Demo

// 重构前
public int getPrice(Order order) {
    // 获取基础价格
    double basePrice = order.getQuantity() * order.getItemPrice();  
    // 获取折扣
    double quantityDiscount = Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
    // 获取运费
    double = shipping = Math.min(basePrice * 0.1100);
    // 计算价格
    return basePrice - quantityDiscount + shipping;
}


// 重构后

// 计算商品价格
public double getPrice(Order order) {
    return calBasePrice(order) - calDiscount(order) + calShipping(calBasePrice(order));
}

// 计算基础价格
public double calBasePrice(Order order) {
    return order.getQuantity() * order.getItemPrice();
}
// 计算折扣
public double calDiscount(Order order) {
    return Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
}
// 计算运费
public double calShipping(double basePrice) {
    return Math.min(basePrice * 0.1100);
}

8.霰弹式修改

定义

每遇到某种变化,都必须在许多不同的类内做出许多小修改

原因

如果需要修改的代码散布四处,不但很难找到它们,也很容易错过某个重要的修改

解决

  • 「搬移函数」和「搬移字段」把所有需要修改的代码放进同一个模块里
  • 如果有很多函数都在操作相似的数据——「函数组合成类」
  • 如果有些函数的功能是转化或者充实数据结构——「函数组合成变换」
  • 如果一些函数的输出可以组合后提供给一段专门使用这些计算结果的逻辑——「拆分阶段」
  • 「内联函数」或是「内联类」,把本不该分散的逻辑拽回一处

Demo

// 重构前
// class1
double coinBack = BigDecimal.valueOf(payAmount).multiply(BigDecimal.valueOf(10)).multiply(BigDecimal.valueOf(0.02));
// class2
double doubleCoinBack = BigDecimal.valueOf(payAmount).multiply(BigDecimal.valueOf(10)).multiply(BigDecimal.valueOf(0.04));


// 重构后
class Coin {
    public double coinBack(double payAmount) {
        return BigDecimal.valueOf(payAmount).multiply(BigDecimal.valueOf(10)).multiply(BigDecimal.valueOf(0.02));
    }
    
    public double doubleCoinBack(double payAmount) {
        return BigDecimal.valueOf(payAmount).multiply(BigDecimal.valueOf(10)).multiply(BigDecimal.valueOf(0.04));
    }
}

9.依恋情结

定义

一个函数跟另一个模块中函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流

原因

应该最大化区域内部的交互、最小化区域外部的交互

解决

  • 判断哪个模块拥有的此函数使用的数据最多,「搬移函数」, 在这之前可以先「提炼函数」对函数分解

Demo

// 重构前
public class Test1 {
    @Resource
    private Test2 test2;
    
    public int getPrice(Order order) {
        // XXX
        // 获取基础价格
        double basePrice = test2.calBasePrice(order);  
        // 获取折扣
        double quantityDiscount = test2.calDiscount(order);
        // 获取运费
        double = shipping = test2.calShipping(order);
        // 计算价格
        return test2.calBasePrice(order) - test2.calDiscount(order) + test2.calShipping(calBasePrice(order));
    }
}

public class Test2 {
    // 计算基础价格
    public double calBasePrice(Order order) {
        return order.getQuantity() * order.getItemPrice();
    }
    // 计算折扣
    public double calDiscount(Order order) {
        return Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
    }
    // 计算运费
    public double calShipping(double basePrice) {
        return Math.min(basePrice * 0.1100);
    }
}

// 重构后
public class Test1 {
    @Resource
    private Test2 test2;
    
    public int getPrice(order) {
        // XXX
        return test2.getPrice(order);
    }
}

public class Test2 {
    // 计算基础价格
    public double calBasePrice(Order order) {
        return order.getQuantity() * order.getItemPrice();
    }
    // 计算折扣
    public double calDiscount(Order order) {
        return Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
    }
    // 计算运费
    public double calShipping(double basePrice) {
        return Math.min(basePrice * 0.1100);
    }
    // 计算商品价格
    public double getPrice(Order order) {
        return calBasePrice(order) - calDiscount(order) + calShipping(calBasePrice(order));
    }
}

10.数据泥团

定义

两个类中相同的字段、许多函数签名汇总相同的参数

原因

这些总是绑在一起出现的数据应该拥有属于它们自己的对象

解决

  • 「提炼类」将它们提炼到一个独立对象中,之后将注意力转移到函数签名上,「引入参数对象」或「保持对象完整」为它瘦身

一个好的评判方法是:删掉众多数据中的一项。如果这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是一个明确信号:你应该为它们产生一个新对象

Demo

// 重构前 订单信息和订单的退款信息
public Date getOrderVO(OrderInfo orderInfo, RufundInfo rufundInfo);



// 重构后
public Date getOrderVO(OrderDto);

class OrderDto {
    OrderInfo orderInfo;
    RufundInfo rufundInfo;
}

11.基本类型偏执

定义

很多时候我们没有创建对自己的问题域有用的基本类型

原因

一些方法可以封装,一些应该绑定在一起的变量没有被绑定,如:钱的金额和单位

解决

  • 「以对象取代基本类型」将原本单独存在的数据值替换为对象

Demo

// 重构前
public Date getTime(int time);



// 重构后
public Date getTime(TimeDto timeDto);

class TimeDto {
    int time;
    int unit;
}

12.重复的switch

定义

  • 狭义:任何switch语句都应该用「以多态取代条件表达式」消除掉
  • 广义:在不同的地方反复使用同样的switch逻辑

原因

每当想增加一个选择分支时,必须找到所有的switch,并逐一更新

解决

  • 多态,「以多态取代条件表达式」

Demo

用策略模式+简单工厂取代switch语句

13.循环语句

定义

代码中传统的for和while

原因

管道操作(如filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作

解决

  • 「以管道取代循环」让这些老古董退休

Demo

class UserInfo {
    long userId;
    String userName;
}
// 重构前
public List<Long> getUserIdList(List<UserInfo> list) {
    List<Long> res = new ArrayList<>();
    for (UserInfo userInfo : list) {
        res.add(userInfo.getUserId);
    }
    return res;
}

// 重构后
public List<Long> getUserIdList(List<UserInfo> list) {
    return res.stream().map(UserInfo::getUserId).collect(Collectors.toList());
}

14.冗余的元素

定义

程序元素(如类和函数)能给代码增加结构

原因

有时不需要这一层额外的结构

解决

  • 「内联函数」或「内联类」
  • 如果这个类处于一个继承体系中,「折叠继承体系」

Demo

// 重构前
interface Strategy {
    // XXX
}

class NewUserStrategy implements Strategy {
    // XXX
}



// 重构后
class NewUserStrategy {
    // XXX
}

15.夸夸其谈通用性

定义

为了“总有一天需要做这事”,企图以各式各样的钩子和特殊情况来处理一些非必要的事情

原因

这么做往往造成系统更难理解和维护

解决

  • 如果某个抽象类其实没有太大作用——「折叠继承体系」
  • 不必要的委托——「内联函数」和「内联类」除掉
  • 如果函数的某些参数未被用上或并非真正需要——改变「函数声明」去掉这些参数

Demo

// 重构前
public long getCoinBack(long coin, int type) {
    if (type == 1) {
        return coin * 0.2;
    } else if (type == 2) {
        
    } else {
        
    }
    return 0;
}

// 重构后
public long getCoinBack(long coin) {
    return coin * 0.2;
}

16.临时字段

定义

类的内部某个字段仅为某种特殊情况而设

原因

这样的代码让人不易理解,因为通常认为对象在所有时候都需要它的所有字段

解决

  • 「提炼类」给这个临时字段创造一个新家,「搬移函数」把所有和这个字段相关的代码都放到新家中

Demo

// 重构前
class SingleVO {
    // XXX 
    // 这两个变量只有在调用了fillStore()函数后才会被赋值
    int singleNum;
    int todaySingleNum;
    // XXX
}



// 重构后
class SingleVO {
    // XXX 原有变量
}

class StoreVO {
    int singleNum;
    int todaySingleNum;
}

17.过长的消息链

定义

用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象,这就是消息链。实际代码中看到的可能是一长串取值函数或一长串临时变量

原因

采取这种方式,意味着客户端代码将与查找过程中的导航结构紧密耦合,一旦对象间的关系发生任何变化,客户端就不得不做出相应修改

解决

  • 「隐藏委托关系」
  • 先观察消息链最终得到的对象是用来干什么的,看看能否以「提炼函数」把使用该对象的代码提炼到一个独立的函数中,再「搬移函数」把这个函数推入消息链。

Demo

// 重构前
public OrderVO getOrderVO(String orderId) {
    OrderInfo orderInfo = tradeService.getOrderInfo(orderId);
    PayInfo payInfo = tradeService.getPayInfo(orderInfo.getPayId());
    OrderDto orderDto = OrderDto.convert(orderInfo);
    RefundInfo refundInfo = tradeService.getRefundInfo(orderDto));
    return OrderVO.convert(orderInfo, payInfo, refundInfo);
}

// 重构后
public OrderVO getOrderVO(String orderId) {
    Order order = tradeService.getOrderInfo(orderId);
    return OrderVO.convert(order);
}

18.中间人

定义

对象的基本特征之一就是封装,封装往往伴随着委托

原因

人们可能过度运用委托,某个类的接口有一半的函数都委托给其他类,这样就是过度运用

解决

  • 「移除中间人」 ,直接和真正负责的对象打交道
  • 如果“不干正事”的函数只有少数几个——「内联函数」把它们放进调用端

Demo

// 重构前
class Person {
    private Manager manager;
    
    public Manager doThings1() {
        
    }
    
    public Manager doThings2() {
        
    }
}

// 重构后
class Person {
    private Manager manager;
    
    public Manager getManager() {
        return manager;
    }
}

19.内幕交易

定义

软件开发者喜欢在模块之间筑起高墙,极其反感大量交换数据,增加耦合,但一定的数据交换不可避免

原因

必须尽量减少这种情况,并把这种交换都放在明面上来

解决

  • 如果两个模块总是私下交换数据——「搬移函数」和「搬移字段」减少私下交流
  • 如果两个模块有共同的兴趣——新建一个模块,把公用数据放在管理良好的地方,或「隐藏委托关系」,把另一个模块变成两者的中介

20.过大的类

定义

类中字段过多

原因

会导致重复的代码过多

解决

  • 使用「提炼类」将几个变量一起提炼到新类内
  • 观察大类的使用者,看使用者是否只用到了这个类所有功能的一个子集,尝试「提炼类」、「提炼超类」或是「以子类取代类型码」将其拆分出来

Demo

// 重构前
class SingleVO {
    // 30个变量
}



// 重构后
class SingleVO {
    
    SingleBaseInfo singleBaseInfo;
    
    SingleDiscountInfo singleDiscountInfo;
    
    SingleAccountInfo singleAccountInfo;
}

class SingleBaseInfo {
    // 单品基本信息 10个变量
}

class SingleDiscountInfo {
    // 单品优惠信息 10个变量
}

class SingleAccountInfo {
    // 单品库存信息 10个变量
}

21.异曲同工的类

定义

两个类功能相似

原因

功能相似的类应该被替换掉

解决

  • 「改变函数声明」将函数签名变得一致
  • 「搬移函数」将某些行为移入类中,直到两者的协议一致为止
  • 搬移过程中的重复代码,「提炼超类」补偿一下

Demo

// 重构前
class TradeOrderService {

}

class OrderService {
    
}

// 重构后
class TradeService {
    
}

22.纯数据类

定义

拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物

原因

这样的类只是一种不会说话的数据容器,它们极狐一定被其他类过分细琐地操控着

解决

  • 如果有public字段——立刻「封装记录」将它们封装起来
  • 对于那些不该被其他类修改的字段,请「移除设值函数」
  • 找出这些取值/设值函数被其他类调用的地点——尝试「搬移函数」把那些调用行为搬移到纯数据类里来。如果无法搬移整个函数,就「提炼函数」产生一个可被搬移的函数

Demo

// 重构前
/class SingleVO {
    int id;
    int name;
    int num;
}



// 重构后
class SingleVO {
    int id;
    int name;
    int num;
    
    public void convertVO(SingleDTO singleDTO) {
        // XXX
    }
    
    public boolean canBuy() {
        // XXX
    }
}

23.被拒绝的遗赠

定义

子类应该继承超类的函数和数据,它们不想或不需要继承

原因

如果拒绝继承超类的实现,这一点我们不介意;但如果拒绝支持超类的接口,这就难以接受了

解决

  • 「以委托取代子类」或者「以委托取代超类」彻底划清界限

Demo

// 重构前
interface Reduce {
    Integer getReduceAmount();
}

class Discount extends Reduce {
    @Override
    public Integer getReduceAmount {
        // XXX
    }
}

class Deduction extends Reduce {
    @Override
    public Integer getReduceAmount {
        // XXX
    }
}

class Coin extends Reduce {
    @Override
    public Integer getReduceAmount {
        return null;
    }
}

// 重构后
class Coin {
    
    private Reduce reduce;
    
}

24.注释

定义

代码的注释

原因

  • 人们把它当作“除臭剂”来使用,很多时候长长的注释只是因为代码很糟糕
  • 在我们用各种重构手法把坏味道去除后,常常会发现注释已经变得多余了

解决

  • 如果需要注释来解释一块代码做了什么——试试「提炼函数」
  • 如果函数已经提炼出来,还是需要注释来解释其行为——试试「改变函数声明」为它改名
  • 如果需要注释说明某些系统的需求规格——试试「引入断言」
  • 注释应该用来记录将来的打算,和并无十足把握的区域

Demo

// 重构前
public int getPrice(Order order) {
    // 获取基础价格
    double basePrice = order.getQuantity() * order.getItemPrice();  
    // 获取折扣
    double quantityDiscount = Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
    // 获取运费
    double = shipping = Math.min(basePrice * 0.1100);
    // 计算价格
    return basePrice - quantityDiscount + shipping;
}


// 重构后

// 计算商品价格
public double getPrice(Order order) {
    return calBasePrice(order) - calDiscount(order) + calShipping(calBasePrice(order));
}

// 计算基础价格
public double calBasePrice(Order order) {
    return order.getQuantity() * order.getItemPrice();
}
// 计算折扣
public double calDiscount(Order order) {
    return Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
}
// 计算运费
public double calShipping(double basePrice) {
    return Math.min(basePrice * 0.1100);
}

七、总结

如果今天的内容只能记住三件事,请记住:

  • 事不过三,三而重构
  • 出现注释的地方常常有坏味道
  • 不要做过度设计

八、参考资料

《重构——改善既有代码的设计》