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

头哥的实践平台的Linux文件/目录管理
一 Linux 文件/目录管理 1.本关的编程任务是补全右侧代码片段中Begin至End中间的代码,具体要求如下: 新创建两个文件空文件file1和file2。 删除系统已存在的两个文件oldFile1和oldFile2。 #!/bin/bash#在以下部分写出完成任务的命令 #***********begi…...

软件测试基本常识
【软件测试面试突击班】如何逼自己一周刷完软件测试八股文教程,刷完面试就稳了,你也可以当高薪软件测试工程师(自动化测试) 一、测试用例的编写 1.在测试中最重要的文档,他是测试工作的核心,是一组在测试时…...

Xmake v2.8.3 发布,改进 Wasm 并支持 Xmake 源码调试
Xmake 是一个基于 Lua 的轻量级跨平台构建工具。 它非常的轻量,没有任何依赖,因为它内置了 Lua 运行时。 它使用 xmake.lua 维护项目构建,相比 makefile/CMakeLists.txt,配置语法更加简洁直观,对新手非常友好&#x…...

Serverless 数仓技术与挑战(内含 PPT 下载)
近期,Databend Labs 联合创始人张雁飞发表了题为「Serverless 数仓技术与挑战」的主题分享。以下为本次分享的精彩内容: 主题: 「Serverless 数仓技术与挑战」 演讲嘉宾: 张雁飞 嘉宾介绍: Databend Labs 联合创始人…...

九牧小牧携手国家队!一场“中国卫浴“和“中国体育”的双向奔赴
文 | 螳螂观察 作者 | 余一 1990年中国第一次举办了综合性国际体育大赛——北京亚运会,来自37个国家和地区,共计6578人的体育代表团参加了那届亚运会,一首《亚洲雄风》成为无数人记忆中的经典。 2023年杭州亚运会于近日正式拉开了帷幕&…...

crypto:Quoted-printable
题目 解压文件后可得到提示文本 好了这个没接触过,参考别的大佬wp QP为可打印字符编码,根据加密方式任何一个8位的字节值可编码为3个字符:一个等号“”后跟随两个十六进制数字(0–9或A–F)表示该字节的数值。 利用网…...

【六级】作文模板-议论文-问题解决
视频来源: https://www.bilibili.com/video/BV1vK4y1e7A6/?spm_id_from333.880.my_history.page.click&vd_sourcefb8dcae0aee3f1aab700c21099045395 1、前言 两类作文: 议论文 (how to 问题解决型) what 某种现象 漫画 &…...

leetcodetop100 (22) 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表 简单的用一个动态数组Arraylist记录,然后倒序遍历赋值给一个新的链表,这种空间复杂度是o(n),估计需要优化。 采用双指针; 我们可以申请两个指针…...

RabbitMQ配置文件_修改RabbitMQ MQTT的1883端口
Centos离线安装RabbitMQ并开启MQTT Docker安装rabbitMQ RabbitMQ集群搭建和测试总结_亲测 Docker安装RabbitMQ集群_亲测成功 rabbitmq.conf 默认没有配置文件,可以手动创建: /etc/rabbitmq/rabbitmq.conf # # RabbitMQ broker section # ## Related doc guide: https://…...

【Graph Net学习】LINE实现Graph Embedding
一、简介 LINE (Large-scale Information Network Embedding,2015) 是一种设计用于处理大规模信息网络的算法。它主要的目标是在给定的大规模信息网络中学习高质量的节点嵌入,并尽量保留网络中信息的丰富性。其具体的表现为在一个低 维空间里以向量形式表示网络中的…...

docker安装使用xdebug
docker安装使用xdebug 1、需要先安装PHP xdebug扩展 1.1 到https://pecl.php.net/package/xdebug下载tgz文件,下载当前最新稳定版本的文件。然后把这个tgz文件放到php/extensions目录下,记得install.sh中要替换解压的文件名: installExtensio…...

(1) ESP32获取图像,并通过电脑端服务器显示图像
目录 一、所需器件工具 二、客户端与服务器进行UDP通信 1、客户端代码 2、服务器端代码 3、效果展示 三、客户端拍照,通过UDP传输到服务器进行显示 1、客户端获取图像并UDP传输 2、电脑端服务器显示图像 3、效果展示 四、代码链接 一、所需器件工具 1.ESP3…...

乐鑫科技全球首批支持蓝牙 Mesh Protocol 1.1 协议
乐鑫科技 (688018.SH) 非常高兴地宣布,其自研的蓝牙 Mesh 协议栈 ESP-BLE-MESH 现已支持最新蓝牙 Mesh Protocol 1.1 协议的全部功能,成为全球首批在蓝牙技术联盟 (Bluetooth SIG) 正式发布该协议之前支持该更新的公司之一。这意味着乐鑫在低功耗蓝牙无线…...

1.算法——数据结构学习
算法是解决特定问题求解步骤的描述。 从1加到100的结果 # include <stdio.h> int main(){ int i, sum 0, n 100; // 执行1次for(i 1; i < n; i){ // 执行n 1次sum sum i; // 执行n次} printf("%d", sum); // 执行1次return 0; }高斯求和…...

信息论基础第二章阅读笔记
信息很难用一个简单的定义准确把握。 对于任何一个概率分布,可以定义一个熵(entropy)的量,它具有许多特性符合度量信息的直观要求。这个概念可以推广到互信息(mutual information),互信息是一种…...

Content-Type的取值
接口发送参数、接收响应数据,都需要双方约定好使用什么格式的数据,例如 json、xml。只有双方按照约定好的格式去解析数据才能正确的收发数据。而 Content-Type 就是用来告诉你数据的格式,这样我们才能知道怎么解析参数。 常见的 Content-Typ…...

【趣味JavaScript】5年前端开发都没有搞懂toString和valueOf这两个方法!
🚀 个人主页 极客小俊 ✍🏻 作者简介:web开发者、设计师、技术分享博主 🐋 希望大家多多支持一下, 我们一起进步!😄 🏅 如果文章对你有帮助的话,欢迎评论 💬点赞…...

Python中的接口是什么?
在Python中,接口是一种约定或协议,用于定义类应该实现哪些方法或属性。接口并不会提供实际的实现,而是只定义了类应该具有哪些方法和属性的签名。 Python中的接口通常通过抽象基类(Abstract Base Class,简称ABC&#…...

自学WEB后端01-安装Express+Node.js框架完成Hello World!
一、前言,网站开发扫盲知识 1.网站搭建开发包括什么? 前端 前端开发主要涉及用户界面(UI)和用户体验(UX),负责实现网站的外观和交互逻辑。前端开发使用HTML、CSS和JavaScript等技术来构建网页…...

从C语言到C++:C++入门知识(1)
朋友们、伙计们,我们又见面了,本期来给大家解读一下有关C语言的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成! C 语 言 专 栏:C语言:从入门到精通 数…...

服务器(Windows系统)自建filebrowser网盘服务器超详细教程
需要依赖(工具) 轻量服务器(云服务器)一台 —— 环境Windows Server 2019filebrowser安装包(https://github.com/filebrowser/filebrowser/releases) 下载安装filebrowser 进入链接下载:https:/…...

扩展欧几里得
扩展欧几里得算法 求 a x b y d axbyd axbyd 的一组解, d gcd ( a , b ) d \gcd(a,b) dgcd(a,b)。 辗转相除递归求解。 假设已经求出 b x ( b m o d a ) y d bx (b \bmod a)y d bx(bmoda)yd 的一组解。 a x b y b x ′ ( b m o d a ) y ′ b x …...

MySQL 事务介绍 (事务篇 一)
什么是事务? 事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。 注意点:默认MySQL的事务是自动提交…...

nvm nodejs的版本管理工具
nvm 全英文名叫 node.js version management,是一个 nodejs 的版本管理工具,为了解决 nodejs 各种版本存在不兼容现象可以通过他安装和切换不同版本的 nodejs。 一、完全删除之前的 node 和 npm 1. 打开 cmd 命令窗口,输入 npm cache clean…...

terraform简单的开始-vpc cvm创建
从网络开始 从创建VPC开始 复用前面的main.tf的代码: terraform {required_providers {tencentcloud {source "tencentcloudstack/tencentcloud"version "1.81.25"}} } variable "region" {description "腾讯云地域"…...

【MySQL】开启 canal同步MySQL增量数据到ES
开启 canal同步MySQL增量数据到ES canal 是阿里知名的开源项目,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。示使用 canal 将 MySQL 增量数据同步到ES。 一、集群模式 图中 server 对应一个 canal 运行实例 ,对应一…...

密码学概论
1.密码学的三大历史阶段: 第一阶段 古典密码学 依赖设备,主要特点 数据安全基于算法的保密,算法不公开,只要破译算法 密文就会被破解, 在1883年第一次提出 加密算法应该基于算法公开 不影响密文和秘钥的安全ÿ…...

渗透测试中的前端调试(一)
前言 前端调试是安全测试的重要组成部分。它能够帮助我们掌握网页的运行原理,包括js脚本的逻辑、加解密的方法、网络请求的参数等。利用这些信息,我们就可以更准确地发现网站的漏洞,制定出有效的攻击策略。前端知识对于安全来说,…...

SPA项目之登录注册--请求问题(POSTGET)以及跨域问题
🥳🥳Welcome Huihuis Code World ! !🥳🥳 接下来看看由辉辉所写的关于VueElementUI的相关操作吧 目录 🥳🥳Welcome Huihuis Code World ! !🥳🥳 一.ElementUI是什么 💡…...