背景
公司Java编码规范中,往往强调禁止使用大量的if-else语句。那么在面对不同的业务逻辑时,除了if-else还有什么选择呢?
策略模式就是专门为此而生的,它能消除大片大片的if-else,提升代码的可读性和可维护性,但也因为策略模式对此类问题的针对性,此时不用,别的地方也用不上它了…不过策略模式也有缺陷,通常要结合工厂模式一同使用。但本文只介绍作者对于策略模式的理解。
用伪代码和if-else写一个简单的商品折扣功能感受下:
/**
* if-else编写打折方案示例:
* 1、普通客户:无折扣、可购买送货上门服务;
* 2、普通会员:一律九折、可购买送货上门服务,消费金额大于等于100元可免服务费;
* 3、超级会员:一律八折、无门槛免费送货上门。
* @param 商品原价
* @param 客户级别
* @return BigDecimal 应付金额或错误码
*
* @author 青青橙
* @since 2019年10月23日
*/
public BigDecimal discountByIfElse(商品原价, 客户级别) {
初始化送货上门服务费为0;
if (客户级别为普通客户) {
if (购买送货上门服务) {
计算送货上门服务费;
}
将商品原价加上送货上门服务费作为返回结果;
} else if (客户级别为普通会员) {
if (购买送货上门服务且消费金额小于100元) {
计算送货上门服务费;
}
将商品原价*九折后再加上送货上门服务费作为返回结果;
} else (客户级别为超级会员) {
将商品原价*八折后作为返回结果;
}
返回错误码;
}
这仅仅只是一个最简模型,而且还用的是伪代码,就已经让人不得不一步一步跟进if块中认真考虑上下文逻辑了。倘若后续需要办活动,满减?优惠券?如何修改又得仔细分析一番。到最后这片代码将变得越来越复杂庞大,实在没法维护时整个功能都得推倒重来。
特点
策略模式是有专用性的,它只能解决下列规则范围内的问题。即使用场景:
1. 在一个模块里面有许多行为,它们之间是平等的,可以相互替换的,互斥的;
2. 只允许在这些行为中选择一种(互斥);
3. 这些行为是动态的,它们后续极可能会变化,甚至废弃。
策略模式的优点:
1. 策略模式可类比多态,多态有啥优点它就有啥优点;
2. 扩展性良好,符合了开闭原则。只要实现接口就可以在现有的系统中增加一个策略, 其他的都不用修改;
3. 避免使用多重条件判断,提升了代码可维护性和可读性;
4. 避免代码重复,策略模式提供了管理这些算法的方法,并恰当地使用了继承,使得公共代码可以提取到父类中。
策略模式的缺点:
1. 每个行为都要提取为一个算法类,算法类的数量可能难以控制;
2. 所有的策略类都需要对外暴露。调用者必须知道有那些策略,并自行决定使用哪一个策略类。这就意味着调用者必须理解这些算法的区别,以便适时选择恰当的算法类。这与迪米特法则是相违背的。我们可以使用其他模式来修正这个缺陷,如工厂方法模式、代理模式或享元模式。
专用名词解释
名词 | 解释 |
---|---|
Strategy —— 抽象策略类 | 策略、算法家族的抽象父接口,能解耦,定义每个策略或算法必须具有的方法和属性 |
ConcreteStrategy —— 具体策略类 | Strategy的具体实现类,即由行为提取出来的策略类,定义单个策略或算法特定的逻辑 |
Context —— 上下文类 | 封装对Strategy的调用,屏蔽高层模块对策略、算法的直接访问。调用者通过它来使用不同策略 |
运用
针对上文if-else方式实现折扣价的算法,来试着写出使用策略模式设计的算法。
首先不考虑上下文业务逻辑,只考虑行为主体是谁?有什么行为?对,是所有客户的打折行为。它就是Strategy了。
/**
* StrategyPattern编写打折方案示例。
* 行为主体:所有策略接口(Strategy)
*
* @author 青青橙
* @since 2019年10月23日
*/
public interface DiscountByMemberType {
/**
* 行为:计算应付价格
* @param 商品原价
* @return BigDecimal 应付金额
*
* @author 青青橙
* @since 2019年10月23日
*/
public BigDecimal discountedPrice(商品原价);
}
再考虑具体有哪些打折行为呢?它们各自的内部业务逻辑是什么?有三种,内部业务逻辑具体是:
用户类型 | 折扣方案 |
---|---|
普通客户 | 无折扣、可购买送货上门服务 |
普通会员 | 一律九折、可购买送货上门服务,消费金额大于等于100元可免服务费 |
超级会员 | 一律八折、无门槛免费送货上门 |
这就是ConcreteStrategy了。
/**
* 普通客户的折扣策略
*
* @author 青青橙
* @since 2019年10月23日
*/
public class OrdinaryCustomersDiscount implements discountByStrategy {
/**
* 该特定策略的内部逻辑(ConcreteStrategy)
* 计算普通客户应付价格
* @param 商品原价
* @return BigDecimal 应付金额
*
* @author 青青橙
* @since 2019年10月23日
*/
@Override
public BigDecimal discountedPrice(商品原价) {
if (购买送货上门服务) {
计算送货上门服务费;
}
将商品原价加上送货上门服务费作为返回结果;
}
}
/**
* 普通会员的折扣策略
*
* @author 青青橙
* @since 2019年10月23日
*/
public class OrdinaryMembersDiscount implements discountByStrategy {
/**
* 该特定策略的内部逻辑(ConcreteStrategy)
* 计算普通会员应付价格
* @param 商品原价
* @return BigDecimal 应付金额
*
* @author 青青橙
* @since 2019年10月23日
*/
@Override
public BigDecimal discountedPrice(商品原价) {
if (购买送货上门服务且消费金额小于100元) {
计算送货上门服务费;
}
将商品原价*九折后再加上送货上门服务费作为返回结果;
}
}
/**
* 超级会员的折扣策略
*
* @author 青青橙
* @since 2019年10月23日
*/
public class SuperMembersDiscount implements discountByStrategy {
/**
* 该特定策略的内部逻辑(ConcreteStrategy)
* 计算超级会员应付价格
* @param 商品原价
* @return BigDecimal 应付金额
*
* @author 青青橙
* @since 2019年10月23日
*/
@Override
public BigDecimal discountedPrice(商品原价) {
将商品原价*八折后作为返回结果;
}
}
那么现在,怎么调用这些行为呢?那就是Context了。
/**
* 针对所有策略的上下文类(Context)
* (这里为了和上文if-else处形成对比,使用了同名变量。实际开发严禁中文命名)
*/
public class Cashier {
/**
* 行为主体:策略。
*/
private DiscountByMemberType 客户级别;
public Cashier(DiscountByMemberType 客户级别) {
this.客户级别 = 客户级别;
}
/**
* 策略对应的行为
*/
public BigDecimal computePrice(BigDecimal 商品原价) {
return this.客户级别.discountedPrice(商品原价);
}
}
整个打折业务已经使用策略模式改进完毕,现在可以调用啦。
public class Test {
public static void main(String[] args) {
//创建需要使用的策略对象
DiscountByMemberType 客户级别 = new OrdinaryCustomersDiscount();
//创建上下文对象
Cashier cashier = new Cashier(客户级别);
//计算价格
BigDecimal cash = cashier.computePrice(商品原价);
System.out.println("普通客户应付:" + cash.doubleValue());
//可复用
客户级别 = new SuperMembersDiscount();
cashier = new Cashier(客户级别);
cash = cashier.computePrice(商品原价);
System.out.println("超级会员应付:" + cash.doubleValue());
}
}