策略模式
问题引入:实现一个商场收银软件,简单的实现就是单价和数量的乘积。
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:使用抽象工厂模式:反射机制