设计模式之中介者模式
来源自《重学Java设计模式》链接提取码:ytc3
中介者模式
中介者模式要解决的就是复杂功能应用之间的重复调用,在这中间添加一层中介者包装服务,对外提供简单、通用、易扩展的服务能力。
这样的设计模式几乎在我们日常生活中和实际业务开发中都会见到,公司的系统中会有一个中台专门为你包装所有接口和提供统一的服务等等,这些都运用了中介者模式,除此以外,你用到的一些中间件,它们包装了底层多种数据库的差异话,提供了非常方便的方式进行应用。
案例场景模拟
在本案例中我们通过模仿Mybatis手写ORM框架,通过这样操作数据库学习中介者运用场景
除了这样的中间件层使用场景外,对于一些外部接口,例如N种奖品服务,可以有中台系统进行统一包装对外提供服务能力。也是中介者模式的一种思想体现。
在本案例中我们会把jdbc层进行包装,让用户在使用数据库服务的同时,可以和使用mybatis一样简单方便,通过这样的源码方式学习中介者模式,也方便对源码只是的拓展学习,增强知识栈。
常规实现
这是一种关于数据库操作的最初的方式。
基本上每一个学习开发的人都学习过直接使用jdbc方式连结数据库,进行CRUD。以下的例子可以当作会回忆。
代码实现
public class JDBCUtil {
private static Logger logger = LoggerFactory.getLogger(JDBCUtil.class);
public static final String URL = "jdbc:mysql://172.25.64.119:3306/itstack_demo_ddd";
public static final String USER = "root";
public static final String PASSWORD = "JRTEST";
public static void main(String[] args) throws Exception {
//1.加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//2. 获得数据库连接
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
//3.操作数据库,实现增删改查
Statement stmt = conn.createStatement();
ResultSet resultSet = stmt.executeQuery("SELECT id, name, age, createTime, updateTime FROM user");
//如果有数据,rs.next()返回true
while (resultSet.next()) {
logger.info("测试结果 姓名:{} 年龄:{}", resultSet.getString("name"),resultSet.getInt("age"));
}
}
}
- 以上是使用JDBC的方式进行直接操作数据库,几乎大家都使用过这样的方式。
测试结果
15:38:10.919 [main] INFO org.itstack.demo.design.JDBCUtil - 测试结果 姓名:⽔
⽔ 年龄:18
15:38:10.922 [main] INFO org.itstack.demo.design.JDBCUtil - 测试结果 姓名:⾖
⾖ 年龄:18
15:38:10.922 [main] INFO org.itstack.demo.design.JDBCUtil - 测试结果 姓名:花
花 年龄:19
Process finished with exit code 0
中介者模式开发ORM框架
中介者模式模型结构
- 以上是对ORM框架实现的核心类,包括了:加载配置文件、对xml文件的解析、获取数据库session、操作数据库以及结果返回。
- 左上是对数据库的定义和处理。基本包括我们常用的方法:
<T> T selectOne
、<T> List<T>
、selectList
等。 - 右侧蓝色部分是对数据库配置的开启session的工厂处理类,这里的工厂会操作
DefaultSqlSession
- 之后是红色地方的
SqlSessionFactoryBuilder
,这个类是对数据库操作的核心类:处理工厂、解析文件、拿到session等。
代码实现
定义SqlSession接口
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具体实现类
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操作隐藏起来,外部调用的时候对入参、出参都有内部进行处理。
定义SqlSessionFactory接口
public interface SqlSessionFactory {
SqlSession openSession();
}
- 开启一个
SqlSession
,这几乎是大家在平时使用中都需要进行操作的内容。虽然你看不见,但是当你有数据库操作的时候都会获取一次执行的SqlSession
。
SqlSessionFactory具体实现类
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
。
SqlSessionFactoryBuilder实现
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>)()链接数据库
连接数据库的地方和我们常见的方式是一样的:Class.forName(dataSource.get("driver"))
;,但是这样包装以后外部是不需要知道具体操作。同时当我们需要链接多套数据库的时候,也是可以在这里扩展。
mapperElement(解析sql语句)
这部分代码块内容相对来说比较长,但是核心的点就是为了解析xml中的sql语句配置。在我们平常的使用中基本会配置一些sql语句,也有一些入参的占位符。在这里我们使用正则表达式的方式进行解析操作。
解析完成的sql语句就有了一个名称和sql的映射关系,当我们进行数据库操作的时候,这个组件就可以通过映射关系获取到对应sql语句进行操作。
测试验证
先要建表
建表语句如下
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');
创建数据库对象类
用户类
public class User {
private Long id;
private String name;
private Integer age;
private Date createTime;
private Date updateTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
学校类
public class School {
private Long id;
private String name;
private String address;
private Date createTime;
private Date updateTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
创建DAO层
用户dao
public interface IUserDao {
User queryUserInfoById(Long id);
}
学校dao
public interface ISchoolDao {
School querySchoolInfoById(Long treeId);
}
ORM配置文件
链接配置
<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/itstack-demo-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>
- 这个配置与我们平常使用的myBatis基本是一样的,包括了数据库的连接池信息以及需要引入的mapper映射文件
操作配置(用户)
<mapper namespace="org.itstack.demo.design.dao.IUserDao">
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.design.po.User">
SELECT id, name, age, createTime, updateTime
FROM user
where id = #{id}
</select>
<select id="queryUserList" parameterType="org.itstack.demo.design.po.User" resultType="org.itstack.demo.design.po.User">
SELECT id, name, age, createTime, updateTime
FROM user
where age = #{age}
</select>
</mapper>
操作配置(学校)
<mapper namespace="org.itstack.demo.design.dao.ISchoolDao">
<select id="querySchoolInfoById" resultType="org.itstack.demo.design.po.School">
SELECT id, name, address, createTime, updateTime
FROM school
where id = #{id}
</select>
</mapper>
单个结果查询测试
@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("org.itstack.demo.design.dao.IUserDao.queryUserInfoById", 1L);
logger.info("测试结果:{}", JSON.toJSONString(user));
} finally {
session.close();
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
- 这里的使用方式和
MyBatis
是一样的,都包括了:资源的加载和解析、SqlSession
工厂构建、开启SqlSession
以及最后执行的查询语句selectOne
测试结果
16:56:51.831 [main] INFO org.itstack.demo.design.demo.ApiTest - 测试结
果:{"age":18,"createTime":1576944000000,"id":1,"name":"⽔ ⽔","updateTime":1576944000000}
Process finished with exit code 0
集合结果查询
@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("org.itstack.demo.design.dao.IUserDao.queryUserList", req);
logger.info("测试结果:{}", JSON.toJSONString(userList));
} finally {
session.close();
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
- 这个测试内容与以上只是查询方法有所不同:
session.selectList
,是查询一个集合结果。
测试结果
16:58:13.963 [main] INFO org.itstack.demo.design.demo.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
框架的原型,在我们日常的开发中,只需要按照配置即可非常简单的操作数据库。 - 除了以上这种组件模式的开发外,还有服务接口的包装也可以使用中介者模式来实现。比如你们公司有很多奖品接口需要在营销活动中对接,那么就可以把这些奖品接口统一收到中台开发一个奖品中心,对外提供服务。这样就不需要每一个需要对接奖品的接口,都需要找具体的提供者,而是找中台服务即可。
- 在上述的实现和测试中可以看到,这种设计模式满足了:
单一职责
和开闭原则
,也就是符合了迪米特原则
。即越少人知道越好。外部的人只需要按照需求进行调用,不需要知道具体的是如何实现的,复杂的一面已经由组件合作服务平台处理。
评论区