- 策略模式
定義了算法族,分別封裝起來,讓它們之間可以互相替換,
此模式讓算法的變化獨立於使用算法的客戶
源碼:https://github.com/youxiu326/sb_promotion.git
- 實體層
一共三個實體,分別為商品實體,促銷實體,促銷結果實體
商品實體定義了商品的銷售價 優惠金額 優惠后金額 數量。。。
促銷實體定義了促銷類型 名稱 參與該促銷的商品集合

package com.youxiu326.entity; import java.io.Serializable; import java.math.BigDecimal; /** * 商品: * * <br>優惠金額 <span color="red">discountAmount</span> * <br>優惠后價格 -1(默認等於銷售金額) <span color="red">finalAmount</span> * <br>銷售價 <span color="red">amount</span> * */ public class Product implements Serializable { private String code; private String name; /** * 銷售價 */ private BigDecimal amount = BigDecimal.ZERO; /** * 優惠了多少金額 */ private BigDecimal discountAmount = BigDecimal.ZERO; /** * 優惠后金額 */ private BigDecimal finalAmount = new BigDecimal("-1"); private Integer quantity; public Product(){} public Product(String code, String name, BigDecimal amount, Integer quantity) { this.code = code; this.name = name; this.amount = amount; this.quantity = quantity; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getName() { return name; } public void setName(String name) { this.name = name; } public BigDecimal getAmount() { return amount; } public double getAmountDouble(){ return amount.doubleValue(); } public void setAmount(BigDecimal amount) { this.amount = amount; } public Integer getQuantity() { return quantity; } public void setQuantity(Integer quantity) { this.quantity = quantity; } public BigDecimal getDiscountAmount() { return discountAmount; } public void setDiscountAmount(BigDecimal discountAmount) { this.discountAmount = discountAmount; } /** * 優惠后金額(默認等於交易金額) * @return */ public BigDecimal getFinalAmount() { if(finalAmount.compareTo(new BigDecimal("-1"))==0) { finalAmount = amount; } return finalAmount; } public void setFinalAmount(BigDecimal finalAmount) { this.finalAmount = finalAmount; } }

package com.youxiu326.entity; import java.io.Serializable; import java.util.List; /** * 促銷實體類 */ public class Promotion implements Serializable { /** * <span color="red">促銷類型:</span> * <br>FREEONE 免最低一件 * <br>REBATE 八折 * <br>REDUCE 滿100減20 */ public static enum Type{ FREEONE,REBATE,REDUCE } private Type type; private String name; /** * 哪些商品應用促銷 */ private List<Product> products; public Promotion(){} public Promotion(Type type, String name,List<Product> products) { this.type = type; this.name = name; this.products = products; } public Type getType() { return type; } public void setType(Type type) { this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Product> getProducts() { return products; } public void setProducts(List<Product> products) { this.products = products; } }

package com.youxiu326.entity; import java.io.Serializable; /** * 促銷結果 */ public class PromotionResult implements Serializable { private String name; private Promotion.Type type; private Object result; public String getName() { return name; } public void setName(String name) { this.name = name; } public Promotion.Type getType() { return type; } public void setType(Promotion.Type type) { this.type = type; } public Object getResult() { return result; } public void setResult(Object result) { this.result = result; } }

package com.youxiu326.exception; /** * 自定義異常 */ public class ServiceException extends Exception { private Exception exception; public ServiceException(String message, Exception exception) { super(message); this.exception = exception; } public ServiceException(String message) { this(message, null); } public ServiceException(Exception exception) { this(null, exception); } public Exception getException() { return exception; } public Exception getRootCause() { if (exception instanceof ServiceException) { return ((ServiceException) exception).getRootCause(); } return exception == null ? this : exception; } @Override public String toString() { if (exception instanceof ServiceException) { return ((ServiceException) exception).toString(); } return exception == null ? super.toString() : exception.toString(); } }
- 促銷實體類
定義了一個抽象類 PromotionStrategy.java
定義了三個促銷策略:
/**
* 滿足價格大於等於500
* <br>減免價格最低一件商品促銷
*/
@Component
public class FreeOneStrategy extends PromotionStrategy/**
* 滿足大於200
* <br>八折促銷
*/
@Component
public class RebateStrategy extends PromotionStrategy/**
* 滿足滿100
* <br>減10促銷
*/
@Component
public class ReduceStrategy extends PromotionStrategy
package com.youxiu326.abst; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import java.math.BigDecimal; import java.util.List; /** * 促銷抽象類 * 定義公共方法,讓子類繼承 * 定義抽象方法,讓子類實現 */ public abstract class PromotionStrategy { public abstract Promotion.Type getType(); /** * 定義執行促銷方法 * @param promotion 促銷 * @param products 參加促銷的商品集合 * @return */ public abstract List<PromotionResult> execute(Promotion promotion, List<Product> products); /* //加法 BigDecimal result1 = num1.add(num2); //減法 BigDecimal result2 = num1.subtract(num2); //乘法 BigDecimal result3 = num1.multiply(num2); //除法 BigDecimal result5 = num2.divide(num1,20,BigDecimal.ROUND_HALF_UP); //絕對值 BigDecimal result4 = num3.abs(); 比較大小 結果是: -1:小於; 0 :等於; 1 :大於; BigDecimal b1 = new BigDecimal("-121454125453.145"); if(b1.compareTo(BigDecimal.ZERO)==-1) { System.out.println("金額為負數!"); } */ //優惠金額 discountAmount //優惠后價格 -1(默認等於銷售金額) finalAmount //銷售價 amount /** * <span color="red">平攤優惠金額</span> * @param products * @param disAmount */ protected void sharedAmount(List<Product> products,BigDecimal disAmount){ //計算總金額 double totalAmountTemp = products.stream().mapToDouble(it->( it.getFinalAmount().multiply(new BigDecimal(it.getQuantity().toString()))).doubleValue() ).sum(); //總金額 BigDecimal totalAmount = new BigDecimal(totalAmountTemp+""); //已分攤金額 BigDecimal sharedAmount = new BigDecimal("0");; //平攤金額到明細 for(int i=0;i<products.size();i++) { Product product = products.get(i); if(i == products.size() - 1) { //② 如果是最后一件商品 ,將剩余優惠金額計算到這個商品內 //例如: // 商品001 銷售價10 數量1 商品002 銷售價20 數量2 商品001,002 總共優惠了5元 // 商品001 已經確定可優惠1元 // 那么最后一個商品002 可以優惠 6-1 5元 product.setDiscountAmount(product.getDiscountAmount().add(disAmount).subtract(sharedAmount)); }else { //該商品總數量 BigDecimal quantity = new BigDecimal(product.getQuantity().toString()); //① 將總優惠金額 * (該商品銷售價/總銷售價) 得出該商品所占優惠金額 // 例如: // 商品001 銷售價10 數量1 商品002 銷售價20 數量2 商品001,002 總共優惠了5元 // 商品001可優惠金額= 5*(10*1/50) 1元 // 商品002可優惠金額= 5*(20*2/50) 4元 //得出該商品可優惠金額 BigDecimal itemDisAmount = disAmount.multiply( (product.getAmount().multiply(quantity).divide(totalAmount,2,BigDecimal.ROUND_HALF_UP)) ); product.setDiscountAmount(product.getDiscountAmount().add(itemDisAmount)); sharedAmount =sharedAmount.add(itemDisAmount); } } // ③計算出 商品優惠后的價格 finalAmount products.stream().forEach(it->{ BigDecimal quantity = new BigDecimal(it.getQuantity().toString()); it.setFinalAmount(it.getAmount().multiply(quantity).subtract(it.getDiscountAmount())); }); } }

package com.youxiu326.strategy; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.OptionalDouble; /** * 滿足價格大於等於500 * <br>減免價格最低一件商品促銷 */ @Component public class FreeOneStrategy extends PromotionStrategy { /** * 指定促銷類型為:FREEONE * @return */ @Override public Promotion.Type getType() { return Promotion.Type.FREEONE; } @Override public List<PromotionResult> execute(Promotion promotion, List<Product> products) { List<PromotionResult> results = new ArrayList<PromotionResult>(); //計算總金額 總數量 double totalAmount = products.stream().mapToDouble(it->( (it.getAmount().multiply(new BigDecimal(it.getQuantity().toString())))).subtract(it.getDiscountAmount()).doubleValue() ).sum(); int totalQuantity = products.stream().mapToInt(it->it.getQuantity()).sum(); //TODO 這兒簡單處理定死了規則 //不滿足促銷規則的返回空促銷 if (totalAmount<500 || totalQuantity<=1){ return results; } //獲得可優惠金額 double reduceAmount = products.stream().mapToDouble(Product::getAmountDouble).min().orElse(0); //平攤金額 sharedAmount(products, new BigDecimal(reduceAmount+"")); //創建減免促銷信息 PromotionResult result = new PromotionResult(); result.setName(promotion.getName()); result.setType(Promotion.Type.FREEONE); result.setResult(reduceAmount); results.add(result); return results; } }

package com.youxiu326.strategy; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; /** * 滿足大於200 * <br>八折促銷 */ @Component public class RebateStrategy extends PromotionStrategy { /** * 指定促銷類型為:REBATE * @return */ @Override public Promotion.Type getType() { return Promotion.Type.REBATE; } @Override public List<PromotionResult> execute(Promotion promotion, List<Product> products) { List<PromotionResult> results = new ArrayList<PromotionResult>(); //計算總金額 總數量 double totalAmount = products.stream().mapToDouble(it->( (it.getAmount().multiply(new BigDecimal(it.getQuantity().toString())))).subtract(it.getDiscountAmount()).doubleValue() ).sum(); int totalQuantity = products.stream().mapToInt(it->it.getQuantity()).sum(); //TODO 這兒簡單處理定死了規則 //不滿足促銷規則的返回空促銷 if (totalAmount<200){ return results; } //獲得可優惠金額 double reduceAmount = totalAmount * 0.2; //平攤金額 sharedAmount(products, new BigDecimal(reduceAmount+"")); //創建減免促銷信息 PromotionResult result = new PromotionResult(); result.setName(promotion.getName()); result.setType(Promotion.Type.REBATE); result.setResult(reduceAmount); results.add(result); return results; } }

package com.youxiu326.strategy; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; /** * 滿足滿100 * <br>減10促銷 */ @Component public class ReduceStrategy extends PromotionStrategy { /** * 指定促銷類型為:REDUCE * @return */ @Override public Promotion.Type getType() { return Promotion.Type.REDUCE; } @Override public List<PromotionResult> execute(Promotion promotion, List<Product> products) { List<PromotionResult> results = new ArrayList<>(); //計算總金額 總數量 double totalAmount = products.stream().mapToDouble(it->( (it.getAmount().multiply(new BigDecimal(it.getQuantity().toString())))).subtract(it.getDiscountAmount()).doubleValue() ).sum(); int totalQuantity = products.stream().mapToInt(it->it.getQuantity()).sum(); //TODO 這兒簡單處理定死了規則 //不滿足促銷規則的返回空促銷 if (totalAmount<100){ return results; } //獲得可優惠金額 double reduceAmount = 10; //平攤金額 sharedAmount(products, new BigDecimal(reduceAmount+"")); //創建減免促銷信息 PromotionResult result = new PromotionResult(); result.setName(promotion.getName()); result.setType(Promotion.Type.REDUCE); result.setResult(reduceAmount); results.add(result); return results; } }
package com.youxiu326.context; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import com.youxiu326.exception.ServiceException; import java.util.Collection; import java.util.List; /** * 促銷上下文 */ public class PromotionContext { /** * 促銷策略 */ private PromotionStrategy strategy; /** * 當前促銷 */ private Promotion promotion; public static Collection<PromotionStrategy> strategys; public PromotionContext(){} public PromotionContext(Promotion promotion) throws ServiceException { this.promotion = promotion; //初始化促銷列表 if(strategys == null)throw new ServiceException("無可用促銷"); //根據傳入的促銷 找到對應的促銷策略 【strategy】 strategy = strategys.stream().filter(it->it.getType() == promotion.getType()).findFirst().orElse(null); if(strategy == null) throw new ServiceException("找不到符合促銷類型"); } public List<PromotionResult> execute(List<Product> products) throws ServiceException{ return strategy.execute(promotion, products); } public Collection<PromotionStrategy> getStrategys() { return strategys; } public void setStrategys(Collection<PromotionStrategy> strategys) { PromotionContext.strategys = strategys; } }
- 促銷service
處理促銷業務邏輯
package com.youxiu326.service; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import com.youxiu326.exception.ServiceException; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; public interface PromotionService { /** * <span color="red">結算方法</span> * <br>結算對象包含: * <br>1.商品優惠價格 * <br>2.優惠后最終價格 * <br>3.優惠信息 * @param products 要結算的商品集合 * @return Object 返回結算后的對象 */ public Map<String,Object> settlement(List<Product> products) throws ServiceException; /** * 查詢可用的促銷 * @return */ public List<Promotion> findUsablePromotions(); /** * 過濾出可以參加指定促銷的商品 * @param promotion 指定促銷 * @param products 要過濾的商品集合 * @return 返回過濾后的商品集合 */ public List<Product> filterProduct(Promotion promotion, List<Product> products); /** * 執行促銷 * @param promotions 促銷集合 * @param products 商品集合 * @return */ public List<PromotionResult> execute(List<Promotion> promotions, List<Product> products) throws ServiceException; }
package com.youxiu326.service.impl; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.context.PromotionContext; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import com.youxiu326.exception.ServiceException; import com.youxiu326.service.PromotionService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 促銷service */ @Service public class PromotionServiceImpl implements PromotionService { private static final Logger LOGGER = LoggerFactory.getLogger(PromotionServiceImpl.class); //@Autowired //private ContextStartup contextStartup; @Autowired private ApplicationContext application; /** * 被@PostConstruct修飾的方法會在服務器加載Servlet的時候運行,並且只會被服務器調用一次, * <br>類似於Serclet的inti()方法 * <br>被@PostConstruct修飾的方法會在構造函數之后,init()方法之前運行 */ @PostConstruct public void init() { PromotionContext.strategys = application.getBeansOfType(PromotionStrategy.class).values(); } @Override public Map<String,Object> settlement(List<Product> products) throws ServiceException{ Map<String,Object> resultMap = new HashMap<>(); //查詢可用促銷 List<Promotion> promotions = findUsablePromotions(); //執行促銷 List<PromotionResult> result = execute(promotions,products); //返回促銷結果 與商品 resultMap.put("promotionResult", result); resultMap.put("products", products); return resultMap; } /** * 執行促銷 * @param promotions 促銷集合 * @param products 商品集合 * @return */ @Override public List<PromotionResult> execute(List<Promotion> promotions, List<Product> products) throws ServiceException { LOGGER.info("促銷開始執行 促銷數量:{} 商品數量:{}",promotions.size(),products.size()); products.stream().forEach(it->LOGGER.info("執行促銷商品信息->編號:{} 價格:{} 數量:{}",it.getCode(),it.getAmount(),it.getQuantity())); //返回促銷結果 List<PromotionResult> promotionResults = new ArrayList<PromotionResult>(); //遍歷執行促銷 for (Promotion promotion : promotions) { //根據傳入的促銷 得到對應的促銷上下文 PromotionContext context = new PromotionContext(promotion); //過濾出可以參加該促銷的商品 List<Product> filterProductList = filterProduct(promotion, products); //根據策略模式 執行先對應的促銷規則,返回促銷結果 List<PromotionResult> result = context.execute(filterProductList); if (result!=null){ promotionResults.addAll(result); } } //TODO 如果有的促銷參加多次,應該要進行一定處理,只取一個即可 LOGGER.info("促銷執行結束"); return promotionResults; } /** * 查詢可用的促銷 * @return */ @Override public List<Promotion> findUsablePromotions(){ //TODO 這兒你可以直接查詢數據庫 List<Promotion> promotions = new ArrayList<>(); Promotion p1 = new Promotion(Promotion.Type.FREEONE,"價格大於等於500免最低一件",null); Promotion p2 = new Promotion(Promotion.Type.REBATE,"大於200八折",null); Promotion p3 = new Promotion(Promotion.Type.REDUCE,"滿100減10",null); promotions.add(p1); promotions.add(p2); promotions.add(p3); LOGGER.info("查詢到可用促銷數量:{}",promotions.size()); return promotions; } /** * 過濾出可以參加指定促銷的商品 * @param promotion 指定促銷 * @param products 要過濾的商品集合 * @return 返回過濾后的商品集合 */ @Override public List<Product> filterProduct(Promotion promotion, List<Product> products){ List<Product> list = new ArrayList<Product>(); products.stream().forEach(it->{ if (isMatching(promotion, it)) { list.add(it); } }); return list; } /** * 判斷該商品是否可以參加該促銷 * @param promotion * @param product * @return */ private boolean isMatching(Promotion promotion, Product product) { //TODO 這里你應該查詢數據庫 1.看滿足該促銷的商品中是否包含該商品,2.如果該促銷未設置商品默認所有商品都滿足 List<Product> products = null; //沒有 所以商品都滿足參加該促銷的要求 返回true if(products == null || products.size() == 0)return true; //如果該商品在該促銷商品集合內 則返回true 否則返回false long count = products.stream().filter(it->it.getCode().equals(product.getCode())).count(); return count>0?true:false; } }
- Junit
package com.youxiu326; import com.youxiu326.entity.Product; import com.youxiu326.exception.ServiceException; import com.youxiu326.service.PromotionService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Map; @RunWith(SpringRunner.class) @SpringBootTest public class SbPromotionApplicationTests { @Autowired PromotionService service; @Test public void contextLoads() throws ServiceException { List<Product> products = new ArrayList<>(); Product p1 = new Product("YX001", "牙刷", new BigDecimal("50"), 2); Product p2 = new Product("YX002", "電視", new BigDecimal("200"), 2); Product p3 = new Product("YX003", "圓珠筆", new BigDecimal("20"), 2); Product p4 = new Product("YX004", "水杯", new BigDecimal("60"), 1); Product p5 = new Product("YX005", "充電寶", new BigDecimal("400"), 1); /*products.add(p1); products.add(p2); products.add(p3); products.add(p4);*/ products.add(p5); Map<String, Object> result = service.settlement(products); System.out.println(result); } }
- 為了方便 寫了一個簡易頁面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <base th:href="${#httpServletRequest.getContextPath()+'/'}"> <meta charset="UTF-8"> <title>springboot+策略模式 實現簡單促銷</title> </head> <body> <h2>提交商品</h2> <body> <table> <tr> <td>商品名稱</td> <td>商品價格</td> <td>商品數量</td> <td>操作</td> </tr> <tr> <td><input id="name" /></td> <td><input id="amount" type="number" /></td> <td><input id="quantity" type="number" /></td> <td><input type="button" value="添加該商品" onclick="data()"/></td> </tr> </table> <h6>購物車商品</h6> <div width="400px" height="400px" id="showShoop"></div> <br> <input type="button" onclick="sub()" value="將這些商品進行促銷"/> <br> <h6>參加促銷信息展示</h6> <div width="400px" height="400px" id="showPro"></div> <h6>已添加商品信息展示</h6> <table> <tr> <td>商品編號</td> <td>商品名稱</td> <td>商品數量</td> <td>優惠后總價</td> <td>總優惠金額</td> </tr> <tbody id="showTab"></tbody> </table> </body> <script src="/jquery-1.11.3.min.js"></script> <script> var products = []; var num = 1; <!-- 准備數據 --> function data(){ var name = $("#name").val(); var amount = $("#amount").val(); var quantity = $("#quantity").val(); if(name=="" || amount=="" || quantity=="" || name==undefined || amount==undefined || quantity==undefined ){ alert("商品格式有誤"); return ; } var code = "youxiu-"+ num; num=num+1; var product = {"code":code,"name":name,"amount":amount,"quantity":quantity}; products.push(product); console.log(products); var em = "<span>"+product.name+"--"+product.quantity+"--"+product.amount+"</span><br>"; $("#showShoop").append(em); } function sub(){ if(products.length<=0){ alert("請添加商品"); retur; } $.ajax({ type: 'POST', url: "/data", data: JSON.stringify(products), contentType: "application/json; charset=utf-8", dataType: "json", success: function(map){ if(map==null){ alert("出錯了"); } var promotionResult = map.promotionResult; var result = map.products; console.log("促銷結果:"); console.log(promotionResult); console.log(); console.log("促銷后商品信息:"); console.log(result); //促銷信息展示 $("#showPro").empty(); for(var i=0;i<promotionResult.length;i++){ var em = "<span>"+promotionResult[i].name+"------"+promotionResult[i].result+"</span><br>"; $("#showPro").append(em); } //促銷商品展示 $("#showTab").empty(); for(var i=0;i<result.length;i++){ var em = "<tr><td>"+result[i].code+"</td><td>"+result[i].name+"</td><td>"+result[i].quantity+"</td><td>"+result[i].finalAmount+"</td><td>"+result[i].discountAmount+"</td></tr>"; $("#showTab").append(em); } }, error:function(map){ alert(map); console.log(map); } }); } </script> </html>
package com.youxiu326.controller; import com.youxiu326.entity.Product; import com.youxiu326.exception.ServiceException; import com.youxiu326.service.PromotionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; /** * 促銷controller */ @Controller public class PromotionCtrl { @Autowired private PromotionService service; @GetMapping("/index") public String index(){ return "/index"; } @PostMapping("/data") @ResponseBody public Map data(@RequestBody List<Product> products){ Map map = null; try { map = service.settlement(products); } catch (ServiceException e) { return null; } return map; } }
演示地址:http://promotion.youxiu326.xin
源碼:https://github.com/youxiu326/sb_promotion.git