日期:2014-05-18  浏览次数:21315 次

《重构》C#版实现(四)switch的多态化处理

上一篇主要重构了Statement方法,在大刀阔斧的调整后,总算是得到了一个易于理解的方法体。当然,其中会带来潜在的效率问题,但记住一点,起码所有的事情都是经过权衡和决策的。可以“果断”来形容,而那种不明就里地乱来属于“武断”又或者说是“鲁莽”的行为当然不被推荐,它们的区别也就在于是否有全面的权衡与决策。
首先,列出到现在为止,Movie类和Rental类的代码如下:

public class Movie
{
	public const int CHILDRENS = 2;
	public const int REGULAR = 0;
	public const int NEW_RELEASE = 1;

	public string Title { get; private set; }
	public int PriceCode { get; private set; }
	
	public Movie(string title, int priceCode)
	{
		Title = title;
		PriceCode = priceCode;
	}
}

public class Rental
{
	public Movie Movie { get; private set; }
	public int DaysRented { get; private set; }
	public double Charge
	{
		get
		{
			double result = 0;
			switch (Movie.PriceCode)
			{
				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;
		}
	}

	public int FrequentRenterPoints
	{
		get
		{
			if (Movie.PriceCode == Movie.NEW_RELEASE && DaysRented > 1)
			{
				return 2;
			}
			else
			{
				return 1;
			}
		}
	}

	public Rental(Movie rented, int days)
	{
		Movie = rented;
		DaysRented = days;
	}
}

一、为价格计算寻找更合适的“家”

《重构》中提到一个考虑:该程序的一个主要变化点在于:影片类型很可能增加或发生变化。而目前来说,跟影片类型先关的代码分散在上面两个类中。在另一本书《代码整洁之道》中,提到一个原则:如果某些代码的修改频度一致,则应该把它们尽可能放在一起。放在我们这儿,意味着,如果我们在Movie类中增加了影片类型,意味着也要去修改Rental类的Charge属性,这种修改涉及到了两个类。假设情景是在大得多的项目中,则有可能是要修改两个不同的文件,甚至是不同的项目或程序集。而后面的这些情景通常会产生大量的BUG。而如果将修改频度一致的内容放在一起(同一个文件,甚至是同一个类中),则内容的同步也会变得更容易,BUG产生的几率会更低。
综上所述,我们需要把Rental关于Charge的计算过程迁移到Movie类中——虽然会带来多一层间接性,以换取程序的高可维护、可扩展性:
1.在Movie类中创建一个ChargeFor方法,并将Rental.Charge属性的代码抄过去:
public double ChargeFor(int daysRented)
{
	double result = 0;
	switch (PriceCode)
	{
		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;
}
注意,在代码迁移时,可能存在一些问题,诸如原本PriceCode是使用Rental的Movie对象来调用的,而现在因为它成为了自身的属性,所以也就没有必要使用Movie对象了。
2.修改Rental.Charge属性,让它通过委托给Movie的新方法来完成计算:
public double Charge
{
	get
	{
		return Movie.ChargeFor(DaysRented);
	}
}
3.运行单元测试,如果不通过则调试、修改直到通过为止
4.重构完成


二、用多态来封装价格算法
其实,就《重构》中所列出的这份代码而言,个人认为不需要更多的优化了。在整个程序中只有一个地方需要对PriceCode进行分派处理,这样的集中也是一种不错的选择——毕竟需要调整价格计算算法时,我们一定会直接定位到该ChargeFor方法。而什么样的信号会明确地告诉我们需要把switch转换成多态呢?我想应该是在代码中出现两处以上对PriceCode的switch分派处理时,这时,多态化的信号就很明确了。而《重构》更多的是想以此为例,演示如何用多态的技术来解决类似的问题。
为了要引入多态,就开始需要介入设计模式了。有很多人可能不了解它,甚至会因此产生抗拒的心态。其实完全没必要,设计模式说白了都很简单,尤其是它们的实现。最难的只是要活学活用它们的适用情境。
1.创建一个Price接口,用来表示影片的可计算概念

public interface Price
{
	double ChargeFor(int daysRented);
}
2.创建一个实现了Price接口的Regular类,实现对REGULAR类型影片的价格计算。重新审视关于该类型影片价格的计算方法,可以得到下面代码:
public sealed class RegularPrice : Price
{
	public double ChargeFor(int daysRented)
	{
		if (daysRented > 2)
		{
			return 2 + (daysRented - 2) * 1.5;
		}
		else
		{
			return 2;
		}
	}
}
3.修改Movie.ChargeFor中,关于REGULAR影片价格计算的片段,如下
public double ChargeFor(int daysRented)
{
	double result = 0;
	switch (PriceCode)
	{
		case Movie.REGULAR:
			result = (new RegularPrice()).ChargeFor(daysRented);
			break;
		case Movie.NEW_RELEASE:
			result += daysRented * 3;
			break;
		case Movie.CHILDRENS:
			result += 1.5;
			if (daysRented > 3)
				result += (daysR