用重构指导Clean Code(二):依恋情结和switch语句

书接上回,我们继续聊如何用重构指导Clean Code。

在Clean Code的3.4节中有这样一段代码(代码清单3-4)。(第3章主要讲的是函数,而3.4节讨论的是switch语句。)

public Money calculatePay(Employee e) throws InvalidEmployeeType {
 switch (e.type) {
  case COMMISSIONED:
   return calculateCommissionedPay(e);
  case HOURLY:
   return calculateHourlyPay(e);
  case SALARIED:
   return calculateSalariedPay(e);
  default:
   throw new InvalidEmployeeType(e.type);
 }
}

Bob大叔细数了这段代码的几处问题:

  1. 太长,当出现新的Employee类型时,还会变得更长

  2. 明显做了不止一件事

  3. 违反了单一权责原则,因为有好几个修改它的理由

  4. 违反了开放闭合原则,因为每当添加新类型时,就必须修改之

  5. 最麻烦的是到处皆有类似结构的函数

其中1和4说的是一回事儿,每当新增加一种类型的Employee,就必须在这个switch里添加新的case。2和3说的是一回事儿,这个方法为三种类型的Employee计算费用,任何一种类型的计算逻辑发生变化,都要修改这个计算方法(或者对应的计算方法)。

对于第5点,Bob大叔的意思是说,calculatePay(Employee e)这个方法所在的类中还会有类似isPayDay(Employee e, Date date)deliverPay(Employee e, Money pay)这样的方法,由于Employee具有不同的类型(type),那么这些方法中肯定也会出现类似的switch语句。

讲到这里还是非常清晰的,然而Bob大叔接下来又开始魔幻操作,直接给出了解决方案:

将switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePayisPaydaydeliverPay等,则藉由Employee接口多态地接受派遣。

哪里来的抽象工厂?Employee怎么就有了派生类?再一看代码

public abstract class Employee {
 public abstract boolean isPayday();
 public abstract Money calculatePay();
 public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
 public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
  switch (r.type) {
   case COMMISSIONED:
    return new CommissionedEmployee(r) ;
   case HOURLY:
    return new HourlyEmployee(r);
   case SALARIED:
    return new SalariedEmploye(r);
   default:
    throw new InvalidEmployeeType(r.type);
  }
 }
}

妙啊,6啊,Bob大叔简直是神啊!

然而,面对同样的问题代码,我们能做出这样的修改吗?我们能构造出Employee的继承体系吗?能抽象出EmployeeFactory吗?反正对我来说,仅看Clean Code是做不到的。Bob大叔的技炫得神乎其神,但无奈只有结果,没有过程,让初学者不明所以。所以,这个时候就要祭出神器《重构》了。

《重构》要求我们在重构代码之前,先识别坏味道。然后针对你想去消除的那个坏味道,按照相应的重构手法,一步一步让这个坏味道消失。我曾经在真实的遗留系统中演练过“坏味道驱动的重构“(SDR,Smell Driven Refactoring,这是我发明的一个词儿),效果还是不错的。

还是上面有问题的代码,我们看看按照《重构》应该怎么重构:

public Money calculatePay(Employee e) throws InvalidEmployeeType {
 switch (e.type) {
  case COMMISSIONED:
   return calculateCommissionedPay(e);
  case HOURLY:
   return calculateHourlyPay(e);
  case SALARIED:
   return calculateSalariedPay(e);
  default:
   throw new InvalidEmployeeType(e.type);
 }
}

这里的坏味道很多,最一目了然的可能就是”Switch Statements(Switch惊悚现身)“,当然还有Bob大叔指出的违反SRP和OCP,但在坏味道中,我们管它叫做”Divergent Change(发散式变化)“。然而要消除这两个坏味道都不是那么容易,我只想从最简单的开始。什么是最简单的呢?

首先calculatePay方法有一个Employee类型的参数,除此之外,它没有使用该方法所在类的任何成员。同理可以推断该方法所使用的其他三个方法应该也只是使用Employee,不会使用该方法所在类的成员。这时,”Feature Envy(依恋情结)“的坏味道就显现出来了。

有一种经典气味是:函数对某个类的兴趣高过对自己所处类的兴趣。这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。

对于该方法,岂止是半打,简直就是全部。老马不但指出了问题,还给出了方案:

疗法显而易见:把这个函数移至另一个地点。你应该使用"Move Method(搬移函数)把它移到它该去的地方”

没什么好说的,挪呗。将calculateCommisstionPaycalculateHourlyPaycalculateSalariedPay方法,以及它自己全部挪到Employee中(常用IDE都支持重构,这里不再赘述):

public Money calculatePay() throws InvalidEmployeeType {
 switch (this.type) {
  case COMMISSIONED:
   return this.calculateCommissionedPay();
  case HOURLY:
   return this.calculateHourlyPay();
  case SALARIED:
   return this.calculateSalariedPay();
  default:
   throw new InvalidEmployeeType(this.type);
 }
}

样一来,该方法只使用Employee自己的数据,”依恋情结“的坏味道不见了。

是祸躲不过,Switch这个坏味道早晚得面对。

从本质上说,switch语句的问题在于重复。你常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。

这也正是Bob大叔所说的”最麻烦的是到处皆有类似结构的函数“。好在老马一如既往给出了解决方案:

面向对象中的多态概念可为此带来优雅的解决办法。

switch语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”,……使用Replace Type Code with Subclasses 或Replace Type Code with State/Strategy。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorphism了。

由于我们已经通过消除”依恋情结“将方法移动到了Employee中,所以接下来只需要从类型码入手就好了。这里我们选择使用Replace Type Code with Subclass(以子类取代类型码)这个重构手法。打开《重构》相关章节一看,给出的例子简直跟我们这里的一模一样,两位大师是商量好的吧?

abstract int getType();
abstract Money calculatePay();


static Employee create(int type) {
  switch (type) {
    case COMMISSIONED:
      return new CommissionedEmployee();
    case HOURLY:
      return new HourlyEmployee();
    case SALARIED:
      return new SalariedEmployee();
    default:
      throw new InvalidEmployeeType(type);
    }
}

怎么样?是不是与Bob大叔的幻改异曲同工?(EmployeeRecord之类的属于细枝末节,不再赘述。)

除此之外,老马还给出了消除类型码的其他方案(Replace Type Code with State/Strategy),以及彻底消除switch的办法(Replace Conditional with Polymorphism)。

这样细致入微的教导,是不是比直接告诉你答案要实用得多?

事实上对于上例这种有很多坏味道的代码,你完全可以按个人喜好来选择不同的重构顺序,得到的结果可能千差万别,也可能殊途同归,但肯定都算是不错的设计。

这就是代码的魅力。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值