行为型模式
目录
1、模板方法模式
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
通俗点讲,就是:定义一个模板结构,将具体内容延迟到子类去实现。
- 意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
- 何时使用:有一些通用的方法。
- 如何解决:将这些通用算法抽象出来。
- 关键代码:在抽象类实现,其他步骤在子类实现。
1.1 模板方式模式UML图
1.2 日常生活中看模板方式模式与应用实例
- 在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。
- 西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。
- spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。
- 做试卷,大家题目都是一样的,只是答案不同
- 对于汽车,车从发动到停车的顺序是相同的,不同的是引擎声、鸣笛声等
- 造房时,地基、走线、水管都一样,只有在建筑后期才有差异
1.3 Java代码实现
接下来我用一个实例来对模板方法模式进行更深一步的介绍。
a. 实例概况
- 背景:小成希望学炒菜:手撕包菜 & 蒜蓉炒菜心
- 冲突:两道菜的炒菜步骤有的重复有的却差异很大,记不住
- 解决方案:利用代码记录下来
b. 使用步骤
步骤1: 创建抽象模板结构(Abstract Class):炒菜的步骤
public abstract class Abstract Class {
//模板方法,用来控制炒菜的流程 (炒菜的流程是一样的-复用)
//申明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序
final void cookProcess(){
//第一步:倒油
this.pourOil();
//第二步:热油
this.HeatOil();
//第三步:倒蔬菜
this.pourVegetable();
//第四步:倒调味料
this.pourSauce();
//第五步:翻炒
this.fry();
}
//定义结构里哪些方法是所有过程都是一样的可复用的,哪些是需要子类进行实现的
//第一步:倒油是一样的,所以直接实现
void pourOil(){
System.out.println("倒油");
}
//第二步:热油是一样的,所以直接实现
void HeatOil(){
System.out.println("热油");
}
//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
//所以声明为抽象方法,具体由子类实现
abstract void pourVegetable();
//第四步:倒调味料是不一样的(一个下辣椒,一个是下蒜蓉)
//所以声明为抽象方法,具体由子类实现
abstract void pourSauce();
//第五步:翻炒是一样的,所以直接实现
void fry();{
System.out.println("炒啊炒啊炒到熟啊");
}
}
步骤2: 创建具体模板(Concrete Class),即”手撕包菜“和”蒜蓉炒菜心“的具体步骤
//炒手撕包菜的类
public class ConcreteClass_BaoCai extend Abstract Class{
@Override
public void pourVegetable(){
System.out.println(”下锅的蔬菜是包菜“);
}
@Override
public void pourSauce(){
System.out.println(”下锅的酱料是辣椒“);
}
}
//炒蒜蓉菜心的类
public class ConcreteClass_CaiXin extend Abstract Class{
@Override
public void pourVegetable(){
System.out.println(”下锅的蔬菜是菜心“);
}
@Override
public void pourSauce(){
System.out.println(”下锅的酱料是蒜蓉“);
}
}
步骤3:客户端调用-炒菜了
public class Template Method{
public static void main(String[] args){
//炒 - 手撕包菜
ConcreteClass_BaoCai BaoCai = new ConcreteClass_BaoCai();
BaoCai.cookProcess();
//炒 - 蒜蓉菜心
ConcreteClass_ CaiXin = new ConcreteClass_CaiXin();
CaiXin.cookProcess();
}
}
结果输出
倒油
热油
下锅的蔬菜是包菜
下锅的酱料是辣椒
炒啊炒啊炒到熟
倒油
热油
下锅的蔬菜是菜心
下锅的酱料是蒜蓉
炒啊炒啊炒到熟
通过上述这个常见的生活例子,我相信你已经完全明白了模板方法模式的原理了!!
2、模板方法模式在源码中的应用
2.1 Netty中两种发生消息的方式
- 在Netty中有两种发生消息的方式,可以直接写到Channel中,也可以写道与ChannelHandler所关联的那个ChannelHandlerContext中对于前一种方式来说,消息会从ChannelPipeline的末尾开始流动,对于后一种方式来说,消息将从ChannelPipeline中的下一个ChannelHandler开始流动
- ChannelHandlerContext与ChannelHandler绑定关系永远不会改变的,因此对其运行缓存是没有任何问题的
- 对于与Channel的同名方法来说ChannelHandlerContext的方法将产生更短的事件流,所以我们应该在可能的情况下利用这个特殊性来提升应用性能
消息发生方式1
消息发生方式2
2.2 MyBatis源码中模板方法模式的提现
在 MyBatis 源码中,有很多模板方法模式的经典应用场景。
本节来介绍模板方法模式在 BaseExecutor 类中的应用。BaseExecutor 是一个基础的 SQL 执行类,实现了大部分 SQL 执行逻辑,然后把几个方法交给子类定制化完成,主要提供了缓存管理和事务管理的基本功能。源码如下。
public abstract class BaseExecutor implements Executor {
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack = 0;
private boolean closed;
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
protected abstract int doUpdate(MappedStatement ms, Object parameter)
throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
throws SQLException;
// 省略....
Executor 是 Mybatis 的核心接口之一,定义了数据库操作的基本方法。BaseExecutor 类中的 query() 方法会先创建 CacheKey 对象,并根据 CacheKey 对象查找一级缓存,如果缓存命中则返回缓存中记录的结果对象,如果未命中则查询数据库得到结果集,之后将结果集映射成结果对象并保存到一级缓存中,同时返回结果对象。
doUpdate、doFlushStatements、doQuery 和 doQueryCursor 这几个方法就是交由子类来实现的,也就是说继承 BaseExecutor 的子类只需要实现这 4 个基本方法来完成数据库的相关操作即可。
BaseExecutor 的子类有 ReuseExecutor、SimpleExecutor、BatchExecutor 和 ClosedExecutor,其类图如下。
这里对这 4 个子类的功能简单介绍一下:
- SimpleExecutor 是 Mybatis 执行 Mapper 语句时默认使用的 Executor,提供最基本的 Mapper 语句执行功能,没有过多的封装。
- ReuseExecutor 提供了 Statement 重用的功能,通过 statementMap 字段缓存使用过的 Statement 对象进行重用,可以减少 SQL 预编译以及创建和销毁 Statement 对象的开销,从而提高性能。
- BatchExecutor 实现了批处理多条 SQL 语句的功能,在客户端缓存多条 SQL 并在合适的时机将多条 SQL 打包发送给数据库执行,从而减少网络方面的开销,提升系统的性能。
- ClosedExecutor 只是某个类的一个内部类。
下面是 SimpleExecutor 的 doUpdate() 方法实现。
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
int var6;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var6 = handler.update(stmt);
} finally {
this.closeStatement(stmt);
}
return var6;
}
再来对比一下 BatchExecutor 的 doUpdate() 方法实现。
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
Statement stmt;
if (sql.equals(this.currentSql) && ms.equals(this.currentStatement)) {
int last = this.statementList.size() - 1;
stmt = (Statement)this.statementList.get(last);
handler.parameterize(stmt);
BatchResult batchResult = (BatchResult)this.batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
Connection connection = this.getConnection(ms.getStatementLog());
stmt = handler.prepare(connection);
handler.parameterize(stmt);
this.currentSql = sql;
this.currentStatement = ms;
this.statementList.add(stmt);
this.batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
handler.batch(stmt);
return -2147482646;
}
细心的小伙伴一定看出了差异,BatchExecutor 的处理逻辑比 SimpleExecutor 更为复杂,调用的核心 API 也有区别,SimpleExecutor 调用的核心方法是 handler.update() 方法,BatchExecutor 调用的核心方法是 handler.batch() 方法。这里暂时不对 MyBatis 源码进行深入分析,感兴趣的小伙伴可以自行继续深入研究。
3、模板方法模式优缺点
3.1 优点
- 提高代码复用性
将相同部分的代码放在抽象的父类中- 提高了拓展性
将不同的代码放入不同的子类中,通过对子类的扩展增加新的行为- 实现了反向控制
通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,实现了反向控制 & 符合“开闭原则”
3.2 缺点
引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度。
3.3 应用场景
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现;
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复;
- 控制子类的扩展。
3.4 注意事项
- 为防恶意操作,一般模板方法都加上final关键字
参考文章:
https://www.jianshu.com/p/7a8b508d7b0f
http://c.biancheng.net/view/8480.html
https://www.cnblogs.com/adamjwh/p/10919149.html
https://blog.youkuaiyun.com/carson_ho/article/details/54910518
https://www.runoob.com/design-pattern/template-pattern.html