CategoryResourceRepost/极客时间专栏/设计模式之美/设计模式与范式:行为型/59 | 模板模式(下):模板模式与Callback回调函数有何区别和联系?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

350 lines
17 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="59 | 模板模式模板模式与Callback回调函数有何区别和联系" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2c/55/2c92ee0b3425f37b484469c338f1ae55.mp3"></audio>
上一节课中,我们学习了模板模式的原理、实现和应用。它常用在框架开发中,通过提供功能扩展点,让框架用户在不修改框架源码的情况下,基于扩展点定制化框架的功能。除此之外,模板模式还可以起到代码复用的作用。
复用和扩展是模板模式的两大作用,实际上,还有另外一个技术概念,也能起到跟模板模式相同的作用,那就是**回调**Callback。今天我们今天就来看一下回调的原理、实现和应用以及它跟模板模式的区别和联系。
话不多说,让我们正式开始今天的学习吧!
## 回调的原理解析
相对于普通的函数调用来说回调是一种双向调用关系。A类事先注册某个函数F到B类A类在调用B类的P函数的时候B类反过来调用A类注册给它的F函数。这里的F函数就是“回调函数”。A调用BB反过来又调用A这种调用机制就叫作“回调”。
A类如何将回调函数传递给B类呢不同的编程语言有不同的实现方法。C语言可以使用函数指针Java则需要使用包裹了回调函数的类对象我们简称为回调对象。这里我用Java语言举例说明一下。代码如下所示
```
public interface ICallback {
void methodToCallback();
}
public class BClass {
public void process(ICallback callback) {
//...
callback.methodToCallback();
//...
}
}
public class AClass {
public static void main(String[] args) {
BClass b = new BClass();
b.process(new ICallback() { //回调对象
@Override
public void methodToCallback() {
System.out.println(&quot;Call back me.&quot;);
}
});
}
}
```
上面就是Java语言中回调的典型代码实现。从代码实现中我们可以看出回调跟模板模式一样也具有复用和扩展的功能。除了回调函数之外BClass类的process()函数中的逻辑都可以复用。如果ICallback、BClass类是框架代码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(&quot;com.mysql.jdbc.Driver&quot;);
conn = DriverManager.getConnection(&quot;jdbc:mysql://localhost:3306/demo&quot;, &quot;xzg&quot;, &quot;xzg&quot;);
//2.创建statement类对象用来执行SQL语句
stmt = conn.createStatement();
//3.ResultSet类用来存放获取的结果集
String sql = &quot;select * from user where id=&quot; + id;
ResultSet resultSet = stmt.executeQuery(sql);
String eid = null, ename = null, price = null;
while (resultSet.next()) {
User user = new User();
user.setId(resultSet.getLong(&quot;id&quot;));
user.setName(resultSet.getString(&quot;name&quot;));
user.setTelephone(resultSet.getString(&quot;telephone&quot;));
return user;
}
} catch (ClassNotFoundException e) {
// TODO: log...
} catch (SQLException e) {
// TODO: log...
} finally {
if (conn != null)
try {
conn.close();
} catch (SQLException e) {
// TODO: log...
}
if (stmt != null)
try {
stmt.close();
} catch (SQLException e) {
// TODO: log...
}
}
return null;
}
}
```
queryUser()函数包含很多流程性质的代码跟业务无关比如加载驱动、创建数据库连接、创建statement、关闭连接、关闭statement、处理异常。针对不同的SQL执行请求这些流程性质的代码是相同的、可以复用的我们不需要每次都重新敲一遍。
针对这个问题Spring提供了JdbcTemplate对JDBC进一步封装来简化数据库编程。使用JdbcTemplate查询用户信息我们只需要编写跟这个业务有关的代码其中包括查询用户的SQL语句、查询结果与User对象之间的映射关系。其他流程性质的代码都封装在了JdbcTemplate类中不需要我们每次都重新编写。我用JdbcTemplate重写了上面的例子代码简单了很多如下所示
```
public class JdbcTemplateDemo {
private JdbcTemplate jdbcTemplate;
public User queryUser(long id) {
String sql = &quot;select * from user where id=&quot;+id;
return jdbcTemplate.query(sql, new UserRowMapper()).get(0);
}
class UserRowMapper implements RowMapper&lt;User&gt; {
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong(&quot;id&quot;));
user.setName(rs.getString(&quot;name&quot;));
user.setTelephone(rs.getString(&quot;telephone&quot;));
return user;
}
}
}
```
那JdbcTemplate底层具体是如何实现的呢我们来看一下它的源码。因为JdbcTemplate代码比较多我只摘抄了部分相关代码贴到了下面。其中JdbcTemplate通过回调的机制将不变的执行流程抽离出来放到模板方法execute()中将可变的部分设计成回调StatementCallback由用户来定制。query()函数是对execute()函数的二次封装,让接口用起来更加方便。
```
@Override
public &lt;T&gt; List&lt;T&gt; query(String sql, RowMapper&lt;T&gt; rowMapper) throws DataAccessException {
return query(sql, new RowMapperResultSetExtractor&lt;T&gt;(rowMapper));
}
@Override
public &lt;T&gt; T query(final String sql, final ResultSetExtractor&lt;T&gt; rse) throws DataAccessException {
Assert.notNull(sql, &quot;SQL must not be null&quot;);
Assert.notNull(rse, &quot;ResultSetExtractor must not be null&quot;);
if (logger.isDebugEnabled()) {
logger.debug(&quot;Executing SQL query [&quot; + sql + &quot;]&quot;);
}
class QueryStatementCallback implements StatementCallback&lt;T&gt;, SqlProvider {
@Override
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
rs = stmt.executeQuery(sql);
ResultSet rsToUse = rs;
if (nativeJdbcExtractor != null) {
rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
}
return rse.extractData(rsToUse);
}
finally {
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback());
}
@Override
public &lt;T&gt; T execute(StatementCallback&lt;T&gt; action) throws DataAccessException {
Assert.notNull(action, &quot;Callback object must not be null&quot;);
Connection con = DataSourceUtils.getConnection(getDataSource());
Statement stmt = null;
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null &amp;&amp;
this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
stmt = conToUse.createStatement();
applyStatementSettings(stmt);
Statement stmtToUse = stmt;
if (this.nativeJdbcExtractor != null) {
stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
}
T result = action.doInStatement(stmtToUse);
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.
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate(&quot;StatementCallback&quot;, getSql(action), ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
```
## 应用举例二setClickListener(
在客户端开发中我们经常给控件注册事件监听器比如下面这段代码就是在Android应用开发中给Button控件的点击事件注册监听器。
```
Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println(&quot;I am clicked.&quot;);
}
});
```
从代码结构上来看事件监听器很像回调即传递一个包含回调函数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 ShutdownHookDemo {
private static class ShutdownHook extends Thread {
public void run() {
System.out.println(&quot;I am called during shutting down.&quot;);
}
}
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(&quot;shutdownHooks&quot;));
}
ApplicationShutdownHooks.add(hook);
}
}
class ApplicationShutdownHooks {
/* The set of registered hooks */
private static IdentityHashMap&lt;Thread, Thread&gt; hooks;
static {
hooks = new IdentityHashMap&lt;&gt;();
} catch (IllegalStateException e) {
hooks = null;
}
}
static synchronized void add(Thread hook) {
if(hooks == null)
throw new IllegalStateException(&quot;Shutdown in progress&quot;);
if (hook.isAlive())
throw new IllegalArgumentException(&quot;Hook already running&quot;);
if (hooks.containsKey(hook))
throw new IllegalArgumentException(&quot;Hook previously registered&quot;);
hooks.put(hook, hook);
}
static void runHooks() {
Collection&lt;Thread&gt; 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 回调
回调的原理、实现和应用到此就都讲完了。接下来,我们从应用场景和代码实现两个角度,来对比一下模板模式和回调。
从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。
从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
前面我们也讲到,组合优于继承。实际上,这里也不例外。在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点。
- 像Java这种只支持单继承的语言基于模板模式编写的子类已经继承了一个父类不再具有继承的能力。
- 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。
还记得上一节课的课堂讨论题目吗?看到这里,相信你应该有了答案了吧?
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
今天,我们重点介绍了回调。它跟模板模式具有相同的作用:代码复用和扩展。在一些框架、类库、组件等的设计中经常会用到。
相对于普通的函数调用回调是一种双向调用关系。A类事先注册某个函数F到B类A类在调用B类的P函数的时候B类反过来调用A类注册给它的F函数。这里的F函数就是“回调函数”。A调用BB反过来又调用A这种调用机制就叫作“回调”。
回调可以细分为同步回调和异步回调。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。回调跟模板模式的区别,更多的是在代码实现上,而非应用场景上。回调基于组合关系来实现,模板模式基于继承关系来实现,回调比模板模式更加灵活。
## 课堂讨论
对于Callback和Hook的区别你有什么不同的理解吗在你熟悉的编程语言中有没有提供相应的语法概念是叫Callback还是Hook呢
欢迎留言和我分享你的想法。如果有收获,欢迎你把这篇文章分享给你的朋友。