- 策略模式
定義了算法族,分別封裝起來,讓它們之間可以互相替換,
此模式讓算法的變化獨立於使用算法的客戶
源碼: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
