• 设计模式:模板模式


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

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

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

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

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

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

    一 模板模式:复用

    Java InputStream的模板方式复用
    输入流可以说是代码总能用得到,public abstract class InputStream这个抽象类有很多子类,看一下截图
    在这里插入图片描述
    这里面复用体现最明显的是read方法,这里面第一个read是抽象方法,是需要子类自己去实现的,剩下的两个具体实现方法,其子类可以直接调用。
    在这里插入图片描述

    
    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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

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

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

    在这里插入图片描述

        /**
         * {@inheritDoc}
         *
         * 

    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(); }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    二 模板模式:扩展

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

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

    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.");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    
    
        HelloServlet
        com.xzg.cd.HelloServlet
    
    
    
        HelloServlet
        /hello
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    当我们在浏览器中输入网址(比如,http://127.0.0.1:8080/hello )的时候,Servlet 容器会接收到相应的请求,并且根据 URL 和 Servlet 之间的映射关系,找到相应的 Servlet(HelloServlet),然后执行它的 service() 方法。service() 方法定义在父类 HttpServlet 中,它会调用 doGet() 或 doPost() 方法,然后输出数据(“Hello world”)到网页。

    以下是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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    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 {
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    三 回调

    复用和扩展不是模板方式也可以实现,还有一个回调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.");
          }
        });
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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 {
        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;
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

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

    
    @Override
    public  List query(String sql, RowMapper rowMapper) throws DataAccessException {
     return query(sql, new RowMapperResultSetExtractor(rowMapper));
    }
    
    @Override
    public  T query(final String sql, final ResultSetExtractor 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, 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 execute(StatementCallback 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());
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    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.");
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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());
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    
    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 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 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) {
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

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

    • 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
    • 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
    • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。
  • 相关阅读:
    (其他) 剑指 Offer 61. 扑克牌中的顺子 ——【Leetcode每日一题】
    虚拟机安装配置Hadoop(图文教程)
    新环境下配置jupyter notebook并启动
    PMP 11.27 考试倒计时22天!冲刺啦!
    WebServer 解析HTTP 请求报文
    linux常用命令之设定acl相关命令 setfacl/getfacl/chattr
    UVA524 素数环 Prime Ring Problem
    【Docker】命令使用大全
    【Java】异常处理及其语法、抛出异常、自定义异常(完结)
    单例模式:饿汉式、懒汉式
  • 原文地址:https://blog.csdn.net/FeiChangWuRao/article/details/126558187