中介者模式
# 定义
中介者模式要解决的就是复杂功能应用之间的重复调用,在这中间添加一层中介者包装服务,对外提供简单、通用、易扩展的服务能力。
这样的设计模式几乎在我们日常生活和实际业务开发中都会见到,例如;飞机降落有小姐姐在塔台喊话、无论哪个方向来的候车都从站台上下、公司的系统中有一个中台专门为你包装所有接口和提供统一的服务等等,这些都运用了中介者模式。除此之外,你用到的一些中间件,他们包装了底层多种数据库的差异化,提供非常简单的方式进行使用。
中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。中介者模式属于行为型模式。
优点:
1. 降低了类的复杂度,将一对多转化成了一对一。
2. 各个类之间的解耦。
3. 符合迪米特原则。
缺点:中介者会庞大,变得复杂难以维护。
使用场景:
1. 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。
2. 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
# 实践
本次模仿 Mybatis 手写 ORM 框架,通过这样操作数据库学习中介者运用场景

>除了这样的中间件层使用场景外,对于一些外部接口,例如 N 种奖品服务,可以由中台系统进行统一包装对外提供服务能力。也是中介者模式的一种思想体现。
>
>在本案例中我们会把 jdbc 层进行包装,让用户在使用数据库服务的时候,可以和使用 mybatis 一样简单方便,通过这样的源码方式学习中介者模式,也方便对源码知识的拓展学习,增强知识栈。

整个代码结构:

- 以上是对 ORM 框架实现的核心类,包括了;加载配置文件、对 xml 解析、获取数据库 session、操作数据库以及结果返回。
- 左上是对数据库的定义和处理,基本包括我们常用的方法;<T> T selectOne、<T> List<T> selectList等。
- 右侧蓝色部分是对数据库配置的开启 session 的工厂处理类,这里的工厂会操作 DefaultSqlSession
- 之后是红色地方的 SqlSessionFactoryBuilder,这个类是对数据库操作的核心类;处理工厂、解析文件、拿到 session 等。
定义 SqlSession 接口
```java
public interface SqlSession {
<T> T selectOne(String statement);
<T> T selectOne(String statement, Object parameter);
<T> List<T> selectList(String statement);
<T> List<T> selectList(String statement, Object parameter);
void close();
}
```
- 这里定义了对数据库操作的查询接口,分为查询一个结果和查询多个结果,同时包括有参数和没有参数的方法。
SqlSession 具体实现类
```java
public class DefaultSqlSession implements SqlSession {
private Connection connection;
private Map<String, XNode> mapperElement;
public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
this.connection = connection;
this.mapperElement = mapperElement;
}
@Override
public <T> T selectOne(String statement) {
try {
XNode xNode = mapperElement.get(statement);
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
ResultSet resultSet = preparedStatement.executeQuery();
List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
return objects.get(0);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public <T> T selectOne(String statement, Object parameter) {
XNode xNode = mapperElement.get(statement);
Map<Integer, String> parameterMap = xNode.getParameter();
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
buildParameter(preparedStatement, parameter, parameterMap);
ResultSet resultSet = preparedStatement.executeQuery();
List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
return objects.get(0);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public <T> List<T> selectList(String statement) {
XNode xNode = mapperElement.get(statement);
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
ResultSet resultSet = preparedStatement.executeQuery();
return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public <T> List<T> selectList(String statement, Object parameter) {
XNode xNode = mapperElement.get(statement);
Map<Integer, String> parameterMap = xNode.getParameter();
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
buildParameter(preparedStatement, parameter, parameterMap);
ResultSet resultSet = preparedStatement.executeQuery();
return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {
int size = parameterMap.size();
// 单个参数
if (parameter instanceof Long) {
for (int i = 1; i <= size; i++) {
preparedStatement.setLong(i, Long.parseLong(parameter.toString()));
}
return;
}
if (parameter instanceof Integer) {
for (int i = 1; i <= size; i++) {
preparedStatement.setInt(i, Integer.parseInt(parameter.toString()));
}
return;
}
if (parameter instanceof String) {
for (int i = 1; i <= size; i++) {
preparedStatement.setString(i, parameter.toString());
}
return;
}
Map<String, Object> fieldMap = new HashMap<>();
// 对象参数
Field[] declaredFields = parameter.getClass().getDeclaredFields();
for (Field field : declaredFields) {
String name = field.getName();
field.setAccessible(true);
Object obj = field.get(parameter);
field.setAccessible(false);
fieldMap.put(name, obj);
}
for (int i = 1; i <= size; i++) {
String parameterDefine = parameterMap.get(i);
Object obj = fieldMap.get(parameterDefine);
if (obj instanceof Short) {
preparedStatement.setShort(i, Short.parseShort(obj.toString()));
continue;
}
if (obj instanceof Integer) {
preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
continue;
}
if (obj instanceof Long) {
preparedStatement.setLong(i, Long.parseLong(obj.toString()));
continue;
}
if (obj instanceof String) {
preparedStatement.setString(i, obj.toString());
continue;
}
if (obj instanceof Date) {
preparedStatement.setDate(i, (java.sql.Date) obj);
}
}
}
private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
List<T> list = new ArrayList<>();
try {
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
// 每次遍历行值
while (resultSet.next()) {
T obj = (T) clazz.newInstance();
for (int i = 1; i <= columnCount; i++) {
Object value = resultSet.getObject(i);
String columnName = metaData.getColumnName(i);
String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
Method method;
if (value instanceof Timestamp) {
method = clazz.getMethod(setMethod, Date.class);
} else {
method = clazz.getMethod(setMethod, value.getClass());
}
method.invoke(obj, value);
}
list.add(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
@Override
public void close() {
if (null == connection) {
return;
}
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
```
- 这里包括了接口定义的方法实现,也就是包装了 jdbc 层。
- 通过这样的包装可以让对数据库的 jdbc 操作隐藏起来,外部调用的时候对入参、出参都有内部进行处理。
定义 XNode 类
```java
@Data
public class XNode {
private String namespace;
private String id;
private String parameterType;
private String resultType;
private String sql;
private Map<Integer, String> parameter;
}
```
定义 SqlSessionFactory 接口
```java
public interface SqlSessionFactory {
SqlSession openSession();
}
```
- 开启一个 SqlSession, 这几乎是大家在平时的使用中都需要进行操作的内容。虽然你看不见,但是当你有数据库操作的时候都会获取每一次执行的 SqlSession。
SqlSessionFactory具体实现类
```java
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
}
}
```
- DefaultSqlSessionFactory,是使用 mybatis 最常用的类,这里我们简单的实现了一个版本。
- 虽然是简单的版本,但是包括了最基本的核心思路。当开启 SqlSession 时会进行返回一个 DefaultSqlSession
- 这个构造函数中向下传递了 Configuration 配置文件,在这个配置文件中包括:Connection connection、Map<String, String> dataSource、Map<String, XNode> mapperElement。
Configuration 类
```java
@Data
public class Configuration {
protected Connection connection;
protected Map<String, String> dataSource;
protected Map<String, XNode> mapperElement;
}
```
SqlSessionFactoryBuilder实现
```java
public class SqlSessionFactoryBuilder {
public DefaultSqlSessionFactory build(Reader reader) {
SAXReader saxReader = new SAXReader();
try {
saxReader.setEntityResolver(new XMLMapperEntityResolver());
Document document = saxReader.read(new InputSource(reader));
Configuration configuration = parseConfiguration(document.getRootElement());
return new DefaultSqlSessionFactory(configuration);
} catch (DocumentException e) {
e.printStackTrace();
}
return null;
}
private Configuration parseConfiguration(Element root) {
Configuration configuration = new Configuration();
configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
configuration.setConnection(connection(configuration.dataSource));
configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
return configuration;
}
// 获取数据源配置信息
private Map<String, String> dataSource(List<Element> list) {
Map<String, String> dataSource = new HashMap<>(4);
Element element = list.get(0);
List content = element.content();
for (Object o : content) {
Element e = (Element) o;
String name = e.attributeValue("name");
String value = e.attributeValue("value");
dataSource.put(name, value);
}
return dataSource;
}
private Connection connection(Map<String, String> dataSource) {
try {
Class.forName(dataSource.get("driver"));
return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return null;
}
// 获取SQL语句信息
private Map<String, XNode> mapperElement(List<Element> list) {
Map<String, XNode> map = new HashMap<>();
Element element = list.get(0);
List content = element.content();
for (Object o : content) {
Element e = (Element) o;
String resource = e.attributeValue("resource");
try {
Reader reader = Resources.getResourceAsReader(resource);
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(new InputSource(reader));
Element root = document.getRootElement();
//命名空间
String namespace = root.attributeValue("namespace");
// SELECT
List<Element> selectNodes = root.selectNodes("select");
for (Element node : selectNodes) {
String id = node.attributeValue("id");
String parameterType = node.attributeValue("parameterType");
String resultType = node.attributeValue("resultType");
String sql = node.getText();
// ? 匹配
Map<Integer, String> parameter = new HashMap<>();
Pattern pattern = Pattern.compile("(#\\{(.*?)})");
Matcher matcher = pattern.matcher(sql);
for (int i = 1; matcher.find(); i++) {
String g1 = matcher.group(1);
String g2 = matcher.group(2);
parameter.put(i, g2);
sql = sql.replace(g1, "?");
}
XNode xNode = new XNode();
xNode.setNamespace(namespace);
xNode.setId(id);
xNode.setParameterType(parameterType);
xNode.setResultType(resultType);
xNode.setSql(sql);
xNode.setParameter(parameter);
map.put(namespace + "." + id, xNode);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
return map;
}
}
```
- 在这个类中包括的核心方法有;build(构建实例化元素)、parseConfiguration(解析配置)、dataSource(获取数据库配置)、connection(Map<String, String> dataSource) (链接数据库)、mapperElement (解析sql语句)
- 接下来我们分别介绍这样的几个核心方法。
- **build(构建实例化元素)**
这个类主要用于创建解析 xml 文件的类,以及初始化 SqlSession 工厂类 DefaultSqlSessionFactory。另外需要注意这段代码 saxReader.setEntityResolver(new XMLMapperEntityResolver());,是为了保证在不联网的时候一样可以解析 xml,否则会需要从互联网获取 dtd 文件。
- **parseConfiguration(解析配置)**
是对 xml 中的元素进行获取,这里主要获取了;dataSource、mappers,而这两个配置一个是我们数据库的链接信息,另外一个是对数据库操作语句的解析。
- **connection(Map<String, String> dataSource) (链接数据库)**
链接数据库的地方和我们常见的方式是一样的;Class.forName(dataSource.get("driver"));,但是这样包装以后外部是不需要知道具体的操作。同时当我们需要链接多套数据库的时候,也是可以在这里扩展。
- **mapperElement (解析sql语句)**
这部分代码块内容相对来说比较长,但是核心的点就是为了解析 xml 中的 sql 语句配置。在我们平常的使用中基本都会配置一些 sql 语句,也有一些入参的占位符。在这里我们使用正则表达式的方式进行解析操作。
解析完成的 sql 语句就有了一个名称和 sql 的映射关系,当我们进行数据库操作的时候,这个组件就可以通过映射关系获取到对应 sql 语句进行操作。
Resources 类
```java
public class Resources {
public static Reader getResourceAsReader(String resource) throws IOException {
return new InputStreamReader(getResourceAsStream(resource));
}
private static InputStream getResourceAsStream(String resource) throws IOException {
ClassLoader[] classLoaders = getClassLoaders();
for (ClassLoader classLoader : classLoaders) {
InputStream inputStream = classLoader.getResourceAsStream(resource);
if (null != inputStream) {
return inputStream;
}
}
throw new IOException("Could not find resource " + resource);
}
private static ClassLoader[] getClassLoaders() {
return new ClassLoader[]{
ClassLoader.getSystemClassLoader(),
Thread.currentThread().getContextClassLoader()};
}
}
```
测试之前需要导入数据库
- 库名:design
- 表名:user、school
```
CREATE TABLE school ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64), address varchar(256), createTime datetime, updateTime datetime, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into school (id, name, address, createTime, updateTime) values (1, '北京大学', '北京市海淀区颐和园路5号', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (2, '南开大学', '中国天津市南开区卫津路94号', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
insert into school (id, name, address, createTime, updateTime) values (3, '同济大学', '上海市彰武路1号同济大厦A楼7楼7区', '2019-10-18 13:35:57', '2019-10-18 13:35:57');
CREATE TABLE user ( id bigint(11) NOT NULL AUTO_INCREMENT, name varchar(32), age int(4), address varchar(128), entryTime datetime, remark varchar(64), createTime datetime, updateTime datetime, status int(4) DEFAULT '0', dateTime varchar(64), PRIMARY KEY (id), INDEX idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (1, '水水', 18, '吉林省榆树市黑林镇尹家村5组', '2019-12-22 00:00:00', '无', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200309');
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (2, '豆豆', 18, '辽宁省大连市清河湾司马道407路', '2019-12-22 00:00:00', '无', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 1, null);
insert into user (id, name, age, address, entryTime, remark, createTime, updateTime, status, dateTime) values (3, '花花', 19, '辽宁省大连市清河湾司马道407路', '2019-12-22 00:00:00', '无', '2019-12-22 00:00:00', '2019-12-22 00:00:00', 0, '20200310');
```
用户类
```java
@Data
public class User {
private Long id;
private String name;
private Integer age;
private Date createTime;
private Date updateTime;
}
```
学校类
```java
@Data
public class School {
private Long id;
private String name;
private String address;
private Date createTime;
private Date updateTime;
}
```
用户DAO
```java
public interface IUserDao {
User queryUserInfoById(Long id);
}
```
学校DAO
```java
public interface ISchoolDao {
School querySchoolInfoById(Long treeId);
}
```
配置文件 **mybatis-config-datasource.xml**
```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/design?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456!"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/User_Mapper.xml"/>
<mapper resource="mapper/School_Mapper.xml"/>
</mappers>
</configuration>
```
操作用户 **User_Mapper.xml**
```
<mapper namespace="fun.lixj.design.dao.IUserDao">
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="fun.lixj.design.model.User">
SELECT id, name, age, createTime, updateTime
FROM user
where id = #{id}
</select>
<select id="queryUserList" parameterType="fun.lixj.design.model.User" resultType="fun.lixj.design.model.User">
SELECT id, name, age, createTime, updateTime
FROM user
where age = #{age}
</select>
</mapper>
```
操作学校 **School_Mapper.xml**
```
<mapper namespace="fun.lixj.design.dao.ISchoolDao">
<select id="querySchoolInfoById" resultType="fun.lixj.design.model.School">
SELECT id, name, address, createTime, updateTime
FROM school
where id = #{id}
</select>
</mapper>
```
单个结果查询测试
```java
@Test
public void test_queryUserInfoById() {
String resource = "mybatis-config-datasource.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlMapper.openSession();
try {
User user = session.selectOne("fun.lixj.design.dao.IUserDao.queryUserInfoById", 1L);
log.info("测试结果:{}", JSON.toJSONString(user));
} finally {
session.close();
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
```
测试结果:
```
14:32:20.100 [main] INFO ApiTest - 测试结果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}
Process finished with exit code 0
```
集合结果查询测试
```java
@Test
public void test_queryUserList() {
String resource = "mybatis-config-datasource.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlMapper.openSession();
try {
User req = new User();
req.setAge(18);
List<User> userList = session.selectList("fun.lixj.design.dao.IUserDao.queryUserList", req);
log.info("测试结果:{}", JSON.toJSONString(userList));
} finally {
session.close();
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
```
测试结果:
```
14:33:14.179 [main] INFO ApiTest - 测试结果:[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]
Process finished with exit code 0
```
# 总结
- 以上通过中介者模式的设计思想我们手写了一个 ORM 框架,隐去了对数据库操作的复杂度,让外部的调用方可以非常简单的进行操作数据库。这也是我们平常使用的 Mybatis 的原型,在我们日常的开发使用中,只需要按照配置即可非常简单的操作数据库。
- 除了以上这种组件模式的开发外,还有服务接口的包装也可以使用中介者模式来实现。比如你们公司有很多的奖品接口需要在营销活动中对接,那么可以把这些奖品接口统一收到中台开发一个奖品中心,对外提供服务。这样就不需要每一个需要对接奖品的接口,都去找具体的提供者,而是找中台服务即可。
- 在上述的实现和测试使用中可以看到,这种模式的设计满足了;单一职责和开闭原则,也就符合了迪米特原则,即越少人知道越好。外部的人只需要按照需求进行调用,不需要知道具体的是如何实现的,复杂的一面已经有组件合作服务平台处理。
>原文链接:https://github.com/fuzhengwei/CodeGuide/blob/master/docs/md/develop/design-pattern/2020-06-27-%E9%87%8D%E5%AD%A6%20Java%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E3%80%8A%E5%AE%9E%E6%88%98%E4%B8%AD%E4%BB%8B%E8%80%85%E6%A8%A1%E5%BC%8F%E3%80%8B.md

【设计模式】中介者模式