背景
现在我们要做一个折扣管理的模块,简要点就是要针对不同的客户,提供不同的折扣报价。针对新客户不打折扣,针对老客户打9折,针对VIP客户打8折。其实这非常简单,我们几分钟就能写好这个程序。
package com.it235.strategy;
import java.math.BigDecimal;
/**
* 特价折扣管理
* 设计模式轻松学Java:君哥聊编程
*
*/
public class QuoteManager {
public BigDecimal quote(BigDecimal originalPrice , String customType){
if ("新客户".equals(customType)) {
System.out.println("抱歉!新客户没有折扣!");
return originalPrice;
}else if ("老客户".equals(customType)) {
System.out.println("恭喜你!老客户打9折!");
originalPrice = originalPrice.multiply(new BigDecimal(0.9))
.setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}else if("VIP客户".equals(customType)){
System.out.println("恭喜你!VIP客户打8折!");
originalPrice = originalPrice.multiply(new BigDecimal(0.8))
.setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
//其他人员都是原价
return originalPrice;
}
}
上面的代码工作的很好,用得很愉快,可是上面的代码是有问题的。上面存在的问题:把不同客户的报价的算法都放在了同一个方法里面,如果条件过多,会出现非常多的ifelse,程序比较臃肿。
所以我们需要对代码进行优化,优化后的代码如下。
package com.it235.strategy;
import java.math.BigDecimal;
public class QuoteManagerImprove {
public BigDecimal quote(BigDecimal originalPrice, String customType){
if ("新客户".equals(customType)) {
return this.quoteNewCustomer(originalPrice);
}else if ("老客户".equals(customType)) {
return this.quoteOldCustomer(originalPrice);
}else if("VIP客户".equals(customType)){
return this.quoteVIPCustomer(originalPrice);
}
//其他人员都是原价
return originalPrice;
}
/**
* 对VIP客户的报价算法
* @param originalPrice 原价
* @return 折后价
*/
private BigDecimal quoteVIPCustomer(BigDecimal originalPrice) {
System.out.println("恭喜!VIP客户打8折");
originalPrice = originalPrice.multiply(new BigDecimal(0.8))
.setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
/**
* 对老客户的报价算法
* @param originalPrice 原价
* @return 折后价
*/
private BigDecimal quoteOldCustomer(BigDecimal originalPrice) {
System.out.println("恭喜!老客户打9折");
originalPrice = originalPrice.multiply(new BigDecimal(0.9))
.setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
/**
* 对新客户的报价算法
* @param originalPrice 原价
* @return 折后价
*/
private BigDecimal quoteNewCustomer(BigDecimal originalPrice) {
System.out.println("抱歉!新客户没有折扣!");
return originalPrice;
}
}
上面的代码比刚开始的时候要好一点,它把每个具体的算法都单独抽出来作为一个方法,当某一个具体的算法有了变动的时候,只需要修改响应的报价算法就可以了。
但是改进后的代码还是有问题的,那有什么问题呢?
- 当我们新增一个客户类型的时候,首先要添加一个该种客户类型的报价算法方法,然后再quote方法中再加一个else if的分支,是不是感觉很是麻烦呢?而且这也违反了设计原则之一的开闭原则(open-closed-principle)
- 我们经常会面临这样的情况,不同的时期使用不同的报价规则,比如在各个节假日举行的各种促销活动时、商场店庆时往往都有普遍的折扣,但是促销时间一旦过去,报价就要回到正常价格上来。按照上面的代码我们就得修改if else里面的代码很是麻烦
那有没有什么办法使得我们的报价管理即可扩展、可维护,又可以方便的响应变化呢?当然有解决方案啦,就是我们下面要讲的策略模式。
定义与特点
- 策略定义
策略是对算法的封装,是一种行为模式,将每一个算法封装到具有共同接口的独立的类中,使算法本身和使用算法的客户端分割开来,相互独立,从而使得它们可以相互替换。
策略的特点
- 测试可减少大量的if-else语句,可自动切换不同的实现
- 扩展性强,添加策略无非就是添加一个具体的实现类而已,代价非常低
参与角色
角色 类别 说明 Strategy 抽象的策略 是一个接口或抽象类 ConcreteStrategy 具体的策略类 实现了抽象的策略 Context 一个普通的类 上下文环境,持有 Stragegy 的引用 - 策略模式简单的 UML
UML代码示例
声明策略接口
//策略接口 interface IStrategy { //定义的抽象算法方法 来约束具体的算法实现方法 public void algorithmMethod(); }
编写具体的策略实现
// 具体的策略实现 class ConcreteStrategyA implements IStrategy { //具体的算法实现 @Override public void algorithmMethod() { System.out.println("this is ConcreteStrategyA method..."); } } // 具体的策略实现 class ConcreteStrategyB implements IStrategy { //具体的算法实现 @Override public void algorithmMethod() { System.out.println("this is ConcreteStrategyB method..."); } }
策略上下文
/** * 策略上下文 */ public class StrategyContext { //持有一个策略实现的引用 private IStrategy strategy; //使用构造器注入具体的策略类 public StrategyContext(IStrategy strategy) { this.strategy = strategy; } public void contextMethod(){ //调用策略实现的方法 strategy.algorithmMethod(); } }
外部客户端调用
//外部客户端 public class Client { public static void main(String[] args) { //1.创建具体测策略实现 IStrategy strategy = new ConcreteStrategyB(); //2.在创建策略上下文的同时,将具体的策略实现对象注入到策略上下文当中 StrategyContext ctx = new StrategyContext(strategy); //3.调用上下文对象的方法来完成对具体策略实现的回调 ctx.contextMethod(); } }
模式实现分析
我们继续针对上述的折扣管理来进行改进,我们可以应用策略模式对其进行改造,不同类型的客户有不同的折扣,我们可以将不同类型的客户的报价规则都封装为一个独立的算法,然后抽象出这些报价算法的公共接口
公共折扣策略接口
import java.math.BigDecimal; //报价策略接口 public interface IQuoteStrategy { //获取折后价的价格 BigDecimal getPrice(BigDecimal originalPrice); }
新客户报价策略实现
import java.math.BigDecimal; //新客户的报价策略实现类 public class NewCustomerQuoteStrategy implements IQuoteStrategy { @Override public BigDecimal getPrice(BigDecimal originalPrice) { System.out.println("抱歉!新客户没有折扣!"); return originalPrice; } }
老客户报价策略实现
import java.math.BigDecimal; //老客户的报价策略实现 public class OldCustomerQuoteStrategy implements IQuoteStrategy { @Override public BigDecimal getPrice(BigDecimal originalPrice) { System.out.println("恭喜!老客户享有9折优惠!"); originalPrice = originalPrice.multiply(new BigDecimal(0.9)) .setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; } }
VIP客户报价策略实现
import java.math.BigDecimal; //VIP客户的报价策略实现 public class VIPCustomerQuoteStrategy implements IQuoteStrategy { @Override public BigDecimal getPrice(BigDecimal originalPrice) { System.out.println("恭喜!VIP客户享有8折优惠!"); originalPrice = originalPrice.multiply(new BigDecimal(0.8)) .setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; } }
报价上下文
import java.math.BigDecimal; //报价上下文角色 public class QuoteContext { //持有一个具体的报价策略 private IQuoteStrategy quoteStrategy; //注入报价策略 public QuoteContext(IQuoteStrategy quoteStrategy){ this.quoteStrategy = quoteStrategy; } //回调具体报价策略的方法 public BigDecimal getPrice(BigDecimal originalPrice){ return quoteStrategy.getPrice(originalPrice); } }
外部客户端调用
import java.math.BigDecimal; //外部客户端 public class Client { public static void main(String[] args) { //1.创建老客户的报价策略 IQuoteStrategy oldQuoteStrategy = new OldCustomerQuoteStrategy(); //2.创建报价上下文对象,并设置具体的报价策略 QuoteContext quoteContext = new QuoteContext(oldQuoteStrategy); //3.调用报价上下文的方法 BigDecimal price = quoteContext.getPrice(new BigDecimal(100)); System.out.println("折扣价为:" +price); } } 结果输出: 恭喜!老客户享有9折优惠! 折扣价为:90.00
此时需求发生变更
这个时候,商场营销部新推出了一个客户类型--MVP用户(Most Valuable Person),可以享受折扣7折优惠,那该怎么办呢?
这个很容易,只要新增一个报价策略的实现,然后外部客户端调用的时候,创建这个新增的报价策略实现,并设置到策略上下文就可以了,对原来已经实现的代码没有任何的改动。
import java.math.BigDecimal; //MVP客户的报价策略实现 public class MVPCustomerQuoteStrategy implements IQuoteStrategy { @Override public BigDecimal getPrice(BigDecimal originalPrice) { System.out.println("哇偶!MVP客户享受7折优惠!!!"); originalPrice = originalPrice.multiply(new BigDecimal(0.7)) .setScale(2,BigDecimal.ROUND_HALF_UP); return originalPrice; } }
外部调用客户端
import java.math.BigDecimal; //外部客户端 public class Client { public static void main(String[] args) { //创建MVP客户的报价策略 IQuoteStrategy mvpQuoteStrategy = new MVPCustomerQuoteStrategy(); //创建报价上下文对象,并设置具体的报价策略 QuoteContext quoteContext = new QuoteContext(mvpQuoteStrategy); //调用报价上下文的方法 BigDecimal price = quoteContext.getPrice(new BigDecimal(100)); System.out.println("折扣价为:" +price); } } 控制台输出: 哇偶!MVP客户享受7折优惠!!! 折扣价为:70.00
- 注意点
策略的核心不是如何实现算法,而是如何更优雅的把这些算法组织起来,让客户端非常好调用「虽然策略非常多,可以自由切换,但是同一时间客户端只能调用一个策略,其实也很好理解,你不可能同时既坐飞机,又坐火车」。
策略模式和简单工厂模式
策略模式和简单工厂非常相似,结构基本上一样,但是它们侧重点不一样
- 策略模式:是一个行为模式,解决策略的切换和扩展,让策略独立于客户端
- 简单工厂模式:是一种创建模式「创建对象」,接收指令创建出具体的对象,让对象的创建和具体的使用客户无关
但是我们在策略模式中可以使用简单工厂模式「把生成策略这一过程使用工厂去实现,这样好不好呢?适合就是最好的」
总结
优点
- 结构清晰,把策略分离成一个个单独的类「替换了传统的 if else」
- 代码耦合度降低,安全性提高「各个策略的细节被屏蔽」
- 扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后使用即可。
缺点
- 客户端必须要知道所有的策略类,否则你不知道该使用那个策略,所以策略模式适用于提前知道所有策略的情况下
- 增加了对象的数量,如果可选的策略有很多的话,那对象的数量也会很多
- 只适合偏平的算法结构,策略均属于同一等级,构成扁平的结构,这就限制了算法的使用层级,且不能被嵌套。