《重构:改善既有代码的设计》—第1章1.4节运用多态取代与价格相关的条件逻辑-阿里云开发者社区

开发者社区> 云计算> 正文
登录阅读全文

《重构:改善既有代码的设计》—第1章1.4节运用多态取代与价格相关的条件逻辑

简介: 如果你很熟悉GoF(Gang of Four,四巨头)[5]所列的各种模式,可能会问:“这是一个State,还是一个Strategy?”答案取决于Price类究竟代表计费方式(此时我喜欢把它叫做Pricer还PricingStrategy),还是代表影片的某个状态(例如“Star Trek X是一部新片”)。

本节书摘来自异步社区《重构:改善既有代码的设计》一书中的第1章,第1.4节运用多态取代与价格相关的条件逻辑,作者【美】Martin Fowler,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.4 运用多态取代与价格相关的条件逻辑
这个问题的第一部分是switch语句。最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。

class Rental...
  double getCharge() {
      double result = 0;
      switch (getMovie().getPriceCode()) {
          case Movie.REGULAR:
              result += 2;
              if (getDaysRented() > 2)
                  result += (getDaysRented() - 2) * 1.5;
              break;
          case Movie.NEW_RELEASE:
            result += getDaysRented() * 3;
            break;
          case Movie.CHILDRENS:
            result += 1.5;
            if (getDaysRented() > 3)
                result += (getDaysRented() - 3) * 1.5;
            break;
        }
        return result;
    }

这暗示getCharge()应该移到Movie类里去:

class Movie...
    double getCharge(int daysRented) {
        double result = 0;
        switch (getPriceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (daysRented > 2)
                    result += (daysRented - 2) * 1.5;
                break;
            case Movie.NEW_RELEASE:
                result += daysRented * 3;
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (daysRented > 3)
                    result += (daysRented - 3) * 1.5;
                break;
            }
            return result;
    }

为了让它得以运作,我必须把租期长度作为参数传递进去。当然,租期长度来自Rental对象。计算费用时需要两项数据:租期长度和影片类型。为什么我选择将租期长度传给Movie对象,而不是将影片类型传给Rental对象呢?因为本系统可能发生的变化是加入新影片类型,这种变化带有不稳定倾向。如果影片类型有所变化,我希望尽量控制它造成的影响,所以选择在Movie对象内计算费用。

我把上述计费方法放进Movie类,然后修改Rental的getCharge(),让它使用这个新函数(图1-12和图1-13):

class Rental... 
  double getCharge() {
      return _movie.getCharge(_daysRented);
  }


7d5c28fdb01660c2b4cbbe094c39e15c9376b1a0


b7a187a7905e455092f60072780b99b0919acd7c

搬移getCharge()之后,我以相同手法处理常客积分计算。这样我就把根据影片类型而变化的所有东西,都放到了影片类型所属的类中。以下是重构前的代码:
class Rental...
  int getFrequentRenterPoints() {
      if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1)
          return 2;
      else
          return 1;
  }

重构后的代码如下:

class Rental...
  int getFrequentRenterPoints() {
      return _movie.getFrequentRenterPoints(_daysRented);
  }

class Movie...
  int getFrequentRenterPoints(int daysRented) {
      if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
          return 2;
      else
          return 1;
  }

终于……我们来到继承
我们有数种影片类型,它们以不同的方式回答相同的问题。这听起来很像子类的工作。我们可以建立Movie的三个子类,每个都有自己的计费法(图1-14)。


ddf44ac2339ea780636189432089e76db8e7e157

这么一来,我就可以用多态来取代switch语句了。很遗憾的是这里有个小问题,不能这么干。一部影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。不过还是有一个解决方法:State模式[Gang of Four]。运用它之后,我们的类看起来像图1-15。

9bc75dc3707bb6e837574bf7e4a96cc918a4b118

加入这一层间接性,我们就可以在Price对象内进行子类化动作[4],于是便可在任何必要时刻修改价格。

如果你很熟悉GoF(Gang of Four,四巨头)[5]所列的各种模式,可能会问:“这是一个State,还是一个Strategy?”答案取决于Price类究竟代表计费方式(此时我喜欢把它叫做Pricer还PricingStrategy),还是代表影片的某个状态(例如“Star Trek X是一部新片”)。在这个阶段,对于模式(和其名称)的选择反映出你对结构的想法。此刻我把它视为影片的某种状态。如果未来我觉得Strategy能更好地说明我的意图,我会再重构它,修改名字,以形成Strategy。

为了引入State模式,我使用三个重构手法。首先运用Replace Type Code with State/Strategy (227),将与类型相关的行为搬移至State模式内。然后运用Move Method (142)将switch语句移到Price类。最后运用Replace Conditional with Polymorphism (255)去掉switch语句。

首先我要使用Replace Type Code with State/Strategy (227)。第一步骤是针对类型代码使用Self Encapsulate Field (171),确保任何时候都通过取值函数和设值函数来访问类型代码。多数访问操作来自其他类,它们已经在使用取值函数。但构造函数仍然直接访问价格代码[6]:

class Movie...
    public Movie(String title, int priceCode) {
      _title= title;
      _priceCode = priceCode;
    }

我可以用一个设值函数来代替:

class Movie
    public Movie(String title, int priceCode) {
      _title = title;
      setPriceCode(priceCode);
    }

然后编译并测试,确保没有破坏任何东西。现在我新建一个Price类,并在其中提供类型相关的行为。为了实现这一点,我在Price类内加入一个抽象函数,并在所有子类中加上对应的具体函数:

abstract class Price {
  abstract int getPriceCode();
}
class ChildrensPrice extends Price {
  int getPriceCode() {
      return Movie.CHILDRENS;
  }
}
class NewReleasePrice extends Price {
  int getPriceCode() {
      return Movie.NEW_RELEASE;
  }
}

class RegularPrice extends Price {
    int getPriceCode() {
        return Movie.REGULAR;
    }
}

然后就可以编译这些新建的类了。

现在,我需要修改Movie类内的“价格代号”访问函数(取值函数/设值函数,如下),让它们使用新类。下面是重构前的样子:

public int getPriceCode() {
    return _priceCode;
}
public setPriceCode(int arg) {
    _priceCode = arg;
}
private int _priceCode;

这意味着我必须在Movie类内保存一个Price对象,而不再是保存一个_priceCode变量。此外我还需要修改访问函数:

class Movie...
  public int getPriceCode() {
        return _price.getPriceCode();
    }
    public void setPriceCode(int arg) {
        switch (arg) {
        case REGULAR:
            _price = new RegularPrice();
            break;
        case CHILDRENS:
            _price = new ChildrensPrice();
            break;
        case NEW_RELEASE:
            _price = new NewReleasePrice();
            break;
        default:
            throw new IllegalArgumentException("Incorrect Price Code");
        }
    }

    private Price _price;

现在我可以重新编译并测试,那些比较复杂的函数根本不知道世界已经变了个样儿。

现在我要对getCharge()实施Move Method (142)。下面是重构前的代码:

class Movie...
  double getCharge(int daysRented) {
      double result = 0;
      switch (getPriceCode()) {
          case Movie.REGULAR:
              result += 2;
              if (daysRented > 2)
                  result += (daysRented - 2) * 1.5;
              break;
          case Movie.NEW_RELEASE:
              result += daysRented * 3;
              break;
          case Movie.CHILDRENS:
              result += 1.5;
              if (daysRented > 3)
                  result += (daysRented - 3) * 1.5;
              break;
        }
        return result;
    }

搬移动作很简单。下面是重构后的代码:

class Movie...
  double getCharge(int daysRented) {
      return _price.getCharge(daysRented);
  }

class Price...
  double getCharge(int daysRented) {
      double result = 0;
      switch (getPriceCode()) {
          case Movie.REGULAR:
              result += 2;
              if (daysRented > 2)
                  result += (daysRented - 2) * 1.5;
              break;
          case Movie.NEW_RELEASE:
              result += daysRented * 3;
              break;
          case Movie.CHILDRENS:
              result += 1.5;
              if (daysRented > 3)
                  result += (daysRented - 3) * 1.5;
              break;
    }
    return result;
}

搬移之后,我就可以开始运用Replace Conditional with Polymorphism (255)了。

下面是重构前的代码:

class Price...
  double getCharge(int daysRented) {
      double result = 0;
      switch (getPriceCode()) {
          case Movie.REGULAR:
              result += 2;
              if (daysRented > 2)
                  result += (daysRented - 2) * 1.5;
              break;
          case Movie.NEW_RELEASE:
              result += daysRented * 3;
              break;
          case Movie.CHILDRENS:
              result += 1.5;
              if (daysRented > 3)
                  result += (daysRented - 3) * 1.5;
              break;
    }
    return result;
}

我的做法是一次取出一个case分支,在相应的类建立一个覆盖函数。先从RegularPrice开始:

class RegularPrice...
    double getCharge(int daysRented) {
        double result = 2;
        if (daysRented > 2)
            result += (daysRented - 2) * 1.5;
        return result;
  }

这个函数覆盖了父类中的case语句,而我暂时还把后者留在原处不动。现在编译并测试,然后取出下一个case分支,再编译并测试。(为了保证被执行的确实是子类中的代码,我喜欢故意丢一个错误进去,然后让它运行,让测试失败。噢,我是不是有点太偏执了?)

class ChildrensPrice
    double getCharge(int daysRented) {
        double result = 1.5;
        if (daysRented > 3)
            result += (daysRented - 3) * 1.5;
        return result;
    }

class NewReleasePrice...
    double getCharge(int daysRented) {
        return daysRented * 3;
    }

处理完所有case分支之后,我就把Price.getCharge()声明为abstract:

class Price...
    abstract double getCharge(int daysRented);

现在我可以运用同样手法处理getFrequentRenterPoints()。重构前的样子如下[7]:

class Movie...
  int getFrequentRenterPoints(int daysRented) {
      if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
          return 2;
      else
          return 1;
  }

首先我把这个函数移到Price类:

class Movie...
  int getFrequentRenterPoints(int daysRented) {
        return _price.getFrequentRenterPoints(daysRented);
  }
class Price...
  int getFrequentRenterPoints(int daysRented) {
      if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1)
          return 2;
      else
          return 1;
}

但是这一次我不把超类函数声明为abstract。我只是为新片类型增加一个覆写函数,并在超类内留下一个已定义的函数,使它成为一种默认行为。

class NewReleasePrice
  int getFrequentRenterPoints(int daysRented) {
        return (daysRented > 1) ? 2 : 1;
  }

class Price...
  int getFrequentRenterPoints(int daysRented) {
        return 1;
  }

引入State模式花了我不少力气,值得吗?这么做的收获是:如果我要修改任何与价格有关的行为,或是添加新的定价标准,或是加入其他取决于价格的行为,程序的修改会容易得多。这个程序的其余部分并不知道我运用了State模式。对于我目前拥有的这么几个小量行为来说,任何功能或特性上的修改也许都不合算,但如果在一个更复杂的系统中,有十多个与价格相关的函数,程序的修改难易度就会有很大的区别。以上所有修改都是小步骤进行,进度似乎太过缓慢,但是我一次都没有打开过调试器,所以整个过程实际上很快就过去了。我写本章文字所用的时间,远比修改那些代码的时间多得多。

现在我已经完成了第二个重要的重构行为。从此,修改影片分类结构,或是改变费用计算规则、改变常客积分计算规则,都容易多了。图1-16和图1-17描述State模式对于价格信息所起的作用。


81da2b9e08ef1eaaa587c7d2678eb26a6e29a6d3


f64a7812c201358e429ba4f624404771ccb5c72e

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
云计算
使用钉钉扫一扫加入圈子
+ 订阅

时时分享云计算技术内容,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。

其他文章
最新文章
相关文章