文章目录
一、设计思想演进
1.1 预想场景
我们有一个会员业务,在售卖产品时价格是根据会员的类型来进行折扣,比如:普通会员不打折,VIP会员打8折,超级VIP会员7折。
现在做一个价格模块的折扣计算功能,针对不同会员类型,进行折扣计算,代码如下:
public class PriceCenter {
/**
* 对原价进行折扣计算
* @param price 原价
* @param memberType 会员类型
* @return 计算折扣后价格
*/
public BigDecimal discount(BigDecimal price, String memberType){
if ("普通会员".equals(memberType)) {
System.out.println("普通会员没有折扣");
}else if ("VIP会员".equals(memberType)) {
System.out.println("vip会员打9折!");
price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
}else if("超级VIP会员".equals(memberType)){
System.out.println("超级VIP会员打5折!");
price = price.multiply(new BigDecimal("0.7")).setScale(2,RoundingMode.HALF_UP);
}
return price;
}
}
一开始代码逻辑很简单,但我们把每种会员的折扣算法都放在了一个方法里,随着业务的增加代码会越来越多,会十分臃肿。
1.2 优化
下面我们进行优化,把每种会员的折扣算法单独封装成一个方法,代码如下:
public class PriceCenter {
/**
* 对原价进行折扣计算
*
* @param price 原价
* @param memberType 会员类型
* @return 计算折扣后价格
*/
public BigDecimal discount(BigDecimal price, String memberType) {
if ("普通会员".equals(memberType)) {
price = memberNormalDiscount(price);
} else if ("VIP会员".equals(memberType)) {
price = memberVIPDiscount(price);
} else if ("超级VIP会员".equals(memberType)) {
price = memberSuperVIPDiscount(price);
}
return price;
}
private BigDecimal memberNormalDiscount(BigDecimal price) {
System.out.println("普通会员没有折扣");
return price;
}
private BigDecimal memberVIPDiscount(BigDecimal price) {
System.out.println("vip会员打9折!");
price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
return price;
}
private BigDecimal memberSuperVIPDiscount(BigDecimal price) {
System.out.println("超级VIP会员打8折!");
price = price.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.HALF_UP);
return price;
}
}
当我们想要修改某个算法的逻辑时只要,修改单独的方法就可以了。
上面看似让代码变得优雅了,但还是存在一些问题:
问题一:每新增一个会员类型的时候,首先要添加一个该种客户类型的报价算法方法,然后在方法discount中再加一个else if的分支,这违反了设计原则之一的开闭原则(open-closed-principle).
开闭原则:
对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。
对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。
问题二:有的时候我们需要做额外的活动针对不同的节日,每个else if中的逻辑要做很大改动,活动过去之后又要恢复,如果刚入职的新手就会想直接改动原来else if种代码,活动过去之后在改动回去,这样其实很不好。
思考:每次执行discount方法时其实只会执行其中一个分支的代码,是否有什么办法可以每次动态变化方法中的内容,于是我们就想到是否可以执行的时候调用固定的类的方法,具体的执行逻辑是否可以通过当一个参数传到这个类中。
为了解决上面的问题,这个时候策略模式就应运而生了
二、什么是策略设计模式?
2.1 概念
其思想是针对一组算法,将每一种算法都封装到具有共同接口的独立的类中,从而是它们可以相互替换。策略模式可以让算法在不影响客户端的情况下发生变化。
2.2 类型
行为型设计模式
2.3 UML结构
2.4 三类角色
- 环境(Context):也叫上下文,它持有一个Strategy的引用。
- 抽象策略(Strategy):这是一个抽象角色,通常由一个接口或抽象类实现。用来约束一系列具体的策略。
- 具体策略(ConcreteStrategy):具体的策略实现。策略即算法。
2.5 一般通用实现*
策略接口:
//策略接口
public interface IStrategy {
//定义的抽象算法方法 来约束具体的算法实现方法
public void algorithmMethod();
}
具体的策略实现:
// 具体的策略实现A
public class ConcreteStrategyA implements IStrategy {
//具体的算法实现
@Override
public void algorithmMethod() {
System.out.println("我是具体的算法A");
}
}
// 具体的策略实现B
public class ConcreteStrategyB implements IStrategy {
//具体的算法实现
@Override
public void algorithmMethod() {
System.out.println("我是具体的算法B");
}
}
策略上下文
/**
* 策略上下文
*/
public class StrategyContext {
//持有一个策略实现的引用
private IStrategy strategy;
//使用构造器注入具体的策略类
public StrategyContext(IStrategy strategy) {
this.strategy = strategy;
}
//也可以通过set方法注入具体策略
public void setStrategy(IStrategy strategy) {
this.strategy = strategy;
}
public void contextMethod(){
//调用策略实现的方法
strategy.algorithmMethod();
}
}
三、根据设计模式改造业务
折扣策略接口
public interface IDiscountStrategy {
BigDecimal discount(BigDecimal price);
}
普通会员折扣策略
public class MemberNormalDiscount implements IDiscountStrategy {
@Override
public BigDecimal discount(BigDecimal price) {
System.out.println("普通会员没有折扣");
return price;
}
}
VIP会员折扣策略
public class MemberVIPDiscount implements IDiscountStrategy {
@Override
public BigDecimal discount(BigDecimal price) {
System.out.println("vip会员打9折!");
price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
return price;
}
}
超级VIP会员折扣策略
public class MemberSuperVIPDiscount implements IDiscountStrategy {
@Override
public BigDecimal discount(BigDecimal price) {
System.out.println("超级VIP会员打8折!");
price = price.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.HALF_UP);
return price;
}
}
折扣上下文
public class DiscountContext {
private IDiscountStrategy discountStrategy;
public DiscountContext(IDiscountStrategy strategy) {
this.discountStrategy = strategy;
}
public void setDiscountStrategy(IDiscountStrategy iDiscountStrategy) {
this.discountStrategy = iDiscountStrategy;
}
public BigDecimal discount(BigDecimal price){
return discountStrategy.discount(price);
}
}
客户端
public class Client {
public static void main(String[] args) {
BigDecimal price1 = new BigDecimal(1000);
MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
DiscountContext discountContext = new DiscountContext(memberNormalDiscount);
price1 = discountContext.discount(price1);
System.out.println(price1);
BigDecimal price2 = new BigDecimal(1000);
discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
price2 = discountContext.discount(price2);
System.out.println(price2);
}
}
同时测试两种算法,结果如下:
普通会员没有折扣
1000
超级VIP会员打8折!
800.00
下面我们来想一想,如果这时候推出了一个员工内部会员,买东西可享5折怎么实现?
我们只要新增一个内部会员折扣策略就可以了,代码如下:
public class MemberInnerDiscount implements IDiscountStrategy {
@Override
public BigDecimal discount(BigDecimal price) {
System.out.println("内部会员打5折!");
price = price.multiply(new BigDecimal("0.5")).setScale(2, RoundingMode.HALF_UP);
return price;
}
}
public class Client {
public static void main(String[] args) {
BigDecimal price1 = new BigDecimal(1000);
MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
DiscountContext discountContext = new DiscountContext(memberNormalDiscount);
price1 = discountContext.discount(price1);
System.out.println(price1);
BigDecimal price2 = new BigDecimal(1000);
discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
price2 = discountContext.discount(price2);
System.out.println(price2);
BigDecimal price3 = new BigDecimal(1000);
discountContext.setDiscountStrategy(new MemberInnerDiscount());
price3 = discountContext.discount(price3);
System.out.println(price3);
}
}
四、if else如何解决?
在策略模式使用过程中我们发现,策略模式其实是通过context来解决执行具体逻辑可以根据需要随时变化,也就是需要什么算法就让context持有。但客户端中if else并没有减少
我们将上面代码改造的更贴近一些现实情况
public class Client {
public static void main(String[] args) {
//会员类型
String memberType = "VIP";
//原价
BigDecimal price = new BigDecimal(1000);
DiscountContext discountContext = new DiscountContext();
if ("normal".equals(memberType)) {
MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
discountContext.setDiscountStrategy(new MemberNormalDiscount());
price = discountContext.discount(price);
} else if ("VIP".equals(memberType)) {
BigDecimal price2 = new BigDecimal(1000);
discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
price = discountContext.discount(price);
}else if ("superVIP".equals(memberType)) {
discountContext.setDiscountStrategy(new MemberInnerDiscount());
price = discountContext.discount(price);
}
System.out.println(price);
}
}
4.1使用工厂方法
工厂方法
/**
* 会员折扣策略工厂
*/
public class MemberDiscountFactory {
private static final Map<String, IDiscountStrategy> map = new HashMap<>();
static {
map.put("normal", new MemberNormalDiscount());
map.put("VIP", new MemberVIPDiscount());
map.put("superVIP", new MemberSuperVIPDiscount());
}
public static IDiscountStrategy getDiscountStrategy(String memberType) {
return map.get(memberType);
}
}
客户端
public class Client1 {
public static void main(String[] args) {
//这里使用最简单方式表示类型
String memberType = "VIP";
//原价
BigDecimal price = new BigDecimal(1000);
//使用工厂获取具体策略
IDiscountStrategy iDiscountStrategy = MemberDiscountFactory.getDiscountStrategy(memberType);
DiscountContext discountContext = new DiscountContext(iDiscountStrategy);
BigDecimal result = discountContext.discount(price);
System.out.println("打折后的价格为"+result);
}
}
发现代码优化后简洁了很多,工厂方法只是一种解决方式,其主要的思路就是将各个策略放到容器中去来方便获取。大家可以发散思路,比如枚举,反射等等
思考:策略的选择我们交给了客户端,及时使用了工厂方法对客户端也会觉得过于麻烦,我们是否可以将具体选择哪些策略的权利交给Context上下文,这样在折扣选择的部分也可以彻底和客户端解耦
五、Context在策略模式中的作用
5.1 作用: 让客户端和具体策略完全解耦
在没有context的情况下, client直接持有策略接口, 如果具体策略改变, 需要更改client的代码, 但是如果让context持有IStrategy
那就不需要更改client
client在整个处理流程中只要知道他调了一个策略就行了, 不必要知道具体策略是什么, 甚至策略的选择都让策略上下文去维护。
有很多策略模式的实现, 都是在context中维护了strategy list, 然后在里面通过参数去选取对应的策略, 这块"选取对应策略"放在context中比放在client中更合适
5.2 再次改造
Context上下文
public class DiscountContext1 {
String memberType;
private IDiscountStrategy discountStrategy;
public DiscountContext1(String memberType) {
this.memberType = memberType;
this.discountStrategy = MemberDiscountFactory.getMedalService(memberType);
}
public String getMemberType() {
return memberType;
}
public void setMemberType(String memberType) {
this.memberType = memberType;
}
public IDiscountStrategy getDiscountStrategy() {
return discountStrategy;
}
public BigDecimal discount(BigDecimal price){
return discountStrategy.discount(price);
}
}
客户端
public class Client2 {
public static void main(String[] args) {
String memberType = "VIP";
//原价
BigDecimal price = new BigDecimal(1000);
DiscountContext1 discountContext = new DiscountContext1(memberType);
BigDecimal result = discountContext.discount(price);
System.out.println("打折后的价格为"+result);
}
}
5.3 上下文当做参数传入到具体策略中
具体的策略对象可以从上下文中获取所需数据,可以将上下文当做参数传入到具体策略中,具体策略通过回调上下文中的方法来获取其所需要的数据。
**假设情景:**要求每个策略能展示会员类型,而且支持以后一些公用字段新增
修改策略接口
public interface IDiscountStrategy1 {
BigDecimal discount(DiscountContext2 discountContext2);
}
修改一个具体策略
public class MemberNormalDiscount1 implements IDiscountStrategy1 {
@Override
public BigDecimal discount(DiscountContext2 discountContext2) {
String memberType = discountContext2.getMemberType();
BigDecimal price = discountContext2.getPrice();
System.out.println(memberType + "没有折扣");
return price;
}
}
修改Factory
public class MemberDiscountFactory1 {
private static final Map<String, IDiscountStrategy1> map = new HashMap<>();
static {
map.put("normal", new MemberNormalDiscount1());
}
public static IDiscountStrategy1 getDiscountStrategy(String memberType) {
return map.get(memberType);
}
}
修改Context
public class DiscountContext2 {
private String memberType;
private BigDecimal price;
private IDiscountStrategy1 discountStrategy;
public DiscountContext2(String memberType) {
this.memberType = memberType;
this.discountStrategy = MemberDiscountFactory1.getDiscountStrategy(memberType);
}
public String getMemberType() {
return memberType;
}
public void setMemberType(String memberType) {
this.memberType = memberType;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public IDiscountStrategy1 getDiscountStrategy() {
return discountStrategy;
}
public BigDecimal discount(BigDecimal price){
this.price = price;
return discountStrategy.discount(this);
}
}
client
public class Client3 {
public static void main(String[] args) {
//这里使用最简单方式表示类型
String memberType = "VIP";
//原价
BigDecimal price = new BigDecimal(1000);
DiscountContext2 discountContext = new DiscountContext2(memberType);
BigDecimal result = discountContext.discount(price);
System.out.println("打折后的价格为"+result);
}
}
测试结果
normal没有折扣
打折后的价格为1000
六、策略模式在JDK中有哪些应用?
6.1 比较器Comparator
在Java的集合框架中,经常需要通过构造方法传入一个比较器Comparator,或者创建比较器传入Collections的静态方法中作为方法参数,进行比较排序等,使用的是策略模式。
在该比较架构中,Comparator就是一个抽象的策略;一个类实现该结构,并实现里面的compare方法,该类成为具体策略类;Collections类就是环境角色,他将集合的比较封装成静态方法对外提供api。
6.2 ThreadPoolExecutor中的四种拒绝策略
在创建线程池时,需要传入拒绝策略,当创建新线程使当前运行的线程数超过maximumPoolSize时,将会使用传入的拒绝策略进行处理。
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
这里使用的就是策略模式。