一、引言
1、设计模式的简介
在软件工程的世界中,设计模式是解决常见问题的最佳实践。它们是经过时间检验的解决方案,可以帮助我们编写可重用的、易于维护的代码。设计模式是我们的工具箱中的一种工具,可以帮助我们更好地理解如何解决特定问题。
设计模式可分为三大类:
- 创建型模型:提供了创建对象的机制,能够提升已有代码的灵活性和可复用性。
- 结构型模式:介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效
- 行为模式:负责对象间的高效沟通和职责委派
本篇文章所讨论的模板方法模式正是行为模式类别中的一种。
2、模板方法模式的概述
在众多设计模式中,模板方法模式是一种非常有用的模式,它在许多现代框架和库中都有应用。模板方法模式是行为模式的一种,它在父类中定义类一个操作中的算法的骨架,将一些步骤延迟到子类中。子类可以在不改变算法结构的情况下,重新定义算法中的某些特定步骤。
让我们通过一个具体的例子来理解一下。假设我们正在开发一个大语言模型的通用客户端,他可以与各种大预言模型进行对话沟通,虽然模型不同,但基本的沟通过程是相同的。获取输入文字、通信、存储对话。
在这个过程中,对于不同的大预言模型,其发送文字,接受文字的细节可能不同,比如说要做不同的配置,调用不同的包来发送消息。但是获取输入文字、存储对话的方法是公用的。包括整个过程算法流程也都是一样的。这种情况下,我们可以使用模板方法模式来实现。
二、定义
1、定义
在我们深入探讨模板方法之前,让我们先明确一下它的定义。模版方法是一种行为设计模式,它在超类中定义了一个算法的骨架,允许子类在不改变算法结构的情况下重定义某些步骤。
Define the skeletion of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algrithm’s structure(定义一个操作中的算法骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。)
2、目的
在软件开发中,模板方法模式的目的是提供一种方法,是的算法有一致的结构,但具体的步骤可以有所不同。这样,我们就可以在不改变算法结构的情况下,灵活地修改或扩展算法的某些部分。
3、通用类图
2.1 模板方法模式通用类图
我们可以看到,一般的模板方法至少有两层,抽象层,和实现层。整个算法实现的主体在抽象层的templateMethod()中,算法的某些更小范围的实现会下放到子类中。因此,抽象层的方法称之为模板方法,实现层的方法被称为基本方法。
tips:为了防止恶意操作,一般模板方法都会加上final关键字,不允许被覆写。
三、实现
1、例子
我们仍然取上文中模板方法模式的概述中提到的例子。
假设我们正在开发一个大语言模型的通用客户端,他可以与各种大预言模型进行对话沟通,虽然模型不同,但基本的沟通过程是相同的。获取输入文字、通信、存储对话。
2、代码实现
在这个例子中,我们首先构造抽象层,我们需要实现一个抽象类ChatAbstract来定义整体流程,并可以在抽象层实现一些通用的过程。
public abstract class ChatAbstract{
public final String chat() {
//获取输入文本
String text = getText();
//通信
String talkText = connectModel(text);
//对话存储
store(talkText);
}
//获取文本实现方法
private String getText() {
String s;
//...
return s;
}
//通信,这个过程需要调用对应大模型的包,实现向大模型发送消息和接受消息的功能
private abstract String connectModel(String text);
//对话信息存储
private void store(String talkText) {
//....
}
}
然后我们就可以将connectModel方法下放到子类中,假设我们需要链接ChatGPT的模型,我们就可以创建一个ChatGPTChat类,继承ChatAbstract抽象类,实现connectModel方法
public class ChatGPTChat extends ChatAbstract{
private String connectModel(String text) {
// ...
}
}
或者其他大模型
public class ChatGLMChat extends ChatAbstract{
private String connectModel(String text) {
// ...
}
}
这里抽象类中的模板方法chat() 比较简单一点,实际上,我们可能需要进行多轮对话,然后需要拼接上一轮对话的text和新的输入文本,然后传入connectChat(text)方法中
public abstract class ChatAbstract{
final String chat() {
String text = "";
while(sign) {
//获取输入文本
String text += getText();
//通信
text = connectModel(text);
}
//对话存储
store(talkText);
}
//获取文本实现方法
private String getText() {
String s;
//...
return s;
}
//通信,这个过程需要调用对应大模型的包,实现向大模型发送消息和接受消息的功能
private abstract String connectModel(String text);
//对话信息存储
private void store(String talkText) {
//....
}
}
然后我们需要在子类中实现while循环中的connectModel方法。
在真实的项目中,模板方法的算法可能会更复杂写,这里只是加了个循环。
3、实现方式总结
- 分析目标算法,确定能否将其拆分为多个步骤。从所有子类的角度出发,考虑哪些步骤能够通用,哪些步骤各不相同。
- 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。在模板方法中根据算法结构依次调用相应步骤。可用final修饰模板方法以防止子类对其进行重写。
- 可以抽离一些公共逻辑并在抽象类中实现,子类就无需考虑公共部分的逻辑
- 可以考虑在算法的关键步骤之间添加钩子。(因为子类是实现模板方法中的部分内容,而对于有些关键的部分,可能会影响算法的流向。比如模板方法中存在if(check()),这个check()函数的返回值就会影响模板方法中的流向,我们可以把check() 函数下放到子类,允许子类覆写,子类可以通过这个函数去控制父类方法中的流向和结果,这种就叫做钩子)
- 为每个算法变体新建一个具体子类,它必须实现所有的抽象步骤,也可以重写邠可选步骤。
四、模板方法的优点和缺点
1、优点
- 你可仅允许客户端重写一个大型算法中特定的部分,使得算法其他部分修改对其造成的影响减小。
- 你可将重复代码提取到一个超类中。
2、缺点
- 部分客户端可能会受到算法框架的限制
- 通过子类抑制默认步骤可能会导致违反里氏替换原则(某些关键步骤是通过子类实现的,子类可能会影响了父类方法的含义)
- 模板方法中的步骤越多,其维护工作可能会越困难。
五、与其他模式的关系
模板方法模式和策略模式都是行为型设计模式,它们都封装了算法,使得算法可以独立于使用它的客户端而变化。然而,它们的主要区别在于控制权的位置和算法的实现方式。
模板方法模式:
模板方法模式在抽象类中定义了一个算法的骨架,并允许子类在不改变算法结构的情况下重定义算法的某些步骤。在这种模式中,抽象类通常包含一些具体的操作(即算法的一部分),并定义一些抽象的操作让子类实现。这样,算法的结构是在抽象类中定义的,而具体的实现是在子类中完成的。
策略模式:
策略模式定义了一系列的算法,并将每一个算法封装起来,使得它们可以互相替换。策略模式让算法独立于使用它的客户端。在策略模式中,客户端决定使用哪个算法,而每个算法都是在它自己的类中实现的。
比较:
- **控制权:**在模板方法模式中,控制权在抽象类中,因为算法的骨架是在抽象类中定义的。而在策略模式中,控制权在客户端,因为客户端决定使用哪个算法。
- **算法的实现:**在模板方法模式中,算法的一部分是在抽象类中实现的,而另一部分是在子类中实现的。而在策略模式中,每个算法都是在它自己的类中完全实现的。
- **扩展性:**模板方法模式通过继承来改变或扩展算法的部分步骤,而策略模式通过组合来改变算法。
- 总的来说,如果你需要改变算法的整体结构和顺序,策略模式可能是一个更好的选择。如果你只需要改变算法的某些部分,而其他部分保持不变,那么模板方法模式可能更适合。
六、鉴赏
1、AQS中模板方法的一处应用
熟悉Java并发编程的朋友应该知道大名鼎鼎的juc包,该包全称是java.util.concurrent,在这个包下面有很多锁,而在这些锁中,有个很关键的元素AbstractQueuedSynchronizer(AQS),这个元素支撑了很多锁的实现。看名称我们可以看出其是一个抽象类,在该类中有一个很关键获取锁的方法
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们可以看到这句注释
{@link #tryAcquire} but is otherwise uninterpreted and can represent anything you like.
{@link #tryAcquire},但没有解释,可以表示任何你喜欢的东西。
我们转而来到该方法中
/**
* Attempts to acquire in exclusive mode. This method should query
* if the state of the object permits it to be acquired in the
* exclusive mode, and if so to acquire it.
*
* <p>This method is always invoked by the thread performing
* acquire. If this method reports failure, the acquire method
* may queue the thread, if it is not already queued, until it is
* signalled by a release from some other thread. This can be used
* to implement method {@link Lock#tryLock()}.
*
* <p>The default
* implementation throws {@link UnsupportedOperationException}.
*
* @param arg the acquire argument. This value is always the one
* passed to an acquire method, or is the value saved on entry
* to a condition wait. The value is otherwise uninterpreted
* and can represent anything you like.
* @return {@code true} if successful. Upon success, this object has
* been acquired.
* @throws IllegalMonitorStateException if acquiring would place this
* synchronizer in an illegal state. This exception must be
* thrown in a consistent fashion for synchronization to work
* correctly.
* @throws UnsupportedOperationException if exclusive mode is not supported
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
我们可以看到,AQS中该方法的实现是直接扔出异常的,也就是说我们需要在子类中覆写该方法。
在ReentrantLock中分别以不同的方式实现了该方法,从而实现了公平锁与非公平锁
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
Sync类继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer{
//....
}
这也是模板方法模式在Java源码中的一个应用。