在講策略模式之前,我們先看一個日常生活中的小例子:
現實生活中我們到商場買東西的時候,賣場往往根據不同的客戶制定不同的報價策略,比如針對新客戶不打折扣,針對老客戶打9折,針對VIP客戶打8折...
現在我們要做一個報價管理的模塊,簡要點就是要針對不同的客戶,提供不同的折扣報價。
如果是有你來做,你會怎麼做?
我們很有可能寫出下面的代碼:
package strategy.examp02;
import java.math.BigDecimal;
public class QuoteManager {
public BigDecimal quote(BigDecimal originalPrice,String customType){
if ("新客戶".equals(customType)) {
System.out.println("抱歉!新客戶沒有折扣!");
return originalPrice;
}else if ("老客戶".equals(customType)) {
System.out.println("恭喜你!老客戶打9折!");
originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}else if("VIP客戶".equals(customType)){
System.out.println("恭喜你!VIP客戶打8折!");
originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
//其他人員都是原價
return originalPrice;
}
}
經過測試,上面的代碼工作的很好,可是上面的代碼是有問題的。上面存在的問題:把不同客戶的報價的算法都放在了同一個方法裏面,使得該方法很是龐大(現在是隻是一個演示,所以看起來還不是很臃腫)。
下面看一下上面的改進,我們把不同客戶的報價的算法都單獨作爲一個方法
package strategy.examp02;
import java.math.BigDecimal;
public class QuoteManagerImprove {
public BigDecimal quote(BigDecimal originalPrice, String customType){
if ("新客戶".equals(customType)) {
return this.quoteNewCustomer(originalPrice);
}else if ("老客戶".equals(customType)) {
return this.quoteOldCustomer(originalPrice);
}else if("VIP客戶".equals(customType)){
return this.quoteVIPCustomer(originalPrice);
}
//其他人員都是原價
return originalPrice;
}
/**
* 對VIP客戶的報價算法
* @param originalPrice 原價
* @return 折後價
*/
private BigDecimal quoteVIPCustomer(BigDecimal originalPrice) {
System.out.println("恭喜!VIP客戶打8折");
originalPrice = originalPrice.multiply(new BigDecimal(0.8)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
/**
* 對老客戶的報價算法
* @param originalPrice 原價
* @return 折後價
*/
private BigDecimal quoteOldCustomer(BigDecimal originalPrice) {
System.out.println("恭喜!老客戶打9折");
originalPrice = originalPrice.multiply(new BigDecimal(0.9)).setScale(2,BigDecimal.ROUND_HALF_UP);
return originalPrice;
}
/**
* 對新客戶的報價算法
* @param originalPrice 原價
* @return 折後價
*/
private BigDecimal quoteNewCustomer(BigDecimal originalPrice) {
System.out.println("抱歉!新客戶沒有折扣!");
return originalPrice;
}
}
上面的代碼比剛開始的時候要好一點,它把每個具體的算法都單獨抽出來作爲一個方法,當某一個具體的算法有了變動的時候,只需要修改響應的報價算法就可以了。
但是改進後的代碼還是有問題的,那有什麼問題呢?
1.當我們新增一個客戶類型的時候,首先要添加一個該種客戶類型的報價算法方法,然後再quote方法中再加一個else if的分支,是不是感覺很是麻煩呢?而且這也違反了設計原則之一的開閉原則(open-closed-principle).
開閉原則:
對於擴展是開放的(Open for extension)。這意味着模塊的行爲是可以擴展的。當應用的需求改變時,我們可以對模塊進行擴展,使其具有滿足那些改變的新行爲。也就是說,我們可以改變模塊的功能。
對於修改是關閉的(Closed for modification)。對模塊行爲進行擴展時,不必改動模塊的源代碼或者二進制代碼。
2.我們經常會面臨這樣的情況,不同的時期使用不同的報價規則,比如在各個節假日舉行的各種促銷活動時、商場店慶時往往都有普遍的折扣,但是促銷時間一旦過去,報價就要回到正常價格上來。按照上面的代碼我們就得修改if else裏面的代 碼很是麻煩
那有沒有什麼辦法使得我們的報價管理即可擴展、可維護,又可以方便的響應變化呢?當然有解決方案啦,就是我們下面要講的策略模式。
定義:
策略模式定義了一系列的算法,並將每一個算法封裝起來,使每個算法可以相互替代,使算法本身和使用算法的客戶端分割開來,相互獨立。
結構:
1.策略接口角色IStrategy:用來約束一系列具體的策略算法,策略上下文角色ConcreteStrategy使用此策略接口來調用具體的策略所實現的算法。
2.具體策略實現角色ConcreteStrategy:具體的策略實現,即具體的算法實現。
3.策略上下文角色StrategyContext:策略上下文,負責和具體的策略實現交互,通常策略上下文對象會持有一個真正的策略實現對象,策略上下文還可以讓具體的策略實現從其中獲取相關數據,回調策略上下文對象的方法。
UML類圖:
UML序列圖:
策略模式代碼的一般通用實現:
策略接口
package strategy.examp01;
//策略接口
public interface IStrategy {
//定義的抽象算法方法 來約束具體的算法實現方法
public void algorithmMethod();
}
具體的策略實現:
package strategy.examp01;
// 具體的策略實現
public class ConcreteStrategy implements IStrategy {
//具體的算法實現
@Override
public void algorithmMethod() {
System.out.println("this is ConcreteStrategy method...");
}
}
package strategy.examp01;
// 具體的策略實現2
public class ConcreteStrategy2 implements IStrategy {
//具體的算法實現
@Override
public void algorithmMethod() {
System.out.println("this is ConcreteStrategy2 method...");
}
}
策略上下文:
package strategy.examp01;
/**
* 策略上下文
*/
public class StrategyContext {
//持有一個策略實現的引用
private IStrategy strategy;
//使用構造器注入具體的策略類
public StrategyContext(IStrategy strategy) {
this.strategy = strategy;
}
public void contextMethod(){
//調用策略實現的方法
strategy.algorithmMethod();
}
}
package strategy.examp01;
//外部客戶端
public class Client {
public static void main(String[] args) {
//1.創建具體測策略實現
IStrategy strategy = new ConcreteStrategy2();
//2.在創建策略上下文的同時,將具體的策略實現對象注入到策略上下文當中
StrategyContext ctx = new StrategyContext(strategy);
//3.調用上下文對象的方法來完成對具體策略實現的回調
ctx.contextMethod();
}
}
策略模式的優點:
1.策略模式的功能就是通過抽象、封裝來定義一系列的算法,使得這些算法可以相互替換,所以爲這些算法定義一個公共的接口,以約束這些算法的功能實現。如果這些算法具有公共的功能,可以將接口變爲抽象類,將公共功能放到抽象父類裏面。
2.策略模式的一系列算法是可以相互替換的、是平等的,寫在一起就是if-else組織結構,如果算法實現裏又有條件語句,就構成了多重條件語句,可以用策略模式,避免這樣的多重條件語句。
3.擴展性更好:在策略模式中擴展策略實現非常的容易,只要新增一個策略實現類,然後在使用策略實現的地方,使用這個新的策略實現就好了。
策略模式的缺點:
1.客戶端必須瞭解所有的策略,清楚它們的不同:
如果由客戶端來決定使用何種算法,那客戶端必須知道所有的策略,清楚各個策略的功能和不同,這樣才能做出正確的選擇,但是這暴露了策略的具體實現。
2.增加了對象的數量:
由於策略模式將每個具體的算法都單獨封裝爲一個策略類,如果可選的策略有很多的話,那對象的數量也會很多。
3.只適合偏平的算法結構:
由於策略模式的各個策略實現是平等的關係(可相互替換),實際上就構成了一個扁平的算法結構。即一個策略接口下面有多個平等的策略實現(多個策略實現是兄弟關係),並且運行時只能有一個算法被使用。這就限制了算法的使用層級,且不能被嵌套。
策略模式的本質:
分離算法,選擇實現。
如果你仔細思考策略模式的結構和功能的話,就會發現:如果沒有上下文,策略模式就回到了最基本的接口和實現了,只要是面向接口編程,就能夠享受到面向接口編程帶來的好處,通過一個統一的策略接口來封裝和分離各個具體的策略實現,無需關係具體的策略實現。
貌似沒有上下文什麼事,但是如果沒有上下文的話,客戶端就必須直接和具體的策略實現進行交互了,尤其是需要提供一些公共功能或者是存儲一些狀態的時候,會大大增加客戶端使用的難度;引入上下文之後,這部分工作可以由上下文來完成,客戶端只需要和上下文進行交互就可以了。這樣可以讓策略模式更具有整體性,客戶端也更加的簡單。
策略模式體現了開閉原則:策略模式把一系列的可變算法進行封裝,從而定義了良好的程序結構,在出現新的算法的時候,可以很容易的將新的算法實現加入到已有的系統中,而已有的實現不需要修改。
策略模式體現了里氏替換原則:策略模式是一個扁平的結構,各個策略實現都是兄弟關係,實現了同一個接口或者繼承了同一個抽象類。這樣只要使用策略的客戶端保持面向抽象編程,就可以動態的切換不同的策略實現以進行替換。
二.很多時候我們通過spring去管理具體的策略對象。
策略接口:
public interface PayStrategy {
public String payType(String type);
}
具體策略實現1
@Component
public class AliPayStrategy implements PayStrategy{
@Override
public String payType(String type) {
// TODO Auto-generated method stub
return "支付寶付款";
}
}
具體策略實現2
@Component
public class WeixinPayStrategy implements PayStrategy {
@Override
public String payType(String type) {
// TODO Auto-generated method stub
return "微信付款";
}
}
我們將策略實現對象的beanId保存到數據庫中,spring的上下文對象可以通過beanId獲取具體的策略實現對象,將策略實現對象交給spring管理,這裏也可以通過工廠的方式去創建具體的策略實現對象
spring的工具類,通過beanid獲取spring管理的對象
@Component
public class SpringUtils implements ApplicationContextAware{
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通過name獲取bean
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
//通過class 獲取Bean
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
//通過name class 獲取bean
public static <T> T getBean(String name, Class<T> clazz) {
return applicationContext.getBean(name,clazz);
}
}
具體的實現策略
@Component
public class PayContextStrategy {
@Autowired
PayChannelRepository payChannelRepository;
// public String payType(String type) {
// if(StringUtils.isEmpty(type)) {
// return "type is not null";
//
// }
// PayStrategy startegy = PayStrategyFactory.getPayStrategy(type);
// if(startegy == null) {
// return "no pay startegy";
// }
// return startegy.payType(type);
// }
public String payType(String type) {
Optional<PayChannel> optional = payChannelRepository.findById(type);
PayChannel payChannel = optional.isPresent()?optional.get() : null;
if(payChannel==null) {
return null;
}
String beanId = payChannel.getStrategyBeanId();
PayStrategy startegy = (PayStrategy) SpringUtils.getBean(beanId);
if(startegy==null) {
return null;
}
return startegy.payType(type);
}
}
執行策略代碼,通過type選擇不同的策略執行。
@RestController
public class PayController {
@Autowired
PayContextStrategy payContextStrategy;
@RequestMapping("/payType")
public String payType(String type) {
return payContextStrategy.payType(type);
}
}
思考:看到上面的代碼是不是感覺很熟悉,統一的方法接口,對方法接口的不同實現,通過實例化不同實現類去調用接口,和工廠模式及其相似。具體還是有區別的,策略模式是一種平均選擇算法,爲了減少if else的使用,工廠模式是爲了去創建用戶所需要的對象,策略模式和工廠模式可以互相實現,但是策略模式比工廠模式更爲複雜一些。