抛出问题
现在有如下需求:商场收银软件,营业员根据客户所购买商品的单价可数量,向客户收费,商场可能会有各种各样的促销活动(如:打9折、打8折、满300减50、满500减100等)。
本能的思维方式还是计算机的思路:
- 输入商品单价、数量;
- 选择一个促销活动,根据活动进行总价计算;
简单工厂实现
首先活动种类可能有很多(假如老板每天都很开心,每天都变着花样去搞活动),所以考虑各种促销活动之间的解耦,和方便扩展,要合理的封装业务类(面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。)并结合简单工厂模式,实现如下:
收费的抽象类
public abstract class CashSuper
{
/// <summary>
/// 根据总金额计算优惠后的金额
/// </summary>
/// <param name="money"></param>
/// <returns></returns>
public abstract double AcceptCash(double money);
}
正常收费
public class CashNormal : CashSuper
{
public override double AcceptCash(double money)
{
// 正常收费
return money;
}
}
打折收费
public class CashRebate : CashSuper
{
private double rebate = 1d;
public CashRebate(double rebate)
{
this.rebate = rebate;
}
public override double AcceptCash(double money)
{
return money * this.rebate;
}
}
满减返利收费
public class CashReduction : CashSuper
{
private double moneyCondition = 0d;
private double moneyReduction = 0d;
public CashReduction(double moneyCondition, double moneyReduction)
{
this.moneyCondition = moneyCondition; // 满减的条件
this.moneyReduction = moneyReduction; // 满减的金额
}
public override double AcceptCash(double money)
{
double reduction = Math.Floor(money % this.moneyCondition) * this.moneyReduction;
return money - reduction;
}
}
商场收费工厂
public class CashFactory
{
public static CashSuper CreateCashAccept(string type)
{
CashSuper cashSuper = null;
switch (type)
{
case "正常收费":
cashSuper = new CashNormal();
break;
case "打八折":
cashSuper = new CashRebate(0.8);
break;
case "满300减50":
cashSuper = new CashReduction(300, 50);
break;
}
return cashSuper;
}
}
客户端核心代码
CashSuper cashSuper = CashFactory.CreateCashAccept("打八折");
double moneny = cashSuper.AcceptCash(500);
到这里完全毫无问题,但是由于工厂本身包括所有的收费方式,商场可能经常性地更改打折额度和返利额度,每次维护或扩展收费方式时都要改动这个工厂,以致于代码需要重新编译部署,所有这里用简单工厂模式不是最好的办法。
策略模式
简单工厂模式只是解决对象的创建问题,面对算法的经常变化,应该有更好的办法。策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。
在上面例子中,商场选择怎样的促销活动,其实都是一些算法,用工厂来生成算法对象,这没有错,但算法本身只是一种策略,而这些算法是随时都可能互相替换的,这就是变化点,而封装变化点是面向对象的一种很重要的思维方式。下面是策略模式的结构图:
首先定义一个 CashContext 类
public class CashContext
{
private CashSuper cashSuper = null; // 声明一个 CashSuper 对象
/// <summary>
/// 通过构造函数传入具体收费策略
/// </summary>
/// <param name="cashSuper"></param>
public CashContext(CashSuper cashSuper)
{
this.cashSuper = cashSuper;
}
/// <summary>
/// 获取最终的金额
/// </summary>
/// <param name="money"></param>
/// <returns></returns>
public double GetFinalCash(double money)
{
return this.cashSuper.AcceptCash(money);
}
}
客户端代码
CashContext context = null;
switch (type)
{
case "正常收费":
context = new CashContext(new CashNormal());
break;
case "打八折":
context = new CashContext(new CashRebate(0.8));
break;
case "满300减50":
context = new CashContext(new CashReduction(300, 50));
break;
}
double moneny = context.GetFinalCash(500);
在上面客户端中要判断选择的收费策略,然后创建选择的收费策略对象,这里就导致客户端要知道所以的收费策略,耦合严重。但是既然是创建对象,而且创建的是同一种抽象类型(CashSuper)的对象,那么岂不是又可以利用简单工厂模式了呢?
策略模式 + 简单工厂模式
对 CashContext 构造函数改下如下:
/// <summary>
/// 传入收费类型,在内部创建具体的收费策略对象
/// </summary>
/// <param name="type"></param>
public CashContext(string type)
{
switch (type)
{
case "正常收费":
cashSuper = new CashNormal();
break;
case "打八折":
cashSuper = new CashRebate(0.8);
break;
case "满300减50":
cashSuper = new CashReduction(300, 50);
break;
}
}
上面传入的不再是具体策略,而是收费的类型,在构造函数中根据收费类型创建具体收费策略(简单工厂)。客户端核心代码:
CashContext context = new CashContext("打八折");
double moneny = context.GetFinalCash(500);
对比简单工厂模式客户端核心代码:
CashSuper cashSuper = CashFactory.CreateCashAccept("打八折");
double moneny = cashSuper.AcceptCash(500);
发现简单工厂模式中客户端需要知道两个类和两个方法,一个收费对象抽象类 CashSuper 和计费的方法 AcceptCash,一个对象工厂的类 CashFactory 和一个创建具体收费对象的方法 CreateCashAccept。而在策略模式 + 简单工厂模式中,客户端只需要知道一个收费的上下文对象 CashContext 和得到最终收费金额的方法 GetFinalCash 即可,显而易见,降低了业务和逻辑和客户端的耦合程度,也易于扩展。
策略模式总结
策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同。它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。
策略模式的 Strategy 类层次为 Context 定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。 如上面的打折、返利等其他算法其实都是对商品收费的一种计算方式,通过继承,可以得到它们的公共功能获得计算费用的结果 GetFinalCash,这使得算法间用了抽象的父类 CashSuper。
策略模式同样简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。
**策略模式封装了变化。**当不同的行为堆砌在一个类中时,很难避免使用条件语句来选择合适的行为。将这些行为封装在一个独立的 Strategy 类中,可以在使用这些行为的类中消除条件语句。
策略模式就是用来封装算法的,在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
在基本的策略模式中,选择所用具体实现的职责由客户端对象承担,并转给策略模式的 Context 对象。这本身并没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现的职责也可以由 Context 来承担,最大化地减轻类客户端的职责。但是它依然不是完美的实现方式,因为在 CashContext 里面同样用到了 switch 选择,如果需要增加一种算法就要改 switch 代码。这里可以通过反射的技术动态的创建收费策略对象。