设计模式——模板方法

概述

模板方法模式(TemplateMethod Pattern),父类中定义处理流程的框架,在子类中实现具体处理。模块方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模式结构

  • 抽象类: 负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下。

    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • 基本方法:是整个算法中的一个步
  • 具体子类: 实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤

适用环境

  • 有多个子类共有的方法,且逻辑相同
  • 重要的、复杂的方法,可以考虑作为模板方法

典型案例

AQS

在并发编程中,ReentrantLock锁大家一定不陌生,看过源码的小伙伴一定知道,ReentrantLock内部基于 AQS 实现。这里 acquire() 相当于模板方法,tryAcquire(arg) 相当于基本方法。
模板方法 中,模板方法一般用 final 修饰,避免子类对整体流程进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
// 模板方法,定义获取同步锁的策略。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
selfInterrupt();
}
// 基本方法,子类实现。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

接下来,我们看下具体的子类实现

ReentrantLock非公平锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

ThreadPoolExecutor线程池Worker类

1
2
3
4
5
6
7
8
9
10
11
12
13
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
// ...
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
}

登录流程

现在的应用, 很多是支持 验证码邮箱用户名 等多种方式登录的。 在后台,除了校验方式不同外,其他流程基本相同。这里我们可以在父类实现主要流程,将不同的逻辑放在子类实现,同时还要保证流程的一致性,这里可以用模板方法来实现。

首先来个超类,定义登录流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class LoginService {


// 模板方法。登录流程
public final void login(Map<String, String> params) {
verifyData();
doSomething();
packAndRetureData();
}

// 普通方法
private void packAndRetureData() {
System.out.println("all return login data");
}

// 普通方法
private void doSomething() {
System.out.println("all do some thing");
}

// 抽象方法,子类实现
protected abstract void verifyData();
}

具体子类,email登录

1
2
3
4
5
6
public class EmailLoginService extends LoginService {
@Override
protected void verifyData() {
System.out.println("verify email data");
}
}

短信登录

1
2
3
4
5
6
public class SmsLoginService extends LoginService {
@Override
protected void verifyData() {
System.out.println("verify sms data");
}
}

登录业务模拟

1
2
3
4
5
6
7
8
9
10
11
/**
* verify email data
* all do some thing
* all return login data
*/
public class LoginTest {
public static void main(String[] args) {
EmailLoginService loginService = new EmailLoginService();
loginService.login(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
43
44
45
46
public abstract class AbstractPayNotifyTemplate {

/**
* 支付异步回调处理
*
* @param params 回调参数
*/
public void onNotify(Map<String, String> params) {
//验证签名
final boolean sign = verifySign(params);
if (sign) {
// 给第三方支付服务器回复支付失败状态
setResponse(PayStatus.ERROR);
return;
}
//从参数获取订单编号并更新订单支付状态,为支付成功
final String orderSn = params.get("out_trade_no");
updateOrderPayStatusSuccess(orderSn);
// 给第三方支付服务器回复支付成功状态
setResponse(PayStatus.SUCCESS);
}

/**
* 验签
*
* @param params 回调参数
* @return 验签结果
*/
protected abstract boolean verifySign(Map<String, String> params);

/**
* 更新订单支付状态为支付成功
*
* @param orderSn 订单编号
*/
private void updateOrderPayStatusSuccess(String orderSn) {
// 根据订单编号更新订单支付状态为支付成功
}

/**
* 给第三方支付返回
*
* @param status 支付状态
*/
protected abstract void setResponse(PayStatus status);
}

具体模板类

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
// 支付宝支付回调类
public class AliPayNotifyTemplate extends AbstractPayNotifyTemplate {

@Override
protected boolean verifySign(Map<String, String> params) {
// 调用支付宝验签接口, 并返回验签结果
return true;
}

@Override
protected void setResponse(PayStatus status) {
String res = Objects.equals(PayStatus.SUCCESS, status) ? "success" : "error";

// 调用 ResponseUtils 直接返回 res 字符串
}
}

// 微信支付回调类
public class WxPayNotifyTemplate extends AbstractPayNotifyTemplate {

@Override
protected boolean verifySign(Map<String, String> params) {
// 调用微信支付验签接口, 并返回验签结果
return true;
}

@Override
protected void setResponse(PayStatus status) {
String returnCode = "FAIL";
String returnMsg = "";
if (Objects.equals(PayStatus.SUCCESS, status)) {
returnCode = "SUCCESS";
returnMsg = "OK";
}
String res = String.format("<xml><return_code><![CDATA[%s]]></return_code><return_msg><![CDATA[%s]]></return_msg></xml>", returnCode, returnMsg);

// 调用 ResponseUtils 返回 res xml格式内容
}
}

总结

从作用上来看,模板方法模式与策略模式都是对一个基类的方法有多种不同的实现。
而策略模式的具体实现中,采用的是组合 + 接口的策略。而模板方法模式就完全基于 Java 语言的多态特性实现的继承。从设计模式的思想来说,组合是优于继承的。
更大的区别在于,策略模式是从一个功能整体上出发的,每种策略对该功能都有不同的实现。而模板方法模式是对一个算法或流程细分为多个步骤,每个步骤都可以被子类重写,但是整个流程是不能被重新定义的。从代码实现上来讲,当模板方法中的需要重写的步骤占了绝大部分时,子类只有几个方法不用重写,在这种情况下不如使用策略模式。而当模板方法中需要重写的步骤不多时,使用策略模式会造成不同的实现类中有很多重复的代码。