徹底搞懂策略設(shè)計(jì)模式
對(duì)于任何一個(gè)軟件系統(tǒng)而言,從組成結(jié)構(gòu)上通常可以分成兩大部分,內(nèi)核組件和擴(kuò)展組件。我們知道,內(nèi)核組件是需要非常穩(wěn)定的,而擴(kuò)展組件則應(yīng)該按需開發(fā),動(dòng)態(tài)替換。
內(nèi)核和擴(kuò)展結(jié)構(gòu)示意圖
顯然,想要實(shí)現(xiàn)這張圖中的效果,我們需要對(duì)擴(kuò)展組件進(jìn)行抽象。
基于這種抽象,就可以實(shí)現(xiàn)不同的擴(kuò)展組件。而因?yàn)檫@些擴(kuò)展組件都是抽象組件的具體實(shí)現(xiàn),所以它們可以相互替換。
抽象組件和擴(kuò)展組件之間的關(guān)系
其實(shí)想要實(shí)現(xiàn)圖中的這種效果,我們有很多種方法。在面向?qū)ο蟮氖澜缰?,我們可以引入一種專門的設(shè)計(jì)模式來做到這一點(diǎn),這種設(shè)計(jì)模式就是我們今天要講的策略模式。
策略模式的基本概念和簡單示例
那么,什么是策略呢?所謂策略,你可以理解它就是 一組算法或?qū)崿F(xiàn)方法的組合。我們知道,實(shí)現(xiàn)某個(gè)功能的方法可能有很多。如果我們把這些方法封裝起來,并能夠確保它們可以相互替換,那么就可以構(gòu)建出一系列的實(shí)現(xiàn)策略,這就是策略模式的由來。這個(gè)模式的結(jié)構(gòu)可以這樣表示:
策略模式的結(jié)構(gòu)示意圖
這張圖中,可以看到,Strategy 是一個(gè)公共接口,代表對(duì)具體實(shí)現(xiàn)方法的抽象。而 ConcreteStrategyA 和 ConcreteStrategyB 分別是 Strategy 接口的兩個(gè)實(shí)現(xiàn)類,代表了不同的實(shí)現(xiàn)方法。同時(shí),我們還注意到,這里有一個(gè)上下文組件 Context,來保持對(duì) Strategy 接口的引用。
顯然,我們可以把這里的 Context 看作是內(nèi)核組件,而 Strategy 接口以及兩個(gè) ConcreteStrategy 實(shí)現(xiàn)類分別看作抽象組件和擴(kuò)展組件。
前面已經(jīng)提到,在面向?qū)ο蟮氖澜缰?,我們通常使用接口來定義一種策略。例如,在這個(gè) Strategy 接口中,我們定義了一個(gè)方法,這個(gè)方法可以用來對(duì)輸入的兩個(gè)數(shù)字執(zhí)行某一個(gè)操作。
public interface Strategy {
public int execute(int num1, int num2);
}
然后,我們就可以基于這個(gè) Strategy 接口,來實(shí)現(xiàn)對(duì)這兩個(gè)數(shù)字的具體計(jì)算方法。這里列舉了常見的加法、減法和乘法。
public class AdditionStrategy implements Strategy{
@Override
public int execute(int num1, int num2) {
return num1+ num2;
}
}
publicclass SubtractionStrategy implements Strategy{
@Override
public int execute(int num1, int num2) {
return num1- num2;
}
}
publicclass MultiplicationStrategy implements Strategy{
@Override
public int execute(int num1, int num2) {
return num1* num2;
}
}
這些算法都非常簡單,而對(duì)應(yīng)的 Context 類也并不復(fù)雜。我們?cè)谶@個(gè)類中注入了一個(gè) Strategy 接口,然后通過這個(gè)接口的 execute 方法,來執(zhí)行具體的計(jì)算方法。
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int performCalculation(int num1, int num2){
return strategy.execute(num1, num2);
}
}
針對(duì)上面 Context,我們也可以編寫對(duì)應(yīng)的測(cè)試類。
public class StrategyTest {
public static void main(String[] args) {
Context context1= new Context(new AdditionStrategy());
System.out.println(context.executeStrategy(1, 1));
Context context2 = new Context(new SubtractionStrategy());
System.out.println( context.executeStrategy(1, 1));
Context context3 = new Context(new MultiplicationStrategy());
System.out.println(context3.executeStrategy(1, 1));
}
}
顯然,策略模式本身的實(shí)現(xiàn)方式非常清晰。但在日常開發(fā)過程中,我們很少碰到像上面的代碼示例這樣簡單的應(yīng)用場(chǎng)景。這時(shí)候,就需要我們理解策略模式的本質(zhì)作用,從繁冗復(fù)雜的代碼結(jié)構(gòu)中識(shí)別出策略模式的應(yīng)用方式,從而更好地把握代碼的結(jié)構(gòu)。
策略模式在主流開源框架中可以說應(yīng)用非常廣泛。接下來,我們就以 MyBatis 框架為例,來分析一下它的應(yīng)用場(chǎng)景和實(shí)現(xiàn)過程。
策略模式在 MyBatis 中的應(yīng)用與實(shí)現(xiàn)
在 MyBatis 中,策略模式的應(yīng)用場(chǎng)景主要就在 SQL 的執(zhí)行器組件——Executor 中。作為 MyBatis 中最核心的接口之一,Executor 接口定義的內(nèi)容非常豐富,這里列舉幾個(gè)比較有代表性的方法:
public interface Executor {
//執(zhí)行 update、insert、delete 三種類型的 SQL 語句
int update(MappedStatement ms, Object parameter) throws SQLException;
//執(zhí)行 selete 類型的 SQL 語句,返回值分為結(jié)果對(duì)象列表或游標(biāo)對(duì)象
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
...
//批量執(zhí)行 SQL 語句
List<BatchResult> flushStatements() throws SQLException;
//提交事務(wù)
void commit(boolean required) throws SQLException;
//回滾事務(wù)
void rollback(boolean required) throws SQLException;
}
在上面代碼中,我們看到了一組用來實(shí)現(xiàn)數(shù)據(jù)庫訪問的常用方法,包括用于執(zhí)行查詢的 query 方法、用于執(zhí)行更新的 update 方法、用于提交和回滾事務(wù)的 commit 和 rollback 方法,以及用于執(zhí)行批量 SQL 的 flushStatements 方法。
在 MyBatis 中,Executor 接口具有一批實(shí)現(xiàn)類。
MyBatis 中 Executor 接口的類層結(jié)構(gòu)圖
- SimpleExecutor:普通執(zhí)行器
這是 MyBatis 中最基礎(chǔ)的、也是默認(rèn)使用的一種 Executor,封裝了對(duì)基本 SQL 語句的各種操作。 - ReuseExecutor:重用執(zhí)行器
顧名思義,這種 Executor 提供了對(duì) SQL 語句進(jìn)行重復(fù)利用的功能特性。基于這種功能特性,SQL 語句的創(chuàng)建、銷毀以及預(yù)編譯過程會(huì)得到優(yōu)化,從而降低資源消耗,提高性能。 - BatchExecutor:批處理執(zhí)行器
從命名上,我們也不難看出,這種 Executor 的作用就是完成對(duì) SQL 語句的批量處理。批處理的優(yōu)勢(shì)同樣是節(jié)省資源消耗,因?yàn)槲覀兛梢砸淮蜗驍?shù)據(jù)庫發(fā)送多條 SQL 語句。
顯然,這三個(gè) Executor 實(shí)現(xiàn)類就是對(duì) Executor 的不同策略實(shí)現(xiàn)。明確了這一點(diǎn)之后,我們接下來還需要明確兩個(gè)問題,也就是:
- 這些具體策略實(shí)現(xiàn)類是如何生成的呢?
- 在 MyBatis 中,哪個(gè)組件扮演了 Context 角色呢?
我們先來看第一個(gè)問題。在 MyBatis 的配置文件中存在一個(gè)配置項(xiàng),這個(gè)配置項(xiàng)用于設(shè)置 Executor 的默認(rèn)類型。正如上面所討論的,這個(gè)配置項(xiàng)指定了 MyBatis 默認(rèn)采用的 Executor 是 SimpleExecutor。
<setting name="defaultExecutorType" value="SIMPLE" />
那么,Executor 是從哪里創(chuàng)建出來的呢?這就涉及到 MyBatis 中與配置相關(guān)的 Configuration 類。Configuration 是一種門面類,但也扮演著工廠類的角色,能夠根據(jù)傳入的策略類型來生成具體的策略對(duì)象。
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} elseif (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
...
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
可以看到,這里通過 Executor 的類型——ExecutorType,來決定構(gòu)建哪一種具體的 Executor 實(shí)現(xiàn)類。請(qǐng)注意,我們?cè)谶@里看到了一個(gè) interceptorChain 對(duì)象,這是 MyBatis 中的攔截器組件,體現(xiàn)的是一種責(zé)任鏈處理機(jī)制。
接下來,我們討論第二個(gè)問題,就是在 MyBatis 中,哪個(gè)組件扮演了 Context 角色呢?答案是 DefaultSqlSession。
DefaultSqlSession 內(nèi)部包含了對(duì) Executor 的引用,而 DefaultSqlSession 是通過 SqlSessionFactory 接口的默認(rèn)實(shí)現(xiàn)類——DefaultSqlSessionFactory 進(jìn)行構(gòu)建的。在 SqlSession 生成過程中,需要指定 ExecutorType。這時(shí)就會(huì)調(diào)用 Configuration 對(duì)象的這個(gè) newExecutor 方法。
DefaultSqlSession 相關(guān)類層結(jié)構(gòu)圖
在具體實(shí)現(xiàn)過程中,策略模式也可以和其他設(shè)計(jì)模式組合在一起使用。例如,MyBatis 針對(duì) Executor 的設(shè)計(jì),同時(shí)使用了模板方法模式和策略模式。來看一下整合了模板方法模式和策略模式的類層結(jié)構(gòu)圖。
Executor 接口完整類層結(jié)構(gòu)圖
總的來說,針對(duì) SQL 執(zhí)行過程,我們知道 MyBatis 分別提供了 SimpleExecutor、ReuseExecutor 以及 BatchExecutor 這三種不同的實(shí)現(xiàn)策略。這三種 Executor 都有一個(gè)共同的父類——BaseExecutor,在這個(gè)類中定義了一組抽象方法,交由它的三個(gè)子類進(jìn)行實(shí)現(xiàn),這種實(shí)現(xiàn)方式就是典型的模板方法設(shè)計(jì)模式。實(shí)際工作中,策略模式和模板方法模式也是一種常見的組合模式。
總結(jié)
最后我來給你總結(jié)一下。
圖片
如果你正在考慮圍繞一個(gè)業(yè)務(wù)場(chǎng)景提供不同的實(shí)現(xiàn)方法,那么可以先停下來,分析一下業(yè)務(wù)場(chǎng)景是否可以使用策略模式進(jìn)行實(shí)現(xiàn)。如果這些不同的實(shí)現(xiàn)方法體現(xiàn)的是算法之間的區(qū)別,而不是執(zhí)行流程上的差異,我們就可以引入今天所介紹的策略模式,并對(duì)其進(jìn)行設(shè)計(jì)。策略模式是一種非常有用的設(shè)計(jì)模式,我們也通過基本的實(shí)現(xiàn)代碼示例給出了它的實(shí)現(xiàn)方法。
就實(shí)現(xiàn)方法而言,采用策略模式的第一步,是設(shè)計(jì)一個(gè)合理的策略接口。然后基于不同算法,為這個(gè)接口提供不同的實(shí)現(xiàn)類。一旦構(gòu)建了多個(gè)實(shí)現(xiàn)類之后,我們就可以針對(duì)具體場(chǎng)景,選擇具體的實(shí)現(xiàn)類,或者提供新的實(shí)現(xiàn)類。策略模式能確保這些實(shí)現(xiàn)類之間相互獨(dú)立,并可以做到靈活替換。