模板方法模式
介绍
模板方法模式(template method pattern)在操作中定义算法的骨架,将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤
模板方法中的算法可以理解为广义上的业务逻辑,并不是特指某一个实际的算法,定义中所说的算法的骨架就是模板,包含算法骨架的方法就是模板方法。
例如:去医院看病一般要经过以下 4 个流程:挂号、取号、排队、医生问诊等,其中挂号、取号、排队对每个病人都是一样的,可以在父类中实现,但是具体医生如何根据病情开药每个人都是不一样的,所以开药这个操作可以延迟到子类中去实现。
模板方法模式是一种基于继承的代码复用技术,它是一种类行为模式,模板方法模式其结构中只存在父类与子类的继承关系。
模板方法的作用主要是提高程序的复用性和扩展性:
- 复用指的是,所有的子类都可以复用父类中提供的模板方法代码。
- 扩展指的是, 框架通过模板模式提供功能扩展点,让框架用户可以在不改变框架源码的情况下,基于扩展点定制化框架的功能。
模式原理

package com.e6yun.project.behavior.templatemethod;
/**
* 抽象父类
*
* @author 21129
*/
public abstract class AbstractClassTemplate {
void step1(String key) {
System.out.println("在模板类中->执行步骤 1");
if (step2(key)) {
step3();
} else {
step4();
}
step5();
}
boolean step2(String key) {
System.out.println("在模板类中-> 执行步骤 2");
if ("x".equalsIgnoreCase(key)) {
return true;
} else {
return false;
}
}
abstract void step3();
abstract void step4();
void step5() {
System.out.println("在模板类中 -> 执行步骤 5");
}
// 模板方法
void run(String key) {
step1(key);
}
}
package com.e6yun.project.behavior.templatemethod;
public class ConcreteClassA extends AbstractClassTemplate{
@Override
void step3() {
System.out.println("在子类中 A-> 执行步骤 3");
}
@Override
void step4() {
System.out.println("在子类中 A-> 执行步骤 4");
}
}
package com.e6yun.project.behavior.templatemethod;
public class ConcreteClassB extends AbstractClassTemplate {
@Override
void step3() {
System.out.println("在子类中 B-> 执行步骤 3");
}
@Override
void step4() {
System.out.println("在子类中 B-> 执行步骤 4");
}
}
应用实例
P2P 公司的借款系统中有一个利息计算模板,利息的计算流程是这样的:
- 用户登录系统,登录时需要输入账号密码,如果登录失败(比如用户密码错误),系统会给出提示。
- 如果用户登录成功,则根据用户的借款的类型不同,使用不同的利息计算方式进行计算。
- 系统需要显示利息
package com.e6yun.project.behavior.templatemethod.example02;
/**
* 账户抽象类
*/
public abstract class Account {
// step1 具体方法-验证用户信息是否正确
public boolean validate(String account, String password) {
System.out.println("账号:" + account + ",密码:" + password);
if (account.equalsIgnoreCase("tom") && password.equals("123456")) {
return true;
} else {
return false;
}
}
// step2 抽象方法-计算利息
public abstract void calculate();
// step3 具体方法-显示利息
public void display() {
System.out.println("显示利息!");
}
// 模板方法
public void handle(String account, String password){
if (!validate(account, password)) {
System.out.println("账户或者密码错误");
return;
}
calculate();
display();
}
}
package com.e6yun.project.behavior.templatemethod.example02;
public class LoanOneMonth extends Account {
@Override
public void calculate() {
System.out.println("借款周期 30 天,利息为借款总额的 3% ");
}
}
package com.e6yun.project.behavior.templatemethod.example02;
public class LoanSevenDays extends Account {
@Override
public void calculate() {
System.out.println("借款周期 7 天,无利息!仅收取贷款金额的 1% 服务费");
}
@Override
public void display() {
System.out.println("七日内借款无利息!");
}
}
模板模式作用一:复用
模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod 中,将可变的部分,method1()、method2() 留给子类实现。所有的子类都可以复用父类模板中定义的流程代码 。
Java InputStream
Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer 等。以 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;
}
}
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();
}
模板模式作用二:扩展
模板模式的第二大作用是扩展,这里说的扩展,并不是代码的扩展性,而是指框架的扩展性,有点类似控制反转。
基于这个作用,模板模式常被用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。
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 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>
当在浏览器中输入网址,访问到 hello 这个 URL,Servlet 容器会接收到相应的请求,并根据 URL 和 Servlet 之间的隐射关系,找到相应的 Servlet ,然后执行它的 service() 方法,service() 方法定义在父类 HttpServlet 中,它会调用 doGet() 或 doPost() 方法,执行相应的处理逻辑。
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);
}
}
HttpServlet 的 service() 方法就是一个模板方法,它实现了整个 HTTP 请求的执行流程,doGet() 、doPost() 是模板中可以由子类来定制的部分。 这就相当于 Servlet 框架提供了一个扩展点,让子类来定制化算法。
Junit TestCase
跟 Java Servlet 类似,Junit 框架也通过模板模式提供了一些功能扩展点(setUp()、tearDown() 等),让框架用户可以在这些扩展点上扩展功能。
在使用 Junit 测试框架来编写单元测试的时候,我们编写的单元测试类都要继承框架提供的 TestCase 类。在 TestCase 类中,runBare() 函数是模板方法,它定义了执行测试用例的整个流程: 先执行 setUp() 做些扫尾工作,然后执行 runTest() 运行真正的测试代码,最后执行 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 {
}
}
尽管setUp()、tearDown() 并不是抽象函数,提供了默认实现,但这部分也是可以在子类中定制的,所以符合模板模式中的定义。
与 Callback 回调函数的区别与联系
复用和扩展是模板方法的两大作用,回调也能起到跟模板模式相同的作用。
回调原理
相对于普通的函数调用来说,回调是一种双向调用关系。A 类实现注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是回调函数。
A 调用 B ,B 反过来又调用 A ,这种调用机制就叫做 回调。
A 类如何将回调函数传递给 B 类呢?不同的编程语言有不同的实现方式,C 语言可以使用函数指针,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.");
}
});
}
}
上面就是 Java 语言中回调的典型实现,回调跟模板 方法模式一样,也具有复用和扩展的功能。除了回调函数之外,BClass 类的 process() 函数中的逻辑都可以复 用。如果 ICallback 、BClass 类是框架代码,AClass 是使用框架的客户端代码,就可以通过 ICallback 定制 process() 函数。框架因此具有了扩展能力。
实际上,回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个会调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。
回调可以分为同步回调和异步回调(延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。
从应用场景上来看,同步回调更像是模板模式,异步回调更像是观察者模式。
JdbcTemplate
Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫做xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切来说应该是同步回调。而同步回调从应用场景上很像模板模式,所以, 在命名上,这些类使用 Template(模板)这个单词作为后缀。
这些 Template 类的设计思路都很相似,以 JdbcTemplate 为例,Java 提供了JDBC 类库来封装不同类型的数据库操作。不过,直接使用 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;
}
}
queryUser() 函数包含了很多流程性质的代码,跟业务无关,比如加载驱动、创建数据库链接、创建 statement、 关闭连接、关闭 statement、处理异常。针对不同的 SQL 执行请求,这些流程性质的代码是相同的、 可以复用的,不需要每次都重新敲一遍。
针对这个问题, Spring 提供了 JdbcTemplate ,对 JDBC 进一步封装,来简化数据库编程。使用 JdbcTemplate 查询用户信息,只需要编写和这个业务有关的代码,其中包括,查询用户的 SQL、查询结果与 User 对象之间的映射。其它流程性质的代码都封装在了 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());
}
}
addShutdownHook()
Hook(钩子) 是 Callback 的一种应用,Callback 更侧重语法机制的描述,Hook 更加侧重应用场景的描述。
Hook 比较经典的应用场景是 Tomcat 和 JVM 的 shutdown hook。 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());
}
}
下面时 addShutdownHook() 的代码实现,如下所示。
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) {
}
}
}
}
}
有关 Hook 的逻辑都被封装到 ApplicationShutdownHooks 类中了。当应用程序关闭的时候,JVM 会调用这个类的 runHooks() 方法,创建多个线程,并发地执行多个 Hook。我们在注册完 Hook 之后,并不需要等待 Hook 执行完成,所以,这也算是一种异步回调。
模板模式 VS 回调
从应用场景上来说,同步回调跟模板模式几乎一致。都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大区别,更像是观察者模式。
从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点:
- Java 只支持单继承,基于模板模式编写的子类,不再具有继承的能力。
- 回调可以使用匿名类来创建回调对象,不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,即便只用到一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,只需要往用到的模板方法中注入回调对象即可。
总结
模板方法模式在一个方法中定义一个算法股价,并将某些步骤推迟到子类中实现。模板方法可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的算法可以理解为是广义上的业务逻辑,包含骨架的方法就是模板方法。
模板模式由两大作用:复用和扩展,其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供的功能扩展点,让框架用户可以在不修改框架源码的基础上,基于扩展点定制化框架的功能。
优点
- 在父类中形式化的定义一个算法,而由它的子类来实现细节处理,在子类实现详细的处理代码时,并不会改变父类算法中步骤的执行顺序。
- 模板方法可以实现一种反向的控制结构,通过子类覆盖父类的钩子方法,来决定某一个特定步骤是否需要执行。
- 在模板方法中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
缺点
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大, 设计页更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
模板方法模式的使用场景一般有
- 多个类有相同的方法并且逻辑可以共用时;
- 将通用的算法或固定流程设计为模板,在每一个具体的子类中再继续优化 算法步骤或流程步骤时。
- 重构超长代码时,发现某一个经常使用的公有方法。