策略模式
作者通过一个商场收费
的例子来引入了策略模式,由于文中的例子基本是C#编写,因此自己仿照着课本的例子写了一遍JAVA代码,这里没设计界面,简单的用Scanner取代了界面输入。
面向过程实现商场收费
为了巩固简单工厂模式,作者在刚开始实现商场收费程序的时候仍然采用了面向过程的思路。具体的实现代码如下:
import java.util.ArrayList;
import java.util.Scanner;
/**
* @Author yirui
* @Date 2024/4/10 10:44
* @Version 1.0
*/
public class AcceptCash {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double totalPrice = 0d;
try {
ArrayList<String> priceList = new ArrayList<>();
String acceptRule;//收费规则:单价,数量,优惠方式。逗号分隔
double price;
int rule,count;
while (true){
acceptRule = sc.nextLine();
if (acceptRule.equals("end")){
break;
}else {
priceList.add(acceptRule);
String[] list = acceptRule.split(",");
price = Double.parseDouble(list[0]);
count = Integer.parseInt(list[1]);
rule = Integer.parseInt(list[2]);
switch (rule){
case 1://正常收费
totalPrice += price*count;
break;
case 2://打八折
totalPrice += price*count*0.8;
break;
case 3://满300-100
totalPrice += (price*count-(Math.floor(price*count/300)*100));
break;
}
}
}
System.out.println(totalPrice);
System.out.println(priceList);
}catch (Exception e){
System.out.println(e.toString());
}finally {
sc.close();
}
}
}
这里简单解释一下上面的代码,由于没有输入框,因此采用了Scanner来替代,控制台输入的标准格式是:单价,数量,规则
。当输入end
表示输入结束。计算逻辑也很简单,只有中间switch那一小段代码块。整个代码的流程如下:先从输入中把单价,数量,规则分别解析出来;再根据规则选择不同的运算逻辑,将最终实收的价格加到总价里面然后输出。最后测试的运行结果如下图所示:
这是很典型的面向过程解题的思路。在这段代码中包含了界面逻辑(从控制台获取输入并解析)、规则判断逻辑和价格计算逻辑;当需求发生变化,都需要在这个类中进行相应的修改,当业务逻辑变复杂后,无论是扩展、维护和复用的角度,这段代码都没法满足。
简单工厂模式实现商场收费
接下来让我们利用面向对象的思维来分析这个问题,并通过前面讲过的简单工厂模式来实现它:
- 首先在这个商场收费问题中,打折的收费规则我们可以把他抽象出一个收费父类
CashSuper
,父类中包含了所有收费规则应该有的方法(所有收费规则的最终目的都是计算出实收费用)。
/**
* @Author yirui
* @Date 2024/4/10 18:06
* @Version 1.0
*/
public abstract class CashSuper {
public abstract double acceptPrice(double price, int count);//计算实收价格
}
- 为不同收费方式(有的可能是打折,有的可能是满减等等)的规则创建收费子类:
/**
* @Author yirui
* @Date 2024/4/10 18:22
* @Version 1.0
*/
public class CashRebate extends CashSuper {//打折收费方式
private double robate;//打几折,这个属性是打折方式特有的,因此不在父类中定义。
public CashRebate(double robate) {//在初始化的时候,一定要指定打几折
this.robate = robate;
}
@Override
public double acceptPrice(double price, int count) {//打折收费的计算规则
return price * count * robate;
}
}
public class CashReturn extends CashSuper {//满减收费方式
private double montyCondition;
private double moneyReturn;
public CashReturn(double montyCondition, double moneyReturn) {//同样,在初始化的时候需要告知满减条件,满多少减多少。
this.montyCondition = montyCondition;
this.moneyReturn = moneyReturn;
}
@Override
public double acceptPrice(double price, int count) {//满减收费的计算规则
double normalMoney = price * count;
double acceptMoney = normalMoney - moneyReturn * Math.floor(normalMoney / montyCondition);
return acceptMoney;
}
}
public class CashNormal extends CashSuper {//正常收费方式
@Override
public double acceptPrice(double price, int count) {//不做活动的计算规则
return price*count;
}
}
- 创建一个工厂类来管理这些收费类的创建,它可以根据收营员选择的规则,来决定创建哪个类,怎么创建这个类。(这里为了方便,只制定了三种收费规则,1表示正常收费,2表示打八折,3表示满300-100)
/**
* @Author yirui
* @Date 2024/4/10 18:42
* @Version 1.0
*/
public class CashFactory {
public static CashSuper createCashAccept(int type){
CashSuper cashSuper = null;
switch (type){
case 1://正常收费
cashSuper = new CashNormal();
break;
case 2://打八折
cashSuper = new CashRebate(0.8);
break;
case 3://满300-100
cashSuper = new CashReturn(300,100);
break;
}
return cashSuper;
}
}
- 修改主程序代码,最终实现商场收费功能
import java.util.ArrayList;
import java.util.Scanner;
/**
* @Author yirui
* @Date 2024/4/10 10:44
* @Version 1.0
*/
public class AcceptCash1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double totalPrice = 0d;
try {
ArrayList<String> priceList = new ArrayList<>();
String acceptRule;//收费规则:单价,数量,优惠方式。逗号分隔
double price;
int rule,count;
while (true){
acceptRule = sc.nextLine();
if (acceptRule.equals("end")){
break;
}else {
priceList.add(acceptRule);
String[] list = acceptRule.split(",");
price = Double.parseDouble(list[0]);
count = Integer.parseInt(list[1]);
rule = Integer.parseInt(list[2]);
CashSuper cashAccept = CashFactory.createCashAccept(rule);//利用工厂创建对象
totalPrice += cashAccept.acceptPrice(price,count);//调用对象方法计算出实收价格,并将其加到总计账单。
}
}
System.out.println(totalPrice);
System.out.println(priceList);
}catch (Exception e){
System.out.println(e.toString());
}finally {
sc.close();
}
}
}
上述代码成功的将页面逻辑额和业务逻辑分类开,主程序负责接收数据并解析,工厂类负责判断收费规则创建出正确的子类,子类负责实际的运算规则。当出现了新的收费规则,或者原本的收费规则发生了变化时,只需要修改子类和工厂类。实际运行结果如下图,可以看到与面向过程的方式结果一样:
当然上面的代码还存在很多能够改进的地方,比如我在工厂类中写死了满减条件和打折倍率,这样每次这些东西发生变化时(比如我突然想满1000-100,或者我亏血大甩卖,全场一折等等),我都需要修改工厂类的内容。这点与作者在文中提出的一样:这个模式只是解决对象的创建问题,而且由于工厂本身包括了所有的收费方式,商场是可能经常性地更改打折额度和返利额度,每次维护或扩展收费方式都要改动这个工厂,以致代码需重新编译部署,这真的是很糟糕的处理方式,所以用它不是最好的办法。
策略模式实现商场收费
在刚看策略模式前面一点的时候,我这个菜鸟是有点懵逼的,这和简单工厂模式有什么区别?还是大佬的随口一句话,让我纠正了我的思维错误:我们前面写的CashSuper父类和它的子类,这种通过继承的方式来编程的方式,它和简单工厂模式有什么关系呢?继承只是面向对象编程的一种常用的方式,在很多设计模式种都会用到,它并不是简单工厂模式所特有的,在我们前面的代码中,真正体现简单工厂模式的只有那个工厂类。
策略模式在书中的定义是这样的:它定义了算法家族,分别封装起来,让他们之间可互相替换,此模式让算法的变化不会影响到使用算法的用户。
它的结构图如下:
如果把简单工厂模式比作生产车间的话,策略模式则类似于我们生活中的“小霸王”(小时候玩的一种游戏机),“小霸王”公司规定了插槽的形状,所有与“小霸王”合作的公司生产的游戏卡,无论他做的是什么游戏,最后所有的游戏卡一定有一个一样的插脚形状(我们给它起个名字叫做“小霸王”游戏卡),不然插不上去。当用户用“小霸王”游戏机打游戏的时候,给游戏机插上游戏卡就可以了,想玩什么游戏就插什么卡(前提是能插上去)。
在上面这个例子里,用来插卡的“小霸王”游戏机相当于Context,“小霸王”游戏卡相当于Strategy,“小霸王”公司规定的插槽的形状其实就相当于Strategy里面的公共接口,而他的子类就是具体的游戏卡,比如魂斗罗,坦克大战,雪人兄弟等等。(这些都是我自己的理解哈,有问题希望大佬及时指正。)
言归正传,文中尝试利用策略模式来实现商场收费。而之前编写的收费父类CashSuper其实就是Strategy,它的子类就是具体的实现。因此我们只需要自己编写一个Context。
/**
* @Author yirui
* @Date 2024/4/10 23:35
* @Version 1.0
*/
public class CashContext {
private CashSuper cashSuper;
public CashContext(CashSuper cashSuper) {
this.cashSuper = cashSuper;
}
public double getResult(double price, int count) {
return cashSuper.acceptPrice(price, count);
}
}
写完这个类后,我们还需要重新编写主程序代码:
import java.util.ArrayList;
import java.util.Scanner;
/**
* @Author yirui
* @Date 2024/4/10 10:44
* @Version 1.0
*/
public class AcceptCash2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double totalPrice = 0d;
try {
ArrayList<String> priceList = new ArrayList<>();
String acceptRule;//收费规则:单价,数量,优惠方式。逗号分隔
double price;
int rule, count;
while (true) {
acceptRule = sc.nextLine();
if (acceptRule.equals("end")) {
break;
} else {
priceList.add(acceptRule);
String[] list = acceptRule.split(",");
price = Double.parseDouble(list[0]);
count = Integer.parseInt(list[1]);
rule = Integer.parseInt(list[2]);
CashContext cashContext = null;
switch (rule) {
case 1://正常收费
cashContext = new CashContext(new CashNormal());
break;
case 2://打八折
cashContext = new CashContext(new CashRebate(0.8));
break;
case 3://满300-100
cashContext = new CashContext(new CashReturn(300,100));
break;
}
totalPrice += cashContext.getResult(price,count);
}
}
System.out.println(totalPrice);
System.out.println(priceList);
} catch (Exception e) {
System.out.println(e.toString());
} finally {
sc.close();
}
}
}
这样做相对于简单工厂模式而言有什么区别呢?
当打折规则发生变化时,我们的Context类不需要发生变化,我们需要做的事是在主函数中创建Context对象时,传入不同的CashSuper的子类。(换游戏卡,不换游戏机)。但是细心的同学可能已经发现了,这样做不是又回到了面向过程的老路了吗,虽然打折的计算逻辑分离出去了,但是界面逻辑和判断创建什么对象的业务逻辑又混到一起去了。没毛病,所以就有了后来的策略模式与简单工厂模式结合使用的例子!这也告诉我们,设计模式并不是相互独立非A即B的,它们可以混着用
。我们下文介绍哈。