Java 中的访问者(Visitor)模式是一种行为型设计模式,它将数据结构与数据操作分离,使得在不修改数据结构的情况下可以增加新的操作。该模式主要包含以下几个角色:
优点:
缺点:
理解:
假设是一名老师,要去给不同的年级的学生上课。不同年级的学生,他们的学习能力不同,需要采取不同的教学方式。
现在你要上课了,步骤如下:
这里的关键点是:
这样做的好处是什么呢?
这样就可以很好地遵守"开闭原则",使得系统扩展相对容易,并提高代码的可维护性。
总的来说,访问者模式的核心思想就是:“将数据结构和作用于结构上的操作解耦,使得操作集合可相对自由地扩展”。这样不仅令系统数据结构的扩展更加灵活,也使给定的操作集更具统一性。
在实际项目中,访问者模式常常被用于需要对一组异构元素执行不同操作的场景。下面是一个在报表系统中应用访问者模式的场景。
需求描述:我们需要开发一个报表系统,可以生成不同类型的报表,包括表格报表(TabularReport)和数据透视表报表(PivotTableReport)。每种报表都需要支持多种输出格式,如Excel、PDF和HTML。
使用访问者模式的优势:
// 抽象访问者,定义访问表格报表和数据透视表报表的方法。
interface ReportVisitor {
void visitTabularReport(TabularReport report);
void visitPivotTableReport(PivotTableReport report);
}
// 具体访问者 - Excel 输出
class ExcelVisitor implements ReportVisitor {
@Override
public void visitTabularReport(TabularReport report) {
// 生成 Excel 表格报表
System.out.println("Generating Excel tabular report...");
}
@Override
public void visitPivotTableReport(PivotTableReport report) {
// 生成 Excel 数据透视表报表
System.out.println("Generating Excel pivot table report...");
}
}
// 具体访问者 - PDF 输出
class PdfVisitor implements ReportVisitor {
// ...
}
// 具体访问者 - HTML 输出
class HtmlVisitor implements ReportVisitor {
// ...
}
// 抽象元素
interface Report {
void accept(ReportVisitor visitor);
}
// 具体元素 - 表格报表
class TabularReport implements Report {
private String data;
public TabularReport(String data) {
this.data = data;
}
@Override
public void accept(ReportVisitor visitor) {
visitor.visitTabularReport(this);
}
// 其他方法...
}
// 具体元素 - 数据透视表报表
class PivotTableReport implements Report {
private String data;
public PivotTableReport(String data) {
this.data = data;
}
@Override
public void accept(ReportVisitor visitor) {
visitor.visitPivotTableReport(this);
}
// 其他方法...
}
// 客户端代码
public class Client {
public static void main(String[] args) {
List<Report> reports = new ArrayList<>();
reports.add(new TabularReport("Tabular report data"));
reports.add(new PivotTableReport("Pivot table report data"));
ReportVisitor excelVisitor = new ExcelVisitor();
for (Report report : reports) {
report.accept(excelVisitor);
}
// 也可以使用其他访问者生成 PDF 或 HTML 报表
}
}
ReportVisitor 接口定义了访问表格报表和数据透视表报表的方法。ExcelVisitor、PdfVisitor 和 HtmlVisitor 是具体的访问者实现,分别用于生成不同格式的报表。Report 接口定义了接受访问者访问的方法 accept()。TabularReport 和 PivotTableReport 是两个具体的报表实现,它们实现了 accept() 方法,将自身作为参数传递给访问者的访问操作。通过使用访问者模式,我们可以很方便地添加新的报表类型或输出格式,而无需修改现有代码。例如,如果需要添加一种新的报表类型,只需创建一个新的具体报表类并实现 Report 接口即可。如果需要添加一种新的输出格式,只需创建一个新的具体访问者类并实现 ReportVisitor 接口即可。
MyBatis 中,访问者模式被广泛地用于处理映射文件(Mapper XML)的解析和执行 SQL 查询操作。具体来说,MyBatis 使用了访问者模式来实现对 Mapper XML 文件中定义的不同元素(如 select、insert、update、delete 等)的解析和执行
在 MyBatis 中,访问者模式的使用主要集中在 org.apache.ibatis.parsing 包中,用于解析映射配置文件和动态 SQL 语句。我们重点分析 GenericTokenParser 类和相关组件。
1. 抽象访问者和抽象元素
在 MyBatis 中,抽象访问者和抽象元素分别定义在 TokenHandler 和 Token 接口中:
// 抽象访问者
public interface TokenHandler {
String handleToken(String content);
}
// 抽象元素
public interface Token {
String getContent();
void accept(TokenHandler handler);
}
TokenHandler 接口定义了访问者如何处理标记的方法。Token 接口定义了元素如何接受访问者的访问操作。2. 具体访问者和具体元素
MyBatis 提供了一些具体的访问者和元素实现,例如:
// 具体访问者 - 处理变量标记
public class VariableTokenHandler implements TokenHandler {
private PropertyParser propertyParser;
public VariableTokenHandler(Properties properties) {
this.propertyParser = new PropertyParser(properties);
}
@Override
public String handleToken(String content) {
return propertyParser.parse(content);
}
}
// 具体元素 - 变量标记
public class VariableToken implements Token {
private String content;
public VariableToken(String content) {
this.content = content;
}
@Override
public String getContent() {
return content;
}
@Override
public void accept(TokenHandler handler) {
replaceBy(handler.handleToken(content));
}
// ...
}
VariableTokenHandler 是一个具体的访问者实现,用于处理变量标记。VariableToken 是一个具体的元素实现,表示一个变量标记,它会将自身传递给访问者进行处理。3. 使用访问者模式解析标记
在 GenericTokenParser 类中,MyBatis 使用访问者模式解析配置文件和动态 SQL 语句中的标记。以下是关键代码:
public class GenericTokenParser {
private final String openToken;
private final String closeToken;
private final TokenHandler handler;
// ...
public String parse(String text) {
// ...
int start = text.indexOf(openToken);
int end = text.indexOf(closeToken, start + openToken.length());
if (start > -1 && end > start) {
StringBuilder builder = new StringBuilder();
// ...
// 创建标记元素并让访问者处理它
Token token = new VariableToken(text.substring(start + openToken.length(), end));
token.accept(handler);
builder.append(handler.handleToken(token.getContent()));
// ...
}
// ...
}
}
在 parse 方法中,MyBatis 会解析文本,识别出标记的起始和结束位置。然后,它会创建一个具体的标记元素(如 VariableToken)。接下来,它会让具体的访问者(如 VariableTokenHandler)访问和处理这个标记元素。
通过这种方式,MyBatis 将标记的解析操作和标记的数据结构分离,符合访问者模式的设计思想。
4. 扩展访问者和元素
由于 MyBatis 使用了访问者模式,因此扩展新的标记类型和处理逻辑变得非常方便。只需要实现新的具体访问者和具体元素,并在 GenericTokenParser 中进行调用即可。
例如,如果需要添加一种新的标记类型 MyToken,我们可以创建如下的具体访问者和具体元素:
// 具体访问者
public class MyTokenHandler implements TokenHandler {
@Override
public String handleToken(String content) {
// 处理 MyToken 的逻辑
return "...";
}
}
// 具体元素
public class MyToken implements Token {
private String content;
public MyToken(String content) {
this.content = content;
}
@Override
public String getContent() {
return content;
}
@Override
public void accept(TokenHandler handler) {
// 将自身传递给访问者
if (handler instanceof MyTokenHandler) {
replaceBy(handler.handleToken(content));
}
}
}
然后,在 GenericTokenParser 中添加相应的处理逻辑:
public String parse(String text) {
// ...
if (isMyToken(text)) {
Token token = new MyToken(extractContent(text));
token.accept(myTokenHandler);
// ... 处理结果
}
// ...
}