模板方法模式
定义
模板方法模式 是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。但调用将以抽象类中定义的方式进行。
模板方法模式建议将算法或者一个系统的某个功能分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象
的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。
模板方法模式结构
- 抽象类 (AbstractClass) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为
抽象
类型, 也可以提供一些默认实现。 - 具体类 (ConcreteClass) 可以重写所有步骤, 但不能重写模板方法自身。
模板方法模式适合应用场景
- 当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
- 模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。
- 当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。
- 在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。 子类间各不同的代码可继续保留在子类中。
实现方式
- 分析目标算法或者系统的某个功能, 确定能否将其分解为多个步骤。 从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同。
- 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 可用
final
最终修饰模板方法以防止子类对其进行重写。 - 虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法。
- 可考虑在算法的关键步骤之间添加钩子。
- 为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。
与其他模式的关系
- 工厂方法模式 是模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。
- 模板方法]基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略模式基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。
使用示例
****模版方法模式在 Java 框架中很常见。 开发者通常使用它来向框架用户提供通过继承实现的、 对标准功能进行扩展的简单方式。
这里是一些核心 Java 程序库中模版方法的示例:
java.io.InputStream
、java.io.OutputStream
、java.io.Reader
和java.io.Writer
的所有非抽象方法。java.util.AbstractList
、java.util.AbstractSet
和java.util.AbstractMap
的所有非抽象方法。javax.servlet.http.HttpServlet
, 所有默认发送 HTTP 405 “方法不允许” 错误响应的doXXX()
方法。 你可随时对其进行重写。
识别方法: 模版方法可以通过行为方法来识别, 该方法已有一个在基类中定义的 “默认” 行为。
Spring的整个继承体系都基本用到模板方法;
- BeanFactory
- JdbcTemplate
- RedisTemplate
我们自己的系统也应该使用模板方法组织类结构
示例
问题空间
假如你正在开发一款分析公司文档的数据挖掘程序。 用户需要向程序输入各种格式 (PDF、 DOC 或 CSV) 的文档, 程序则会试图从这些文件中抽取有意义的数据, 并以统一的格式将其返回给用户。
该程序的首个版本仅支持 DOC 文件。 在接下来的一个版本中, 程序能够支持 CSV 文件。 一个月后, 你 “教会” 了程序从 PDF 文件中抽取数据。
一段时间后, 你发现这三个类中包含许多相似代码。 尽管这些类处理不同数据格式的代码完全不同, 但数据处理和分析的代码却几乎完全一样。 如果能在保持算法结构完整的情况下去除重复代码, 这难道不是一件很棒的事情吗?
还有另一个与使用这些类的客户端代码相关的问题: 客户端代码中包含许多条件语句, 以根据不同的处理对象类型选择合适的处理过程。 如果所有处理数据的类都拥有相同的接口或基类, 那么你就可以去除客户端代码中的条件语句, 转而使用多态机制来在处理对象上调用函数。
解决方案空间
让我们考虑如何在数据挖掘应用
中用模板方法解决上述问题。 我们可为图中的三个解析算法创建一个基类, 该类将定义调用了一系列不同文档处理步骤的模板方法。
模板方法将算法分解为步骤, 并允许子类重写这些步骤, 而非重写实际的模板方法。
首先, 我们将所有步骤声明为 抽象
类型, 强制要求子类自行实现这些方法。 在我们的例子中, 子类中已有所有必要的实现, 因此我们只需调整这些方法的签名, 使之与超类的方法匹配即可。
现在, 让我们看看如何去除重复代码。 对于不同的数据格式, 打开和关闭文件以及抽取和解析数据的代码都不同, 因此无需修改这些方法。 但分析原始数据和生成报告等其他步骤的实现方式非常相似, 因此可将其提取到基类中, 以让子类共享这些代码。
正如你所看到的那样, 我们有两种类型的步骤:
- 抽象步骤必须由各个子类来实现
- 可选步骤已有一些默认实现, 但仍可在需要时进行重写
还有另一种名为钩子的步骤。 钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。
重写算法的标准步骤
在本例中, 模版方法模式定义了一个可与社交网络协作的算法。 与特定社交网络相匹配的子类将根据社交网络所提供的 API 来实现这些步骤。
- 类图如下
- 完整代码
package com.danshan.study.designpattern.templatemethod;
/**
* @className: Social
* @Description: 社交基类
* @技术博客:https://blog.csdn.net/qq_30621637?spm=1011.2124.3001.5343
* @微信公众号:bybyte01
* @version: v1.0.0
* @author: fajian.chen
* @date: 2024/9/27 13:34
*/
public abstract class Social {
protected String userName;
protected String password;
Social() {}
/**
* 发送消息:登录->发送->退出
* @param message
* @return
*/
public boolean post(String message) {
if (logIn(this.userName, this.password)) {
boolean result = sendData(message.getBytes());
logOut();
return result;
}
return false;
}
/**
* 登录
* @param userName
* @param password
* @return
*/
abstract boolean logIn(String userName, String password);
/**
* 发送
* @param data
* @return
*/
abstract boolean sendData(byte[] data);
/**
* 退出
*/
abstract void logOut();
}
package com.danshan.study.designpattern.templatemethod;
/**
* @className: DingTalk
* @Description: TODO
* @技术博客:https://blog.csdn.net/qq_30621637?spm=1011.2124.3001.5343
* @微信公众号:bybyte01
* @version: v1.0.0
* @author: fajian.chen
* @date: 2024/9/27 13:33
*/
public class DingTalk extends Social {
public DingTalk(String userName, String password) {
this.userName = userName;
this.password = password;
}
public boolean logIn(String userName, String password) {
for (int i = 0; i < this.password.length(); i++) {
System.out.print("*");
}
simulateNetworkLatency();
return true;
}
public boolean sendData(byte[] data) {
boolean messagePosted = true;
if (messagePosted) {
System.out.println("Message: '" + new String(data) + "");
return true;
} else {
return false;
}
}
public void logOut() {
System.out.println("User: '" + userName + "'");
}
private void simulateNetworkLatency() {
try {
int i = 0;
System.out.println();
while (i < 10) {
System.out.print(".");
Thread.sleep(500);
i++;
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
package com.danshan.study.designpattern.templatemethod;
/**
* Class of social network
*/
public class WeChat extends Social {
public WeChat(String userName, String password) {
this.userName = userName;
this.password = password;
}
public boolean logIn(String userName, String password) {
for (int i = 0; i < this.password.length(); i++) {
System.out.print("*");
}
simulateNetworkLatency();
return true;
}
public boolean sendData(byte[] data) {
boolean messagePosted = true;
if (messagePosted) {
System.out.println("Message: '" + new String(data) + "'");
return true;
} else {
return false;
}
}
public void logOut() {
System.out.println("User: '" + userName + "' ");
}
private void simulateNetworkLatency() {
try {
int i = 0;
System.out.println();
while (i < 10) {
System.out.print(".");
Thread.sleep(500);
i++;
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
package com.danshan.study.designpattern.templatemethod;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @className: SocialDemoClient
* @Description: TODO
* @技术博客:https://blog.csdn.net/qq_30621637?spm=1011.2124.3001.5343
* @微信公众号:bybyte01
* @version: v1.0.0
* @author: fajian.chen
* @date: 2024/9/27 13:32
*/
public class SocialDemoClient {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("用户名: ");
String userName = reader.readLine();
System.out.print("密码: ");
String password = reader.readLine();
System.out.print("消息: ");
String message = reader.readLine();
System.out.println("\n选择 社交工具发送消息.\n" +
"1 - 微信\n" +
"2 - 钉钉");
int choice = Integer.parseInt(reader.readLine());
Social network = getSocial(choice, userName, password);
network.post(message);
}
private static Social getSocial(int choice, String userName, String password) {
if (choice == 1) {
return new WeChat(userName, password);
}
return new DingTalk(userName, password);
}
}
延伸阅读