有朋友问这个项目中有没有用到什么设计模式,刚好,我们这个项目中需要计算优惠金额,所以,想到了策略模式。
关于优惠券系统的设计与实现,已经完结;
可能大家更多是关注做完项目,如何应对面试,我一共整理了10多个面试题,后期如有朋友反馈,会继续添加。
需要的话直接扫描下方二维码,可以免费获取设计文档和源码。
业务场景
我们回顾一下优惠券使用类型有:现金抵扣、满减以及折扣。
假设当前支付总金额是200.99元。此时,用到了一张优惠券,优惠券使用类型是折扣,优惠券面值是1(表示打1者)。前面的实现中,计算优惠金额是在支付代码中实现的。
实际上来说,肯定是需要优惠券功能模块里提供一个计算工具类,支付模块直接使用工具类就只能知道最终支付金额了。
关于业务,我们就说这么多,我们呢来看策略模式。
策略模式
策略模式(strategy pattern)的原始定义是:
定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
经典话语:条条大路通北京
我们可以这么理解:每一条路都是一个策略。
下面我们来看一下java中策略模式该如何实现。
// 策略接口
interface Strategy {
void execute();
}
// 具体策略A
class ConcreteStrategyA implements Strategy {
public void execute() {
System.out.println("策略 A 执行。");
}
}
// 具体策略B
class ConcreteStrategyB implements Strategy {
public void execute() {
System.out.println("策略 B 执行。");
}
}
// 环境类
class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public void execute() {
strategy.execute();
}
}
// 客户端
public class StrategyPatternExample {
public static void main(String[] args) {
// 使用策略A
Context context = new Context(new ConcreteStrategyA());
context.execute();
// 切换到策略B
context.setStrategy(new ConcreteStrategyB());
context.execute();
}
}
策略模式的主要角色如下:
抽象策略(Strategy)类 :这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
具体策略(Concrete Strategy)类 :实现了抽象策略定义的接口,提供具体的算法实现或行为。
环境或上下文(Context)类 :是使用算法的角色, 持有一个策略类的引用,最终给客户端调用。
我们在来看我们前面说的优惠金额计算的实现。
业务场景实现
先定义一个优惠券使用类型枚举类。
@Getter
public enum CouponUseTypeEnum {
/**
* 现金
*/
CASH(1, "现金"),
/**
* 折扣
*/
DISCOUNT(2, "折扣"),
/**
* 满减
*/
FULL_REDUCE(3, "满减");
private final int type;
private final String desc;
private static final Map<Integer, CouponUseTypeEnum> couponUseTypeEnumMap = new HashMap<>();
static {
for (CouponUseTypeEnum value : CouponUseTypeEnum.values()) {
couponUseTypeEnumMap.put(value.getType(), value);
}
}
CouponUseTypeEnum(int type, String desc) {
this.type = type;
this.desc = desc;
}
public static String getDescByType(int type) {
return couponUseTypeEnumMap.get(type).desc;
}
}
这个枚举没什么好说的,简单的不能再简单了。
定义计算接口
public interface CalculateDiscountAmountService {
BigDecimal calculateDiscountAmount(BigDecimal totalAmount, BigDecimal couponFaceAmount);
}
两个参数:
总金额
优惠券面值
实现类
折扣类型的实现
@Service
public class CalculateDiscountAmount4DiscountServiceImpl implements CalculateDiscountAmountService {
@Override
public BigDecimal calculateDiscountAmount(BigDecimal totalAmount, BigDecimal couponFaceAmount) {
//比例值 couponFaceAmount=1 表示一折,乘以10后的比例值为10% 0.1
BigDecimal proportion = couponFaceAmount.divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
//优惠金额=总金额*比例值/100
BigDecimal discountAmount = totalAmount.multiply(proportion).setScale(3, RoundingMode.HALF_UP);
//实际支付金额=总金额-优惠金额
return totalAmount.subtract(discountAmount).setScale(2, RoundingMode.HALF_UP);
}
}
另外两个也类似:
@Service
public class CalculateDiscountAmount4FullReduceServiceImpl implements CalculateDiscountAmountService {
@Override
public BigDecimal calculateDiscountAmount(BigDecimal totalAmount, BigDecimal couponFaceAmount) {
BigDecimal discountAmount= 计算优惠金额
return discountAmount;
}
}
@Service
public class CalculateDiscountAmount4DeductionServiceImpl implements CalculateDiscountAmountService {
@Override
public BigDecimal calculateDiscountAmount(BigDecimal totalAmount, BigDecimal couponFaceAmount) {
BigDecimal discountAmount= 计算优惠金额
return discountAmount;
}
}
这样就实现了优惠金额的实际计算。
但是,对于调用者来说,只有一个使用类型,所以,我们需要把优惠券使用类型和实现类关联起来。
public class CalculateDiscountAmountStrategy {
public final static Map<Integer, Class<? extends CalculateDiscountAmountService>> DISCOUNT_AMOUNT_MAP = new HashMap<>();
static {
DISCOUNT_AMOUNT_MAP.put(CouponUseTypeEnum.CASH.getType(), CalculateDiscountAmount4DeductionServiceImpl.class);
DISCOUNT_AMOUNT_MAP.put(CouponUseTypeEnum.DISCOUNT.getType(), CalculateDiscountAmount4DiscountServiceImpl.class);
DISCOUNT_AMOUNT_MAP.put(CouponUseTypeEnum.FULL_REDUCE.getType(), CalculateDiscountAmount4FullReduceServiceImpl.class);
}
}
现在已经关联起来了,我们该如何用呢?
使用
@GetMapping("/test2")
public String test2() {
BigDecimal totalAmount= new BigDecimal("200.99");//200.99-20.099= 180.891
BigDecimal faceAmount= new BigDecimal("1");
int type=2;
Class<? extends CalculateDiscountAmountService> serviceClass = CalculateDiscountAmountStrategy.DISCOUNT_AMOUNT_MAP.get(type);
CalculateDiscountAmountService service = ApplicationContextFactory.getBean(serviceClass);
BigDecimal bigDecimal = service.calculateDiscountAmount(totalAmount, faceAmount);
System.out.println(bigDecimal);
return "success";
}
上面代码中:
定义了totalAmount为200.99
优惠券面值faceAmount=1 (表示打1折)
优惠券使用类型是2(对应枚举中就是折扣类型)
最后计算出优惠后总金额180.89=200.99-20.099 (因为人民币只到分,所以保留两位小数)。
总结
看得出来,如果优惠券使用类型增加,我们只需要在枚举中加一个类型,再创建一个具体实现类,最后在维护好优惠券使用类和计算实现类中再加入一个关系即可。
对于调用方,其实并不知道我们加了一个使用类型。同时,如果有多个地方需要计算优惠金额,我们也可以复用代码。
上面说的是好处,其实策略模式也是有缺点:导致创建很多类。
另外,我们这个案例中,其实除了策略模式以外, 还用到了工厂模式。只是这个对象的生产是Spring帮我们做的。