模板方法(Template Method)是一个比较简单的模式。
它的主要思想是,定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现好了,这样不同的子类就可以定义出不同的步骤。

本文将从以下几点对模版方法设计模式进行讨论:

  • 模版方法的特点
  • 模版方法的使用场景
  • 模版方法的实现
  • 模版方法的应用

1. 模版方法的特点

  • 把子类中不变的逻辑抽离到父类(模版类)中,去除了子类中的重复代码,易于阅读和维护;
  • 模版类中的具体实现细节留给子类实现,有助于功能的扩展和维护;
  • 模版类的扩展和修改交给子类,符合开闭原则。

2. 使用场景

  • 当多个子类具有公用的方法,却执行流程逻辑相同时。可以把核心的算法和重要的功能设计为模板方法,子类去实现相关细节功能;
  • 重要的、复杂的方法,且可能有不同的实现,可以考虑作为模板方法。

举个例子🌰:

现在有一个查询的业务,具体的流程很简单,就是调用方法从数据库查询数据。但是可能使用MySQL,也可能使用Oracle或者Postgre SQL等,也可能在查询数据库之前先走一遍缓存等。但是整体流程的动作都是查询数据库,然后拿到数据对其数据做相应的业务操作。那么我们就可以尝试采用模版模式来实现,使用不同的子类各自实现queryData(Condition condition)方法。 ^1e1832

3. 模版方法的实现

经过上面的描述,对其模版方法模式有了一个初步的认识。接下来我们通过两个例子来进一步了解其实现方法。

3.1 案例一

案例场景为 [[设计模式——模版方法#^1e1832 | 2 中举的查询数据库业务的例子]]。

1、先写模版类QueryServiceTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class QueryServiceTemplate {  

public Data query(String condition){
// 从数据库中查询数据
Data data = queryFromDB(condition);

// 对查出来的数据进行业务操作
Data res = doSomething(data);
return res;
}

abstract protected Data queryFromDB(String condition);

private Data doSomething(Data data){
// ...
return data;
}
}

2、实现通过MySQL的查询

1
2
3
4
5
6
7
8
public class MySqlQueryService extends QueryServiceTemplate{  
@Override
public Data queryFromDB(String condition) {
// 从mysql中查询数据
//...
return data;
}
}

3、如果要对其扩展,使用Oracle数据库查询呢?

1
2
3
4
5
6
7
8
public class OracleQueryService extends QueryServiceTemplate{  
@Override
public Data queryFromDB(String condition) {
// 从oracle中查询数据
//...
return data;
}
}

4、具体的使用

1
2
3
4
5
6
7
8
9
10
11
public class Test {  
public static void main(String[] args) {
// 使用MySQL
QueryServiceTemplate mySqlQueryService = new MySqlQueryService();
Data data = mySqlQueryService.query("id == 1");

// 使用Oracle
OracleQueryService oracleQueryService = new OracleQueryService();
Data query = oracleQueryService.query("id == 1");
}
}

3.2 案例二

此案例由于是实际项目中的,要相对案例一复杂一点点。案例场景:在简单实现RPC的项目中的负载均衡部分的实现。已知负载均衡的策略有多种,切负载均衡的方案比较多,比如轮训、最短响应时间、一致性哈希等方案。由于业务不是本文的重点,因此不多介绍其业务相关的内容。先看看用例图:

Pasted image 20240607123446

1、同样,第一步先创建模版类

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

// 用于保存各个服务和其对应的选择器
private final Map<String, LoadBalanceSelector> SERVICE_SELECTOR_CACHE = new ConcurrentHashMap<>();

public InetSocketAddress selectServiceAddr(String serviceName) {
log.debug("Get the node serving: {}", serviceName);
LoadBalanceSelector selector = SERVICE_SELECTOR_CACHE.get(serviceName);
if (selector == null) {
List<InetSocketAddress> inetSocketAddresses = XRpcBootStrap.getInstance().getConfiguration().getRegistry().lookUp(serviceName);

// 调用子类的获取负载均衡选择器方法
selector = getLoadBalanceSelector(inetSocketAddresses);
SERVICE_SELECTOR_CACHE.put(serviceName, selector);
}

return selector.getNext();
}


protected abstract LoadBalanceSelector getLoadBalanceSelector(List<InetSocketAddress> inetSocketAddresses);
}

2、实现轮训负载均衡策略的负载均衡器

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
@Slf4j  
public class RoundRobinLoadBalancer extends AbstractLoadBalancer{
@Override
protected LoadBalanceSelector getLoadBalanceSelector(List<InetSocketAddress> inetSocketAddresses) {
return new RoundRobinSelector(inetSocketAddresses);
}


// 负载均衡选择器
private static class RoundRobinSelector implements LoadBalanceSelector{
private final List<InetSocketAddress> serviceList;
private final AtomicInteger idx;

public RoundRobinSelector(List<InetSocketAddress> serviceList) {
this.serviceList = serviceList;
this.idx = new AtomicInteger(0);
}

@Override
public InetSocketAddress getNext() {
if(serviceList == null || serviceList.isEmpty()){
log.error("The service list is empty when a node is selected for load balancing");
throw new LoadBalancerException();
}

InetSocketAddress inetSocketAddress = serviceList.get(idx.get());
if(idx.get() == serviceList.size() - 1){
idx.set(0);
}else{
idx.incrementAndGet();
}
return inetSocketAddress;
}
}
}

3、调用部分

1
2
// 拿到服务节点的地址  
InetSocketAddress addr = conf.getLoadBalancer().selectServiceAddr(interfaceRef.getName());

4、如果此时还要扩展一个新的一致性哈希负载均衡器呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 代码较多,此处省略。本文的讨论重点不在于业务细节
@Slf4j
public class ConsistentHashLoadBalancer extends AbstractLoadBalancer{
@Override
protected LoadBalanceSelector getLoadBalanceSelector(List<InetSocketAddress> inetSocketAddresses) {
return new ConsistentHashSelector(inetSocketAddresses);
}

private static class ConsistentHashSelector implements LoadBalanceSelector {
@Override
public InetSocketAddress getNext() {
// ......
}
}
}

4. 模版方法的应用

4.1 抽象同步队列——AQS

提到模板方法的应用,首当其冲的就是AQS了。AQS就是典型的模版方法的使用案例。它将大部分的流程都已经实现,只提供了tryAcquire()tryRelease()tryAcquireShared()tryReleaseShared()isHeldExclusively()几个方法用于子类的实现。ReentrantLockCountDownLatchSemaphore等同步组件都是以它为基础实现的。如AQS 中的acquire(int arg)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class AbstractQueuedSynchronizer  
extends AbstractOwnableSynchronizer
implements java.io.Serializable {

public final void acquire(int arg) {
// 先尝试获取锁
if (!tryAcquire(arg) &&
// 若没抢到,则入队
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 若被中断,则进行自中断
selfInterrupt();
}

protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
}

其中tryAcquire()是抽象方法,即靠子类来实现细节。

4.2 JDK中的IO模块

此模块中用到模版方法的地方有很多,如InputStreamOutputStreamReaderWriter等。比如在InputStream中:

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
public abstract class InputStream implements Closeable {
public abstract int read() throws IOException;
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;
}
// 子类需要实现read
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;
}
}

4.3 集合

在集合模块中,也有很多地方用到了模版方法的设计模式。比如AbstractList

1
2
3
4
5
6
7
8
9
10
11
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
public boolean add(E e) {
add(size(), e);
return true;
}

public void add(int index, E element) {
throw new UnsupportedOperationException();
}

}

以上的 4.1 和4.3中的例子中,可以发现,模版类中的留给子类实现的方法也可以不作为抽象方法。而是抛出了一个不支持此操作的异常。为什么要这么做呢?

这是因为有时候模版方法定义的一些留给子类实现的方法,不一定要求每个子类都实现。
比如AQS中的一些供共享锁实现的方法,那么如果实现非共享锁的话,就无需再实现一遍,就可以这么操作。如果将其作为抽象方法的话。那么所有子类都必须要实现所有的抽象方法,包括无需实现的。会多出很多无效的代码。这个方法也可以在自己的项目中借鉴。

5. 总结

模板方法模式的一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过目标模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制框架的功能