复用和扩展是模板模式的两大作用,实际上,还有另外一个技术也能起到跟模板模式相同的作用,那就是回调(Callback)。
    由于模板方法模式存在子类影响父类以及继承泛滥的缺点,为了解决这两个问题,利用回调函数代替子类继承是一个很好的解决方案。其类图如下:
    image.png
    此时,Template类仍然只是提供了一个框架,其基本功能和AbstractClass类似,不同之处在于,Template不是抽象类,而是一个具体类(一般声明为final类),其代码如下:

    1. public final class Template {
    2. private void baseOperation() {
    3. }
    4. public void templateMethod(Callback callback) {
    5. baseOperation();
    6. callback.customOperation();
    7. }
    8. }

    Callback及其子类代码如下:

    1. public interface Callback {
    2. void customOperation();
    3. }
    4. public SubCallback implements Callback {
    5. @Override
    6. public void customOperation() {
    7. //do custom things
    8. }
    9. }

    客户端类变化也很小,其代码如下:

    1. //客户端代码
    2. public Client {
    3. public static void main(String[] args) {
    4. Template template = new Template();
    5. applyTemplate(template)
    6. }
    7. public static void applyTemplate(Template template) {
    8. Callback callback = new SubCallback();
    9. template.templateMethod(callback);
    10. }
    11. }

    这里结合回调函数以后,Template是一个稳定的final类,无法被继承,不存在子类行为影响父类结果的问题,而Callback是一个接口,为了继承而继承的问题消失了。

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

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

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

    以JdbcTemplate来举例,再看JdbcTemplate类之前,先了解下直接使用JDBC来编写操作数据库究竟有多复杂:

    1. public class JdbcDemo {
    2. public User queryUser(long id) {
    3. Connection conn = null;
    4. Statement stmt = null;
    5. try {
    6. //1.加载驱动
    7. Class.forName("com.mysql.jdbc.Driver");
    8. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "xzg", "xzg");
    9. //2.创建statement类对象,用来执行SQL语句
    10. stmt = conn.createStatement();
    11. //3.ResultSet类,用来存放获取的结果集
    12. String sql = "select * from user where id=" + id;
    13. ResultSet resultSet = stmt.executeQuery(sql);
    14. String eid = null, ename = null, price = null;
    15. while (resultSet.next()) {
    16. User user = new User();
    17. user.setId(resultSet.getLong("id"));
    18. user.setName(resultSet.getString("name"));
    19. user.setTelephone(resultSet.getString("telephone"));
    20. return user;
    21. }
    22. } catch (ClassNotFoundException e) {
    23. // TODO: log...
    24. } catch (SQLException e) {
    25. // TODO: log...
    26. } finally {
    27. if (conn != null)
    28. try {
    29. conn.close();
    30. } catch (SQLException e) {
    31. // TODO: log...
    32. }
    33. if (stmt != null)
    34. try {
    35. stmt.close();
    36. } catch (SQLException e) {
    37. // TODO: log...
    38. }
    39. }
    40. return null;
    41. }
    42. }

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

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

    1. public class JdbcTemplateDemo {
    2. private JdbcTemplate jdbcTemplate;
    3. public User queryUser(long id) {
    4. String sql = "select * from user where id="+id;
    5. return jdbcTemplate.query(sql, new UserRowMapper()).get(0);
    6. }
    7. class UserRowMapper implements RowMapper<User> {
    8. public User mapRow(ResultSet rs, int rowNum) throws SQLException {
    9. User user = new User();
    10. user.setId(rs.getLong("id"));
    11. user.setName(rs.getString("name"));
    12. user.setTelephone(rs.getString("telephone"));
    13. return user;
    14. }
    15. }
    16. }

    JdbcTemplate代码很多,这里摘抄部分相关代码如下,其中JdbcTemplate通过回调机制,将不变的执行流程抽离出来,放到模板方法execute()中,将可变的部分设计成回调StatementCallback,由用户定制。query()函数是对execute()函数的二次封装,让接口用起来更加方便。

    1. public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
    2. public JdbcTemplate() {
    3. }
    4. public JdbcTemplate(DataSource dataSource) {
    5. setDataSource(dataSource);
    6. afterPropertiesSet();
    7. }
    8. public JdbcTemplate(DataSource dataSource, boolean lazyInit) {
    9. setDataSource(dataSource);
    10. setLazyInit(lazyInit);
    11. afterPropertiesSet();
    12. }
    13. protected Connection createConnectionProxy(Connection con) {
    14. return (Connection) Proxy.newProxyInstance(
    15. ConnectionProxy.class.getClassLoader(),
    16. new Class<?>[] {ConnectionProxy.class},
    17. new CloseSuppressingInvocationHandler(con));
    18. }
    19. @Override
    20. public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    21. Assert.notNull(action, "Callback object must not be null");
    22. Connection con = DataSourceUtils.getConnection(getDataSource());
    23. Statement stmt = null;
    24. try {
    25. Connection conToUse = con;
    26. if (this.nativeJdbcExtractor != null &&
    27. this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
    28. conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
    29. }
    30. stmt = conToUse.createStatement();
    31. applyStatementSettings(stmt);
    32. Statement stmtToUse = stmt;
    33. if (this.nativeJdbcExtractor != null) {
    34. stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
    35. }
    36. // 调用回调方法
    37. T result = action.doInStatement(stmtToUse);
    38. handleWarnings(stmt);
    39. return result;
    40. }
    41. catch (SQLException ex) {
    42. // Release Connection early, to avoid potential connection pool deadlock
    43. // in the case when the exception translator hasn't been initialized yet.
    44. JdbcUtils.closeStatement(stmt);
    45. stmt = null;
    46. DataSourceUtils.releaseConnection(con, getDataSource());
    47. con = null;
    48. throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
    49. }
    50. finally {
    51. JdbcUtils.closeStatement(stmt);
    52. DataSourceUtils.releaseConnection(con, getDataSource());
    53. }
    54. }
    55. @Override
    56. public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
    57. Assert.notNull(sql, "SQL must not be null");
    58. Assert.notNull(rse, "ResultSetExtractor must not be null");
    59. if (logger.isDebugEnabled()) {
    60. logger.debug("Executing SQL query [" + sql + "]");
    61. }
    62. // 封装回调方法
    63. class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
    64. @Override
    65. public T doInStatement(Statement stmt) throws SQLException {
    66. ResultSet rs = null;
    67. try {
    68. rs = stmt.executeQuery(sql);
    69. ResultSet rsToUse = rs;
    70. if (nativeJdbcExtractor != null) {
    71. rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
    72. }
    73. return rse.extractData(rsToUse);
    74. }
    75. finally {
    76. JdbcUtils.closeResultSet(rs);
    77. }
    78. }
    79. @Override
    80. public String getSql() {
    81. return sql;
    82. }
    83. }
    84. return execute(new QueryStatementCallback());
    85. }
    86. //...
    87. }

    模板方法模式VS回调
    从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。

    从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

    根据软件开发的原则,组合优于继承,在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点:

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