10k

设计模式之美-课程笔记37-模板模式

模板模式

模板模式主要解决的是复用和扩展两个问题。

在JDK、Servlet、JUnit中的应用

原理与实现

  1. 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤延迟到子类中实现。模板方法模式可以让子类子啊不改变算法整体结构的情况下,重新定义算法(业务逻辑)中的某些步骤。
  2. 看一个例子:-> template 被定义成final不可被重写。
public abstract class AbstractClass {
  public final void templateMethod() {
    //...
    method1();
    //...
    method2();
    //...
  }
  
  protected abstract void method1();
  protected abstract void method2();
}

public class ConcreteClass1 extends AbstractClass {
  @Override
  protected void method1() {
    //...
  }
  
  @Override
  protected void method2() {
    //...
  }
}

public class ConcreteClass2 extends AbstractClass {
  @Override
  protected void method1() {
    //...
  }
  
  @Override
  protected void method2() {
    //...
  }
}

AbstractClass demo = ConcreteClass1();
demo.templateMethod();

模板模式作用一:复用

模板模式把一个算法中不变的流程抽象到父类的模板方法templateMethod中,将可变的部分method1, method2留给子类ConcreteClass1和ConcreteClass2来实现。所有的子类都可以复用父类中模板方法定义的流程代码。

1. Java 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;
  }
}

read() 函数是一个模板方法,定义了读取数据的整个流程,并且暴露了一个可以由子类来定制的抽象方法。这个方法也被命名成了read,只是参数跟模板方法不同。

2. Java AbstractList

addAll函数可以看做模板方法,add是子类需要重写的方法,尽管没有命名为抽象方法,但是函数实现直接抛异常。

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);
    boolean modified = false;
    for (E e : c) {
        add(index++, e);
        modified = true;
    }
    return modified;
}

public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

模板模式作用而:扩展

  1. 有点像控制反转;
  2. 可以让用户在不修改框架源码的情况下定制框架的功能。
1. Java Servlet
  1. 使用Servlet开发Web项目,需要定义一个集成HttpServlet的类,并且重写齐总的doGet或者doPost方法,来分别处理get和post请求。
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.");
  }
}
  1. 另外我们还需要在配置文件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>
  1. 当我们在浏览器中输入网址,Servlet受到相应的请求并且根据URL和Servlet之间的映射关系,找到相应的Servlet,并且执行他的service方法,service方法定义在父类的HttpServlet中,他会调用doGet或者doPost方法,上面的例子中如果请求/hello,则会返回‘Hello World’。
  2. 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);
    }
}
  1. service方法就是一个模板方法,它实现了整个HTTP请求的执行流程,doGet和doPost是子类中可以定制的部分,相当于提供了扩展点,让用户可以在不修改框架源码的情况下,将业务代码通过扩展点镶嵌到框架中。
2. JUnit TestCase
  1. 与上面的例子类似。
  2. 在编写单元测试的时候我们需要继承TestCase类,其中runBare函数是模板方法,它定义了执行测试用例的整体流程:先执行setUp做些准备工作,然后执行runTest运行真正的测试代码。最后执行tearDown做扫尾工作。
  3. 尽管 setUp()、tearDown() 并不是抽象函数,还提供了默认的实现,不强制子类去重新实现,但这部分也是可以在子类中定制的,所以也符合模板模式的定义。
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函数的区别和联系

  1. 回调也可以起到模板模式相同的作用。

回调的原理解析

  1. A调用B的方法的时候,某一步B又反过来调用A的另一方法。为什么这么做:可以理解为A在做一件事,然后用到了B,而且他需要B做完之后告诉他、返回给他点什么,这个时候就要注册一个回调函数,这个函数在B中调用,是A提供的方法,通过这个回调函数,带着一些信息调回A中,A得以继续执行。
    1. 这不就是一个函数吗?为什么还要折腾回调什么的-> 如果这B系统和A系统是两个外部系统,所以只能通过这样的方式调用通信。
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.");
      }
    });
  }
}

如果IcallBack和BClass都是框架代码,AClass是客户端代码,那么ICallback相当于提供了一个”口子“可以定制BClass的process函数。使框架有了扩展能力。

这个地方看起来很像新建一个线程,runable是一个Callback入口

  1. 另外在更高层次的代码设计上这个回调的思路也比较常用。比如通过第三方支付系统来实现支付功能,用户发起支付请求之后一般不会一直阻塞到支付结果返回,而是注册回调接口给第三方系统,等第三方支付系统执行完成之后,将结果通过回调接口返回给用户。
  2. 回调分为同步回调和异步回调。
    1. 同步回调是指在函数返回之前执行回调函数。上面的代码是同步回调,process函数在返回之前,执行完回调函数methodToCallback。
    2. 异步回调指的是在函数返回之后执行回调函数。支付系统的例子是异步回调,发起支付后不需要等待回调接口被调用就直接返回。
    3. 同步回调更像模板模式,异步回调有些像观察者模式(发消息给远端然后结束。)

应用举例一: JdbcTemplate

使用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", "xzg", "xzg");

      //2.创建statement类对象,用来执行SQL语句
      stmt = conn.createStatement();

      //3.ResultSet类,用来存放获取的结果集
      String sql = "select * from user where id=" + id;
      ResultSet resultSet = stmt.executeQuery(sql);

      String eid = null, ename = null, price = null;

      while (resultSet.next()) {
        User user = new User();
        user.setId(resultSet.getLong("id"));
        user.setName(resultSet.getString("name"));
        user.setTelephone(resultSet.getString("telephone"));
        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;
  }

}

JdbcTemplate提供了进一步封装,简化数据库编程。

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通过回调机制,将不变的执行流程抽离出来放到模板方法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 button = (Button)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    System.out.println("I am clicked.");
  }
});
  1. 在客户端开发中,对空间注册一个监听器。
  2. 这里是一个异步回调。我们在setOnClickListener中注册好回调函数后。不需要等待回调函数执行。
  3. 这里整个片段是A类,OnClickListener是B类,onClick就是回调函数。

应用举例三:addShutDownHook()

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

当应用程序关闭的时候,JVM会调用ranHooks方法,创建多个线程并发指定多个hook。我们在注册完hook之后不需要等到hook执行完成,这也算一种异步回调

模板模式 VS 回调

  1. 从应用场景看:同步回调和模板模式几乎一样,在一个大的框架中,自由替换其中某个步骤,起到代码复用和拓展的目的。而异步回调更像观察者模式。
  2. 从代码实现看:两者完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象。模板模式基于继承来实现,子类重写父类方法。
  3. 组合优于继承
    • Java中,基于模板模式编写子类,以及继承了一个父类,不再具有继承能力;
    • 回调可以使用匿名类创建回调对象,可以不用实现定义类;模板模式需要针对不同的实现都要定义不同的子类;
    • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。
Thoughts? Leave a comment