IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    10k发表于 2023-09-14 00:00:00
    love 0

    模板模式

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

    在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中,基于模板模式编写子类,以及继承了一个父类,不再具有继承能力;
      • 回调可以使用匿名类创建回调对象,可以不用实现定义类;模板模式需要针对不同的实现都要定义不同的子类;
      • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。


沪ICP备19023445号-2号
友情链接