一、引言
在上一篇博客中我们介绍了工厂模式中的简单工厂和工厂方法模式,它们较好的解决了对象的创建问题,降低了代码各模块之间的耦合性,方便了系统的改进和升级。但是,在一些需要经常改动的应用中,工厂模式需要频繁改动工厂类,以致代码需要频繁编译和部署,所以工厂模式在此场景下不是最好的方式。
考虑一个商场结算系统,由于商场经常举行不同的促销活动,如打折、满减等,其结算方案需要经常改动。现在先分析一下这个例子,其实每一种结算方案都是一种算法,或者说是一种解决问题的策略,而且这些策略要被频繁的替换,这就是变化点,而封装变化正是面向对象的一种很重要的思维方式,策略模式就是专门用于封装策略的变化的。
二、分析
策略模式将每一中策略封装起来,并从中抽象出一个策略父类,从而构建了一个策略家族,从而使这个策略家族中的每一种策略都可以互相替换,并且替换过程不会对客户端产生影响。
在策略模式中每一中策略都被封装成了一个具体策略类ConcretStrategy,这些类都继承自一个策略父类Strategy,在策略父类中定义了执行策略的接口;该模式还将策略的更换封装成了一个上下文类Context,该类维护了一个Strategy类型的引用,用不同的ConcretStrategy进行配置,并且定义了一个供客户端调用策略的接口方法ContextInterface()。下图即为策略模式的UML图。
图-1 策略模式UML图
三、实现
下面我们将以商场收费系统为例,来说明策略模式的具体实现。首先要定义一个抽象策略类CashStrategy,用以定义所有收费策略的公共接口receiveMoney(doubic money),在所有具体收费策略子类中将重写该方法。
<span style="font-size:14px;">/**
* 收银系统的终极父类,所有用于计算收银总额的算法子类均继承自该类。
*/
abstract class CashStrategy {
public abstract double receiveMoney(double money);
}/*CashStrategy*/</span>
接着,我们定义了三种收费策略:正常收费、打折收费和满减返现收费,它们均继承抽象策略类并重写了receiveMoney方法已实现各自的功能。
/**
* 正常收费类
*/
class NormalCash extends CashStrategy {
@Override
public double receiveMoney(double money) {
return money;
}// receiveMoney
}/*NormalCash*/
/**
* 打折收费类
*/
class RebateCash extends CashStrategy {
public double rebate;
/**
* RebateCash构造函数,确定打折额度
* @param rebate 打折额度
*/
public RebateCash(double rebate) {
this.rebate = rebate;
}//constructor
@Override
public double receiveMoney(double money) {
return (money * rebate);
}// receiveMoney
}/*RebateCash*/
/**
* 返现收费类
*/
class ReturnCash extends CashStrategy {
public double limite;
public double count;
/**
* ReturnCash构造函数,确定满减的上限和满减的额度。
* @param limite 满减上限
* @param count 满减额度
*/
public ReturnCash(double limite, double count) {
super();
this.limite = limite;
this.count = count;
}// constructor
@Override
public double receiveMoney(double money) {
double temp = 0;
temp = money - Math.floor(money / limite) * count;
return temp;
}// receiveMoney
}/*ReturnCash*/
接下来就要定义上下文类Context,该类是策略模式中最核心的一个类。该类维护了一个终极父类的引用,该成员变量在创建context类时被指向了一个具体策略子类对象。这样在策略发生变化时,就不必对Contex类的代码进行更改,而只需要在客户端创建Contex类时,传递不同的具体策略子类对象即可,具体实现请看代码。
/**
* 上下文类,该类维护了一个终极父类的引用,有一个调用子类方法的接口函数。
*/
class CashContext {
protected CashStrategy cs; // 维护了一个抽象策略类型的引用
/**
* 在构造上下文类时就传递给cs一个具体策略类的对象实例,这样在收费策略发生变化时就不需要
* 改动Context类,只需要在构造Context对象时传入不同的具体策略类对象即可。
* @param cs 具体的收费策略
*/
public CashContext(CashStrategy cs) {
this.cs = cs;
}// constructor
/**
* 通过该方法,用户可以执行具体策略类中的功能
* @param money
* @return
*/
public double cashInterface(double money) {
return cs.receiveMoney(money);
}// cashInterface
}/*CashContex*/
最后是客户端程序
/**
* 策略模式例程,编写一个超市收银系统的代码,能够根据商品的价格、数量
* 来计算收银总额,并将商品列表打印出来。注意预留商品打折和返现接口。
*/
public class StrategyPatternDemo {
/**
* 客户端程序入口,首先要根据需要创建Context类,然后调用具体策略的功能接口计算最终的收
* 费金额
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
double total = 0;
double actualBill = 0;
CashContext context = null;
LinkedList<String> prodList = new LinkedList<>();
// 根据要求的收费策略创建Context类
System.out.print("请输入收费策略(1:正常收费;2:打八折;3:满300返100):");
String stgy = sc.nextLine();
switch(stgy) {
case "1": case "正常收费":
context = new CashContext(new NormalCash());
break;
case "2": case "打八折":
context = new CashContext(new RebateCash(0.8));
break;
case "3": case "慢300返100":
context = new CashContext(new ReturnCash(300, 100));
break;
default:
sc.close();
throw new Exception("不正确的收费策略!");
}// switch
// 输入商品列表并计算收费金额
int ID = 1;
System.out.print("请输入商品价格:");
String price = sc.nextLine();
while(!price.equals("over")) {
double single = Double.parseDouble(price);
int num = 0;
System.out.print("请输入商品数量:");
num = Integer.parseInt(sc.nextLine());
prodList.add((ID++) + "\t\t" + price + "\t\t" + num);
total = single * num + total;
System.out.print("请输入商品价格:");
price = sc.nextLine();
}// while
actualBill = context.cashInterface(total);
sc.close();
// 显示
System.out.println("序号" + "\t\t" + "单价" + "\t\t\t" + "数量");
for(String str : prodList)
System.out.println(str);
System.out.println("总额:" + String.format("%.2f", total));
System.out.println("应收金额:" + String.format("%.2f", actualBill));
}// main
}/*StrategyPatternDemo*/
执行结果:
四、策略模式与简单工厂结合
从上面的例子我们可以看到,策略模式封装了策略的变化,但是却又把判断分支语句放回了客户端中,解决这个问题的思路是将策略模式与简单工厂(点击转到简单工厂模式)结合起来,Context类的构造方法写成简单工厂类中ceatInstance()方法的形式。
下面的CashFactoryContext继承了CashContext类,只是对构造方法进行了升级,将客户端中的判断分支语句转移到了Context类的构造方法中。
class CashFactoryContext extends CashContext {
public CashFactoryContext(String strategy) throws Exception {
super(null);
switch(strategy) {
case "1": case "正常收费":
cs = new NormalCash();
break;
case "2": case "打八折":
cs = new RebateCash(0.8);
break;
case "3": case "慢300返100":
cs = new ReturnCash(300, 100);
break;
default:
throw new Exception("不正确的收费策略!");
}// switch
}// constructor
}/*CashFactoryContext*/
客户端代码:
public class StrategyFactoryDemo {
/**
* 客户端程序入口,可以看出与策略模式相比,客户端程序里少了判断语句,并且用户仅用到了
* CashFactoryContext一个类
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
double total = 0;
double actualBill = 0;
CashFactoryContext factoryContext = null;
LinkedList<String> prodList = new LinkedList<>();
// 根据要求的收费策略创建Context类
System.out.print("请输入收费策略(1:正常收费;2:打八折;3:满300返100):");
String stgy = sc.nextLine();
factoryContext = new CashFactoryContext(stgy);
// 输入商品列表并计算收费金额
int ID = 1;
System.out.print("请输入商品价格:");
String price = sc.nextLine();
while(!price.equals("over")) {
double single = Double.parseDouble(price);
int num = 0;
System.out.print("请输入商品数量:");
num = Integer.parseInt(sc.nextLine());
prodList.add((ID++) + "\t\t" + price + "\t\t" + num);
total = single * num + total;
System.out.print("请输入商品价格:");
price = sc.nextLine();
}// while
actualBill = factoryContext.cashInterface(total);
sc.close();
// 显示
System.out.println("序号" + "\t\t" + "单价" + "\t\t\t" + "数量");
for(String str : prodList)
System.out.println(str);
System.out.println("总额:" + String.format("%.2f", total));
System.out.println("应收金额:" + String.format("%.2f", actualBill));
}// main
}/*StrategyFactoryDemo*/
执行结果:
读到这里大家可能已经产生了一些疑惑,似乎修改后的策略模式与简单工厂(点击转到工厂模式)的区别不大,但是实际上,策略模式依然与工厂模式存在一个重要的区别。在简单工厂模式中,客户端要想获得一个子类对象,就必须要知道抽象功能父类、工厂类和具体功能子类共三个类,但是在改进的策略模式中,具体策略子类的变换和创建都被封装在了Context类中,与客户端隔离开来,在客户端中只需要用到抽象策略类和Context类两个类即可实现所需的功能,进一步降低了耦合。
五、总结
策略模式是一种定义一系列策略(算法)的方法,这些策略完成的都是同一类工作,只是实现的方式不同,策略模式以相同的方式调用所有策略,降低了使用策略的类与各具体策略类之间的耦合。
在基本的策略模式中,选择所用具体策略的职责有客户端承担,并没有解除客户端需要选择判断的压力,将策略模式与简单工厂模式结合后,选择具体策略的操作被封装到了Context类中,减轻了客户端的职责。但是在添加或删除新的策略子类时,还还需要修改Context构造方法中的判断分支结构,违背了开放封闭原则。当然,这一缺点可以通过Java反射机制得以解决,我们将在后续的博文中进行介绍,读者此时可以先不必深究。