✨作者:猫十二懿
❤️🔥账号:CSDN 、掘金 、个人博客 、Github
🎉公众号:猫十二懿
策略模式
问题引入:实现一个商场收银软件,简单的实现就是单价和数量的乘积。
1、商场收银软件
下面就来看看这个实现程序:
/**
* @author Shier
* CreateTime 2023/4/10 21:40
*/
public class ShopCash {
public static void main(String[] args) {
//商品单价
double price = 0d;
//商品购买数量
int num = 0;
//当前商品合计费用
double totalPrices = 0d;
//总计所有商品费用
double total = 0d;
Scanner sc = new Scanner(System.in);
// 不断输入,知道输入的价格或和数量小于0
do {
System.out.print("请输入商品单价:");
price = Double.parseDouble(sc.nextLine());
System.out.print("请输入商品数量:");
num = Integer.parseInt(sc.nextLine());
if (price > 0 && num > 0) {
totalPrices = price * num;
total = total + totalPrices;
System.out.println("单价:" + price + "元 数量:" + num + " 合计:" + totalPrices + "元");
System.out.println("总计:" + total + "元");
}
}
while (price > 0 && num > 0);
}
}
以上的简单实现的存在着一定的问题的,比如如果商场举办活动,所有的商品打八折,又该如何实现。
起始很简单:只要(total + totalPrices)*0.8
即可,但是这样真的好吗。如果商场活动结束了呢,又得来改程序,然后再重新安装软件,这样虽不说很麻烦啊,而且现实生活中也不会的。
2、增加打折变量
将以上的程序修改如下:
/**
* @author Shier
* CreateTime 2023/4/10 21:40
*/
public class ShopCashDemo02 {
public static void main(String[] args) {
//商品单价
double price = 0d;
//商品购买数量
int num = 0;
//当前商品合计费用
double totalPrices = 0d;
// 打几折
int discount = 0;
//总计所有商品费用
double total = 0d;
Scanner sc = new Scanner(System.in);
// 不断输入,知道输入的价格或和数量小于0
do {
System.out.println("请输入商品折扣模式(1.正常收费 2.打八折 3.打七折):");
discount = Integer.parseInt(sc.nextLine());
System.out.print("请输入商品单价:");
price = Double.parseDouble(sc.nextLine());
System.out.print("请输入商品数量:");
num = Integer.parseInt(sc.nextLine());
if (price > 0 && num > 0) {
// 判断打折
switch (discount) {
case 1:
totalPrices = price * num;
break;
case 2:
totalPrices = price * num * 0.8;
break;
case 3:
totalPrices = price * num * 0.7;
break;
default:
break;
}
}
totalPrices = price * num;
total = total + totalPrices;
System.out.println("单价:" + price + "元 数量:" + num + " 合计:" + totalPrices + "元");
System.out.println("总计:" + total + "元");
}
while (price > 0 && num > 0);
}
}
通过一个变量控制打几折,来进行计算总的价格。这样子比说明的第一个简单的实现看起来灵活了很多。
但是你观察发现,是不是除了打几折以外,其他的代码基本都一样的。如果再增加多几个需求(比如满100饭20等活动),哪又该如何实现?
可以先写一个父类,再继承它实现多个打折和返利的子类,利用多态,完成这个代码。
3、简单工厂实现
这里打折基本都是一样的,只要有个初始化参数就可以了。满几送几的,需要两个参数才行。
面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。
打一折和打九折只是形式的不同,抽象分析出来,所有的打折算法都是一样的,所以打折算法应该是一个类。
具体的代码实现如下:
/**
* @author Shier
* CreateTime 2023/4/10 21:51
* 抽象类
*/
public abstract class CashSuper {
public abstract double acceptCash(double price,int num);
}
/**
* @author Shier
* CreateTime 2023/4/10 21:54
*
* 正常收费,原价返回
*/
public class CashNormal extends CashSuper {
public double acceptCash(double price,int num){
return price * num;
}
}
/**
* @author Shier
* CreateTime 2023/4/10 21:53
*
* 打折收费
*/
public class CashRebate extends CashSuper {
private double moneyRebate = 1d;
//初始化时必需输入折扣率。八折就输入0.8
public CashRebate(double moneyRebate){
this.moneyRebate = moneyRebate;
}
//计算收费时需要在原价基础上乘以折扣率
public double acceptCash(double price,int num){
return price * num * this.moneyRebate;
}
}
/**
* @author Shier
* CreateTime 2023/4/10 21:55
*
* 返利
*/
public class CashReturn extends CashSuper {
//返利条件
private double moneyCondition = 0d;
//返利值
private double moneyReturn = 0d;
//返利收费。初始化时需要输入返利条件和返利值。
//比如“满300返100”,就是moneyCondition=300,moneyReturn=100
public CashReturn(double moneyCondition,double moneyReturn){
this.moneyCondition = moneyCondition;
this.moneyReturn = moneyReturn;
}
//计算收费时,当达到返利条件,就原价减去返利值
public double acceptCash(double price,int num){
double result = price * num;
if (moneyCondition>0 && result >= moneyCondition)
// 比如 result = 420 - (420/300) * 100 = 320 320就是优惠之后的价格,这个情况是满300减一百,满六百见两百了
//result = result - Math.floor(result / moneyCondition) * moneyReturn;
// 只要大于300,不管多大,都只有100返利
result = result - moneyReturn;
return result;
}
}
/**
* @author Shier
* CreateTime 2023/4/10 21:50
* 收费工厂
*/
public class CashFactory {
public static CashSuper createCashAccept(int cashType){
CashSuper cs = null;
switch (cashType) {
case 1:
//正常收费
cs = new CashNormal();
break;
case 2:
//打八折
cs = new CashRebate(0.8d);
break;
case 3:
//打七折
cs = new CashRebate(0.7d);
break;
case 4:
//满300返100
cs = new CashReturn(300d,100d);
break;
default:
break;
}
return cs;
}
}
以上类与类之间的关系如下:
还有其他的需求,只要在收费对象工厂添加对应的条件即可。
简单工厂模式虽然也能解决这个问题,但这个模式只是解决对象的创建问题,而且由于工厂本身包括所有的收费方式,商场是可能经常性地更改打折额度和返利额度,每次维护或扩展收费方式都要改动这个工厂,以致代码需重新编译部署,这真的是很糟糕的处理方式,所以用它不是最好的办法。面对算法的时常变动,应该有更好的办法。
4、策略模式
策略模式(Strategy Pattern)是一种面向对象设计模式,它在一个对象中封装了不同的算法,使得这些算法可以相互替换。通过使用策略模式,客户端可以选择不同的算法来完成特定的任务,同时还可以轻松地替换算法和添加新的算法。
策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式当算法的变化时,不会影响到使用算法的客户。
在策略模式中,一般有三个角色:策略接口、具体策略类和环境类。
- 策略接口定义了所有具体策略类所需要实现的方法
- 具体策略类实现了策略接口,并提供了不同的算法实现
- 环境类则持有一个策略接口类型的引用,并将实际的算法执行委托给该引用所指向的具体策略类。
策略模式的优点
- 可以方便地扩展和修改算法的实现,而不必修改环境类的代码。
- 可以将算法的实现与其他部分的代码分离,提高代码的可维护性和可复用性。
对比简单工厂模式
商场收银时如何促销,用打折还是返利,其实都是一些算法,用工厂来生成算法对象,这没有错,但算法本身只是一种策略,最重要的是这些算法是随时都可能互相替换的,这就是变化点,而封装变化点是我们面向对象的一种很重要的思维方式。
策略模式结构图
具体说明:
Strategy类,定义所有支持的算法的公共接口:
ConcreteStrategy类,封装了具体的算法或行为,继承于Strategy:
Context类,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用:
客户端:
5、策略模式实现
观察发现,我们只要修改客户端,并且曾加一个Context类即可,下面的商场收银系统的结构图
将上面的简单工厂模式进行修改:
CashContext
/**
* @author Shier
* CreateTime 2023/4/11 22:55
* CashContext类
*/
public class CashContext {
/**
* CashSuper对象
*/
private CashSuper cashSuper;
/**
* 通过构造方法,传入具体的收费策略
*/
public CashContext(CashSuper cashSuper) {
this.cashSuper = cashSuper;
}
/**
* 根据不同的收费策略返回不同的结构
*/
public double getResult(double price, int num) {
return cashSuper.acceptCash(price, num);
}
}
客户端修改如下:
/**
* @author Shier
* CreateTime 2023/4/10 21:40
*/
public class ShopCashDemo04 {
public static void main(String[] args) {
//商品单价
double price = 0d;
//商品购买数量
int num = 0;
//当前商品合计费用
double totalPrices = 0d;
// 打几折
int discount = 0;
//总计所有商品费用
double total = 0d;
Scanner sc = new Scanner(System.in);
// 不断输入,知道输入的价格或和数量小于0
do {
System.out.print("请输入商品折扣模式(1.正常收费 2.打八折 3.打七折 4.满300返100):");
discount = Integer.parseInt(sc.nextLine());
System.out.print("请输入商品单价:");
price = Double.parseDouble(sc.nextLine());
System.out.print("请输入商品数量:");
num = Integer.parseInt(sc.nextLine());
if (price > 0 && num > 0) {
// Context
CashContext cashContext = null;
//根据用户输入,将对应的策略对象作为参数传入CashContent对象中
switch (discount) {
case 1:
cashContext = new CashContext(new CashNormal());
break;
case 2:
cashContext = new CashContext(new CashRebate(0.8d));
break;
case 3:
cashContext = new CashContext(new CashRebate(0.7d));
break;
case 4:
cashContext = new CashContext(new CashReturn(300d, 100d));
break;
default:
break;
}
//通过Context的getResult方法的调用,可以得到收取费用的结果
//让具体算法与客户进行了隔离
totalPrices = cashContext.getResult(price, num);
total = total + totalPrices;
System.out.println("单价:" + price + "元 数量:" + num + " 合计:" + totalPrices + "元");
System.out.println("总计:" + total + "元");
}
}
while (price > 0 && num > 0);
}
}
但是你发现又多了很多的switch判断,这样又回到了最初的样子。下面再次将客户端的代码迁移到CashContext中
CashContext
/**
* 通过构造方法,传入具体的收费策略
*/
public CashContext(int cashType) {
//根据用户输入,将对应的策略对象作为参数传入CashContent对象中
switch (cashType) {
case 1:
cashSuper = new CashNormal();
break;
case 2:
cashSuper = new CashRebate(0.8d);
break;
case 3:
cashSuper = new CashRebate(0.7d);
break;
case 4:
cashSuper = new CashReturn(300d, 100d);
break;
default:
break;
}
}
在对CashContext进行调用时,就让他传入对应的折扣模式,然后则去创建对应的具体策略。
客户端修改:
if (price > 0 && num > 0) {
// 根据用户输入的折扣模式,调用不同CashContext中对应的对象
CashContext cashContext = new CashContext(discount);
// 将计算放在各个具体策略类当中去完成
totalPrices = cashContext.getResult(price, num);
total = total + totalPrices;
System.out.println("单价:" + price + "元 数量:" + num + " 合计:" + totalPrices + "元");
System.out.println("总计:" + total + "元");
}
策略模式与简单工厂模式对比
对比项 | 策略模式 | 简单工厂模式 |
---|---|---|
定义 | 定义一系列算法,使算法的使用与算法分离开来,封装的算法具有一定独立性 | 一个工厂类根据传入的参数动态决定创建哪种产品类的实例 |
解决问题 | 解决在多重条件分支语句下的代码臃肿和难以维护的问题 | 解决对象的创建问题,通过工厂类统一创建对象,避免客户端直接调用产品类 |
应用场景 | 处理不同的业务场景,如支付策略、商品促销等 | 对象的创建需要一定的复杂度,使用简单工厂可以减少客户端代码的复杂度 |
类别 | 行为型设计模式 | 创建型设计模式 |
耦合度 | 低 | 高 |
优点 | 可以动态切换算法,易于扩展和添加新的算法实现 | 适用于大量的产品创建,客户端只需要知道产品的类型即可 |
缺点 | 增加类的数量,提高了系统的复杂度 | 工厂类职责过重,违反了单一职责原则 |
下图说明两个模式之间的耦合度,关联越多的类,耦合度越高,反之月底。
在上面的商场收银系统中,客户端实例化的是CashContext的对象,调用的是CashContext的方法GetResult,这使得具体的收费算法彻底地与客户端分离。连算法的父类CashSuper都不让客户端认识了。
6、解析策略模式
6.1 策略模式总结
策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合
6.2 策略模式的优点
- 简化单元测试:通过对应的接口进行单独测试,就不用测试到其他的功能接口。、
- 算法可重用性强:将算法封装成独立的类,使其可以在不同的应用中被复用,提高了代码的可维护性和可扩展性。
- 策略切换方便:对于相同的行为,在不同的场景下可能需要不同的实现算法,策略模式可以方便地切换算法实现。
- 避免使用多重条件分支语句:使用策略模式可以避免使用复杂的if-else分支语句,提高代码的可读性和可维护性,降低代码的圈复杂度
- 扩展性良好:向系统中增加新的策略类很容易,不需要修改原有代码,符合开闭原则。
但是上面的程序还是不够完美的,因为每次都要去修改CashContext中的switch代码(有需求就修改,但是需求的变更是要成本,开通VIP帮你一步到位😎)
PS:使用抽象工厂模式:反射机制
模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合**
6.2 策略模式的优点
- 简化单元测试:通过对应的接口进行单独测试,就不用测试到其他的功能接口。、
- 算法可重用性强:将算法封装成独立的类,使其可以在不同的应用中被复用,提高了代码的可维护性和可扩展性。
- 策略切换方便:对于相同的行为,在不同的场景下可能需要不同的实现算法,策略模式可以方便地切换算法实现。
- 避免使用多重条件分支语句:使用策略模式可以避免使用复杂的if-else分支语句,提高代码的可读性和可维护性,降低代码的圈复杂度
- 扩展性良好:向系统中增加新的策略类很容易,不需要修改原有代码,符合开闭原则。
但是上面的程序还是不够完美的,因为每次都要去修改CashContext中的switch代码(有需求就修改,但是需求的变更是要成本,开通VIP帮你一步到位😎)
PS:使用抽象工厂模式:反射机制