读懂设计模式之单例模式(实战设计模式策略模式)

背景

以优惠券业务为例,可能存在多种优惠券,满减券,折扣券,无门槛券,减至券等。用户在购买一件商品,并且有一张优惠券时,需要计算优惠后金额,计算金额时需要判断该券的类型。假设一开始产品提出需要实现满减券,折扣券,无门槛券三种优惠券类型,得出如下代码:

初始代码

优惠券类型枚举

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)类:持有一个策略类的引用,最终给客户端调用。
UML图

读懂设计模式之单例模式(实战设计模式策略模式)(1)

模式基本实现上下文类

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); } } 复制代码

优化后的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); 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元)的话,优化效果会更加明显。例如下图中不仅计算了实际价格,还展示了优惠文案。

读懂设计模式之单例模式(实战设计模式策略模式)(2)

完整代码见:...待补充

优缺点优点
  1. 有效避免了if-else与switch-case过多的情况,通过定义新的子类很容易增加新的策略和转换,适应了开闭原则
  2. 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码,如上面的计算总价方法calculateTotalPrice。
  3. 通过结合工厂模式,可以避免让客户感知到具体策略类,通常客户只需要感知抽象策略类即可。
缺点
  1. 策略模式会造成很多的策略类,增加维护难度,一般建议算法族放到一个包下单独维护较好。
  2. 如果不结合工厂模式,那客户端必须自己来选择合适的策略,必须清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。
总结

当if-else或者switch-case较少,且未来也不怎么会变化时,其实一般不一定需要使用策略模式来优化,少许的if-else看起来也很清晰,否则我认为就属于过度设计了。一般情况,策略模式都是结合工厂模式使用,可以更好的对策略类进行管理,降低客户端的使用成本。策略模式良好的践行了开闭原则,单一职责原则,里氏替换原则。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页