对于任何一个软件系统而言,从组成结构上通常可以分成两大部分,内核组件和扩展组件。我们知道,内核组件是需要非常稳定的,而扩展组件则应该按需开发,动态替换。
显然,想要实现这张图中的效果,我们需要对扩展组件进行抽象。
基于这种抽象,就可以实现不同的扩展组件。而因为这些扩展组件都是抽象组件的具体实现,所以它们可以相互替换。
其实想要实现图中的这种效果,我们有很多种方法。在面向对象的世界中,我们可以引入一种专门的设计模式来做到这一点,这种设计模式就是我们今天要讲的策略模式。
策略模式的基本概念和简单示例
那么,什么是策略呢?所谓策略,你可以理解它就是 一组算法或实现方法的组合。我们知道,实现某个功能的方法可能有很多。如果我们把这些方法封装起来,并能够确保它们可以相互替换,那么就可以构建出一系列的实现策略,这就是策略模式的由来。这个模式的结构可以这样表示:
这张图中,可以看到,Strategy 是一个公共接口,代表对具体实现方法的抽象。而 ConcreteStrategyA 和 ConcreteStrategyB 分别是 Strategy 接口的两个实现类,代表了不同的实现方法。同时,我们还注意到,这里有一个上下文组件 Context,来保持对 Strategy 接口的引用。
显然,我们可以把这里的 Context 看作是内核组件,而 Strategy 接口以及两个 ConcreteStrategy 实现类分别看作抽象组件和扩展组件。
前面已经提到,在面向对象的世界中,我们通常使用接口来定义一种策略。例如,在这个 Strategy 接口中,我们定义了一个方法,这个方法可以用来对输入的两个数字执行某一个操作。
public interface Strategy {
public int execute(int num1, int num2);
}
然后,我们就可以基于这个 Strategy 接口,来实现对这两个数字的具体计算方法。这里列举了常见的加法、减法和乘法。
public class AdditionStrategy implements Strategy{
@Override
public int execute(int num1, int num2) {
return num1+ num2;
}
}
public class SubtractionStrategy implements Strategy{
@Override
public int execute(int num1, int num2) {
return num1- num2;
}
}
public class MultiplicationStrategy implements Strategy{
@Override
public int execute(int num1, int num2) {
return num1* num2;
}
}
这些算法都非常简单,而对应的 Context 类也并不复杂。我们在这个类中注入了一个 Strategy 接口,然后通过这个接口的 execute 方法,来执行具体的计算方法。
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);
}
}
针对上面 Context,我们也可以编写对应的测试类。
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));
}
}
显然,策略模式本身的实现方式非常清晰。但在日常开发过程中,我们很少碰到像上面的代码示例这样简单的应用场景。这时候,就需要我们理解策略模式的本质作用,从繁冗复杂的代码结构中识别出策略模式的应用方式,从而更好地把握代码的结构。
策略模式在主流开源框架中可以说应用非常广泛。接下来,我们就以 MyBatis 框架为例,来分析一下它的应用场景和实现过程。
策略模式在 MyBatis 中的应用与实现
在 MyBatis 中,策略模式的应用场景主要就在 SQL 的执行器组件——Executor 中。作为 MyBatis 中最核心的接口之一,Executor 接口定义的内容非常丰富,这里列举几个比较有代表性的方法:
public interface Executor {
//执行 update、insert、delete 三种类型的 SQL 语句
int update(MappedStatement ms, Object parameter) throws SQLException;
//执行 selete 类型的 SQL 语句,返回值分为结果对象列表或游标对象
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
...
//批量执行 SQL 语句
List<BatchResult> flushStatements() throws SQLException;
//提交事务
void commit(boolean required) throws SQLException;
//回滚事务
void rollback(boolean required) throws SQLException;
}
在上面代码中,我们看到了一组用来实现数据库访问的常用方法,包括用于执行查询的 query 方法、用于执行更新的 update 方法、用于提交和回滚事务的 commit 和 rollback 方法,以及用于执行批量 SQL 的 flushStatements 方法。
在 MyBatis 中,Executor 接口具有一批实现类。
-
SimpleExecutor:普通执行器
这是 MyBatis 中最基础的、也是默认使用的一种 Executor,封装了对基本 SQL 语句的各种操作。
-
ReuseExecutor:重用执行器
顾名思义,这种 Executor 提供了对 SQL 语句进行重复利用的功能特性。基于这种功能特性,SQL 语句的创建、销毁以及预编译过程会得到优化,从而降低资源消耗,提高性能。
-
BatchExecutor:批处理执行器
从命名上,我们也不难看出,这种 Executor 的作用就是完成对 SQL 语句的批量处理。批处理的优势同样是节省资源消耗,因为我们可以一次向数据库发送多条 SQL 语句。
显然,这三个 Executor 实现类就是对 Executor 的不同策略实现。明确了这一点之后,我们接下来还需要明确两个问题,也就是:
- 这些具体策略实现类是如何生成的呢?
- 在 MyBatis 中,哪个组件扮演了 Context 角色呢?
我们先来看第一个问题。在 MyBatis 的配置文件中存在一个配置项,这个配置项用于设置 Executor 的默认类型。正如上面所讨论的,这个配置项指定了 MyBatis 默认采用的 Executor 是 SimpleExecutor。
<setting name="defaultExecutorType" value="SIMPLE" />
那么,Executor 是从哪里创建出来的呢?这就涉及到 MyBatis 中与配置相关的 Configuration 类。Configuration 是一种门面类,但也扮演着工厂类的角色,能够根据传入的策略类型来生成具体的策略对象。
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);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
...
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
可以看到,这里通过 Executor 的类型——ExecutorType,来决定构建哪一种具体的 Executor 实现类。请注意,我们在这里看到了一个 interceptorChain 对象,这是 MyBatis 中的拦截器组件,体现的是一种责任链处理机制。
接下来,我们讨论第二个问题,就是在 MyBatis 中,哪个组件扮演了 Context 角色呢?答案是 DefaultSqlSession。
DefaultSqlSession 内部包含了对 Executor 的引用,而 DefaultSqlSession 是通过 SqlSessionFactory 接口的默认实现类——DefaultSqlSessionFactory 进行构建的。在 SqlSession 生成过程中,需要指定 ExecutorType。这时就会调用 Configuration 对象的这个 newExecutor 方法。
在具体实现过程中,策略模式也可以和其他设计模式组合在一起使用。例如,MyBatis 针对 Executor 的设计,同时使用了模板方法模式和策略模式。来看一下整合了模板方法模式和策略模式的类层结构图。
总的来说,针对 SQL 执行过程,我们知道 MyBatis 分别提供了 SimpleExecutor、ReuseExecutor 以及 BatchExecutor 这三种不同的实现策略。这三种 Executor 都有一个共同的父类——BaseExecutor,在这个类中定义了一组抽象方法,交由它的三个子类进行实现,这种实现方式就是典型的模板方法设计模式。实际工作中,策略模式和模板方法模式也是一种常见的组合模式。
总结
最后我来给你总结一下。
如果你正在考虑围绕一个业务场景提供不同的实现方法,那么可以先停下来,分析一下业务场景是否可以使用策略模式进行实现。如果这些不同的实现方法体现的是算法之间的区别,而不是执行流程上的差异,我们就可以引入今天所介绍的策略模式,并对其进行设计。策略模式是一种非常有用的设计模式,我们也通过基本的实现代码示例给出了它的实现方法。
就实现方法而言,采用策略模式的第一步,是设计一个合理的策略接口。然后基于不同算法,为这个接口提供不同的实现类。一旦构建了多个实现类之后,我们就可以针对具体场景,选择具体的实现类,或者提供新的实现类。策略模式能确保这些实现类之间相互独立,并可以做到灵活替换。