您的当前位置:首页正文

设计模式:模板模式

2024-11-07 来源:个人技术集锦

遇到代码重构复用,几乎领导都会说到模板方法,我这边几乎想的都是工厂方法配合策略。后来一段时间我其实可以理解的。

首先我觉得可能这个模板方式就是一个抽象abstract的父类,然后多个子类继承,觉得给自己的操作空间太小。工厂模式我用的比较熟练,然后策略模式。但是后面我发现我的方式,免不了扩展后的一大堆if else或者建一个map来存储这些不同的环境条件,用的时候直接“查表”。

但是在后面的一次改造中,我发现模板方法在某些方面有他自己很大的优势。首先工厂模式是一种创建型的设计模式,他搭配策略模式是一种行为型的设计模式,一个创建,一个用。

模板方法是一种行为型设计模式,经典的设计模式就是一个抽象父类,里面定义了一堆抽象方法,实际上你可以把这个模板方法里面的抽象父类理解成一个模板,里面的方法是这个“模板”的“骨架”。每一个继承这个类的子类都要按照这个“模板”里面的“骨架”去实现具体的逻辑,每一个不同的子类,虽然是不同逻辑但大家的“骨架”结构是相同的,做到了“合而不同”,这也就体现了模板模式的优点:复用。

除此之外,抽象类里可以有具体实现的方法,这些非抽象方法,可以在不修改这个代码结构的同时,让子类获取新的功能,同理父类扩展子类,子类也可以反过来扩展父类的方法,去自定义定制化一些东西。

结合小争哥的例子,我们在代码中去学习模板模式。

一 模板模式:复用

Java InputStream的模板方式复用
输入流可以说是代码总能用得到,public abstract class InputStream这个抽象类有很多子类,看一下截图


public abstract class InputStream implements Closeable {
  //...省略其他代码...
  
  public int read(byte b[], int off, int len) throws IOException {
    if (b == null) {
      throw new NullPointerException();
    } else if (off < 0 || len < 0 || len > b.length - off) {
      throw new IndexOutOfBoundsException();
    } else if (len == 0) {
      return 0;
    }

    int c = read();
    if (c == -1) {
      return -1;
    }
    b[off] = (byte)c;

    int i = 1;
    try {
      for (; i < len ; i++) {
        c = read();
        if (c == -1) {
          break;
        }
        b[off + i] = (byte)c;
      }
    } catch (IOException ee) {
    }
    return i;
  }
  
  public abstract int read() throws IOException;
}

public class ByteArrayInputStream extends InputStream {
  //...省略其他代码...
  
  @Override
  public synchronized int read() {
    return (pos < count) ? (buf[pos++] & 0xff) : -1;
  }
}

同理还有AbstractList,addAll方法是模板方法,模板类已经替你实现好了,你子类直接用就可以,像自定义也可以自己重写。

但是add方法需要子类重写,但是他又不是抽象方法,怎么提醒子类去实现这个方法,add方法的实现是直接给个异常。这其实也可以借鉴,比如说有些方法,是非必要的,可能继承模板的子类有些是必须,有些不是,不是必须没必要强制别人去用,必须的你好要提醒人家去自己实现,可以采用这种方式。只要你用这个方法,就报错“UnsupportedOperationException”,提示你自己实现。

    /**
     * {@inheritDoc}
     *
     * <p>This implementation always throws an
     * {@code UnsupportedOperationException}.
     *
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     * @throws IndexOutOfBoundsException     {@inheritDoc}
     */
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

二 模板模式:扩展

模板模式的第二大作用的是扩展。这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似我们之前讲到的控制反转。

模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。

Java Servlet
Web框架基础是Servlet,你的post,get请求都是来自Servlet,但是你定制化你的psot,get请求该怎么样呢?你自定义一个xxxServlet继承HttpServlet就可以了,然后重写post,get方法。


public class HelloServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doPost(req, resp);
  }
  
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.getWriter().write("Hello World.");
  }
}

我们还需要在配置文件 web.xml 中做如下配置。Tomcat、Jetty 等 Servlet 容器在启动的时候,会自动加载这个配置文件中的 URL 和 Servlet 之间的映射关系。


<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.xzg.cd.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

输入“http://127.0.0.1:8080/hello”,hello对应的HelloServlet,这边具体的过程我直接把小争哥的专栏内容抄过来。

以下是HttpServlet的service具体内容


public void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException
{
    HttpServletRequest  request;
    HttpServletResponse response;
    if (!(req instanceof HttpServletRequest &&
            res instanceof HttpServletResponse)) {
        throw new ServletException("non-HTTP request or response");
    }
    request = (HttpServletRequest) req;
    response = (HttpServletResponse) res;
    service(request, response);
}

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
{
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            if (ifModifiedSince < lastModified) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }
    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);
    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);
    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);
    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);
    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);
    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

JUnit TestCase
JUnit 框架也通过模板模式提供了一些功能扩展点(setUp()、tearDown() 等),让框架用户可以在这些扩展点上扩展功能,这俩方法一个是测试方法前置方法负责准备的,一个是后置方法负责收尾的。

而且这个setUp,tearDown方法也和我们在AbstractList里面的那个add方法一样,不是抽象方法,setUp,tearDown也没有像add方法一样,抛个异常出来,比较“不负责”,如果真的具体实现没具体看的话,可能会有“坑”等着你。(这种方式跟抽象类型的模板方法是一样的,也可以理解为不是抽象的模板方法)


public abstract class TestCase extends Assert implements Test {
  public void runBare() throws Throwable {
    Throwable exception = null;
    setUp();
    try {
      runTest();
    } catch (Throwable running) {
      exception = running;
    } finally {
      try {
        tearDown();
      } catch (Throwable tearingDown) {
        if (exception == null) exception = tearingDown;
      }
    }
    if (exception != null) throw exception;
  }
  
  /**
  * Sets up the fixture, for example, open a network connection.
  * This method is called before a test is executed.
  */
  protected void setUp() throws Exception {
  }

  /**
  * Tears down the fixture, for example, close a network connection.
  * This method is called after a test is executed.
  */
  protected void tearDown() throws Exception {
  }
}

三 回调

复用和扩展不是模板方式也可以实现,还有一个回调CallBack的方式也可以起到相同的作用,但是我个人认为一个更注重框架类,一个更专注使用类。模板要定一个骨架,回调直接通过组合的方式“插入”来完成对应的作用。

相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 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("Call back me.");
      }
    });
  }
}

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

这里是JdbcTemplate,涉及到各种设计库不同的规范,如何做到扩展复用,以下是一个查询的user的demo代码:


public class JdbcTemplateDemo {
  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> {
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
      User user = new User();
      user.setId(rs.getLong("id"));
      user.setName(rs.getString("name"));
      user.setTelephone(rs.getString("telephone"));
      return user;
    }
  }
}

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

(这里面有点意思,就是模板方式模板方法是变的,这里回调的回调方法是是不变的,变的让客户自己实现,自己返回的是一个不变的东西)


@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
 return query(sql, new RowMapperResultSetExtractor<T>(rowMapper));
}

@Override
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 + "]");
 }

 class QueryStatementCallback implements StatementCallback<T>, 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 <T> T execute(StatementCallback<T> action) throws DataAccessException {
 Assert.notNull(action, "Callback object must not be null");

 Connection con = DataSourceUtils.getConnection(getDataSource());
 Statement stmt = null;
 try {
  Connection conToUse = con;
  if (this.nativeJdbcExtractor != null &&
    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("StatementCallback", getSql(action), ex);
 }
 finally {
  JdbcUtils.closeStatement(stmt);
  DataSourceUtils.releaseConnection(con, getDataSource());
 }
}

setClickListener()
安卓的Button注册监听事件,这里传参数传的是一个类似函数一样的东西,这也是函数编程的一种形式。

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


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

addShutdownHook()
Hook 可以翻译成“钩子”,代码线性执行,有时需要一个“钩子”,勾一些东西去执行。

网上有人认为 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("I am called during shutting down.");
    }
  }

  public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new ShutdownHook());
  }

}

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 {
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            hooks = null;
        }
    }

    static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }

    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) {
                }
            }
        }
    }
}

模板基于继承,回调基于组合,回调对继承的优点有:

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