设计模式之道-模板方法模式
文章目录
- 模板方法模式
- 简介
- 作用
- 模板方法模式的缺点
- 模板方法模式的应用场景
- 业务场景
- 开源框架中的应用
- 对比回调和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等技术来构建网页…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
在rocky linux 9.5上在线安装 docker
前面是指南,后面是日志 sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io -y docker version sudo systemctl start docker sudo systemctl status docker …...
centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...
uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
客户案例 | 短视频点播企业海外视频加速与成本优化:MediaPackage+Cloudfront 技术重构实践
01技术背景与业务挑战 某短视频点播企业深耕国内用户市场,但其后台应用系统部署于东南亚印尼 IDC 机房。 随着业务规模扩大,传统架构已较难满足当前企业发展的需求,企业面临着三重挑战: ① 业务:国内用户访问海外服…...
