个人主页:danci_
系列专栏:《设计模式》
制定明确可量化的目标,坚持默默的做事。
转载自:探索设计模式的魅力:揭开访问者模式的神秘面纱-轻松增强对象行为
探索设计模式的魅力:揭开访问者模式的神秘面纱轻松增强对象行为
访问者模式(Visitor Pattern)是一种将算法与对象结构分离的设计模式。它允许我们为对象结构中的各种元素添加新的操作,而无需修改这些元素的类。以下是两个典型场景,其中访问者模式非常适合使用。
下面我们来实现文档编辑器的格式化操作 ✏️。
用一坨坨代码来实现,可以通过面向对象的继承和多态性来实现这个场景。下面是一个简化的Java实现示例:
// 抽象类 TextElement 表示文档中的文本元素
public abstract class TextElement {
// 添加样式的抽象方法
public abstract void addStyle(String style);
// 导出为特定格式的抽象方法
public abstract String exportTo(String format);
}
// 段落类
public class Paragraph extends TextElement {
private String text;
public Paragraph(String text) {
this.text = text;
}
@Override
public void addStyle(String style) {
// 实现段落的样式添加逻辑
System.out.println("Added style " + style + " to paragraph.");
}
@Override
public String exportTo(String format) {
// 实现段落的导出逻辑
if ("html".equals(format)) {
return ""
+ text + "";
} else if ("pdf".equals(format)) {
// 这里简化处理,实际导出PDF会更复杂
return "Paragraph text for PDF: " + text;
}
return "Unsupported format";
}
}
public class Image extends TextElement {
private String url;
public Image(String url) {
this.url = url;
}
@Override
public void addStyle(String style) {
// 图片可能不支持样式,或者支持有限的样式
System.out.println("Added style (if applicable) " + style + " to image.");
}
@Override
public String exportTo(String format) {
// 实现图片的导出逻辑
if ("html".equals(format)) {
return " + url + "\" />";
} else if ("pdf".equals(format)) {
// 简化处理
return "Image URL for PDF: " + url;
}
return "Unsupported format";
}
}
类似地,可以创建 Table 类等其他文本元素的子类…
4. DocumentEditor 类表示文档编辑器,包含一组文本元素,并提供格式化和导出功能
public class DocumentEditor {
private List<TextElement> elements = new ArrayList<>();
public void addElement(TextElement element) {
elements.add(element);
}
public void addStyleToAll(String style) {
for (TextElement element : elements) {
element.addStyle(style);
}
}
public String exportDocumentTo(String format) {
StringBuilder sb = new StringBuilder();
for (TextElement element : elements) {
sb.append(element.exportTo(format));
}
return sb.toString();
}
}
在这个实现中,我们避免了使用设计模式,而是直接利用了Java的面向对象特性。每个文本元素都是一个TextElement的子类,并且实现了自己的样式添加和导出逻辑。DocumentEditor类负责管理这些元素,并提供对整个文档进行样式添加和导出的功能。
虽然上述实现没有使用设计模式,但也体现出了如下优点:
然而,没有复杂的设计下体现上述优点的同时也伴随着一些潜在的缺点,比如代码的可维护性、灵活性和可扩展性可能会受到限制。对于更大或更复杂的项目,可能需要考虑使用设计模式和其他高级技术来改善代码的结构和质量。
缺点(问题)下面逐一分析:
违反的设计原则(问题)下面逐一分析:
️单一职责原则(Single Responsibility Principle, SRP):
TextElement 类可能违反了单一职责原则,因为它同时负责样式添加和导出功能。这意味着如果需要修改样式添加或导出的逻辑,可能会影响到TextElement类的其他部分。
DocumentEditor 类也可能违反了这一原则,因为它既负责管理文本元素,又负责样式添加和文档导出。
️开闭原则(Open/Closed Principle, OCP):
正如之前提到的,如果需要添加新的文本元素类型或新的导出格式,可能需要修改现有的TextElement子类或exportTo方法。这违反了开闭原则,即对扩展开放,对修改封闭。
️里氏替换原则(Liskov Substitution Principle, LSP):
在这个实现中,没有明显的违反里氏替换原则的情况,因为所有的子类(如Paragraph和Image)都可以替换它们的基类TextElement而不影响程序的正确性。但是,如果某些子类不支持特定的样式或导出格式,而基类的方法假设所有子类都支持,这可能会在未来导致问题。
️接口隔离原则(Interface Segregation Principle, ISP):
由于没有使用接口来定义行为,这个原则在这里不太适用。但是,如果我们把TextElement看作是一个接口(尽管它是一个抽象类),那么它可能违反了接口隔离原则,因为它包含了添加样式和导出两种不相关的行为。
️依赖倒置原则(Dependency Inversion Principle, DIP):
在这个实现中,高层模块(如DocumentEditor)依赖于低层模块(如具体的TextElement子类),而不是依赖于抽象。这意味着如果添加新的文本元素类型,可能需要修改DocumentEditor类来适应新的类型。这违反了依赖倒置原则,即高层模块不应该依赖于低层模块,它们都应该依赖于抽象。
️迪米特法则(Law of Demeter, LoD)或最少知识原则(Least Knowledge Principle, LKP):
DocumentEditor类直接与TextElement及其子类交互,这可能违反了迪米特法则,因为它可能知道太多关于这些类的内部细节。理想情况下,DocumentEditor应该只通过TextElement的公共接口与之交互。
为了改善上述的实现设计,考虑引入接口来定义行为,使用设计模式(如访问者模式)来分离操作和数据结构,以及遵循上述设计原则来组织代码。这样可以提高代码的可维护性、可扩展性和灵活性。
访问者模式(Visitor Pattern)是一种行为设计模式,它允许你在不改变各类的前提下定义新的操作,即在不修改已存在的类的结构的情况下增加新的操作。 |
分析关键因素
是否适合使用访问者模式的场景中,我们可以考虑以下几个关键因素:
当我们识别到一个系统中存在多种类型的对象、需要对这些对象执行多种不同的操作、并且希望在不修改对象类的情况下添加新的操作时,就可以考虑使用访问者模式。在文档编辑器的场景中,这些条件都得到了满足,因此访问者模式是一个合适的设计选择。
分析适用原因
在这个复杂的文档编辑器场景中,访问者模式非常适用,原因主要有以下几点:
多种类型的文本元素:
文档编辑器支持段落、图片、表格等多种文本元素,每种元素可能需要不同的处理方式。访问者模式允许我们定义多个访问者类,每个类专门处理一种类型的元素,从而实现操作的分离和专业化。
操作易于变化:
对文档进行格式化、样式调整或导出为不同格式时,操作可能会随着用户需求的变化而频繁更改。访问者模式通过将操作逻辑封装在独立的访问者类中,使得这些变化可以独立于文档结构进行,从而降低了代码的耦合性,提高了系统的可维护性。
不改变元素类:
访问者模式允许我们在不修改现有元素类的情况下增加新的操作。这意味着当需要添加新的格式化选项或导出格式时,我们不需要改动段落、图片、表格等类的代码,只需创建新的访问者类即可。这符合开闭原则,即对扩展开放,对修改封闭。
单一职责原则:
每个访问者类只负责一种特定的操作,比如添加样式或导出为特定格式。这使得代码更加清晰、易于理解和维护。同时,这也便于团队成员之间的分工协作,不同的人员可以专注于不同的访问者实现。
灵活性:
访问者模式提供了很大的灵活性,允许我们在运行时动态地改变元素的操作行为。例如,我们可以根据用户的选择来切换不同的访问者,以实现不同的格式化或导出效果。
访问者模式在处理多种类型文本元素且操作易于变化的场景中具有显著优势。在文档编辑器的例子中,通过定义适当的访问者和元素接口,我们可以实现一个可扩展、可维护且高度灵活的系统。
主要组件:
访问者接口(Visitor Interface):
- 定义了对每一个具体元素类(ConcreteElement)的访问操作。
- 通常包含多个visit方法,每个方法对应一个具体元素类。
具体访问者(Concrete Visitor):
- 实现了访问者接口,并为每一种具体元素类提供了相应的访问操作。
- 包含了对各种元素执行具体操作的业务逻辑。
抽象元素(Element):
- 定义了一个accept方法,用于接受访问者对象。
- 该方法通常接受一个访问者接口类型的参数。
具体元素(Concrete Element):、
- 实现了元素接口,并提供了accept方法的具体实现。
- 在accept方法中调用访问者对象的对应visit方法。
对象结构(Object Structure):
- 是一个元素的集合,如列表、组合结构等。
- 可以遍历其包含的元素,并调用它们的accept方法以接受访问者。
客户端(Client):
- 创建访问者对象,并将其传递给对象结构中的元素进行访问。
- 负责控制访问过程的开始和结束。
交互和通信过程:
初始化阶段:
- 客户端创建访问者对象。
- 客户端获取或创建对象结构,并向其添加元素对象。
客户端调用对象结构的方法:
-客户端调用对象结构中的方法(例如visitAll),该方法负责遍历所有的接受者对象。
访问阶段:
对象结构遍历其所包含的所有接受者对象,对每个接受者对象执行以下步骤。
-对象结构依次遍历其内部的元素,对于每个元素:
-元素对象调用其accept方法,并传入访问者对象。
接受者调用访问者的方法:
对于每个接受者对象,对象结构调用其accept方法,并将访问者对象
- 访问者对象根据元素的具体类型调用相应的visit方法。作为参数传入。
- visit方法内,访问者可以对元素执行特定的操作。
完成阶段:
- 一旦所有元素都被访问过,访问过程结束。
接受者对象与访问者交互:
在accept方法中,接受者对象调用访问者对象的相应方法(例如visitElement),并将自己作为参数传入。
- 客户端可以销毁或重用访问者和对象结构。
访问者执行操作:
访问者在被调用的方法中执行针对接受者对象的操作。
要使用访问者模式重构上述场景,我们首先需要定义一个访问者接口,用于表示可以对文本元素执行的操作。然后,我们为每个具体的操作创建访问者实现。此外,我们还需要在文本元素类层次结构中引入一个接受访问者的方法。
下面是一个使用访问者模式实现的文档编辑器的格式化操作 ✏️的示例:
public interface TextElementVisitor {
void visit(Paragraph paragraph);
void visit(Image image);
// 可以添加更多的visit方法以处理其他类型的TextElement
}
public class ExporterVisitor implements TextElementVisitor {
private StringBuilder export;
public ExporterVisitor() {
this.export = new StringBuilder();
}
public void visit(Paragraph paragraph) {
export.append(""
).append(paragraph.getText()).append("\n");
}
public void visit(Image image) {
export.append(").append(image.getSource()).append("\" />\n");
}
public String getExport() {
return export.toString();
}
}
public class StylerVisitor implements TextElementVisitor {
private String style;
public StylerVisitor(String style) {
this.style = style;
}
public void visit(Paragraph paragraph) {
paragraph.setStyle(style);
}
// 假设Image不支持设置样式,因此不实现该方法
public void visit(Image image) {
// Do nothing or throw an UnsupportedOperationException
}
}
public abstract class TextElement {
public abstract void accept(TextElementVisitor visitor);
// 其他公共方法和属性
}
public class Paragraph extends TextElement {
private String text;
private String style; // 可以添加更多属性和方法
public Paragraph(String text) {
this.text = text;
}
public void setStyle(String style) {
this.style = style;
}
public String getText() {
return text;
}
public void accept(TextElementVisitor visitor) {
visitor.visit(this);
}
}
public class Image extends TextElement {
private String source; // 可以添加更多属性和方法
public Image(String source) {
this.source = source;
}
public String getSource() {
return source;
}
public void accept(TextElementVisitor visitor) {
visitor.visit(this);
}
}
public class DocumentEditor {
private List<TextElement> elements = new ArrayList<>();
public void addElement(TextElement element) {
elements.add(element);
}
public void acceptVisitor(TextElementVisitor visitor) {
for (TextElement element : elements) {
element.accept(visitor);
}
}
}
public class ClientCode {
public static void main(String[] args) {
DocumentEditor editor = new DocumentEditor();
editor.addElement(new Paragraph("Hello, World!"));
editor.addElement(new Image("path/to/image.jpg"));
// 使用ExporterVisitor导出文档
ExporterVisitor exporter = new ExporterVisitor();
editor.acceptVisitor(exporter);
System.out.println(exporter.getExport());
// 使用StylerVisitor为段落添加样式(注意:这个示例中Image不支持样式)
StylerVisitor styler = new StylerVisitor("bold");
editor.acceptVisitor(styler);
// 这里需要额外的逻辑来验证或处理样式是否已正确应用
}
}
在这个重构后的示例中,我们定义了一个TextElementVisitor接口,并为导出和添加样式创建了两个具体的访问者实现。文本元素类现在有一个accept方法,它接受一个访问者并调用访问者的相应visit方法。DocumentEditor类使用acceptVisitor方法来应用访问者到其包含的文本元素上。
这种方式的好处是,我们可以很容易地添加新的访问者来实现新的操作,而无需修改现有的文本元素类。这提高了代码的可扩展性和可维护性。同时,每个访问者都可以专注于自己的职责,实现了单一职责原则。
优点
使用访问者模式重构后可以有效地解决 1.3 痛点 中提到的一些缺点。通过引入访问者设计模式解决了若干设计原则的应用问题,具体体现在以下几个方面:
然而,值得注意的是,访问者模式并非没有缺点。它可能违反迪米特法则(Law of Demeter),因为具体元素类(如Paragraph和Image)需要了解并直接调用访问者的方法,这增加了它们之间的耦合度。此外,如果频繁添加新的文本元素类型或新的操作,访问者模式可能会导致类的数量迅速增加,从而增加系统的复杂性。
总的来说,访问者模式在上述实现中通过引入抽象访问者和具体访问者类来分离操作和对象结构,从而提高了代码的可扩展性、可维护性和可读性。这符合开闭原则、单一职责原则和依赖倒置原则等面向对象设计原则的要求。如下更多优点:
缺点
上述实现虽然有许多优点,但也存在一些潜在的缺点,这些缺点通常与访问者设计模式的特性和使用场景相关:
注:本文只转载部分内容,三连 或 更多请跳转原文。