模板模式
模板模式主要解决的是复用和扩展两个问题。
在JDK、Servlet、JUnit中的应用
原理与实现
- 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤延迟到子类中实现。模板方法模式可以让子类子啊不改变算法整体结构的情况下,重新定义算法(业务逻辑)中的某些步骤。
- 看一个例子:-> 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. Java Servlet
- 使用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."); } }
- 另外我们还需要在配置文件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>
- 当我们在浏览器中输入网址,Servlet受到相应的请求并且根据URL和Servlet之间的映射关系,找到相应的Servlet,并且执行他的service方法,service方法定义在父类的HttpServlet中,他会调用doGet或者doPost方法,上面的例子中如果请求
/hello
,则会返回‘Hello World’。 - 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); } }
- service方法就是一个模板方法,它实现了整个HTTP请求的执行流程,doGet和doPost是子类中可以定制的部分,相当于提供了扩展点,让用户可以在不修改框架源码的情况下,将业务代码通过扩展点镶嵌到框架中。
2. JUnit TestCase
- 与上面的例子类似。
- 在编写单元测试的时候我们需要继承TestCase类,其中runBare函数是模板方法,它定义了执行测试用例的整体流程:先执行setUp做些准备工作,然后执行runTest运行真正的测试代码。最后执行tearDown做扫尾工作。
- 尽管 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函数的区别和联系
- 回调也可以起到模板模式相同的作用。
回调的原理解析
- A调用B的方法的时候,某一步B又反过来调用A的另一方法。为什么这么做:可以理解为A在做一件事,然后用到了B,而且他需要B做完之后告诉他、返回给他点什么,这个时候就要注册一个回调函数,这个函数在B中调用,是A提供的方法,通过这个回调函数,带着一些信息调回A中,A得以继续执行。
- 这不就是一个函数吗?为什么还要折腾回调什么的-> 如果这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入口
- 另外在更高层次的代码设计上这个回调的思路也比较常用。比如通过第三方支付系统来实现支付功能,用户发起支付请求之后一般不会一直阻塞到支付结果返回,而是注册回调接口给第三方系统,等第三方支付系统执行完成之后,将结果通过回调接口返回给用户。
- 回调分为同步回调和异步回调。
- 同步回调是指在函数返回之前执行回调函数。上面的代码是同步回调,process函数在返回之前,执行完回调函数methodToCallback。
- 异步回调指的是在函数返回之后执行回调函数。支付系统的例子是异步回调,发起支付后不需要等待回调接口被调用就直接返回。
- 同步回调更像模板模式,异步回调有些像观察者模式(发消息给远端然后结束。)
应用举例一: 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."); } });
- 在客户端开发中,对空间注册一个监听器。
- 这里是一个异步回调。我们在setOnClickListener中注册好回调函数后。不需要等待回调函数执行。
- 这里整个片段是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 回调
- 从应用场景看:同步回调和模板模式几乎一样,在一个大的框架中,自由替换其中某个步骤,起到代码复用和拓展的目的。而异步回调更像观察者模式。
- 从代码实现看:两者完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象。模板模式基于继承来实现,子类重写父类方法。
- 组合优于继承
- Java中,基于模板模式编写子类,以及继承了一个父类,不再具有继承能力;
- 回调可以使用匿名类创建回调对象,可以不用实现定义类;模板模式需要针对不同的实现都要定义不同的子类;
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。