对于任何一个系统而言,势必存在一系列的类和对象,这些类和对象之间相互协作,完成某一项业务功能。当我们需要对系统中某一个类进行调整时,如何确保对现有的其他类所造成的影响最小,这是我们在设计上的一大目标。为了实现这一目标,就需要构建低耦合的软件系统。低耦合是软件设计的基本原则之一,下图展示了这一原则的一个示例结构。
那么,问题就来了,低耦合设计原则只是提供了一种方法论支持,我们应该如何来具体实现这一原则呢?方法有很多,而今天我们要介绍的门面模式就是其中一种具有代表性的实现方式,在Mybatis、Spring等主流开源框架中应用广泛。
门面模式的基本结构
门面的英文叫做Façade,该模式的意图可以这样描述:为子系统中的一组接口提供一个一致的界面。门面模式也常被称为外观模式,该模式定义了一个高层的访问入口,这个访问入口使得这一子系统更加容易使用。门面模式的示意图如下图所示。
从作用上讲,门面模式与微服务架构中的服务网关比较类似,可以认为服务网关是门面模式在微服务架构中的具体应用。通常,微服务的API粒度与客户端的要求不一定完全匹配。各个服务一般提供细粒度的API,这意味着客户端通常需要与多个服务进行交互。更为重要的是,服务网关能够起到客户端与微服务之间的隔离作用,随着业务需求的变化和时间的演进,网关背后的各个服务的划分和实现可能要做相应的调整和升级,这种调整和升级需要实现对客户端透明。下图展示了微服务架构中服务网关的基本结构。
讲完门面模式的基本概念和作用,我们来看它的类层结构,一个门面模式的常见组成方式如下图所示。
上图中,门面类Facade分别对接了背后的三个类SystemA、SystemB和SystemC,从而为Client提供统一的访问入口。基于上图,我们可以来设计一个简单的案例。假设SystemA类的实现如下所示。
public class SystemA{
public void performA() {
System.out.println("SystemA的操作");
}
}
可以看到,这里只是简单的执行了一个performA方法并打印日志。而SystemB和SystemC的实现也类似。
public class SystemB{
public void performB() {
System.out.println("SystemB的操作");
}
}
public class SystemC{
public void performC() {
System.out.println("SystemC的操作");
}
}
基于SystemA、SystemB和SystemC,对应提供统一访问入口的Façade类实现如下所示。
public class Facade {
private SystemA systemA = new SystemA();
private SystemB systemB = new SystemB();
private SystemC systemC = new SystemC();
public void service1() {
//1.执行SystemA的业务逻辑
systemA.performA();
//2.执行SystemB的业务逻辑
systemB.performB();
//3.执行SystemC的业务逻辑
systemC.performC();
}
public void service2() {
//1.执行SystemA的业务逻辑
systemA.performA();
//2.执行SystemC的业务逻辑
systemC.performC();
}
…
}
显然,对于使用Façade类的客户端而言,我们只需要关于上述service1方法的调用方式是否有变化。而如果我们想要构建另一个针对这几个子系统的操作流程,那么可以创建一个service2方法,并合理设计SystemA、SystemB和SystemC这三个类的执行逻辑即可。
显然,一旦具备Façade这样的门面类,我们就可以把位于底层的类进行排列组合形成统一的对外访问入口,而原有的各个底层类不需要做任何的调整。
门面模式在Mybatis中应用
在Mybatis中,门面模式应用也比较多,但并不像装饰器模式那样从类的命名上就可以直接进行判断,而是需要做一些挖掘。
SqlSession
我们知道在Mybatis中存在一个DefaultSqlSession类,该类是SqlSession接口的默认实现。在Mybatis中,我们会不止一次看到以Default-作为前缀的这一命名规则,例如用于创建SqlSession的工厂类DefaultSqlSessionFactory。
而在这些以Default-作为前缀的实现类背后,很多地方就使用了门面模式。这里,我们以最为典型的DefaultSqlSession为例来讨论门面模式的具体实现方式。
SqlSession接口的作用实际上就是封装了底层API,使得客户类不需要关注这些底层细节,转而应用门面类所提供的接口,这样做的好处就是避免底层那些功能强大但层次较低的接口使我们的调用更复杂。而我们已经知道DefaultSqlSession是一个门面类,那么从门面模式角度讲,学习该类的最直接的方法就是查看它的变量。让我们来翻看DefaultSqlSession的代码。
public class DefaultSqlSession implements SqlSession {
//配置类
private final Configuration configuration;
//执行器类
private final Executor executor;
// 省略其他代码
}
从上述代码中,我们看到DefaultSqlSession中使用了配置类Configuration和执行器类Executor,这两个类的角色就相当于充当了它的底层API。
我们先来看Configuration,Configuration中保存着Mybatis的各种环境信息、数据源、插件、解析后的sqlMap等对象,是Mybatis中非常核心的一个配置信息类。而在DefaultSqlSession中,Configuration类的主要作用是获取MappedStatement并提供给Executor进行SQL的执行。同时,Configuration类还能通过类型直接获取Mapper对象。
//获取MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//通过类型直接获取Mapper对象
Mapper mapper = configuration.getMapper(type, this);
另一方面,Executor是Mybatis中封装语句执行、调用结果集解析的核心接口。DefaultSqlSession为我们屏蔽了一系列底层操作,包括从配置信息中获取映射的SQL语句封装类并交给Executor执行,也包括根据传入的参数进行查询、更新和获取游标等操作。以代表查询的select方法为例,如下代码展示了使用Executor最终获得结果集的过程。
@Override
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
通过以上的门面模式实现方式,我们使用Mybatis时就不需要获得底层的Configuration和Executor对象,而只需要和SqlSession这个门面打交道。下图展示了DefaultSqlSession的这种门面效果。
Configuration
我们再来看前面介绍到的Configuration类,实际上Configuration本身也是一个非常典型的门面类。Configuration类的代码比较复杂,围绕这个类我们会展开很多讨论。今天我们关注于它在屏蔽底层接口上所起到的作用。事实上,Configuration类主要对一些创建对象的操作进行了封装,这些对象包括ParameterHandler、ResultSetHandler、StatementHandler和Executor等。
这里以创建Executor为例,来看一下Configuration类中的newExecutor方法,如下所示。
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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
上述newExecutor方法封装了如何创建Executor的过程,可以看到Executor的创建过程比较复杂,需要根据传入的类型选择创建具体哪种Executor。Configuration类通过该方法屏蔽了这一复杂过程。注意到这里出来了一个interceptorChain变量,这是典型的拦截器链。拦截器链是责任链模式这一设计模式的典型应用,我们放在后面的讲座中再进行详细展开。
如果你正在考虑对系统中的某些类做一些修改,那么面临的一个问题可能是需要判断这次修改对其他类的影响有多大。考虑到类与类之间总归存在一定的耦合度,所以对类结构的设计就需要尽量考虑耦合度问题。针对如何降低类之间的耦合度,我们就可以引入今天介绍的门面模式。门面模式是一种非常有用的设计模式,我们通过基本的代码示例给出了它的实现方法。
另一方面,我们也注意到实现门面模式的关键是需要对系统中的类层结构有一个合理的划分,确保位于底层的类不直接面向客户端,而是统一由门面类进行协调和整合。