摘要:
设计模式不是新技术而是一种解决特定环境下问题的策略,是各位前辈总结出的经验,不用设计模式能实现功能开发但使用设计模式写出的代码耦合性更低,扩展性更好。
在所有的文章中都会说项目中的if else 可以使用策略模式来解决,其实设计模式的书看了很多遍但一直不理解说使用策略模式替换if else 的判断,后来突然理解了,说的使用策略模式代替if else 其实不是完全不使用if else 而是说不用在项目的业务代码中if else 中在把处理具体策略的代码耦合在一起,而使用策略模式就是降低了这种耦合即业务代码和策略代码的耦合降低,如果后续有策略的增加只需要继续扩展而不用在改业务代码,这也就是设计模式中常说的低耦合高内聚、开闭原则(对扩展是开放的,对修改是关闭的)。
插曲:
蚂蚁金服面试笔试题请写出策略模式和模板方法模式,并说说他们的区别是什么?
OK 开启策略模式学习的旅途……
一、策略模式的定义
策略模式定义了一个拥有共同行为的算法族,每个算法都被封装起来,可以互相替换,独立于客户端而变化。策略模式本身的实现比较简单,但是结合单例模式+简单工厂模式+注解+反射,可以构造出近乎完善的策略模式,彻底的消除if-else。
二、策略模式使用场景
策略模式是oop中最著名的设计模式之一,是对方法行为的抽象,可以归类为行为设计模式,也是oop中interface经典的应用。
1、我们可以从三个方面来理解策略模式:
- 算法族
使用多种不同的处理方式,做同样的事情,仅仅是具体行为有差别。这些处理方式,组合构成算法策略族,它们的共性,体现在策略接口行为上。
- 算法封装
将各个算法封装到不同的类中,这样有助于客户端来选择合适的算法。
- 可互相替换
客户端可以在运行时选择使用哪个算法,而且算法可以进行替换,所以客户端依赖于策略接口即面向接口编程。
2、策略模式的使用场景:
- 针对同一问题的多种处理方式,仅仅是具体行为有差别时;
- 需要安全地封装多种同一类型的操作时;
- 同一抽象类有多个子类,而客户端需要使用 if-else 或者 switch-case 来选择具体子类时。
3、策略模式UML类图如下:

可以看到策略模式涉及到三个角色:
环境(Context)角色:持有一个Strategy的引用。
抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
具体策略(ConcreteStrategy)角色:实现类包装了具体算法或行为。
三、以会员结算场景为例代码展示
场景描述:如果是会员则会记录该用户的累计消费额,当累计消费额度达到不同会员等级时使用不同的折扣率,其实这就是对同一问题的多种处理方式,所以使用策略模式来看看……
1、不使用策略模式直接将会员等级判断和业务代码耦合
public static void main(String[] args) {
/**
* buyPrice: 用户本次消费额
* cusPrice: 用户累计消费额
*/
Customer customer = Customer.builder().id(110).buyPrice(1000D).cusPrice(3000D).build();
double consume = 0.00D;
// 判断用户等级
// 非会员
if (customer.getCusPrice() >= Constants.Level.NVIP_LEVEL) {
consume = customer.getBuyPrice() * Constants.Ratio.NVIP_RATIO;
}
//会员
if (customer.getCusPrice() >= Constants.Level.VIP_LEVEL) {
consume = customer.getBuyPrice() * Constants.Ratio.VIP_RATIO;
}
// 超级会员
if (customer.getCusPrice() >= Constants.Level.VVIP_LEVEL) {
consume = customer.getBuyPrice() * Constants.Ratio.VVIP_RATIO;
}
//钻石会员
if (customer.getCusPrice() >= Constants.Level.DVIP_LEVEL) {
consume = customer.getBuyPrice() * Constants.Ratio.DVIP_RATIO;
}
System.out.println(consume);
}
这是我们最常见的最低级的写法,直接将业务代码和策略代码耦合,如果要改会员策略,添加策略等都要修改业务代码,违反了开闭原则也违反了低耦合原则而且改动面比较大,一粗心可能还有漏改的情况。
2、使用简单工厂类,将判断流程写到工厂
(1)、工厂类
static class RatioFactory {
public static double result(Customer customer) {
double consume = 0.00D;
// 判断用户等级
// 非会员
if (customer.getCusPrice() >= Constants.Level.NVIP_LEVEL) {
consume = customer.getBuyPrice() * Constants.Ratio.NVIP_RATIO;
}
//会员
if (customer.getCusPrice() >= Constants.Level.VIP_LEVEL) {
consume = customer.getBuyPrice() * Constants.Ratio.VIP_RATIO;
}
// 超级会员
if (customer.getCusPrice() >= Constants.Level.VVIP_LEVEL) {
consume = customer.getBuyPrice() * Constants.Ratio.VVIP_RATIO;
}
//钻石会员
if (customer.getCusPrice() >= Constants.Level.DVIP_LEVEL) {
consume = customer.getBuyPrice() * Constants.Ratio.DVIP_RATIO;
}
return consume;
}
}
(2)、业务代码
public static void main(String[] args) {
/**
* buyPrice: 用户本次消费额
* cusPrice: 用户累计消费额
*/
Customer customer = Customer.builder().id(110).buyPrice(1000D).cusPrice(3000D).build();
double consume = RatioFactory.result(customer);
System.out.println(consume);
}
这样业务代码很干净,策略代码和业务代码没有耦合……而且不用在业务代码中使用if else 来判断,添加策略代码,只从业务代码来看的话这样很完美了。
但是工厂类中的代码有问题,如果我们要添加策略,还是需要改代码,添加if else 来判断,还是不满足开闭原则。
其实上面两种都没有使用策略模式,因为按照策略模式的定义需要有三个角色即策略抽象类、策略实现类、环境类……
3、完美实现:策略接口(抽象类)+策略实现类+策略选择工厂+计算工具类+注解+反射……
(1)、会员等级常量类
public interface Constants {
/**
* 等级
*/
class Level {
public static final int NVIP_LEVEL = 0;
public static final int VIP_LEVEL = 1000;
public static final int VVIP_LEVEL = 2000;
public static final int DVIP_LEVEL = 3000;
}
/**
* 折扣率
*/
class Ratio {
public static final float NVIP_RATIO = 1F;
public static final float VIP_RATIO = 0.95F;
public static final float VVIP_RATIO = 0.9F;
public static final float DVIP_RATIO = 0.85F;
}
}
(2)、用户Bean
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Customer {
/**
* 用户ID
*/
private Integer id;
/**
* 本次消费金额
*/
private Double buyPrice;
/**
* 累计消费金额
*/
private Double cusPrice;
/**
* 折扣率
*/
private Float ratio;
}
(3)、会员等级、折扣率注解
/**
* 会员等级和折扣率
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface VipAnnotation {
// 等级
int level();
// 等级对应的折扣率
float ratio();
}
(4)、策略抽象类
@VipAnnotation(level = Constants.Level.NVIP_LEVEL, ratio = Constants.Ratio.NVIP_RATIO)
public interface VipDesign {
/**
* 返回折扣后金额
*
* @param customer
* @return
* @throws Exception
*/
Double getRatio(Customer customer) throws Exception;
}
(5)、策略实现类非会员
@VipAnnotation(level = Constants.Level.NVIP_LEVEL, ratio = Constants.Ratio.NVIP_RATIO)
public class NVipDesignImpl implements VipDesign {
@Override
public Double getRatio(Customer customer) throws Exception {
//获取当前策略类注释,可以从中获取会员等级金额和折扣率
VipAnnotation annotation = this.getClass().getAnnotation(VipAnnotation.class);
double res = CountRatio.getResult(customer, annotation);
return res;
}
}
(6)、策略实现类会员
@VipAnnotation(level = Constants.Level.VIP_LEVEL, ratio = Constants.Ratio.VIP_RATIO)
public class VipDesignImpl implements VipDesign {
@Override
public Double getRatio(Customer customer) throws Exception {
VipAnnotation annotation = this.getClass().getAnnotation(VipAnnotation.class);
double res = CountRatio.getResult(customer, annotation);
return res;
}
}
(7)、策略实现类重要会员
@VipAnnotation(level = Constants.Level.VVIP_LEVEL, ratio = Constants.Ratio.VVIP_RATIO)
public class VVipDesignImpl implements VipDesign {
@Override
public Double getRatio(Customer customer) throws Exception {
VipAnnotation annotation = this.getClass().getAnnotation(VipAnnotation.class);
double res = CountRatio.getResult(customer, annotation);
return res;
}
}
(8)、策略实现类钻石会员
@VipAnnotation(level = Constants.Level.DVIP_LEVEL, ratio = Constants.Ratio.DVIP_RATIO)
public class DVipDesignImpl implements VipDesign {
@Override
public Double getRatio(Customer customer) throws Exception {
VipAnnotation annotation = this.getClass().getAnnotation(VipAnnotation.class);
double res = CountRatio.getResult(customer, annotation);
return res;
}
}
(9)、金额计算工具类
public class CountRatio {
public static double getResult(Customer customer, VipAnnotation annotation) throws Exception {
double radio = 0;
if (null == annotation) {
throw new Exception("获取注解为空");
}
if (null != annotation) {
//从策略类注解中获取折扣率
radio = annotation.ratio();
}
if (radio <= 0) {
throw new Exception("获取注释折扣率为0");
}
//打折计算
double res = customer.getBuyPrice() * radio;
// 用户金额累加
customer.setCusPrice(customer.getCusPrice() + customer.getBuyPrice());
//本次折扣率
customer.setRatio(annotation.ratio());
// 返回收款金额
return res;
}
}
(10)、按照消费者累计金额获取策略类(这里代替了业务或者是工厂类中的if else 判断)
这个工厂类的实现可以按照项目业务需要修改实现方式,比如使用@PostConstruct或者spring 容器的ApplicationListener<ContextRefreshedEvent> 接口实现类来加载……
public class DesignFactoryClass {
/**
* 可以使用ioc容器加载
*/
private static List<Class<? extends VipDesign>> classList;
/**
* 静态加载
*/
static {
/**
* 会员打折策略
*/
classList = new ArrayList<>(8);
classList.add(NVipDesignImpl.class);
classList.add(VipDesignImpl.class);
classList.add(VVipDesignImpl.class);
classList.add(DVipDesignImpl.class);
//desc sort
classList.sort((o1, o2) -> {
int level1 = o1.getAnnotation(VipAnnotation.class).level();
int level2 = o2.getAnnotation(VipAnnotation.class).level();
return level1 > level2 ? -1 : (level1 == level2 ? 0 : 1);
});
}
/**
* 按照用户累计消费金额判断用户会员等级,获取会员打折策略类
*
* @param customer
* @return
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static VipDesign getInstance(final Customer customer) {
//默认非会员结算
VipDesign vipDesign = new NVipDesignImpl();
//按照用户累计消费金额获取具体策略Class 实例
Class<? extends VipDesign> aClass = classList.stream().filter(e -> e.getAnnotation(VipAnnotation.class).level() <= customer.getCusPrice()).findFirst().orElse(NVipDesignImpl.class);
try {
// 通过反射获取策略类实例
vipDesign = aClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return vipDesign;
}
}
(11)、业务类
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Test.class);
try {
Customer customer1 = Customer.builder().id(110).cusPrice(900D).buyPrice(100D).build();
double res = DesignFactoryClass.getInstance(customer1).getRatio(customer1);
logger.info("累计消费金额:{},本次消费金额:{},折扣率:{},本次消费折扣后金额:{}", customer1.getCusPrice(), customer1.getBuyPrice(), customer1.getRatio(), res);
customer1.setBuyPrice(1000D);
double res1 = DesignFactoryClass.getInstance(customer1).getRatio(customer1);
logger.info("累计消费金额:{},本次消费金额:{},折扣率:{},本次消费折扣后金额:{}", customer1.getCusPrice(), customer1.getBuyPrice(), customer1.getRatio(), res1);
customer1.setBuyPrice(1000D);
double res2 = DesignFactoryClass.getInstance(customer1).getRatio(customer1);
logger.info("累计消费金额:{},本次消费金额:{},折扣率:{},本次消费折扣后金额:{}", customer1.getCusPrice(), customer1.getBuyPrice(), customer1.getRatio(), res2);
customer1.setBuyPrice(1000D);
double res3 = DesignFactoryClass.getInstance(customer1).getRatio(customer1);
logger.info("累计消费金额:{},本次消费金额:{},折扣率:{},本次消费折扣后金额:{}", customer1.getCusPrice(), customer1.getBuyPrice(), customer1.getRatio(), res3);
} catch (Exception e) {
e.printStackTrace();
}
}
(12)、执行结果
12:09:15.216 [main] INFO com.sb.design.Test - 累计消费金额:1000.0,本次消费金额:100.0,折扣率:1.0,本次消费折扣后金额:100.0
12:09:15.223 [main] INFO com.sb.design.Test - 累计消费金额:2000.0,本次消费金额:1000.0,折扣率:0.95,本次消费折扣后金额:949.999988079071
12:09:15.223 [main] INFO com.sb.design.Test - 累计消费金额:3000.0,本次消费金额:1000.0,折扣率:0.9,本次消费折扣后金额:899.9999761581421
12:09:15.223 [main] INFO com.sb.design.Test - 累计消费金额:4000.0,本次消费金额:1000.0,折扣率:0.85,本次消费折扣后金额:850.0000238418579
Process finished with exit code 0
OK 使用策略模式完成了会员结算问题,相比之前写法使用策略模式、工厂方法解决了策略代码耦合业务代码问题并且在策略工厂中使用反射取消了if 的判断,如果后期修改会员等级、计算方式等都可以直接添加策略类,不用改其他代码,符合开闭原则。而且业务类不知道有多少个策略,也符合最小知晓原则。
四、使用策略模式优点
- 易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开放封闭原则。
- 避免使用多重条件选择语句,充分体现面向对象设计思想。
- 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换。
- 每个策略类使用一个策略类,符合单一职责原则。
- 客户端与策略算法解耦,两者都依赖于抽象策略接口,符合依赖反转原则。
- 客户端不需要知道都有哪些策略类,符合最小知识原则。
五、知识点扩展
模板方法模式和策略模式这两个模式一起被问的概率还是挺高,当时面试蚂蚁金服时就面试过,画两个设计模式的类图,图画在脑子中,下面看看两者的区别与联系。
联系:都可以通过继承抽象类实现抽象方法实现不同功能
区别:策略模式是一个问题的多种解决方案算法的单独封装,而模板方法模式是解决问题的流程相似但是每个子类都是为了解决一个具体的业务需求而产生所以各个子类是不能互换。
六、总结:
策略模式是一个业务场景中需要的多种策略、算法的封装,这些算法是可以替换调用。模板方法模式是多个业务场景解决的流程相似,所以讲相似部分抽象为父类,不同部分抽象成抽象方法子类实现,每个子类解决一个业务场景不能互换。
下一篇我们一起学习模板方法模式约起来……
来源:CSDN
作者:QH_JAVA
链接:https://blog.csdn.net/QH_JAVA/article/details/104441516