15.模版模式设计思想
目录介绍
- 01.模版方法模式的定义
- 02.模版方法模式的结构
01.模版方法模式的定义
- 模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。
02.模版方法模式的结构
- 模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。
- 模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本方法汇总起来的方法叫做模板方法(template method),这个设计模式的名字就是从此而来。
- 模板方法所代表的行为称为顶级行为,其逻辑称为顶级逻辑。模板方法模式的静态结构图如下所示:
- 这里涉及到两个角色:
- 抽象模板(Abstract Template)角色有如下责任:
- 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
- 定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
- 具体模板(Concrete Template)角色又如下责任:
- 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
- 每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。 抽象模板角色类,abstractMethod()、hookMethod()等基本方法是顶级逻辑的组成步骤,这个顶级逻辑由templateMethod()方法代表。
- 抽象模板(Abstract Template)角色有如下责任:
public abstract class AbstractTemplate {
/**
* 模板方法
*/
public void templateMethod(){
//调用基本方法
abstractMethod();
hookMethod();
concreteMethod();
}
/**
* 基本方法的声明(由子类实现)
*/
protected abstract void abstractMethod();
/**
* 基本方法(空方法)
*/
protected void hookMethod(){}
/**
* 基本方法(已经实现)
*/
private final void concreteMethod(){
//业务相关的代码
}
}
具体模板角色类,实现了父类所声明的基本方法,abstractMethod()方法所代表的就是强制子类实现的剩余逻辑,而hookMethod()方法是可选择实现的逻辑,不是必须实现的。
public class ConcreteTemplate extends AbstractTemplate{
//基本方法的实现
@Override
public void abstractMethod() {
//业务相关的代码
}
//重写父类的方法
@Override
public void hookMethod() {
//业务相关的代码
}
}
模板模式的关键是:子类可以置换掉父类的可变部分,但是子类却不可以改变模板方法所代表的顶级逻辑。
每当定义一个新的子类时,不要按照控制流程的思路去想,而应当按照“责任”的思路去想。换言之,应当考虑哪些操作是必须置换掉的,哪些操作是可以置换掉的,以及哪些操作是不可以置换掉的。使用模板模式可以使这些责任变得清晰。
三、模板方法模式中的方法
模板方法中的方法可以分为两大类:模板方法和基本方法。
模板方法
一个模板方法是定义在抽象类中的,把基本操作方法组合在一起形成一个总算法或一个总行为的方法。
一个抽象类可以有任意多个模板方法,而不限于一个。每一个模板方法都可以调用任意多个具体方法。
基本方法
基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。
● 抽象方法:一个抽象方法由抽象类声明,由具体子类实现。在Java语言里抽象方法以abstract关键字标示。
● 具体方法:一个具体方法由抽象类声明并实现,而子类并不实现或置换。
● 钩子方法:一个钩子方法由抽象类声明并实现,而子类会加以扩展。通常抽象类给出的实现是一个空实现,作为方法的默认实现。
在上面的例子中,AbstractTemplate是一个抽象类,它带有三个方法。其中abstractMethod()是一个抽象方法,它由抽象类声明为抽象方法,并由子类实现;hookMethod()是一个钩子方法,它由抽象类声明并提供默认实现,并且由子类置换掉。concreteMethod()是一个具体方法,它由抽象类声明并实现。
默认钩子方法
一个钩子方法常常由抽象类给出一个空实现作为此方法的默认实现。这种空的钩子方法叫做“Do Nothing Hook”。具体模版类中可以选择是否重写钩子方法,通常重写钩子方法是为了对模版方法中的步骤进行控制,判断钩子方法中的状态,是否进行下一步操作。
四、模版方法的具体实例
考虑一个计算存款利息的例子。假设系统需要支持两种存款账号,即货币市场(Money Market)账号和定期存款(Certificate of Deposite)账号。这两种账号的存款利息是不同的,因此,在计算一个存户的存款利息额时,必须区分两种不同的账号类型。
这个系统的总行为应当是计算出利息,这也就决定了作为一个模板方法模式的顶级逻辑应当是利息计算。由于利息计算涉及到两个步骤:一个基本方法给出账号种类,另一个基本方法给出利息百分比。这两个基本方法构成具体逻辑,因为账号的类型不同,所以具体逻辑会有所不同。
显然,系统需要一个抽象角色给出顶级行为的实现,而将两个作为细节步骤的基本方法留给具体子类实现。由于需要考虑的账号有两种:一是货币市场账号,二是定期存款账号。系统的类结构如下图所示。
抽象模板角色类
public abstract class Account {
/**
* 模板方法,计算利息数额
* @return 返回利息数额
*/
public final double calculateInterest(){
double interestRate = doCalculateInterestRate();
String accountType = doCalculateAccountType();
double amount = calculateAmount(accountType);
return amount * interestRate;
}
/**
* 基本方法留给子类实现
*/
protected abstract String doCalculateAccountType();
/**
* 基本方法留给子类实现
*/
protected abstract double doCalculateInterestRate();
/**
* 基本方法,已经实现
*/
private double calculateAmount(String accountType){
/**
* 省略相关的业务逻辑
*/
return 7243.00;
}
}
具体模板角色类
public class MoneyMarketAccount extends Account {
@Override
protected String doCalculateAccountType() {
return "Money Market";
}
@Override
protected double doCalculateInterestRate() {
return 0.045;
}
}
public class CDAccount extends Account {
@Override
protected String doCalculateAccountType() {
return "Certificate of Deposite";
}
@Override
protected double doCalculateInterestRate() {
return 0.06;
}
}
客户端类
public class Client {
public static void main(String[] args) {
Account account = new MoneyMarketAccount();
System.out.println("货币市场账号的利息数额为:" + account.calculateInterest());
account = new CDAccount();
System.out.println("定期账号的利息数额为:" + account.calculateInterest());
}
}
五、模板方法模式效果与适用场景
模板方法模式是基于继承的代码复用技术,它体现了面向对象的诸多重要思想,是一种使用较为频繁的模式。模板方法模式广泛应用于框架设计中,以确保通过父类来控制处理流程的逻辑顺序(如框架的初始化,测试流程的设置等)。
在以下情况下可以考虑使用模板方法模式:
(1) 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
(2) 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
(3) 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
六、模版方法模式的优缺点
优点
(1) 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。
(2) 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。
(3) 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。
(4) 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
缺点
需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。
01.前沿介绍
- 复用和扩展是模板模式的两大作用,实际上,还有另外一个技术概念,也能起到跟模板模式相同的作用,那就是回调(Callback)。今天我们今天就来看一下,回调的原理、实现和应用,以及它跟模板模式的区别和联系。
02.回调的原理解析
- 相对于普通的函数调用来说,回调是一种双向调用关系。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."); } }); } }
- 上面就是 Java 语言中回调的典型代码实现。从代码实现中,我们可以看出,回调跟模板模式一样,也具有复用和扩展的功能。除了回调函数之外,BClass 类的 process() 函数中的逻辑都可以复用。如果 ICallback、BClass 类是框架代码,AClass 是使用框架的客户端代码,我们可以通过 ICallback 定制 process() 函数,也就是说,框架因此具有了扩展的能力。
- 实际上,回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。
- 回调可以分为同步回调和异步回调(或者延迟回调)。
- 同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。
- 上面的代码实际上是同步回调的实现方式,在 process() 函数返回之前,执行完回调函数 methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。
03.JdbcTemplate案例
- Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫作 xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切地说应该是同步回调。而同步回调从应用场景上很像模板模式,所以,在命名上,这些类使用 Template(模板)这个单词作为后缀。
- 这些 Template 类的设计思路都很相近,所以,我们只拿其中的 JdbcTemplate 来举例分析一下。对于其他 Template 类,你可以阅读源码自行分析。
- 在前面的章节中,我们也多次提到,Java 提供了 JDBC 类库来封装不同类型的数据库操作。不过,直接使用 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; } }
04.setClickListener()
- 在客户端开发中,我们经常给控件注册事件监听器,比如下面这段代码,就是在 Android 应用开发中,给 Button 控件的点击事件注册监听器。
Button button = (Button)findViewById(R.id.button); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { System.out.println("I am clicked."); } });
- 从代码结构上来看,事件监听器很像回调,即传递一个包含回调函数(onClick())的对象给另一个函数。从应用场景上来看,它又很像观察者模式,即事先注册观察者(OnClickListener),当用户点击按钮的时候,发送点击事件给观察者,并且执行相应的 onClick() 函数。
- 我们前面讲到,回调分为同步回调和异步回调。这里的回调算是异步回调,我们往 setOnClickListener() 函数中注册好回调函数之后,并不需要等待回调函数执行。这也印证了我们前面讲的,异步回调比较像观察者模式。
05.addShutdownHook()
- Hook 可以翻译成“钩子”,那它跟 Callback 有什么区别呢?网上有人认为 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()); } }
- 我们再来看 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 执行完成,所以,这也算是一种异步回调。
06.模板模式VS回调
- 回调的原理、实现和应用到此就都讲完了。接下来,我们从应用场景和代码实现两个角度,来对比一下模板模式和回调。
- 从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。
- 从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
- 前面我们也讲到,组合优于继承。实际上,这里也不例外。在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点。
- 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
- 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。