一文搞清楚模板模式的定义及其在MyBatis中的使用
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/52/ )。
说起模板大家应该都熟悉。一般情况下,模板中规定了大体的框架,只留下一些细节供使用者来修改和完善。使用同一模板做出的不同产品都具有一致的框架。
设计模式中的模板模式与上述模板的概念相同。在模板模式中,需要使用一个抽象类定义一套操作的整体步骤(即,模板),而抽象类的子类则完成每个步骤的具体实现。这样,抽象类的不同子类总是遵循了同样的一套模板。
例如我们定义一套打扫卫生的模板,如下面代码所示。它为所有的打扫卫生工作定义了四个大的步骤:准备(prepare)、实施(implement)、善后(windup)、汇报(report)。
public abstract class Cleaning {
public void clean(){
prepare();
implement();
windup();
report();
}
abstract void prepare();
abstract void implement();
abstract void windup();
void report() {
System.out.println("告诉别人已经打扫完成。");
}
}
这样,我们可以基于该模板完成一些具体的打扫卫生工作。下面分别给出擦玻璃和擦黑板的实现:
public class WipeGlass extends Cleaning{
@Override
void prepare() {
System.out.println("找到抹布。");
System.out.println("浸湿和清洗抹布。");
}
@Override
void implement() {
System.out.println("擦玻璃。");
}
@Override
void windup() {
System.out.println("清理窗台。");
}
}
public class WipeBlackboard extends Cleaning {
@Override
void prepare() {
System.out.println("找到黑板檫。");
}
@Override
void implement() {
System.out.println("用力擦黑板。");
}
@Override
void windup() {
System.out.println("清理粉笔屑。");
}
}
可以看到,虽然具体的行为不同,但是CleanGlass和WipeBlackboard两者遵循的模板是一样的,都是由Cleaning类给定的。这就是模板模式的作用,即确定了一套操作的框架,而子类只需要在此框架的基础上定义具体的实现。
MyBatis中很多地方使用了模板模式。作为一个ORM框架,处理Java对象和数据库关系之间的映射是MyBatis工作中的重要部分。然而在Java中存在Integer、String、Data等各种类型的数据,在数据库中也存在varchar、longvarchar、tinyint等各种类型的数据。不同类型的字段所需的读、写方法各不相同,因此需要对不同类型的字段采取相应的处理方式。
例如,User中有下面几个属性。
private Integer id;
private String name;
private Integer sex;
private String schoolName;
则在对User对象的属性进行读取和赋值时,需要用Integer
的相关处理方式来操作id
、sex
属性,而要用String
的相关处理方式来操作name
、schoolName
属性。
在type包中,将每种类型对应的处理方式封装在了对应的类型处理器TypeHandler
中。例如IntegerTypeHandler
负责完成对Integer
类型的处理。
type包一共有43个类型处理器,这些类型处理器的名称也均以“TypeHandler
”结尾。而TypeHandler和BaseTypeHandler则分别是类型处理器接口和类型处理器基类。
下图展示了type包中类型处理器相关的类图。
TypeHandler<T>
是一个接口,其中定义了进行数据处理操作的几个抽象方法。而BaseTypeHandler<T>
继承了TypeHandler<T>
接口,并实现了TypeHandler<T>
中的接口。
在类型处理器相关类的设计中采用了模板模式,BaseTypeHandler<T>
作为所有类型处理器的基类,定义了模板的框架。而在各个具体的实现类中则实现了具体的细节。(均参考自书籍《通用源码阅读指导书——MyBatis源码详解》)
以下面代码中BaseTypeHandler
的getResult(ResultSet, String)
方法为例。该方法完成了异常处理等统一的工作,而与具体类型相关的getNullableResult(ResultSet, String)
操作则通过抽象方法交给具体的类型处理器实现。这就是典型的模板模式。
/**
* 从结果集中读出一个结果
* @param rs 结果集
* @param columnName 要读取的结果的列名称
* @return 结果值
* @throws SQLException
*/
public T getResult(ResultSet rs, String columnName) throws SQLException {
try {
return getNullableResult(rs, columnName);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e);
}
}
BaseTypeHandler<T>
交给具体的类型处理器实现的抽象方法一共只有4个。在每个类型处理器都需要实现这四个方法:
- void setNonNullParameter(PreparedStatement, int, T, JdbcType):向PreparedStatement对象中的指定变量位置写入一个不为null的值
- T getNullableResult(ResultSet, String):从ResultSet中按照字段名读出一个可能为null的数据
- T getNullableResult(ResultSet, int):从ResultSet中按照字段编号读出一个可能为null的数据
- T getNullableResult(CallableStatement, int):从CallableStatement按照字段编号读出一个可能为null的数据
因为上面的抽象方法跟具体的类型相关,因此存在泛型参数T。在每种类型处理器中,都给出了泛型参数的值。以IntegerTypeHandler
为例,它设置泛型参数值为Integer
。IntegerTypeHandler
类的源码如下所示。
public class IntegerTypeHandler extends BaseTypeHandler<Integer> {
/**
* 从结果集中读出一个可能为null的结果
* @param rs 结果集
* @param columnName 要读取结果的列名称
* @return 结果值
* @throws SQLException
*/
@Override
public Integer getNullableResult(ResultSet rs, String columnName)
throws SQLException {
int result = rs.getInt(columnName);
return result == 0 && rs.wasNull() ? null : result;
}
// 省略其他方法
}
上述代码中,IntegerTypeHandler
处理的类型是Integer
,这表明其getNullableResult
方法给出的就是一个Integer
结果。
通过模板模式,MyBatis为不同的数据类型提供了相同的处理流程,从而能够完成各种类型的属性的处理。可见,阅读源码对于深入理解设计模式的实现大有裨益。这里,推荐大家阅读《通用源码阅读指导书——MyBatis源码详解》。
《通用源码阅读指导书——MyBatis源码详解》是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注解。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用,非常推荐。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。