不讲故事的设计模式-模板方法模式
文章目录
- 模板方法模式
- 简介
- 作用
- 模板方法模式的缺点
- 模板方法模式的应用场景
- 业务场景
- 开源框架中的应用
- 对比回调和Hook模式
- 关于组合优先于继承
- 关于设计模式乱用的现象
模板方法模式
简介
模板方法模式是一种行为型设计模式,该设计模式的核心在于通过抽象出一套相对标准的处理步骤,并可灵活的将任意步骤交给子类去进行扩展,使得可以在不改变整体业务处理流程的前提下,通过定义不同的子类实现即可完成业务处理的扩展。
我们可以举个简单的例子,比如对于下面定义的method
方法中调用的a、b、c
三个子方法,可以通过不同的子类实现来完成不同业务逻辑的处理。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();protected abstract void b();protected abstract void a();}
还可以这样定义,此时相当于b
方法在父类中有一套默认的处理,子类可以根据需要选择重写或者不重写。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();protected void b() {// 默认处理逻辑。。。}protected abstract void a();}
当然,还可以将b
方法声明为private
或者加上final
关键字从而禁止子类重写,此时b
方法的逻辑就完全由父类统一管理。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();private void b() {// 固定处理逻辑。。。}protected abstract void a();}
作用
模板方法模式主要有两大作用:复用和扩展。
复用:复用指的是像method
这样的方法,所有子类都可以拿来使用,复用该方法中定义的这套处理逻辑。
扩展:扩展的能力就更加强大了,狭义上可以针对代码进行扩展,子类可以独立增加功能逻辑,而不影响其他的子类,符合开闭原则,广义上可以针对整个框架进行扩展,比如像下面这段代码逻辑:
public class Temp {public final void method() {a();b();c();d();}protected void c() {// 默认处理逻辑。。。};private void b() {// 固定处理逻辑。。。}protected void a() {// 默认处理逻辑。。。}protected void d() {// 强制子类必须重写throw new UnsupportedOperationException();}}
框架默认可以直接使用,但同时也预留了a
、c
、d
三个方法的扩展能力,且d
方法还通过抛出异常的方式,强制要求子类必须重写,所以现在完全可以通过方法重写的方式实现框架的功能扩展。
这种框架扩展的方式的典型案例就是Servlet
中定义的service
方法,该方法分别预留了doGet
和doPost
等扩展方法。
模板方法模式的缺点
从另一个角度来说,设计模式本身实际上并不存在什么缺点,真正导致出现这些问题的原因还是使用设计模式的方式,尤其是新手在刚了解到设计模式的时候,往往会试图到处找场景去套用各种设计模式,甚至一个方法能用上好几种,这就是典型的手里拿个锤子,看什么都是钉子。所以,如果按照这样的使用方式,通常就会导致子类或者实现类非常多,但逻辑却很少,或相似;方法为了兼容各种场景而过于抽象,导致代码复杂度增加,可阅读性也变差。
针对模板方式模式来说,因为通常情况下是通过继承机制来实现业务流程的不变部分和可变部分的分离,因此,如果可变部分的业务逻辑并不复杂,或者不变部分和可变部分的关系不清晰时,就不适合用模板方法模式了。
模板方法模式的应用场景
业务的整体处理流程是固定的,但其中的个别部分是易变的,或者可扩展的,此时就可以使用模板方法模式,下面我们分别举一些常见的业务场景和开源框架的应用来说明。
业务场景
订单结算场景
订单结算在电商平台是非常常见的功能,整个结算过程一定会包含:订单生成、库存校验、费用计算、结果通知,但比如其中费用计算则可能在优惠券、折扣、运费等地方又有所不同,因此可以将整个结算过程抽象为一个模板类,具体的结算类只需要继承该模板类,并实现具体的计算规则即可。
任务活动场景
常见的任务活动,主要包含三步骤:任务事件接收、任务规则匹配、任务奖励触发,而往往事件接收和奖励触发都是比较统一的,规则匹配则跟具体的任务相关,所以可以用模板方法模式来实现。
开源框架中的应用
Spring MVC
handleRequestInternal
由子类实现
public abstract class AbstractController extends WebContentGenerator implements Controller {@Override@Nullablepublic ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {if (HttpMethod.OPTIONS.matches(request.getMethod())) {response.setHeader("Allow", getAllowHeader());return null;}// Delegate to WebContentGenerator for checking and preparing.checkRequest(request);prepareResponse(response);// Execute handleRequestInternal in synchronized block if required.if (this.synchronizeOnSession) {HttpSession session = request.getSession(false);if (session != null) {Object mutex = WebUtils.getSessionMutex(session);synchronized (mutex) {return handleRequestInternal(request, response);}}}return handleRequestInternal(request, response);}/*** Template method. Subclasses must implement this.* The contract is the same as for {@code handleRequest}.* @see #handleRequest*/@Nullableprotected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)throws Exception;}
MyBatis
BaseExecutor
是MyBatis
中经典的模板方法模式应用,其主要是用来执行SQL
,query
方法是模板方法的主流程,doQuery
方法是其留给子类实现的。
public abstract class BaseExecutor implements Executor {// 几个do开头的方法都是留给子类实现的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; @Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}@SuppressWarnings("unchecked")@Overridepublic <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 #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}
}private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 具体query方式,交由子类实现list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {localCache.removeObject(key);}localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;}
JDK AbstractCollection抽象类
AbstractCollection
中实现了Set
接口中定义的addAll
方法,该方法又是基于add
方法来实现的,具体代码如下所示:
public boolean addAll(Collection<? extends E> c) {boolean modified = false;for (E e : c)if (add(e))modified = true;return modified;
}
但AbstractCollection
本身并不处理add
方法,而是希望子类自己去实现,如果调用者不小心直接调用了AbstractCollection
的add
方法,则会直接抛出异常。
public boolean add(E e) {throw new UnsupportedOperationException();
}
对比回调和Hook模式
回调和Hook
这两种模式,在一定程度上也能起到模板方法模式的效果,他们都可以在一套流程中预留某个扩展点,然后将这个扩展点交由请求方自己来实现,最常见的就是支付场景,在请求支付的时候,往往是不会同步等待支付结果的,而是在请求的同时注册一个回调接口,这样三方支付系统完成支付之后,就会回调这个接口来完成支付结果的通知。
虽然从应用场景上来回调或者Hook
模式和模板方法模式差不多,但从代码实现方式来看,却有很大差异,模板方法模式是基于继承的方式来实现的,这实际上是有很大的局限性,而回调或者Hook
模式则是基于组合方式来实现的,我们都知道组合优于继承,其次,回调或者Hook
模式还可以基于匿名类的方式来实现,不用事先定义类,显然更加灵活,当然,回调也有其问题,使用不当,容易出现调用关系混乱,系统层次混乱等现象。
关于组合优先于继承
继承是实现代码重用的重要手段之一,但并非是实现代码重用的最佳方式,继承打破了封装性,因此很容易在使用时产生问题,为了更好的说明这一点,我们来举个例子,假设我们现在需要为HashSet``添加一个计数功能,即看看HashSet
自创建以来,一共被添加过多少个元素,我们可以用下面这种方式来实现:
public class CountHashSet<E> extends HashSet<E> {private int addCount = 0;public CountHashSet() {}@Overridepublic boolean add(E e) {addCount++;return super.add(e);}@Overridepublic boolean addAll(Collection<? extends E> c) {addCount += c.size();return super.addAll(c);}public int getAddCount() {return addCount;}
}class Main {public static void main(String[] args) {CountHashSet<Integer> countHashSet = new CountHashSet<>();countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}
很遗憾最终输出结果并不是3
,而是6
,问题就在于前面介绍的AbstractCollection
关于addAll
的实现方式,很明显在addAll
方法中调用add
方法时被重复统计了,你不能因此说是addAll
的实现方法有问题。
也许你只要像下面这段代码一样,就能修复这个问题,但这又依赖一个事实:addAll
方法是在add
方法中实现的,这实际上并不是什么标准,你也不能保证在之后的版本中不会发生变化。
public class CountHashSet<E> extends HashSet<E> {private int addCount = 0;public CountHashSet() {}@Overridepublic boolean add(E e) {addCount++;return super.add(e);}// @Override
// public boolean addAll(Collection<? extends E> c) {
// addCount += c.size();
// return super.addAll(c);
// }public int getAddCount() {return addCount;}
}class Main {public static void main(String[] args) {CountHashSet<Integer> countHashSet = new CountHashSet<>();countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}
使用组合的方式
public class ForwardingSet<E> implements Set<E> {private final Set<E> s;public ForwardingSet(Set<E> s) {this.s = s;}@Overridepublic int size() {return s.size();}@Overridepublic boolean isEmpty() {return s.isEmpty();}@Overridepublic boolean contains(Object o) {return s.contains(o);}@Overridepublic Iterator<E> iterator() {return s.iterator();}@Overridepublic Object[] toArray() {return s.toArray();}@Overridepublic <T> T[] toArray(T[] a) {return s.toArray(a);}@Overridepublic boolean add(E e) {return s.add(e);}@Overridepublic boolean remove(Object o) {return s.remove(o);}@Overridepublic boolean containsAll(Collection<?> c) {return s.containsAll(c);}@Overridepublic boolean addAll(Collection<? extends E> c) {return s.addAll(c);}@Overridepublic boolean retainAll(Collection<?> c) {return s.retainAll(c);}@Overridepublic boolean removeAll(Collection<?> c) {return s.removeAll(c);}@Overridepublic void clear() {s.clear();}
}
class CountSet<E> extends ForwardingSet<E> {private int addCount = 0;public CountSet(Set<E> s) {super(s);}@Overridepublic boolean add(E e) {addCount++;return super.add(e);}@Overridepublic boolean addAll(Collection<? extends E> c) {addCount += c.size();return super.addAll(c);}public int getAddCount() {return addCount;}
}class Main {public static void main(String[] args) {CountSet<Integer> countHashSet = new CountSet<>(new HashSet<>());countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}
看吧,这就是使用组合的威力,组合更像是装饰者模式,他可以在不改变原有类的功能的前提下,轻松实现功能的扩展,最重要的是,他比继承要可靠的多。
关于设计模式乱用的现象
最后,再来聊聊关于设计模式乱用的问题,主要突出为以下两个阶段:
- 新手:这经常发生在刚接触设计模式不久的阶段,急于找地方使用的情况,开发人员不考虑实际的业务场景,完全是为了用设计模式而用设计模式,甚至是先想好要用什么样的设计模式,然后让业务逻辑尽量往这个模式上去套。
- 胜任者:过了新手阶段之后,此时你对设计模式也有一定使用经验了,开始意识到胡乱使用设计模式造成的问题了,懂得了理解业务场景才是关键,那还有什么问题呢?此时的阶段就好比术和道的区别,术是多变的,就像我们常说的23种设计模式一样,而道是不变的,无论哪种设计模式始终都是以几种设计原则为依据,正所谓万变不离其宗,设计模式的使用不应当局限于形式上,要能灵活变换。
- 精通者:如果跨过新手阶段的关键在于多写多练的话,那么要跨过胜任者阶段则要多思考了,得道的关键在于领悟。
相关文章:

不讲故事的设计模式-模板方法模式
文章目录 模板方法模式简介作用模板方法模式的缺点模板方法模式的应用场景业务场景开源框架中的应用 对比回调和Hook模式关于组合优先于继承 关于设计模式乱用的现象 模板方法模式 简介 模板方法模式是一种行为型设计模式,该设计模式的核心在于通过抽象出一套相对…...

基于SpringBoot的酒店客房管理系统
基于SpringBoot的酒店管理系统、酒店客房管理系统 开发语言:Java数据库:MySQL技术:SpringBoot、Vue、Mybaits Plus、ELementUI工具:IDEA/Ecilpse、Navicat、Maven 系统展示 首页 管理员界面 用户界面 代码展示 <temp…...

消息队列-RabbitMQ(二)
接上文《消息队列-RabbitMQ(一)》 1、RabbitMQ概念...

程序通过命令行获取操作系统名称+版本+CPU名称等:Part2
文章目录 (一)沿用的方法(二)问题和调整(2.1)Windows11的版本号是10.0(2.2)Golang和管道符号(Linux)(2.3)最大内存容量 vs 当前安装内…...

微软最热门的10款前端开源项目!
本文来盘点微软开源的十大前端项目,这些项目在 Github 上获得了超过 45 万 Star! Visual Studio Code Visual Studio Code 是一款由微软开发的开源的代码编辑器。它支持多种编程语言,如C、C、C#、Python、JavaScript 和 TypeScript 等&…...

C#(CSharp)入门实践项目(简易回合制游戏)
项目名称 木木夕营救公主 项目介绍 这是一个小游戏,你将扮演一个英雄(木木夕),去打败恶龙,拯救出公主,该项目采用回合制战斗模式,由于角色的血量和攻击为随机数,所以需要靠运气才…...

GEO生信数据挖掘(五)提取临床信息构建分组,分组数据可视化(绘制层次聚类图,绘制PCA图)
检索到目标数据集后,开始数据挖掘,本文以阿尔兹海默症数据集GSE1297为例 上节做了很多的基因数据清洗(离群值处理、低表达基因、归一化、log2处理)操作,本节介绍构建临床分组信息。 我们已经学习了提取表达矩阵的临床…...

golang时间问题汇总(用法常见问题:插入数据库时间自动+8)
golang时间问题汇总(用法&常见问题) 1 用法 1.1 time.Parse() func main() {timeStr : "2023-09-26 20:56:23"allDate, _ : time.Parse("2006-01-02 15:04:05", timeStr)fmt.Println("全部解析", allDate) timeStr…...

TCP网络连接中的三次握手和四次挥手
作者:逍遥Sean 简介:一个主修Java的Web网站\游戏服务器后端开发者 主页:https://blog.csdn.net/Ureliable 觉得博主文章不错的话,可以三连支持一下~ 如有需要我的支持,请私信或评论留言! TCP网络连接中的三…...

游戏服务商Latis Global参展2023 ChinaJoy B2B
第20届ChinaJoy于2023年7月在上海举行了为期四天的博览会,参展观众达到了33.8万人次。ChinaJoy是全球最具知名度与影响力的年度盛会之一,涵盖了包括游戏、动漫、互联网影视、电子竞技、潮流玩具、智能娱乐在内的多个数字娱乐领域。ChinaJoy不仅仅代表了数字娱乐领域的最新风向,…...

oracle常用sql
oracle常用sql oracle常用sql查询当前会话id(sid),会话序列号(serial#),操作系统进程id(spid)查询数据库信息查询实例信息查询字符集查看回收站情况数据库系统PSU信息数据库大小查看表空间状况常规库表空间情况查询,非CDBCBD表空间情况查询当前客户端信息资源使用情况…...

手游模拟器长时间运行后,游戏掉帧且不恢复
1)手游模拟器长时间运行后,游戏掉帧且不恢复 2)FrameBuffer Fetch无论哪种模式在确定支持的手机上显示全紫 3)协程中yield return CoFunction()和yield return StartCoroutine(CoFunction())的区别 这是第353篇UWA技术知识分享的推…...

linux下离线安装telnet
安装过程概要: (一)互联网端下载rpm包; (二)上传到服务器root目录下; (三)安装telnet服务和测试: 详细内容: (一)互联…...

Unity 发布WebGL平台,C#与JavaScript交互
发布H5平台,接入SDK,比如微信等,涉及到C#与JS的交互。 jslib(JavaScript Library)是Unity的一种机制,允许你在C#中通过JavaScript代码来执行一些操作。这是一种高级的技巧,主要用于一些特殊情况…...

利用 Forcing InnoDB Recovery 特性解决 MySQL 重启失败的问题
问题 由于异常断电或者系统异常重启时 MySQL 没有正常退出导致 MySQL 无法启动,启动时报错如下: [System] [Server] /usr/sbin/mysqld (mysqld 8.0.30) starting as process 2665 [System] [InnoDB] InnoDB initialization has started. [System] [Inn…...

windows修改键位F11变insert(改键盘映射)
这里是通过改变windows的注册表来实现的 1.按住winr打开运行,在运行中输入“regedit”,再点击“确定”按钮。如下图 2.找到注册表的目录 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout 3.在Keyboard Layout右击新建 -> 二进…...

安装gpu版本的paddle和paddleclas
安装gpu版本的paddle python -m pip install paddlepaddle-gpu2.3.2.post111 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html以上支持cuda11.1版本 其他需求可查阅文档在这里 安装paddleclas 1 在虚拟环境中安装所需的Python库: pip inst…...

61从零开始学Java之处理大数字相关的类有哪些?
作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦 千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者 前言 我们知道,在现实世界里,实际上数字是有无穷个的,就比如0和1之间&a…...

vscode 搜索界面的files to include files to exclude 是什么功能?
在VSCode(Visual Studio Code)中,搜索功能是一个强大的工具,可以帮助你在项目中快速查找特定的文本、代码或其他内容。搜索界面的 “files to include” 和 “files to exclude” 提供了一种方式来定制你的搜索范围。 files to in…...

数据计算-第15届蓝桥杯第一次STEMA测评Scratch真题精选
[导读]:超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成,后续会不定期解读蓝桥杯真题,这是Scratch蓝桥杯真题解析第154讲。 第15届蓝桥杯第1次STEMA测评已于2023年8月20日落下帷幕,编程题一共有6题,分别如下&a…...

谈谈前端和后端的选择
引言 在我的印象中,也是视线里,后端都是在一个黑屏的页面,左边一个文件类,右边在不停的写sql,一只手放在键盘上,一边写,一遍不停的关联进入,感觉很无趣,他们的分享不是什么java集成&…...

Vue3最佳实践 第六章 Pinia,Vuex与axios,VueUse 1(Pinia)
Pinia状态管理 在 Vue3 中项目中组件之间传递数据时,可以使用 Props 和 Emit,还可以使用 Provide/Inject 来代替 Props 和 Emit。Props 和 Emit 只能在具有父子关系的组件之间传递数据,所以层级越深,过程就越复杂。为了解决此类问…...

Java比较器之equals、comparable、comparator
文章目录 前言一、基本类型比较1.2.equals3.和equals的区别 二、对象的比较1.覆写基类的equals2.基于Comparable接口类的比较3.基于Comparator比较器比较4.三种方式对比 前言 在Java中,基本类型的对象可以直接比较,而自定义类型,默认是用equ…...

Virtio-user使用简介
一、简述 DPDK支持几种方式让用户空间的报文重新进入内核协议栈(这种dpdk和kernel直接通信的路径叫做exception path),例如tap/tun设备使用,kni,Virtio-user。这里主要讲Virtio-user使用,Virtio-user是virtio PMD的虚拟设备&…...

点云从入门到精通技术详解100篇-基于深度学习的三维植物点云分割网络
目录 前言 研究现状及趋势 传统的植物表型分割方法 现行的植物表型分割方法...

C语言 Cortex-A7核 SPI 实验
1 实验目的 1、数码管显示相同的值0000 1111 2222 .... 9999 2、数码管不同的值1234 2 代码 include/spi.h #ifndef __SPI_H__ #define __SPI_H__ #includ…...

Spring工具类--ReflectionUtils的使用
原文网址:Spring工具类系列--ReflectionUtils的使用_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Spring的ReflectionUtils的使用。 ReflectionUtils工具类的作用:便利地进行反射操作。 Spring还有一个工具类:ReflectUtils,它们在…...

zemax西德莫尔目镜
高性能的军用光学仪器 在两个双胶合透镜之间,增加了一块平凸透镜 半视场角增大到35度 入瞳直径4mm波长0.51、0.56、0.61半视场35焦距27.9mm 镜头参数: 成像效果:...

C++ 拷贝构造函数
介绍和示例 拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于: 通过使用另一个同类型的对象来初始化新创建的对象。 复制对象把它作为参数传递给函数。 复制对象…...

怎么使用 Flink 向 Apache Doris 表中写 Bitmap 类型的数据
Bitmap是一种经典的数据结构,用于高效地对大量的二进制数据进行压缩存储和快速查询。Doris支持bitmap数据类型,在Flink计算场景中,可以结合Flink doris Connector对bitmap数据做计算。 社区里很多小伙伴在是Doris Flink Connector的时候&…...