以優惠券業務為例,可能存在多種優惠券,滿減券,折扣券,無門檻券,減至券等。用戶在購買一件商品,并且有一張優惠券時,需要計算優惠後金額,計算金額時需要判斷該券的類型。假設一開始産品提出需要實現滿減券,折扣券,無門檻券三種優惠券類型,得出如下代碼:
初始代碼優惠券類型枚舉
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)模式的定義:定義了一系列算法,并将每個算法封裝起來,使它們可以相互替換,且算法的變化不會影響使用算法的客戶。
模式的結構策略模式的主要角色如下。
模式基本實現上下文類
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較少,且未來也不怎麼會變化時,其實一般不一定需要使用策略模式來優化,少許的if-else看起來也很清晰,否則我認為就屬于過度設計了。一般情況,策略模式都是結合工廠模式使用,可以更好的對策略類進行管理,降低客戶端的使用成本。策略模式良好的踐行了開閉原則,單一職責原則,裡氏替換原則。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!