文章目錄
一、設計思想演進
1.1 預想場景
我們有一個會員業務,在售賣產品時價格是根據會員的類型來進行折扣,比如:普通會員不打折,VIP會員打8折,超級VIP會員7折。
現在做一個價格模塊的折扣計算功能,針對不同會員類型,進行折扣計算,代碼如下:
public class PriceCenter {
/**
* 對原價進行折扣計算
* @param price 原價
* @param memberType 會員類型
* @return 計算折扣後價格
*/
public BigDecimal discount(BigDecimal price, String memberType){
if ("普通會員".equals(memberType)) {
System.out.println("普通會員沒有折扣");
}else if ("VIP會員".equals(memberType)) {
System.out.println("vip會員打9折!");
price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
}else if("超級VIP會員".equals(memberType)){
System.out.println("超級VIP會員打5折!");
price = price.multiply(new BigDecimal("0.7")).setScale(2,RoundingMode.HALF_UP);
}
return price;
}
}
一開始代碼邏輯很簡單,但我們把每種會員的折扣算法都放在了一個方法裏,隨着業務的增加代碼會越來越多,會十分臃腫。
1.2 優化
下面我們進行優化,把每種會員的折扣算法單獨封裝成一個方法,代碼如下:
public class PriceCenter {
/**
* 對原價進行折扣計算
*
* @param price 原價
* @param memberType 會員類型
* @return 計算折扣後價格
*/
public BigDecimal discount(BigDecimal price, String memberType) {
if ("普通會員".equals(memberType)) {
price = memberNormalDiscount(price);
} else if ("VIP會員".equals(memberType)) {
price = memberVIPDiscount(price);
} else if ("超級VIP會員".equals(memberType)) {
price = memberSuperVIPDiscount(price);
}
return price;
}
private BigDecimal memberNormalDiscount(BigDecimal price) {
System.out.println("普通會員沒有折扣");
return price;
}
private BigDecimal memberVIPDiscount(BigDecimal price) {
System.out.println("vip會員打9折!");
price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
return price;
}
private BigDecimal memberSuperVIPDiscount(BigDecimal price) {
System.out.println("超級VIP會員打8折!");
price = price.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.HALF_UP);
return price;
}
}
當我們想要修改某個算法的邏輯時只要,修改單獨的方法就可以了。
上面看似讓代碼變得優雅了,但還是存在一些問題:
問題一:每新增一個會員類型的時候,首先要添加一個該種客戶類型的報價算法方法,然後在方法discount中再加一個else if的分支,這違反了設計原則之一的開閉原則(open-closed-principle).
開閉原則:
對於擴展是開放的(Open for extension)。這意味着模塊的行爲是可以擴展的。當應用的需求改變時,我們可以對模塊進行擴展,使其具有滿足那些改變的新行爲。也就是說,我們可以改變模塊的功能。
對於修改是關閉的(Closed for modification)。對模塊行爲進行擴展時,不必改動模塊的源代碼或者二進制代碼。
問題二:有的時候我們需要做額外的活動針對不同的節日,每個else if中的邏輯要做很大改動,活動過去之後又要恢復,如果剛入職的新手就會想直接改動原來else if種代碼,活動過去之後在改動回去,這樣其實很不好。
思考:每次執行discount方法時其實只會執行其中一個分支的代碼,是否有什麼辦法可以每次動態變化方法中的內容,於是我們就想到是否可以執行的時候調用固定的類的方法,具體的執行邏輯是否可以通過當一個參數傳到這個類中。
爲了解決上面的問題,這個時候策略模式就應運而生了
二、什麼是策略設計模式?
2.1 概念
其思想是針對一組算法,將每一種算法都封裝到具有共同接口的獨立的類中,從而是它們可以相互替換。策略模式可以讓算法在不影響客戶端的情況下發生變化。
2.2 類型
行爲型設計模式
2.3 UML結構
2.4 三類角色
- 環境(Context):也叫上下文,它持有一個Strategy的引用。
- 抽象策略(Strategy):這是一個抽象角色,通常由一個接口或抽象類實現。用來約束一系列具體的策略。
- 具體策略(ConcreteStrategy):具體的策略實現。策略即算法。
2.5 一般通用實現*
策略接口:
//策略接口
public interface IStrategy {
//定義的抽象算法方法 來約束具體的算法實現方法
public void algorithmMethod();
}
具體的策略實現:
// 具體的策略實現A
public class ConcreteStrategyA implements IStrategy {
//具體的算法實現
@Override
public void algorithmMethod() {
System.out.println("我是具體的算法A");
}
}
// 具體的策略實現B
public class ConcreteStrategyB implements IStrategy {
//具體的算法實現
@Override
public void algorithmMethod() {
System.out.println("我是具體的算法B");
}
}
策略上下文
/**
* 策略上下文
*/
public class StrategyContext {
//持有一個策略實現的引用
private IStrategy strategy;
//使用構造器注入具體的策略類
public StrategyContext(IStrategy strategy) {
this.strategy = strategy;
}
//也可以通過set方法注入具體策略
public void setStrategy(IStrategy strategy) {
this.strategy = strategy;
}
public void contextMethod(){
//調用策略實現的方法
strategy.algorithmMethod();
}
}
三、根據設計模式改造業務
折扣策略接口
public interface IDiscountStrategy {
BigDecimal discount(BigDecimal price);
}
普通會員折扣策略
public class MemberNormalDiscount implements IDiscountStrategy {
@Override
public BigDecimal discount(BigDecimal price) {
System.out.println("普通會員沒有折扣");
return price;
}
}
VIP會員折扣策略
public class MemberVIPDiscount implements IDiscountStrategy {
@Override
public BigDecimal discount(BigDecimal price) {
System.out.println("vip會員打9折!");
price = price.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_UP);
return price;
}
}
超級VIP會員折扣策略
public class MemberSuperVIPDiscount implements IDiscountStrategy {
@Override
public BigDecimal discount(BigDecimal price) {
System.out.println("超級VIP會員打8折!");
price = price.multiply(new BigDecimal("0.8")).setScale(2, RoundingMode.HALF_UP);
return price;
}
}
折扣上下文
public class DiscountContext {
private IDiscountStrategy discountStrategy;
public DiscountContext(IDiscountStrategy strategy) {
this.discountStrategy = strategy;
}
public void setDiscountStrategy(IDiscountStrategy iDiscountStrategy) {
this.discountStrategy = iDiscountStrategy;
}
public BigDecimal discount(BigDecimal price){
return discountStrategy.discount(price);
}
}
客戶端
public class Client {
public static void main(String[] args) {
BigDecimal price1 = new BigDecimal(1000);
MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
DiscountContext discountContext = new DiscountContext(memberNormalDiscount);
price1 = discountContext.discount(price1);
System.out.println(price1);
BigDecimal price2 = new BigDecimal(1000);
discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
price2 = discountContext.discount(price2);
System.out.println(price2);
}
}
同時測試兩種算法,結果如下:
普通會員沒有折扣
1000
超級VIP會員打8折!
800.00
下面我們來想一想,如果這時候推出了一個員工內部會員,買東西可享5折怎麼實現?
我們只要新增一個內部會員折扣策略就可以了,代碼如下:
public class MemberInnerDiscount implements IDiscountStrategy {
@Override
public BigDecimal discount(BigDecimal price) {
System.out.println("內部會員打5折!");
price = price.multiply(new BigDecimal("0.5")).setScale(2, RoundingMode.HALF_UP);
return price;
}
}
public class Client {
public static void main(String[] args) {
BigDecimal price1 = new BigDecimal(1000);
MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
DiscountContext discountContext = new DiscountContext(memberNormalDiscount);
price1 = discountContext.discount(price1);
System.out.println(price1);
BigDecimal price2 = new BigDecimal(1000);
discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
price2 = discountContext.discount(price2);
System.out.println(price2);
BigDecimal price3 = new BigDecimal(1000);
discountContext.setDiscountStrategy(new MemberInnerDiscount());
price3 = discountContext.discount(price3);
System.out.println(price3);
}
}
四、if else如何解決?
在策略模式使用過程中我們發現,策略模式其實是通過context來解決執行具體邏輯可以根據需要隨時變化,也就是需要什麼算法就讓context持有。但客戶端中if else並沒有減少
我們將上面代碼改造的更貼近一些現實情況
public class Client {
public static void main(String[] args) {
//會員類型
String memberType = "VIP";
//原價
BigDecimal price = new BigDecimal(1000);
DiscountContext discountContext = new DiscountContext();
if ("normal".equals(memberType)) {
MemberNormalDiscount memberNormalDiscount = new MemberNormalDiscount();
discountContext.setDiscountStrategy(new MemberNormalDiscount());
price = discountContext.discount(price);
} else if ("VIP".equals(memberType)) {
BigDecimal price2 = new BigDecimal(1000);
discountContext.setDiscountStrategy(new MemberSuperVIPDiscount());
price = discountContext.discount(price);
}else if ("superVIP".equals(memberType)) {
discountContext.setDiscountStrategy(new MemberInnerDiscount());
price = discountContext.discount(price);
}
System.out.println(price);
}
}
4.1使用工廠方法
工廠方法
/**
* 會員折扣策略工廠
*/
public class MemberDiscountFactory {
private static final Map<String, IDiscountStrategy> map = new HashMap<>();
static {
map.put("normal", new MemberNormalDiscount());
map.put("VIP", new MemberVIPDiscount());
map.put("superVIP", new MemberSuperVIPDiscount());
}
public static IDiscountStrategy getDiscountStrategy(String memberType) {
return map.get(memberType);
}
}
客戶端
public class Client1 {
public static void main(String[] args) {
//這裏使用最簡單方式表示類型
String memberType = "VIP";
//原價
BigDecimal price = new BigDecimal(1000);
//使用工廠獲取具體策略
IDiscountStrategy iDiscountStrategy = MemberDiscountFactory.getDiscountStrategy(memberType);
DiscountContext discountContext = new DiscountContext(iDiscountStrategy);
BigDecimal result = discountContext.discount(price);
System.out.println("打折後的價格爲"+result);
}
}
發現代碼優化後簡潔了很多,工廠方法只是一種解決方式,其主要的思路就是將各個策略放到容器中去來方便獲取。大家可以發散思路,比如枚舉,反射等等
思考:策略的選擇我們交給了客戶端,及時使用了工廠方法對客戶端也會覺得過於麻煩,我們是否可以將具體選擇哪些策略的權利交給Context上下文,這樣在折扣選擇的部分也可以徹底和客戶端解耦
五、Context在策略模式中的作用
5.1 作用: 讓客戶端和具體策略完全解耦
在沒有context的情況下, client直接持有策略接口, 如果具體策略改變, 需要更改client的代碼, 但是如果讓context持有IStrategy
那就不需要更改client
client在整個處理流程中只要知道他調了一個策略就行了, 不必要知道具體策略是什麼, 甚至策略的選擇都讓策略上下文去維護。
有很多策略模式的實現, 都是在context中維護了strategy list, 然後在裏面通過參數去選取對應的策略, 這塊"選取對應策略"放在context中比放在client中更合適
5.2 再次改造
Context上下文
public class DiscountContext1 {
String memberType;
private IDiscountStrategy discountStrategy;
public DiscountContext1(String memberType) {
this.memberType = memberType;
this.discountStrategy = MemberDiscountFactory.getMedalService(memberType);
}
public String getMemberType() {
return memberType;
}
public void setMemberType(String memberType) {
this.memberType = memberType;
}
public IDiscountStrategy getDiscountStrategy() {
return discountStrategy;
}
public BigDecimal discount(BigDecimal price){
return discountStrategy.discount(price);
}
}
客戶端
public class Client2 {
public static void main(String[] args) {
String memberType = "VIP";
//原價
BigDecimal price = new BigDecimal(1000);
DiscountContext1 discountContext = new DiscountContext1(memberType);
BigDecimal result = discountContext.discount(price);
System.out.println("打折後的價格爲"+result);
}
}
5.3 上下文當做參數傳入到具體策略中
具體的策略對象可以從上下文中獲取所需數據,可以將上下文當做參數傳入到具體策略中,具體策略通過回調上下文中的方法來獲取其所需要的數據。
**假設情景:**要求每個策略能展示會員類型,而且支持以後一些公用字段新增
修改策略接口
public interface IDiscountStrategy1 {
BigDecimal discount(DiscountContext2 discountContext2);
}
修改一個具體策略
public class MemberNormalDiscount1 implements IDiscountStrategy1 {
@Override
public BigDecimal discount(DiscountContext2 discountContext2) {
String memberType = discountContext2.getMemberType();
BigDecimal price = discountContext2.getPrice();
System.out.println(memberType + "沒有折扣");
return price;
}
}
修改Factory
public class MemberDiscountFactory1 {
private static final Map<String, IDiscountStrategy1> map = new HashMap<>();
static {
map.put("normal", new MemberNormalDiscount1());
}
public static IDiscountStrategy1 getDiscountStrategy(String memberType) {
return map.get(memberType);
}
}
修改Context
public class DiscountContext2 {
private String memberType;
private BigDecimal price;
private IDiscountStrategy1 discountStrategy;
public DiscountContext2(String memberType) {
this.memberType = memberType;
this.discountStrategy = MemberDiscountFactory1.getDiscountStrategy(memberType);
}
public String getMemberType() {
return memberType;
}
public void setMemberType(String memberType) {
this.memberType = memberType;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public IDiscountStrategy1 getDiscountStrategy() {
return discountStrategy;
}
public BigDecimal discount(BigDecimal price){
this.price = price;
return discountStrategy.discount(this);
}
}
client
public class Client3 {
public static void main(String[] args) {
//這裏使用最簡單方式表示類型
String memberType = "VIP";
//原價
BigDecimal price = new BigDecimal(1000);
DiscountContext2 discountContext = new DiscountContext2(memberType);
BigDecimal result = discountContext.discount(price);
System.out.println("打折後的價格爲"+result);
}
}
測試結果
normal沒有折扣
打折後的價格爲1000
六、策略模式在JDK中有哪些應用?
6.1 比較器Comparator
在Java的集合框架中,經常需要通過構造方法傳入一個比較器Comparator,或者創建比較器傳入Collections的靜態方法中作爲方法參數,進行比較排序等,使用的是策略模式。
在該比較架構中,Comparator就是一個抽象的策略;一個類實現該結構,並實現裏面的compare方法,該類成爲具體策略類;Collections類就是環境角色,他將集合的比較封裝成靜態方法對外提供api。
6.2 ThreadPoolExecutor中的四種拒絕策略
在創建線程池時,需要傳入拒絕策略,當創建新線程使當前運行的線程數超過maximumPoolSize時,將會使用傳入的拒絕策略進行處理。
- AbortPolicy:直接拋出異常。
- CallerRunsPolicy:只用調用者所在線程來運行任務。
- DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
- DiscardPolicy:不處理,丟棄掉。
這裏使用的就是策略模式。