访问者模式
一、介绍
Java 中的访问者(Visitor)模式是一种行为型设计模式,它将数据结构与数据操作分离,使得在不修改数据结构的情况下可以增加新的操作。该模式主要包含以下几个角色:
- 抽象访问者(Visitor): 定义了一个访问具体元素的接口,为每个具体元素类声明一个访问操作。
- 具体访问者(ConcreteVisitor): 实现了抽象访问者角色所声明的接口,定义了相应的访问操作。
- 抽象元素(Element): 声明一个接受访问操作的接口,这个接口的入口参数是抽象访问者角色。
- 具体元素(ConcreteElement): 实现了抽象元素角色所定义的接受访问操作的接口。
- 对象结构(ObjectStructure): 主要是用来存储元素对象的容器,提供让访问者对象遍历容器中所有元素的方法。
优点:
- 符合单一职责原则,将数据结构与数据操作分离,提高了代码的可维护性。
- 增加新的操作非常方便,只需要添加一个新的访问者即可,而不需要修改已有的数据结构代码。
- 访问者模式使得数据结构对象的操作与维护相分离,符合开放-封闭原则。
缺点:
- 增加新的数据结构类比较困难,因为每增加一个新的数据结构类,就需要修改所有的访问者实现。
- 具体元素对访问者公布细节,会带来一些对象状态的透明性问题。
- 访问者模式具有一定的复杂性,使用不当会增加系统的复杂度。
理解:
假设是一名老师,要去给不同的年级的学生上课。不同年级的学生,他们的学习能力不同,需要采取不同的教学方式。
- 首先,学生就是我们的"元素(Element)“,不同年级的学生就是不同的"具体元素(ConcreteElement)”。
- 然后,你作为老师就相当于一个"访问者(Visitor)“。不同的授课方式就是"具体访问者(ConcreteVisitor)”。
现在你要上课了,步骤如下:
- 你(访问者)进入一个教室(对象结构),里面坐着不同年级的学生们(元素们)。
- 你会观察一下教室里都有哪些年级的学生,比如一年级学生、二年级学生等等。
- 根据不同年级学生的情况,你会采取不同的授课方式。比如对一年级生,你会使用简单生动的教学方式;对二年级生,你就可以讲一些深入的知识了。
这里的关键点是:
- 不同年级的学生(元素)并不知道你(访问者)会采取什么样的授课方式,它们只知道接受授课。
- 而你作为老师(访问者),可以根据不同年级的学生采取不同的授课方式。
这样做的好处是什么呢?
- 新增一个年级的学生(元素)非常容易,只需要新增一个"具体元素"就行了,不需要修改之前所有的"访问者"。
- 如果要新增一种授课方式(访问者),你只需要新增一个"具体访问者"就行了,不需要修改所有的"元素"。
这样就可以很好地遵守"开闭原则",使得系统扩展相对容易,并提高代码的可维护性。
总的来说,访问者模式的核心思想就是:“将数据结构和作用于结构上的操作解耦,使得操作集合可相对自由地扩展”。这样不仅令系统数据结构的扩展更加灵活,也使给定的操作集更具统一性。
二、报表系统开发
在实际项目中,访问者模式常常被用于需要对一组异构元素执行不同操作的场景。下面是一个在报表系统中应用访问者模式的场景。
需求描述:我们需要开发一个报表系统,可以生成不同类型的报表,包括表格报表(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()
方法,将自身作为参数传递给访问者的访问操作。- 在客户端代码中,我们创建了一个报表列表,包含表格报表和数据透视表报表。然后创建了一个 Excel 访问者,并让它访问每个报表元素,生成相应的 Excel 报表。
通过使用访问者模式,我们可以很方便地添加新的报表类型或输出格式,而无需修改现有代码。例如,如果需要添加一种新的报表类型,只需创建一个新的具体报表类并实现 Report
接口即可。如果需要添加一种新的输出格式,只需创建一个新的具体访问者类并实现 ReportVisitor
接口即可。
三、MyBatis中如何使用访问者模式?
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);
// ... 处理结果
}
// ...
}
标签:Java,String,元素,content,源码,访问者,public,报表
From: https://blog.csdn.net/weixin_44716935/article/details/136672151