策略模式
大家一定都使用過電子地圖。在地圖中輸入出發(fā)地和目的地,然后再選取你的出行方式,就可以計算出最優(yōu)線路以及預(yù)估的時長。出行方式有駕車、公交、步行、騎行等。出行方式不同,計算的線路和時間當(dāng)然也不同。
其實出行方式換個詞就是出行策略。而策略模式就是針對此類問題的設(shè)計模式。生活中這種例子太多了,比如購物促銷打折的策略、計算稅費的策略等等。相應(yīng)的策略模式也是一種常用的設(shè)計模式。本節(jié)我們會以電子地圖為例,比較工廠模式和策略模式,講解策略模式的原理。最后結(jié)合工廠模式改造策略模式的代碼實現(xiàn),以達到更高的設(shè)計目標(biāo)。
1. 實現(xiàn)策略模式
接下來我們就以電子地圖為例,講解如何用策略模式實現(xiàn)。不過先別著急,上一節(jié)我們學(xué)習(xí)了工廠模式,看起來電子地圖也可以用工廠模式來實現(xiàn)。所以我們先來看看用工廠模式如何實現(xiàn)。下面的例子為了方便展示,接口入?yún)⒅挥谐鲂蟹绞?,省略了出發(fā)地和目的地。計算結(jié)果是預(yù)估時長。
1.1 工廠模式實現(xiàn)電子地圖
首先我們需要一個策略接口,不同策略實現(xiàn)該接口。再搭配一個策略工廠。客戶端代碼只需要根據(jù)用戶的出行方式,讓工廠返回具體實現(xiàn)即可,由具體的實現(xiàn)來提供算法計算。以工廠模式實現(xiàn)的電子地圖代碼如下。
TravelStrategy
接口代碼:
public interface TravelStrategy {
int calculateMinCost();
}
TravelStrategy
接口的實現(xiàn)代碼:
public class SelfDrivingStrategy implements TravelStrategy {
@Override
public int calculateMinCost() {
return 30;
}
}
TravelStrategyFactory
代碼:
public class TravelStrategyFactory {
public TravelStrategy createTravelStrategy(String travelWay) {
if ("selfDriving".equals(travelWay)) {
return new SelfDrivingStrategy();
} if ("bicycle".equals(travelWay)) {
return new BicycleStrategy();
} else {
return new PublicTransportStrategy();
}
}
}
TravelService
對外提供計算方法,通過工廠生成所需要的 strategy。代碼如下:
public class TravelService {
private TravelStrategyFactory travelStrategyFactory = new TravelStrategyFactory();
public int calculateMinCost(String travelWay) {
TravelStrategy travelStrategy = travelStrategyFactory.createTravelStrategy(travelWay);
return travelStrategy.calculateMinCost();
}
}
代碼結(jié)構(gòu)和我們上一節(jié)講解的音樂推薦器幾乎一模一樣??此埔埠芎玫亟鉀Q了我們的設(shè)計問題。接下來我們看看如何用策略模式解決這個問題,然后我們再對兩種模式做對比。
1.2 策略模式實現(xiàn)電子地圖
使用策略模式,需要增加一個策略上下文類(Context
)。Context
類持有策略實現(xiàn)的引用,并且對外提供計算方法。Context
類根據(jù)持有策略的不同,實現(xiàn)不同的計算邏輯??蛻舳舜a只需要調(diào)用 Context
類的計算方法即可。如果想切換策略實現(xiàn),那么只需要改變Context
類持有的策略實現(xiàn)即可。
TravelStrategy
接口和實現(xiàn)的代碼不變,請參照上面工廠模式中給出的代碼。其他代碼如下:
StrategyContext
類:
public class StrategyContext {
private TravelStrategy strategy;
public StrategyContext(TravelStrategy strategy) {
this.strategy = strategy;
}
public int calculateMinCost(){
return strategy.calculateMinCost();
}
StrategyContext
持有某種 TravelStrategy
的實現(xiàn),它對外提供的calculateMinCost 方法,實際是對 TravelStrategy
做了一層代理。想切換不同算法的時候,只需更改 StrategyContext
持有的 TravelStrategy
實現(xiàn)。
TravelService
對外提供計算方法,代碼如下:
public class TravelService {
private StrategyContext strategyContext;
public int calculateMinCost(String travelWay){
if ("selfDriving".equals(travelWay)) {
strategyContext = new StrategyContext(new SelfDrivingStrategy());
} if ("bicycle".equals(travelWay)) {
strategyContext = new StrategyContext(new BicycleStrategy());
} else {
strategyContext = new StrategyContext(new PublicTransportStrategy());
}
return strategyContext.calculateMinCost();
}
}
可以看到 TravelService
中只會和 Context
打交道,初始化 Context
時,根據(jù)不同的出行方式,設(shè)置不同的策略??吹竭@里你是不是會有疑問,使用工廠模式消除了客戶端代碼的條件語句。怎么使用策略模式,條件語句又回來了?別急,我們繼續(xù)向下看。
最后我們看一下策略模式的類圖:
2. 策略模式優(yōu)缺點
2.1 優(yōu)點
- 使用策略模式,可以根據(jù)策略接口,定義一系列可供復(fù)用的算法或者行為;
- 調(diào)用方只需要持有
Context
的引用即可。而無需知道具體的策略實現(xiàn)。滿足迪米特法則; Context
在策略的方法之外可以做一些通用的切面邏輯。
GOF的《設(shè)計模式》著作中認(rèn)為策略模式可以消除一些條件語句,我對此持懷疑態(tài)度。正如上面的例子,雖然由于Context在初始化的時候已經(jīng)指定了策略實現(xiàn),在計算邏輯中不需要根據(jù)條件選擇邏輯分支。但是,客戶端代碼在初始化Context的時候,如何判斷應(yīng)該傳入哪個策略實現(xiàn)呢?其實在客戶端代碼或者別的地方還是缺少不了條件判斷。所以這里消除條件語句,只是針對算法邏輯的條件判斷。
第一個優(yōu)點是策略模式解決的核心問題。但其實工廠模式也是可以做到的。第二點,我認(rèn)為很重要,客戶端代碼只需要和 Context
打交道即可,避免了和不同策略類、工廠類的接觸。工廠模式中,客戶端代碼需要知道工廠類和產(chǎn)品類,兩個類。正好復(fù)習(xí)一下迪米特法則,如果兩個類沒有必要直接通信,那么兩個類就沒有必要相互作用??梢酝ㄟ^第三方來間接調(diào)用。
2.2 缺點
- 客戶端代碼需要知道不同的策略以及如何選擇策略。因此可以看到上面的客戶端代碼有著丑陋的條件判斷;
- 由于策略類實現(xiàn)同樣的接口,所以參數(shù)列表要保持一致,但可能并不是所有的策略都需要全部參數(shù)。
3. 策略模式與工廠模式結(jié)合使用
針對第一個缺點。我們可以通過策略模式與工廠模式結(jié)合使用來改進。通過進一步封裝,消除客戶端代碼的條件選擇。
我們修改一下StrategyContext類,代碼如下:
public class StrategyContext {
private TravelStrategy strategy;
public StrategyContext(String travelWay) {
if ("selfDriving".equals(travelWay)) {
strategy = new SelfDrivingStrategy();
} if ("bicycle".equals(travelWay)) {
strategy = new BicycleStrategy();
} else {
strategy = new PublicTransportStrategy();
}
}
public int calculateMinCost(){
return strategy.calculateMinCost();
}
}
可以看到我們初始化的邏輯和工廠的邏輯很相似。這樣條件判斷就提煉到 Context
類中了。而客戶端代碼將會簡潔很多,只需要在初始化 StrategyContext
時,傳入相應(yīng)的出行方式即可。代碼如下:
public class TravelService {
private StrategyContext strategyContext;
public int calculateMinCost(String travelWay){
strategyContext = new StrategyContext(travelWay);
return strategyContext.calculateMinCost();
}
}
改進后,客戶端代碼現(xiàn)在已經(jīng)完全不知道策略對象的存在了。條件判斷也被消除了。其實很多時候我們都是通過搭配不同設(shè)計模式來達到我們的設(shè)計目標(biāo)的。
策略+工廠模式類圖如下:
4. 策略模式適用場景
當(dāng)存在多種邏輯不同,但屬于同一類型的行為或者算法時,可以考慮使用策略模式。以此來消除你算法代碼中的條件判斷。同時讓你的代碼滿足多種設(shè)計原則。
很多時候,工廠模式和策略模式都可以為你解決同類問題。但你要想清楚,你想要的是一個對象,還是僅僅想要一個計算結(jié)果。如果你需要的是一個對象,并且想用它做很多事情。那么請使用工廠模式。而你僅僅想要一個特定算法的計算結(jié)果,那么請使用策略模式。
策略模式屬于對象行為模式,而工廠屬于創(chuàng)建型模式。策略模式和工廠模式對比如下:
5. 小結(jié)
策略模式解決的問題是如何封裝可供復(fù)用的算法或者行為。策略模式滿足了單一職責(zé)、開閉、迪米特法則、依賴倒轉(zhuǎn)等原則。我們一定想清楚策略模式的適用場景,否則某些時候你會搞不清到底用工廠模式還是策略模式。最后提醒大家,設(shè)計模式很多時候都是混合使用,我們不應(yīng)該局限于使用某一種設(shè)計模式來解決問題。