读懂设计模式之单例模式(实战设计模式策略模式)
以优惠券业务为例,可能存在多种优惠券,满减券,折扣券,无门槛券,减至券等。用户在购买一件商品,并且有一张优惠券时,需要计算优惠后金额,计算金额时需要判断该券的类型。假设一开始产品提出需要实现满减券,折扣券,无门槛券三种优惠券类型,得出如下代码:
初始代码优惠券类型枚举
public enum CouponTypeEnum {
DiscountCoupon(1, "折扣券"),
FullCutCoupon(2, "满减券"),
NoThresholdReducedToCoupon(3, "无门槛扣减券"),
//ReducedToCoupon(4, "减至券"),
;
private int code;
private String desc;
CouponTypeEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return this.code;
}
public String getDesc() {
return this.desc;
}
public static CouponTypeEnum getByCode(int code) {
for (CouponTypeEnum couponTypeEnums : values()) {
if (code == couponTypeEnums.getCode()) {
return couponTypeEnums;
}
}
throw new IllegalArgumentException("CouponTypeEnum not exist, code=" code);
}
}
复制代码
业务处理service类
@Service
public class CouponService {
@Autowired
private CouponRepository couponRepository;
/**
* 计算优惠
*
* @param quantity
* @param sellingPrice
* @param couponId
* @return
*/
public CouponCalcResult calculate(Integer quantity, Long sellingPrice, Long couponId) {
Coupon coupon = couponRepository.get(couponId);
CouponTypeEnum couponTypeEnum = CouponTypeEnum.getByCode(coupon.getType());
//获取优惠配置,例如xx折,满xx元减yy元少
CouponConfig couponConfig = JsonUtils.fromJson(coupon.getConfig(), CouponConfig.class);
CouponCalcParams couponCalcParams = new CouponCalcParams(sellingPrice, quantity);
CouponCalcResult couponCalcResult;
switch (couponTypeEnum) {
case DiscountCoupon:
couponCalcResult = discountCouponCalculate(couponCalcParams, couponConfig);
break;
case FullCutCoupon:
couponCalcResult = fullCutCouponCalculate(couponCalcParams, couponConfig);
break;
case NoThresholdReducedToCoupon:
couponCalcResult = noThresholdReduceCouponCalculate(couponCalcParams, couponConfig);
break;
default:
throw new IllegalArgumentException("couponTypeEnum error");
}
return couponCalcResult;
}
/**
* 计算原总价
*
* @param quantity
* @param sellingPrice
* @return
*/
Long calculateTotalPrice(Integer quantity, Long sellingPrice) {
return quantity * sellingPrice;
}
/**
* 折扣券计算优惠
*/
private CouponCalcResult discountCouponCalculate(CouponCalcParams couponCalcParams, CouponConfig couponConfig) {
if (couponConfig == null || couponConfig.getDiscount() == null) {
throw new IllegalArgumentException("couponConfig error");
}
CouponCalcResult result = new CouponCalcResult();
// 计算总价
Long totalPrice = calculateTotalPrice(couponCalcParams.getQuantity(), couponCalcParams.getSellingPrice());
Long amount = totalPrice * (1000 - couponConfig.getDiscount()) / 1000;
if (couponConfig.getMaxReductionAmount() != null && amount >= couponConfig.getMaxReductionAmount()) {
amount = couponConfig.getMaxReductionAmount();
}
result.setAmount(amount); //优惠金额
result.setActualAmount(totalPrice - amount); //优惠后实际金额
return result;
}
/**
* 满减券计算优惠
*/
private CouponCalcResult fullCutCouponCalculate(CouponCalcParams couponCalcParams, CouponConfig couponConfig) {
if (couponConfig == null || couponConfig.getThresholdAmount() == null || couponConfig.getReductionAmount() == null) {
throw new IllegalArgumentException("couponConfig error");
}
CouponCalcResult result = new CouponCalcResult();
// 计算总价
Long totalPrice = calculateTotalPrice(couponCalcParams.getQuantity(), couponCalcParams.getSellingPrice());
Long actualAmount = totalPrice;
if (totalPrice >= couponConfig.getThresholdAmount()) {
actualAmount -= couponConfig.getReductionAmount();
}
result.setAmount(totalPrice - actualAmount);
result.setActualAmount(actualAmount);
return result;
}
/**
* 无门槛扣减券计算优惠
*/
private CouponCalcResult noThresholdReduceCouponCalculate(CouponCalcParams couponCalcParams, CouponConfig couponConfig) {
if (couponConfig == null || couponConfig.getThresholdAmount() == null) {
throw new IllegalArgumentException("couponConfig error");
}
CouponCalcResult result = new CouponCalcResult();
// 计算总价
Long totalPrice = calculateTotalPrice(couponCalcParams.getQuantity(), couponCalcParams.getSellingPrice());
//计算实际应付金额
long actualAmount = totalPrice - couponConfig.getReductionAmount();
// actualAmount 取值到角
actualAmount = actualAmount < 0 ? 0 : actualAmount;
result.setActualAmount(actualAmount);
result.setAmount( totalPrice - actualAmount);
return result;
}
}
复制代码
其中couponConfig目前有如下属性,
@Data
public class CouponConfig {
// 折扣保留了小数点后两位,用整数表示时要乘以1000
private Integer discount;
// 最多减多少(单位 分)
private Long maxReductionAmount;
//总价满多少(单位分)
private Long thresholdAmount;
//总价减多少(单位分)
private Long reductionAmount;
// 单价减至多少元
private Long unitReduceToAmount;
}
复制代码
比如是折扣券,只关心discount和maxReductionAmount两个字段,存在数据库中可能为如下配置,表示打9折,最多减100元。
{"discount":900,"maxReductionAmount":10000}
复制代码
随着业务的迭代,新增了优惠券类型减至券,那CouponService类中需要做如下更改:
@Service
public class CouponService {
@Autowired
private CouponRepository couponRepository;
/**
* 计算优惠
*
* @param quantity
* @param sellingPrice
* @param couponId
* @return
*/
public CouponCalcResult calculate(Integer quantity, Long sellingPrice, Long couponId) {
Coupon coupon = couponRepository.get(couponId);
CouponTypeEnum couponTypeEnum = CouponTypeEnum.getByCode(coupon.getType());
//获取优惠配置,例如xx折,满xx元减yy元少
CouponConfig couponConfig = JsonUtils.fromJson(coupon.getConfig(), CouponConfig.class);
CouponCalcParams couponCalcParams = new CouponCalcParams(sellingPrice, quantity);
CouponCalcResult couponCalcResult;
switch (couponTypeEnum) {
case DiscountCoupon:
couponCalcResult = discountCouponCalculate(quantity, sellingPrice, couponConfig);
break;
case FullCutCoupon:
couponCalcResult = fullCutCouponCalculate(quantity, sellingPrice, couponConfig);
break;
case NoThresholdReducedToCoupon:
couponCalcResult = noThresholdReduceCouponCalculate(quantity, sellingPrice, couponConfig);
break;
case ReducedToCoupon: //新增
couponCalcResult = reduceToCouponCalculate(quantity, sellingPrice, couponConfig);
break;
default:
throw new IllegalArgumentException("couponTypeEnum error");
}
return couponCalcResult;
}
/**
* 计算原总价
* 折扣券计算优惠
* 满减券计算优惠
* 无门槛扣减券计算优惠
* 代码一致
*/
/**
* 减至券计算优惠
*/
private CouponCalcResult reduceToCouponCalculate(CouponCalcParams couponCalcParams, CouponConfig couponConfig) {
if (couponConfig == null || couponConfig.getUnitReduceToAmount() == null) {
throw new IllegalArgumentException("couponConfig error");
}
CouponCalcResult result = new CouponCalcResult();
// 计算总价
Long totalPrice = calculateTotalPrice(couponCalcParams.getQuantity(), couponCalcParams.getSellingPrice());
//计算实际应付金额
long actualAmount = couponConfig.getUnitReduceToAmount() * couponCalcParams.getQuantity();
result.setActualAmount(actualAmount);
result.setAmount( totalPrice - actualAmount);
return result;
}
}
复制代码
可以看出,这里我们对switch case进行了更改,违背了开闭原则,最好对这块代码进行回归测试。并且在当前类上增加了减至券的计算方法,导致该类变得更加复杂。但其实只要客户端知道当前是折扣券之后,其实只需要关心折扣券计算方法而已。根据单一职责原则与里氏替换原则的指导,我们考虑使用策略模式对其进行优化。
定义策略(Strategy)模式的定义:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
模式的结构策略模式的主要角色如下。
- 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
模式基本实现上下文类
public class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public Strategy getStrategy() {
return strategy;
}
public void setStrategy(Strategy state) {
this.strategy = state;
}
public void handle() {
strategy.handle();
}
}
复制代码
抽象策略类
public interface Strategy {
void handle();
}
复制代码
具体策略类A
public class AStrategy implements Strategy{
@Override
public void handle() {
System.out.println("AStrategy");
}
}
复制代码
具体策略类B
public class BStrategy implements Strategy{
@Override
public void handle() {
System.out.println("BStrategy");
}
}
复制代码
测试Client类
public class ClientTest {
public static void main(String[] args) {
AStrategy aStrategy = new AStrategy();
Context context = new Context(aStrategy);
context.handle();
BStrategy bStrategy = new BStrategy();
context.setStrategy(bStrategy);
context.handle();
}
}
复制代码
执行结果
AStrategy
BStrategy
复制代码
在Context不主动set最新的Strategy时,handle可重复执行。
上面的基本代码中有两个问题,一是一般客户端无需感知Strategy的继承簇,即无需感知到AStrategy和BStrategy,二是在使用之前依靠客户端自己new一个实例出来,并且set到context中使用,其实没有必要,因为各个具体策略之间没有像状态模式那样的耦合关系,可以不需要维护这个上下文关系。为了解决这两个问题,对策略类的管理可以利用工厂来实现。
策略工厂类
public class StrategyFactory {
private static final Map<String, Strategy> strategyMap = new HashMap<>();
//如果是spring环境下可以通过@PostConstruct完成注册
static {
register("A", new AStrategy());
register("B", new BStrategy());
}
public static void register(String code, Strategy strategy) {
strategyMap.put(code, strategy);
}
public static Strategy get(String code) {
return strategyMap.get(code);
}
}
复制代码
客户端实现变为
public class ClientTest {
public static void main(String[] args) {
Strategy strategy = StrategyFactory.get("A");
strategy.handle();
strategy = StrategyFactory.get("B");
strategy.handle();
}
}
复制代码
基于此我们对优惠券计算的代码进行优化,由于目前项目一般都是使用springboot进行开发,下面给出优惠券计算在springboot中实现的代码。
定义抽象优惠券类
public abstract class AbstractCouponCalculator {
abstract CouponTypeEnum getCouponTypeEnum();
@PostConstruct
void register() {
CouponCalculateFactory.register(getCouponTypeEnum(), this);
}
/**
* 计算原总价
* @param params
* @return
*/
Long calculateTotalPrice(CouponCalcParams params) {
return params.getSellingPrice() * params.getQuantity();
}
/**
* 计算金额
* @param params
* @return
*/
public abstract CouponCalcResult calculate(CouponCalcParams params, CouponConfig couponConfig);
}
复制代码
以折扣券为例
@Component
public class DiscountCouponCalculator extends AbstractCouponCalculator {
@Override
CouponTypeEnum getCouponTypeEnum() {
return CouponTypeEnum.DiscountCoupon;
}
@Override
public CouponCalcResult calculate(CouponCalcParams params, CouponConfig couponConfig) {
CouponCalcResult result = new CouponCalcResult();
// 计算总价
Long totalPrice = calculateTotalPrice(params);
Long amount = totalPrice * (1000 - couponConfig.getDiscount()) / 1000;
if (couponConfig.getMaxReductionAmount() != null && amount >= couponConfig.getMaxReductionAmount()) {
amount = couponConfig.getMaxReductionAmount();
}
result.setAmount(amount);
result.setActualAmount( totalPrice - amount );
return result;
}
}
复制代码
public class CouponCalculateFactory {
private static final Map<CouponTypeEnum, AbstractCouponCalculator> calculatorMap = new HashMap<>();
public static void register(CouponTypeEnum couponTypeEnum, AbstractCouponCalculator couponCalculator) {
calculatorMap.put(couponTypeEnum, couponCalculator);
}
public static AbstractCouponCalculator get(CouponTypeEnum couponTypeEnum) {
return calculatorMap.get(couponTypeEnum);
}
}
复制代码
@Service
public class CouponService {
@Autowired
private CouponRepository couponRepository;
/**
* 计算优惠
*
* @param quantity
* @param sellingPrice
* @param couponId
* @return
*/
public CouponCalcResult calculate(Integer quantity, Long sellingPrice, Long couponId) {
Coupon coupon = couponRepository.get(couponId);
CouponConfig couponConfig = JsonUtils.fromJson( coupon.getConfig(), CouponConfig.class);
AbstractCouponCalculator couponCalculator = CouponCalculateFactory.get(CouponTypeEnum.getByCode(coupon.getType()));
CouponCalcParams couponCalcParams = new CouponCalcParams(sellingPrice, quantity);
return couponCalculator.calculate(couponCalcParams, couponConfig);
}
}
复制代码
可以看出当前的CouponService变得简约了很多,可读性自然也提高了很多。如果策略类中不止包含了一个方法,比如当前只有calculate方法,如果还有display方法(用于展示最后计算出来的优惠效果文案,例如xx折,低至xx元,满xx减yy元)的话,优化效果会更加明显。例如下图中不仅计算了实际价格,还展示了优惠文案。
完整代码见:...待补充
优缺点优点- 有效避免了if-else与switch-case过多的情况,通过定义新的子类很容易增加新的策略和转换,适应了开闭原则
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码,如上面的计算总价方法calculateTotalPrice。
- 通过结合工厂模式,可以避免让客户感知到具体策略类,通常客户只需要感知抽象策略类即可。
- 策略模式会造成很多的策略类,增加维护难度,一般建议算法族放到一个包下单独维护较好。
- 如果不结合工厂模式,那客户端必须自己来选择合适的策略,必须清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。
当if-else或者switch-case较少,且未来也不怎么会变化时,其实一般不一定需要使用策略模式来优化,少许的if-else看起来也很清晰,否则我认为就属于过度设计了。一般情况,策略模式都是结合工厂模式使用,可以更好的对策略类进行管理,降低客户端的使用成本。策略模式良好的践行了开闭原则,单一职责原则,里氏替换原则。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com