定义
模板模式的定义简洁而有力:定义一个操作的算法骨架,将部分步骤延迟到子类中实现,从而让子类在不改变算法整体结构的前提下,重新定义某些特定步骤。这一模式的核心在于分离算法的通用流程与个性化实现,如同搭建一座房屋,先确定整体框架,再根据不同需求装修内部空间。
例如,在制作咖啡和茶的过程中,都有烧水、冲泡等相似步骤,但具体冲泡方式和添加配料各有不同。通过模板模式,我们可以将烧水、冲泡等通用步骤定义在父类的算法骨架中,而把添加咖啡粉、茶叶或特殊配料等个性化步骤交由子类实现,既保证了流程的一致性,又满足了多样化需求。
类图
角色
-
抽象类(Abstract Class):抽象类是模板模式的核心中枢,肩负着给出算法轮廓和骨架的重任。它由一个模板方法和若干基本方法构成。模板方法如同整个算法的指挥家,按照特定顺序调用各个基本方法,把控算法的整体流程;基本方法则是算法的具体组成部分,包括抽象方法、具体方法和钩子方法。
-
具体子类(Concrete Class):具体子类负责实现抽象类中定义的抽象方法和钩子方法,它们是算法顶级逻辑的具体组成步骤。通过实现这些方法,具体子类为算法注入了独特的个性,使其能够适应不同的业务场景和需求。
模板方法分类
-
抽象方法:在抽象类中声明,必须由具体子类实现。这些方法代表了算法中需要根据具体情况进行个性化定制的部分,如在制作饮品的例子中,冲泡的具体方式就是抽象方法,咖啡和茶的冲泡方式各不相同。
-
具体方法:在抽象类中已经实现,子类可以继承使用,也可以根据需要重写。具体方法实现了算法中相对固定、通用的部分,为子类提供了基础功能。
-
钩子方法:在抽象类中同样有实现,分为两种类型。一种是用于判断的逻辑方法,如判断是否满足某个条件再执行后续操作;另一种是需要子类重写的空方法,为子类提供了扩展点,子类可以根据实际需求决定是否重写钩子方法来增强算法功能。
-
模板方法:模板方法定义了算法的骨架,它将算法的各个步骤有序串联起来。在模板方法中,抽象类规定了算法执行的先后顺序,子类无需改变这个顺序,只需专注于实现抽象方法和钩子方法,从而确保算法的稳定性和一致性。
优缺点
优点
-
封装不变,扩展可变:模板模式将算法中不变的部分封装在父类中,可变的部分交由子类实现。这种设计方式使得代码的稳定性和可扩展性得到了很好的平衡,父类的算法骨架可以被多个子类复用,子类则能根据具体需求灵活扩展,避免了重复开发。
-
提取公共代码,提升复用性:通过在父类中提取公共代码,模板模式有效地减少了代码冗余。多个子类可以共享父类的通用方法,不仅提高了代码的复用率,还降低了维护成本,当需要修改公共代码时,只需在父类中进行修改,所有子类都能自动受益。
-
遵循开闭原则,便于拓展:模板模式允许子类通过实现抽象方法和钩子方法来增加新的功能,而无需修改父类的算法骨架。这完全符合开闭原则(对扩展开放,对修改关闭),使得系统在面对需求变化时能够轻松应对,具有良好的可维护性和扩展性。
缺点
-
子类数量增多,系统复杂度上升:由于每个不同的实现都需要定义一个子类,随着业务需求的增加,子类的数量会不断增长,导致系统变得庞大且复杂。这不仅增加了代码的管理难度,也使得开发者在理解和维护代码时需要花费更多精力。
-
反向控制结构,代码阅读困难:在模板模式中,父类的模板方法调用子类实现的抽象方法,子类的执行结果会影响父类的最终结果,形成一种反向的控制结构。这种结构与常规的代码执行逻辑有所不同,提高了代码阅读和理解的难度,尤其是对于初次接触模板模式的开发者来说,需要花费更多时间理清代码之间的调用关系。
应用场景
-
算法步骤固定,部分易变:当算法的整体步骤相对固定,但其中某些步骤容易发生变化时,模板模式是理想的选择。通过将易变的步骤抽象出来,由子类实现,既保证了算法的稳定性,又能快速适应变化。例如,在电商系统中,订单处理流程通常包括创建订单、支付、发货等固定步骤,但不同支付方式(如支付宝、微信支付)和发货策略(普通快递、加急快递)存在差异,此时就可以使用模板模式来处理。
-
提取子类公共行为:当多个子类存在公共行为时,利用模板模式将这些公共行为提取到父类中,可以有效避免代码重复。通过分析子类的代码,识别出相同的操作,并将其封装为父类的方法,子类只需继承父类并实现个性化部分,就能实现代码的复用和优化。
-
控制子类扩展:如果需要对子类的扩展进行控制,模板模式的钩子方法可以发挥重要作用。通过在模板方法中特定点调用钩子方法,只允许在这些指定点进行拓展,确保子类的扩展不会破坏算法的整体结构和逻辑。
实战案例
以常见的日志记录系统为例,日志记录通常包括获取日志信息、格式化日志、写入日志文件等步骤。我们可以使用模板模式构建日志记录框架:
-
抽象日志记录类:定义模板方法,包含获取日志信息、格式化日志、写入日志文件等步骤,并将格式化日志和写入日志文件定义为抽象方法,交由子类实现。
-
具体日志记录子类:如文本日志记录类、数据库日志记录类等,分别实现抽象类中的抽象方法,实现不同格式和存储方式的日志记录。例如,文本日志记录类将日志格式化为文本格式并写入文件,数据库日志记录类将日志信息存储到数据库中。
代码示例
-
抽象日志记录类(
AbstractLogger
):-
定义了一个模板方法
log()
,该方法按照获取日志信息、格式化日志、写入日志文件的顺序调用相应的方法,形成了日志记录的算法骨架。 -
getLogInfo()
方法提供了获取日志信息的默认实现,实际使用时可根据需求重写。 -
formatLog()
和writeLog()
是抽象方法,需要具体子类实现。
-
// 抽象日志记录类
abstract class AbstractLogger {
// 模板方法,定义日志记录的算法骨架
public final void log() {
String logInfo = getLogInfo();
String formattedLog = formatLog(logInfo);
writeLog(formattedLog);
}
// 获取日志信息,这里简单实现返回一个固定信息,实际可根据需求修改
protected String getLogInfo() {
return "This is a log message.";
}
// 抽象方法:格式化日志,由子类实现
protected abstract String formatLog(String logInfo);
// 抽象方法:写入日志,由子类实现
protected abstract void writeLog(String formattedLog);
}
-
文本日志记录类(
TextLogger
):-
继承自
AbstractLogger
,实现了formatLog()
方法,将日志信息格式化为文本日志格式。 -
实现了
writeLog()
方法,将格式化后的日志信息写入text_log.txt
文件。
-
import java.io.FileWriter;
import java.io.IOException;
// 文本日志记录类
class TextLogger extends AbstractLogger {
@Override
protected String formatLog(String logInfo) {
return "Text Log: " + logInfo;
}
@Override
protected void writeLog(String formattedLog) {
try (FileWriter writer = new FileWriter("text_log.txt", true)) {
writer.write(formattedLog + "\n");
System.out.println("Text log written to text_log.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
}
-
数据库日志记录类(
DatabaseLogger
):-
同样继承自
AbstractLogger
,实现了formatLog()
方法,将日志信息格式化为数据库日志格式。 -
实现了
writeLog()
方法,将格式化后的日志信息插入到数据库的logs
表中。
-
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
// 数据库日志记录类
class DatabaseLogger extends AbstractLogger {
@Override
protected String formatLog(String logInfo) {
return "Database Log: " + logInfo;
}
@Override
protected void writeLog(String formattedLog) {
String url = "jdbc:mysql://localhost:3306/your_database";
String username = "your_username";
String password = "your_password";
try (Connection connection = DriverManager.getConnection(url, username, password)) {
String sql = "INSERT INTO logs (log_message) VALUES (?)";
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, formattedLog);
preparedStatement.executeUpdate();
System.out.println("Database log written to database");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
-
测试(
Main
):创建了TextLogger
和DatabaseLogger
的实例,并调用它们的log()
方法进行日志记录测试。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
// 数据库日志记录类
class DatabaseLogger extends AbstractLogger {
@Override
protected String formatLog(String logInfo) {
return "Database Log: " + logInfo;
}
@Override
protected void writeLog(String formattedLog) {
String url = "jdbc:mysql://localhost:3306/your_database";
String username = "your_username";
String password = "your_password";
try (Connection connection = DriverManager.getConnection(url, username, password)) {
String sql = "INSERT INTO logs (log_message) VALUES (?)";
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, formattedLog);
preparedStatement.executeUpdate();
System.out.println("Database log written to database");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
通过这种方式,使用模板模式实现了日志记录框架,不同的日志记录方式可以通过具体子类灵活扩展,而日志记录的整体流程保持不变。
总结
模板模式凭借其独特的设计理念和强大的功能,在软件开发中发挥着重要作用。它帮助开发者构建出稳定、可复用且易于扩展的算法框架,尽管存在一些缺点,但在合适的应用场景下,其优势远远超过劣势。掌握模板模式,将为开发者在面对复杂算法设计时提供有力的武器,提升代码的质量和开发效率。