设计模式学习笔记 - 设计模式与范式 -行为型:3.模板模式(下):模板模式与Callback回调函数的区别和联系

news/2024/5/12 0:59:12/文章来源:https://blog.csdn.net/chenjian723122704/article/details/137249334

上篇文章 学习了模板模式的原理、实现和应用。它常用在框架开发中,通过提供功能扩展点,让框架用户在不修改框架源码的情况下,基于扩展点定制化框架的功能。此外,模板模式还可以起到代码复用的作用。

复用和扩展是模板模式的两大作用,实际上,还有另一个技术概念,也能起到和模板模式相同的作用,那就是回调(Callback)。本章来看一下,回调的原理、实现和应用,以及它和模版模式的区别和联系。


回调的原理解析

相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类调用 B 类的 P 函数时,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是 “回调函数”。 A 调用 B,B 反过来又调用 A,这种调用机制就是回调。

A 类如何将回调函数传递给 B 类呢?不同的编程语言有不同的实现方法。C 语言可以使用函数指针,Java 则需要使用包裹了回调函数的类对象。这里使用 Java 语言举例说明,代码如下所示:

public interface ICallback {void methodCallback();
}public class BClass {public void process(ICallback callback) {// ...callback.methodCallback();// ...}
}public class AClass {public static void main(String[] args) {BClass b = new BClass();b.process(new ICallback() { // 回调对象@Overridepublic void methodCallback() {System.out.println("Call back me.");}});}
}

上面就是 Java 语言中回调的典型代码。从代码实现中,可以看出,回调和模板模式一样,也具有复用和扩展功能。除了回调函数之外,BClassprocess() 函数中的逻辑都是可以复用的。如果 ICallbackBClass 是框架代码,AClass 是客户端代码,我们可以通过 ICallback 定制 process() 函数,也就是说框架具有了扩展的能力。

实际上,回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求后,一般不会直接阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。

回调可以分为同步回调和异步回调。

  • 同步回调指在函数返回之前执行回调函数。
  • 异步回调指的是在函数返回之后执行回调函数。

上面的代码实际上是同步回调的实现方式,在 process()函数返回之前,执行完回调函数 methodToCallback()。而上面的支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板方法模式,异步回调看起来更像是观察者模式。

应用举例一:JdbcTemplate

Spring 提供了很多 Template 类,比如 JdbcTemplate、RedisTemplate、RestTemplate。尽管名字都叫 xxxTemplate,但它们并非基于模板模式来实现,而是基于回调来实现的,确切的说应该是同步回调。而同步回调从应用场景上看很像模板模式,所以,在命名上,这些类使用 Template 这个单词作为后缀。

这些 Template 类的设计思路都很相近,所以,我们只拿其中的 JdbcTemplate 来举例分析下。对于其他的 Template,你可以自行阅读源码。

在前面的章节中,我们也多次提到,Java 提供了 JDBC 类库来封装不同类型的数据库操作。不过,直接使用 JDBC 来编写操作数据库的代码,还是有点复杂。比如,下面这段代码使用 JDBC 来查询用户信息的代码。

public class JdbcDemo {public User queryUser(long id) {Connection conn = null;Statement stmt = null;try {// 1.加载驱动Class.forName("com.mysql.jdbc.Driver");conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "root", "root");// 2.创建类对象,用来执行SQLstmt = conn.createStatement();// 3.ResultSet类,用来存放获取的结果集String sql = "select * from user where id=" + id;ResultSet resultSet = stmt.executeQuery(sql);while (resultSet.next()) {User user = new User();user.setId(resultSet.getLong("id"));user.setName(resultSet.getLong("name"));user.setTelephone(resultSet.getLong("telephone"));return user;}} catch (ClassNotFoundException e) {// log...} catch (SQLException e) {// log...} finally {if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}if (stmt != null) {try {stmt.close();} catch (SQLException e) {e.printStackTrace();}}}return null;}
}

queryUser() 函数包含很多流程性质的代码,跟业务无关,比如,加载驱动、创建数据库连接、创建statement、关闭连接、关闭 statement、处理异常。针对你不同的 SQL 执行请求,这些流程性质的代码是相同的、可以复用,我们不需要每次都重新敲一遍。

针对这个问题,Spring 提供了 JdbcTemplate,对 JDBC 进一步封装,来简化数据库编程。使用 JdbcTemplate 查询用户信息,我们只需要编写跟这个业务有关的代码,其中包括,查询用户的 SQL 语句、查询结果与 User 对象之间的映射关系。其他流程性质的代码都封装在了 JdbcTemplate 类中,不需要我们每次都重新编写。下面是使用 JdbcTemplate 重写了上面的例子,代码简单的了很多。

public class JdbcDemo {private JdbcTemplate jdbcTemplate;public User queryUser(long id) {String sql = "select * from user where id=" + id;return jdbcTemplate.query(sql, new UserRowMapper()).get(0);}class UserRowMapper implements RowMapper<User> {@Overridepublic User mapRow(ResultSet rs, int rowNum) throws SQLException {User user = new User();user.setId(rs.getLong("id"));user.setName(rs.getLong("name"));user.setTelephone(rs.getLong("telephone"));return user;}}
}

JdbcTemplate 底层具体是如何实现的呢?我们来看一下源码。因为 JdbcTemplate 代码比较多,我只摘抄了少部分相关源码,贴在了下面。其中, JdbcTemplate 通过回调的机制,将不变的执行流程抽离出来,放到模版方法 execute() 中,将可变的部分设计成回调 StatementCallback,由用户来定制。query() 函数是对 execute() 函数的二次封装,让接口调用起来更加方便。

public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {// ...public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));}// ...public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {Assert.notNull(sql, "SQL must not be null");Assert.notNull(rse, "ResultSetExtractor must not be null");if (logger.isDebugEnabled()) {logger.debug("Executing SQL query [" + sql + "]");}/*** Callback to execute the query.*/class QueryStatementCallback implements StatementCallback<T>, SqlProvider {@Override@Nullablepublic T doInStatement(Statement stmt) throws SQLException {ResultSet rs = null;try {rs = stmt.executeQuery(sql);return rse.extractData(rs);}finally {JdbcUtils.closeResultSet(rs);}}@Overridepublic String getSql() {return sql;}}return execute(new QueryStatementCallback());}// ...public <T> T execute(StatementCallback<T> action) throws DataAccessException {Assert.notNull(action, "Callback object must not be null");Connection con = DataSourceUtils.getConnection(obtainDataSource());Statement stmt = null;try {stmt = con.createStatement();applyStatementSettings(stmt);T result = action.doInStatement(stmt);handleWarnings(stmt);return result;}catch (SQLException ex) {// Release Connection early, to avoid potential connection pool deadlock// in the case when the exception translator hasn't been initialized yet.String sql = getSql(action);JdbcUtils.closeStatement(stmt);stmt = null;DataSourceUtils.releaseConnection(con, getDataSource());con = null;throw translateException("StatementCallback", sql, ex);}finally {JdbcUtils.closeStatement(stmt);DataSourceUtils.releaseConnection(con, getDataSource());}}
}

应用举例二:setClickListener()

在客户端开发中,经常会给控件注册事件监听,比如下面这段代码,就是在 Android 应用开发中,给 Button 控件注册点击事件注册监听器。

Button button = (Button)findByView(R.id.button);
button.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {System.out.println("I am clicked");}
})

从代码结构上看,事件监听器很像回调,即传递一个包含回调函数(onClick())的对象给另一个函数。从应用场景上来看,它又像观察者模式,即事先注册观察者 (OnClickListener),当用户点击按钮的时候,发送点击事件给观察者,并且执行相应的 onClick() 函数。

我们前面降到,回调分为同步回调和异步回调。这里的回调算式异步回调,我们往 setOnClickListener() 函数中注册号回调函数之后,并不需要等待回调函数的执行。这也印证了我们前面讲的,异步回调比较像观察者模式。

应用举例三:addShutdownHook()

Hook 可以翻译为 “钩子”,它和 Callback 有什么区别呢?

有人认为 Hook 就是 Callback,两种说的是一回事。而有人觉得 Hook 是 Callback 的一种应用。Callback 更侧重语法机制的描述,Hook 更加侧重应用场景的描述。个人也认可后一种说法。不过,这个不重要,只要是看到了代码能认识,遇到场景会用就行了。

Hook 比较经典的应用场景是 Tomcat 和 JVM 中的 shutdown hook。接下来,通过 JVM 来举例说明下。JVM 提供了 Runtime.addShutdownHook(Thread hook) 方法,可以注册一个 JVM 关闭的 Hook。当应用程序关闭的时候,JVM 会自动调用 Hook 代码。如下所示:

public class ShutdownDemo {private static class ShutdownHook extends Thread {@Overridepublic void run() {System.out.println("I am called during shutting down.");}}public static void main(String[] args) {Runtime.getRuntime().addShutdownHook(new ShutdownHook());}
}

再来看下 addShutdownHook() 的代码实现。这里只给出了部分代码

public class Runtime {// ...public void addShutdownHook(Thread hook) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("shutdownHooks"));}ApplicationShutdownHooks.add(hook);}// ...
}class ApplicationShutdownHooks {/* The set of registered hooks */private static IdentityHashMap<Thread, Thread> hooks;static {try {Shutdown.add(1 /* shutdown hook invocation order */,false /* not registered if shutdown in progress */,new Runnable() {public void run() {runHooks();}});hooks = new IdentityHashMap<>();} catch (IllegalStateException e) {// application shutdown hooks cannot be added if// shutdown is in progress.hooks = null;}}// ...static void runHooks() {Collection<Thread> threads;synchronized(ApplicationShutdownHooks.class) {threads = hooks.keySet();hooks = null;}for (Thread hook : threads) {hook.start();}for (Thread hook : threads) {while (true) {try {hook.join();break;} catch (InterruptedException ignored) {}}}}
}

从代码中可以发现,有关 Hook 的逻辑都被封装到 ApplicationShutdownHooks 类中了。当应用程序关闭时,JVM 会调用这个类的 runHooks() 方法,创建多个线程,并发地执行多个 Hook。我们在注册完 Hook 之后,并不需要等待 Hook 执行完成,所以这也算是一种异步回调。

模板模式 VS 回调

回调的原理、实现和应用到此就讲完了。接下来,我们从应用场景和代码实现两个角度,来对比下模板模式和回调。

  • 从应用场景上来看,同步回调跟模板代码几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。
  • 从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

前面也讲过,组合优于继承。实际上,这里也不例外。在代码实现上,回调相对于模板模式会更加灵活,主要体现在以下几点:

  1. 像 Java 这种单继承的语音,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
  2. 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的视线都要定义不同的子类。
  3. 如果某个类中定义了多个模板方法,每个方法都有对象的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。

总结

本章,重点介绍了回调。它跟模板模式具有相同的作用:代码复用和扩展。在一些框架、类库、组件等设计中经常会用到。

相对于普通的函数调用,回调是一种双向调用关系。A 类实现注册某个函数 F 到 B 类。A 类在调用 B 类时,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是回调函数。A 调用 B,B 反过来又调用 A,这种调用机制叫回调。

回调可分为同步回调和异步回调。从应用场景上来看,同步回调看起来更像模板模式,异步回调更像是观察者模式。回调跟模板模式的区别,更多的是在代码实现上,而非应用场景上。回调基于组合关系来实现,而模板模式基于继承关系来实现,回调比模板更加灵活。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_1045592.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python画图Matplotlib和Seaborn

python画图Matplotlib和Season 一、Matplotlib1、介绍2、安装3、内容二、Seaborn1、介绍2、安装3、内容一、Matplotlib Matplotlib官网 1、介绍 Matplotlib 是一个 Python 的绘图库,用于创建高质量的二维图表和一些基本的三维图表。它广泛应用于科学计算、数据分析、工程学和…

泛微OA 自定义多选浏览框

1、建模引擎-》应用建模-》表单 2、建模引擎-》应用建模-》模块 3、建模引擎-》应用建模-》查询 4、把查询页面挂到前端页面。 效果展示&#xff1a; 5、建模引擎-》应用建模-》浏览框 6、流程表单中字段应用

IP-GUARD内置用户系统同步飞书组织架构使用说明

一、功能简介 实现将飞书的通讯录组织架构同步到内置用户系统。 二、功能配置 2.1 飞书创建自建应用 在浏览器上打开飞书开放平台 https://open.feishu.cn ,登录管理员账号后点击开发 者后台 在开发者后台点击创建企业自建应用,填写自建应用程序名称以及描述,设置图标,点…

SSRF靶场

SSRF概述 ​ 强制服务器发送一个攻击者的请求 ​ 互联网上的很多web应用提供了从其他服务器&#xff08;也可以是本地)获取数据的功能。使用用户指定的URL&#xff0c;web应用可以获取图片&#xff08;载入图片&#xff09;、文件资源&#xff08;下载或读取)。如下图所示&…

利用Leaflet + React:构建WEBGIS

React是 Facebook 开发的一个开源库&#xff0c;用于构建用户界面。就其本身而言&#xff0c;Leaflet是一个用于将地图发布到网络的JavaScript 库。这两个工具的组合很简单&#xff0c;允许您创建动态网络地图。在本文中&#xff0c;我们将看到这种组合的一些特征以及一些简单的…

【MySQL数据库 | 第二十五篇】深入探讨MVCC底层原理

前言&#xff1a; 在当今互联网时代&#xff0c;数据库扮演着数据存储和管理的关键角色。对于大型Web应用程序和企业级系统而言&#xff0c;高效地处理并发访问和事务管理是至关重要的。多版本并发控制&#xff08;MVCC&#xff09;是一种数据库事务处理的技术&#xff0c;旨…

三种常见webshell工具的流量特征分析

又来跟师傅们分享小技巧了&#xff0c;这次简单介绍一下三种常见的webshell流量分析&#xff0c;希望能对参加HW蓝队的师傅们有所帮助。 什么是webshell webshell就是以asp、php、jsp或者cgi等网页文件形式存在的一种代码执行环境&#xff0c;主要用于网站管理、服务器管理、…

机器学习中的激活函数

激活函数存在的意义&#xff1a; 激活函数决定了某个神经元是否被激活&#xff0c;当这个神经元接收到的信息是有用或无用的时候&#xff0c;激活函数决定了对这个神经元接收到的信息是留下还是抛弃。如果不加激活函数&#xff0c;神经元仅仅做线性变换&#xff0c;那么该神经网…

蓝桥杯——17

学习视频&#xff1a;18-深搜的剪枝策略练习_哔哩哔哩_bilibili Q&#xff1a;找数字 #include<iostream> #include<cstring> using namespace std; int n; bool ok; void dfs(int num, int cnt) {if (cnt > 19) {return;}if (ok) {return;}if (num % n 0) {…

MySQL-基本SQL语句编写:运算符练习

运算符练习 1.选择工资不在5000到12000的员工的姓名和工资 SELECT last_name,salary FROM employees #where salary not between 5000 and 12000; WHERE salary < 5000 OR salary > 12000;2.选择在20或50号部门工作的员工姓名和部门号 SELECT last_name,department_id…

基于springboot+vue+Mysql的职称评审管理系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

4月6号排序算法(2)

堆排序 讲堆排序之前我们需要了解几个定义 什么叫做最大堆&#xff0c;父亲节点&#xff0c;以及孩子节点 将根节点最大的堆叫做最大堆或大根堆&#xff0c;根节点最小的堆叫做最小堆或小根堆。 每个节点都是它的子树的根节点的父亲 。 反过来每个节点都是它父亲的孩子 。 …

Java从坚持到精通-SpringSecurity

1.安全框架是什么 安全框架的本质就是一堆过滤器的组成&#xff0c;目的在于保护系统资源&#xff0c;所以在到达资源之前会做一系列的验证工作&#xff0c;这些验证工作通过一系列的过滤器完成。安全框架通常的功能有认证、授权、防止常见的网络攻击&#xff0c;以此为核心拓…

IP地址中网络号的查看方法

IP地址是互联网中设备的标识&#xff0c;它由网络号和主机号两部分组成。网络号用于标识设备所连接的网络&#xff0c;而主机号则用于标识该网络中的具体设备。了解如何查看IP地址中的网络号对于网络管理员和需要进行网络配置的用户来说至关重要。虎观代理将介绍几种常见的查看…

linux之文件系统、inode和动静态库制作和发布

一、背景 1.没有被打开的文件都在磁盘上 --- 磁盘级文件 2.对磁盘级别的文件&#xff0c;我们的侧重点 单个文件角度 -- 这个文件在哪里&#xff0c;有多大&#xff0c;其他属性是什么&#xff1f; 站在系统角度 -- 一共有多少文件&#xff1f;各自属性在哪里&#xff1f…

Vue3---基础2(component)

主要讲解 component 的创建 以及vue插件的安装 Vue.js Devtools 为谷歌浏览器的Vue插件&#xff0c;可以在调试工具内查看组件的数据等 下载 有两种下载方式 1. 谷歌应用商店 打开Chrome应用商店去下载&#xff0c;这个方法需要魔法 2. 极简插件 极简插件官网_Chrome插件下载_…

react渲染列表信息(简单易学)

1.新建个文件夹&#xff0c;启动终端&#xff0c;使用create-react-app my-react命令创建项目&#xff0c;其中my-react是自定义项目名称。 2.删除根目录src文件夹下多余文件&#xff0c;保留index.js和index.css文件 3.安装scss需要的依赖&#xff0c;使用npm install --sav…

常见性能测试工具对比

在性能测试工作中&#xff0c;我们常常会遇到好几个工具&#xff0c;但是每一个工具都有自己的优势&#xff0c;一时间不知道怎么选择。 今天我们就将性能测试常用的工具进行对比&#xff0c;这样大家在选择工具的时候心里就有底啦&#xff01; 阿里云PTS 性能测试PTS&#xff…

【Linux】shell 脚本基础使用

在终端中输入命令可以完成一些常用的操作&#xff0c;但是我们都是一条一条输入命令&#xff0c;比较麻烦&#xff0c;为了解决这个问题&#xff0c;就会涉及到 shell 脚本&#xff0c;它可以将很多条命令放到一个文件里面&#xff0c;然后直接运行这个文件即可。 shell 脚本类…

【UnityRPG游戏制作】Unity_RPG项目之界面面板分离和搭建

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…