tft每日頭條

 > 生活

 > 讀懂設計模式之單例模式

讀懂設計模式之單例模式

生活 更新时间:2024-10-18 20:26:26
背景

以優惠券業務為例,可能存在多種優惠券,滿減券,折扣券,無門檻券,減至券等。用戶在購買一件商品,并且有一張優惠券時,需要計算優惠後金額,計算金額時需要判斷該券的類型。假設一開始産品提出需要實現滿減券,折扣券,無門檻券三種優惠券類型,得出如下代碼:

初始代碼

優惠券類型枚舉

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看起來也很清晰,否則我認為就屬于過度設計了。一般情況,策略模式都是結合工廠模式使用,可以更好的對策略類進行管理,降低客戶端的使用成本。策略模式良好的踐行了開閉原則,單一職責原則,裡氏替換原則。

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved