• 了解模板模式


    如果大家觉得文章有错误内容,欢迎留言或者私信讨论~

      大部分的设计模式原理和实现都非常简单,难的是掌握应用场景,搞清楚能解决什么问题。模板模式也不例外。模板模式主要是用来解决复用和扩展两个问题。

    实现和原理

    Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

      翻译成中文就是:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
      这里的“算法”可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
      了解原理之后,代码实现就可以更加简单。在经典的模板模式的实现中,模板方法可以定义为 final,避免被子类重写。需要子类重写的方法可以被定义为 abstract,可以强迫子类去实现。如下代码示例:

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

    模板模式作用之一:复用

      开篇的时候,就讲到模板模式有两大作用:复用和扩展。先来讲讲复用。
      模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1()、method2() 留给子类 ContreteClass1 和 ContreteClass2 来实现。所有的子类都可以复用父类中模板方法定义的流程代码。我们通过两个小例子来更直观地体会一下。

    1. Java InputStream

      Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer。我们拿 InputStream 来举例说明一下。
      在 InputStream 部分相关代码贴到下面。在代码中,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

    2. Java AbstractList

      在 Java AbstractList 类中, addAll() 函数可以作为模板方法,add() 是子类需要重写的方法,尽管没有声明为 abstract 的,但函数实现直接抛出了 UnsupportedOperationException 异常。如果子类补充些是不能使用的。

    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    模板模式作用之二:扩展

      这里的扩展并不是指的代码的扩展性,而是指的是框架的扩展性,让用户可以在不修改框架源码的情况下,定制化框架的功能。让我们通过 Java Servlet 来解释一下。

    1. Java Servlet

      对于涉及 Java Web 的项目开发常用的开发框架是 Spring MVC。利用它,我们只要关心到业务代码的编写,底层的原理几乎不会涉及。但是如果我们抛开这些高级框架来开发 Web 项目,必然会用到 Servlet。实际上,使用比较底层的 Servlet 来开发 Web 项目也不
    难。我们只需要定义一个继承 HttpServlet 的类,并且重写其中的 doGet() 或 doPost() 方法,来分别处理 get 和 post 请求。具体的代码示例如下所示:

    public class HelloServlet extends HttpServlet {
    	@Override
    	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws
    		this.doPost(req, resp);
    	}
    	
    	@Override
    	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throw
    		resp.getWriter().write("Hello World.");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      除此之外,我们还需要在配置文件 web.xml 中做配置。让 Tomcat 启动时自动加载配置文件中的 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

      我们来看看,HttpServlet 的 service() 的函数长什么样子。

    @Override
    public void service(ServletRequest req, ServletResponse res)
        throws ServletException, IOException {
    
        HttpServletRequest  request;
        HttpServletResponse response;
    
        try {
            request = (HttpServletRequest) req;
            response = (HttpServletResponse) res;
        } catch (ClassCastException e) {
            throw new ServletException(lStrings.getString("http.non_http"));
        }
        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;
                try {
                    ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                } catch (IllegalArgumentException iae) {
                    // Invalid date header - proceed as if none was set
                    ifModifiedSince = -1;
                }
                if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                    // 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 {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //
    
            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
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

      从上面的代码中我们可以看出,HttpServlet 的 service() 方法就是一个模板方法,它实现了整个 HTTP 请求的执行流程,doGet()、doPost() 是模板中可以由子类来定制的部分。实际上,这就相当于 Servlet 框架提供了一个扩展点(doGet()、doPost() 方法),让框架用户在不用修改 Servlet 框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。

    Call 回调函数

      除去复用和扩展之外,还有一个技术概念可以与模板联系,那就是回调。回调的概念就是指:相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调“。

      A 类如何将回调函数传递给 B 类呢?在 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

      这里的回调不仅能够复用,并且如果 ICallback、BClass 类是框架代码,AClass 是使用框架的客户端代码,我们可以通过 ICallback 定制 process() 函数,也就是说,框架因此具有了扩展的能力。

      实际上,回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。

      回调可以分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。上面的代码实际上是同步回调的实现方式,在 process() 函数返回之前,执行完回调函数 methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。

    举例: JdbcTemplate

      Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫作 xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切地说应该是同步回调。
      这些 Template 类的涉及思路都很相近,所以拿 JdbcTemplate 来分析举例一下。
      在我们最初学习 Spring 的 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", "x
    			//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;
    	}
    }
    
    • 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

      queryUser() 函数包含很多流程性质的代码,跟业务无关,比如,加载驱动、创建数据库连接、创建 statement、关闭连接、关闭 statement、处理异常。针对不同的 SQL 执行请求,这些流程性质的代码是相同的、可以复用的,我们不需要每次都重新敲一遍。

      针对这个问题,Spring 提供了 JdbcTemplate,对 JDBC 进一步封装,来简化数据库编程。

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

      那 JdbcTemplate 底层具体是如何实现的呢?这里只是部分。其中, JdbcTemplate 通过回调的机制,将不变的执行流程抽离出来,放到模板方法 execute() 中,将可变的部分设计成回调 StatementCallback,由用户来定制。query() 函数是对 execute() 函数的二次封装,让接口用起来更加方便。

    @Override
    public <T> List<T> query(String sql, RowMapper<T> rowMapper)
    	return query(sql, new RowMapperResultSetExtractor<T>(rowMapper));
    }
    
    @Override
    public <T> T query(final String sql, final ResultSetExtractor<T> rse)
    	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)
    	} 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

      回调作用跟模板类似,但更加灵活。主要区别就是模板通过继承来实现,而回调则通过组合来实现。

  • 相关阅读:
    Transformer编码器和解码器的输入与输出
    Linux17 jdk tomcat idea mysql在linux上安装
    量子笔记:全局相位、相对相位、布洛赫球面
    【iVX 开发 - 入门】UI 组件介绍及实操详解
    给电瓶车“消消火”——TSINGSEE青犀智能电瓶车棚监控方案
    Oracle EBS API创建AP发票报错:ZX_TAX_STATUS_NOT_EFFECTIVE和ZX_REGIME_NOT_EFF_IN_SUBSCR-
    C++ 背包问题——多重背包
    基于java最短路径算法的简单实现
    【PAT甲级】1050 String Subtraction
    如何建立你的财务体系?
  • 原文地址:https://blog.csdn.net/qq_43654226/article/details/126733396