模板模式
定义
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
翻译一下
模板方法模式,是在一个事先定义后的一套业务代码中,将某些可扩展的业务代码推迟到子类中去实现,即事先约定后一套流程,然后对外暴露一些可动态改变的业务。
用途
模板模式作用场景主要有两个:
复用和扩展
适用于一个业务逻辑整体逻辑确定,但是有些业务的具体逻辑需要推迟交给子类去完善。
代码体现
编写公共的流程代码,但是在流程代码中调用抽象方法,不具体实现,推迟给子类实现。
在代码的实现思路上可以更清楚地理解其的使用场景:
1.复用指的的是流程代码的复用性
2.扩展指的是流程代码中在处理某些业务时候,调用抽象方法,不具体实现,推迟给子类实现。
代码实现
public class TemplateMethodPattern {
public static void main(String[] args) {
AbstractClass tm = new ConcreteClass();
tm.TemplateMethod();
}
}
//抽象类
abstract class AbstractClass {
//模板方法 客户端代码实际调用的方法
public void TemplateMethod() {
SpecificMethod();
abstractMethod1();
abstractMethod2();
}
//具体方法 某一个业务的公共流程代码
public void SpecificMethod() {
System.out.println("抽象类中的具体方法被调用...");
}
//抽象方法1
public abstract void abstractMethod1();
//抽象方法2
public abstract void abstractMethod2();
}
//具体子类
class ConcreteClass extends AbstractClass {
public void abstractMethod1() {
System.out.println("抽象方法1的实现被调用...");
}
public void abstractMethod2() {
System.out.println("抽象方法2的实现被调用...");
}
}
使用场景
1.复用
流程代码的复用性
这里的复用指的是所有的子类都可以复用父类中模板方法定义的流程代码。
例子
Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer。我们拿 InputStream 来举例说明一下。我把 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;
}
}
2.扩展
框架的扩展性,其实还是框架流程代码某些具体实现推迟给子类实现。
这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性。
仍然拿上面的例子举例
上面我们着眼于流程代码的复用性,来说明其适用于可复用的业务场景,同理,其read()方法推迟给子类实现,可以直接体现其适用于扩展的业务场景。
public abstract class InputStream implements Closeable {
//...省略其他代码...
//这个膜版代码从模板模式的角度看可分为两部分,第一部分体现其流程代码可复用的特点:6-12行和15-32行 第二部分体现其可扩展的特点:14行 调用抽象方法。
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;
}
}
模板模式的缺点
假设一个框架中的某个类暴露了两个模板方法,并且定义了一堆供模板方法调用的抽象方法,代码示例如下所示。在项目开发中,即便我们只用到这个类的其中一个模板方法,我们还是要在子类中把所有的抽象方法都实现一遍,这相当于无效劳动
public abstract class AbstractClass {
public final void templateMethod1() {
//...
method1();
//...
method2();
//...
}
public final void templateMethod2() {
//...
method3();
//...
method4();
//...
}
protected abstract void method1();
protected abstract void method2();
protected abstract void method3();
protected abstract void method4();
}
而且模板模式需要子类继承父类,这种模式,让子类放弃了继承其他类的能力,且也有可能需要在子类中实现一些子类根本用不到的抽象类。
模板模式这种思路无法就是先制定一套流程代码,将其中某些实现逻辑推迟给子类去实现。所以按照这个思路出发,代码完全可以有另一套实现形式:
回调
回调
相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。
即被调用方可以反向调用其调用方的代码。
其实这里说的更多的是同步回调,即被调用者的代码,可以在客户端中实现
代码体现:
定义一个回调接口,调用者中某个方法参数为回调接口,客户端使用的时候,必须在其方法中实现回调接口的具体逻辑。
即将模板模式中的流程代码写在函数中,函数中将扩展的逻辑延迟到接口的实现类中,而这个接口正是函数的入参。最体现这一特点的是下面例子中的JdbcTemplate execute(接口参数)方法。
code
回调接口
1 public interface Callback {
2 String callBack();
3 }
调用者
public class Another {
private Callback callback;
//调用实现类的方法
public void setCallback(Callback callback) {
this.callback = callback;
}
//业务需要的时候,通过委派,来调用实现类的具体方法
public void doCallback(){
System.out.println(callback.callBack());
}
}
测试回调函数
public class TestCallcack {
public static void main(String[] args) {
//创建调用者的实现类
Another another = new Another();
//将回掉接口注册到实现类中
another.setCallback(new Callback() {
@Override
public String callBack() {
return "you are a pig";
}
});
//执行回调函数
another.doCallback();
}
}
举例
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));
}
//query() 函数是对 execute() 函数的二次封装
@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 + "]");
}
//可扩展的地方 设计成回调 实现StatementCallback接口 比如这里扩展为查询
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());
}
//流程代码-模板代码
//这就是和模板模式最大的不同,模板模式是通过子类继承实现流程代码,回调是通过将流程代码定义在方法中,对外提供需要客户端扩展的StatementCallback(接口参数)
@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());
}
}
回调相对于模板模式会更加灵活,主要体现在下面几点
- 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
- 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。
综上可以看出
回调比模板模式代码实现的更加灵活且避免了实现一些多余的方法
网友评论