引言
访问者模式是一种行为设计模式,旨在解决对象结构与操作逻辑的耦合问题。在软件系统开发中,当面临复杂的对象结构(如多种类型对象组成的树形或图形结构),且需要对这些对象执行不同操作时,传统方式将操作直接写在对象类中会导致类职责过多,不利于维护和扩展。而访问者模式通过将操作与对象结构分离,允许在不改变现有对象结构的情况下定义新操作,元素接受访问者访问,访问者定义对不同类型元素的操作逻辑,从而为应对这种复杂情况提供了有效的解决方案。
一、定义与描述
访问者模式是一种行为设计模式,它允许你在不修改现有对象结构的情况下定义新的操作。这种模式将算法与对象结构分离,使得在增加新的操作时不需要对对象结构中的元素类进行修改。
二、抽象背景
在许多软件系统中,我们经常会遇到对一组对象进行操作的情况。这些操作可能会随着系统的发展而不断增加。如果直接在对象类中添加这些操作,会导致类的职责过重,违背单一职责原则,并且对象类的结构可能会变得复杂且难以维护。访问者模式的出现就是为了解决这个问题,它将这些操作封装到独立的访问者类中。
三、适用场景与现实问题解决
数据结构与操作分离
当我们有一个复杂的数据结构(如树形结构、图形结构),并且需要对这个结构中的元素执行多种不同的操作时,访问者模式非常有用。例如,在编译器中,对抽象语法树(AST)的操作,如类型检查、代码生成等,可以使用访问者模式将这些操作与AST的节点类分离。
操作易变场景
如果操作的逻辑经常变化,使用访问者模式可以方便地添加、修改或删除操作,而不会影响到对象结构。比如在一个电商系统中,对于商品的价格计算可能有多种方式(普通用户价格、会员价格、促销价格等),这些价格计算逻辑可以通过访问者模式与商品类分离。
四、访问者模式的现实生活的例子
医院看病
医院有不同类型的科室(如内科、外科、牙科等),这些科室可以看作是被访问的对象结构中的元素。而医生可以看作是访问者。不同的医生(不同的访问者)对不同科室(不同的元素)有不同的操作(诊断、治疗等)。例如,内科医生会对内科病人进行特定的检查和治疗,外科医生则对需要手术的病人进行操作,而不会影响科室的基本结构。
旅游景点
旅游景点有各种不同的景点(如古建筑、自然景观、游乐设施等),游客是访问者。不同的游客(如摄影师、历史学家、普通游客)对不同的景点有不同的行为(摄影师会拍照,历史学家会研究古建筑的历史,普通游客则是观光)。景点的结构(布局、设施等)不会因为游客的不同行为而改变。
五、初衷与问题解决
初衷是为了在不改变对象结构的情况下,方便地增加新的操作。通过将操作封装到访问者类中,解决了对象类职责过重、操作与对象结构耦合过强的问题。当需要添加新的操作时,只需要创建一个新的访问者类,而不需要修改对象结构中的元素类。
六、代码示例
Java示例
// 元素接口
interface Element {
void accept(Visitor visitor);
}
// 具体元素A
class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationA() {
return "ConcreteElementA operation";
}
}
// 具体元素B
class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationB() {
return "ConcreteElementB operation";
}
}
// 访问者接口
interface Visitor {
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
}
// 具体访问者
class ConcreteVisitor implements Visitor {
@Override
public void visit(ConcreteElementA element) {
System.out.println("ConcreteVisitor visits " + element.operationA());
}
@Override
public void visit(ConcreteElementB element) {
System.out.println("ConcreteVisitor visits " + element.operationB());
}
}
public class VisitorPatternJava {
public static void main(String[] args) {
Element elementA = new ConcreteElementA();
Element elementB = new ConcreteElementB();
Visitor visitor = new ConcreteVisitor();
elementA.accept(visitor);
elementB.accept(visitor);
}
}
类图:
流程图:
时序图:
C++示例
#include <iostream>
// 抽象元素类
class Element {
public:
virtual void accept(class Visitor* visitor) = 0;
};
// 具体元素A
class ConcreteElementA : public Element {
public:
void accept(Visitor* visitor) override;
std::string operationA() {
return "ConcreteElementA operation";
}
};
// 具体元素B
class ConcreteElementB : public Element {
public:
void accept(Visitor* visitor) override;
std::string operationB() {
return "ConcreteElementB operation";
}
};
// 抽象访问者类
class Visitor {
public:
virtual void visit(ConcreteElementA* element) = 0;
virtual void visit(ConcreteElementB* element) = 0;
};
// 具体访问者类
class ConcreteVisitor : public Visitor {
public:
void visit(ConcreteElementA* element) override {
std::cout << "ConcreteVisitor visits " << element->operationA() << std::endl;
}
void visit(ConcreteElementB* element) override {
std::cout << "ConcreteVisitor visits " << element->operationB() << std::endl;
}
};
void ConcreteElementA::accept(Visitor* visitor) {
visitor->visit(this);
}
void ConcreteElementB::accept(Visitor* visitor) {
visitor->visit(this);
}
int main() {
Element* elementA = new ConcreteElementA();
Element* elementB = new ConcreteElementB();
Visitor* visitor = new ConcreteVisitor();
elementA->accept(visitor);
elementB->accept(visitor);
delete elementA;
delete elementB;
delete visitor;
return 0;
}
Python示例
# 抽象元素类
class Element:
def accept(self, visitor):
pass
# 具体元素A
class ConcreteElementA(Element):
def accept(self, visitor):
visitor.visit(self)
def operationA(self):
return "ConcreteElementA operation"
# 具体元素B
class ConcreteElementB(Element):
def accept(self, visitor):
visitor.visit(self)
def operationB(self):
return "ConcreteElementB operation"
# 抽象访问者类
class Visitor:
def visit(self, element):
pass
# 具体访问者类
class ConcreteVisitor(Visitor):
def visit(self, element):
if isinstance(element, ConcreteElementA):
print(f"ConcreteVisitor visits {element.operationA()}")
elif isinstance(element, ConcreteElementB):
print(f"ConcreteVisitor visits {element.operationB()}")
if __name__ == "__main__":
elementA = ConcreteElementA()
elementB = ConcreteElementB()
visitor = ConcreteVisitor()
elementA.accept(visitor)
elementB.accept(visitor)
Go示例
package main
import (
"fmt"
)
// 元素接口
type Element interface {
accept(Visitor)
}
// 具体元素A
type ConcreteElementA struct{}
func (c *ConcreteElementA) accept(v Visitor) {
v.visit(c)
}
func (c *ConcreteElementA) operationA() string {
return "ConcreteElementA operation"
}
// 具体元素B
type ConcreteElementB struct{}
func (c *ConcreteElementB) accept(v Visitor) {
v.visit(c)
}
func (c *ConcreteElementB) operationB() string {
return "ConcreteElementB operation"
}
// 访问者接口
type Visitor interface {
visit(*ConcreteElementA)
visit(*ConcreteElementB)
}
// 具体访问者
type ConcreteVisitor struct{}
func (c *ConcreteVisitor) visit(elementA *ConcreteElementA) {
fmt.Printf("ConcreteVisitor visits %s\n", elementA.operationA())
}
func (c *ConcreteVisitor) visit(elementB *ConcreteElementB) {
fmt.Printf("ConcreteVisitor visits %s\n", elementB.operationB())
}
func main() {
elementA := &ConcreteElementA{}
elementB := &ConcreteElementB{}
visitor := &ConcreteVisitor{}
elementA.accept(visitor)
elementB.accept(visitor)
}
七、访问者模式的优缺点
优点
解耦操作与对象结构
使得操作和对象结构可以独立变化,提高了代码的可维护性和可扩展性。
易于添加新操作
当有新的操作需求时,只需创建新的访问者类,不需要修改对象结构中的元素类。
符合单一职责原则
元素类专注于自身的结构和数据,访问者类专注于操作逻辑。
缺点
违反开闭原则
如果要在对象结构中增加新的元素类,可能需要修改所有的访问者类,这违反了开闭原则。
增加复杂性
访问者模式引入了额外的层次结构(访问者和元素的多态调用),增加了代码的复杂性,对于简单的应用场景可能会显得过于复杂。
优点 | |
解耦操作与对象结构 | 使得操作和对象结构可以独立变化,提高了代码的可维护性和可扩展性。 |
易于添加新操作 | 当有新的操作需求时,只需创建新的访问者类,不需要修改对象结构中的元素类。 |
符合单一职责原则 | 元素类专注于自身的结构和数据,访问者类专注于操作逻辑。 |
缺点 | |
违反开闭原则 | 如果要在对象结构中增加新的元素类,可能需要修改所有的访问者类,这违反了开闭原则。 |
增加复杂性 | 访问者模式引入了额外的层次结构(访问者和元素的多态调用),增加了代码的复杂性,对于简单的应用场景可能会显得过于复杂。 |
八、访问者模式的升级版
双分派访问者模式
传统的访问者模式是单分派的,即根据对象的运行时类型选择要执行的方法。双分派访问者模式在此基础上,通过在访问者和元素之间进行两次方法调用,根据访问者和元素的具体类型来确定操作。这种方式可以更灵活地处理不同类型的元素和访问者之间的交互,进一步提高了模式的灵活性。
反射式访问者模式
利用编程语言的反射机制,可以动态地创建访问者类或者确定访问者类中的操作方法。这种模式在处理复杂的对象结构和动态变化的操作需求时非常有用。例如,在一个插件式的系统中,新的插件(可以看作是访问者)可能会不断加入,反射式访问者模式可以在不重新编译整个系统的情况下,根据插件的信息动态地执行相应的操作。
基于接口的访问者模式改进
在传统的访问者模式中,访问者接口和元素接口可能会因为元素类型和操作的增加而变得臃肿。可以采用基于接口的改进方式,将访问者接口和元素接口拆分成更小的子接口,每个子接口负责特定类型的操作或元素。这样可以提高代码的可读性和可维护性,并且更符合接口隔离原则。
访问者模式类型 | 描述 | 适用场景示例 |
---|---|---|
双分派访问者模式 | 传统单分派基础上,在访问者和元素间两次方法调用,依两者具体类型确定操作,提高灵活性 | 处理不同类型元素与访问者复杂交互时 |
反射式访问者模式 | 利用编程语言反射机制,动态创建访问者类或确定操作方法,应对复杂对象结构和动态需求有用 | 插件式系统,新插件不断加入时 |
基于接口的访问者模式改进 | 拆分访问者接口和元素接口为更小子接口,负责特定操作或元素,提高代码可读性与可维护性,符合接口隔离原则 | 访问者接口和元素接口因类型和操作增加而臃肿时 |