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

    xiaoxiao2023-12-17  171

    本节书摘来自异步社区《重构:改善既有代码的设计》一书中的第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); }

    搬移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)。

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

    最新回复(0)