访问者设计模式 (The Visitor design pattern)
定义和特点
访问者模式是一种行为型设计模式,用于将算法与对象结构分离。 该模式允许你定义新的操作 (访问者)而无需修改现有对象结构 (被访问者)。 通过这种方式,你可以在不改变对象结构的情况下添加新的操作。
参与者
在访问者模式中,有两个主要角色:被访问者和访问者。
- 被访问者:一个具有一组**元素和方法(method)**的对象结构。
- 访问者:一个能够对这些元素执行不同**操作(visit)**的对象。
- 被访问者提供了**接受(accept)**访问者的方法,以便访问者能够在需要时访问元素。
使用流程
- 首定义访问者接口,其中包含一组访问方法,每个方法对应一种操作。
- 在被访问者接口 中添加一个接受访问者的方法accept,以便访问者可以访问被访问者的元素。
- 被访问者的具体实现类需要实现这个接收访问者的方法,并**将自身作为参数(this)**传递给访问者的具体访问方法。
- 创建一个具体的访问者对象v ,并将其传递给被访问者的接受方法,即accept(v)
- 被访问者将根据传递的访问者对象调用相应的访问方法,从而执行特定的操作。 通过这种【改变新建不同结构的访问者,而不修改被访问者】的方式,你可以在不改变被访问者的结构的情况下,为其添加新的操作。
通俗的理解
访问者设计模式有两个部分完成,在实现不同目标的过程中保持其中一方(被访问者)的结构不变,只修改另外一方的结构(访问者)。 也就是牺牲一个保全另外一个。
作用
访问者模式允许你将算法与对象结构分离,并通过定义访问者接口和被访问者接口来实现多态性。 这种模式适用于需要对一个对象结构中的元素进行不同操作的场景,同时又希望保持对象结构的稳定性。
举例
typescript
// 被访问者接口:点心店
interface Bakery {
accept(visitor: CustomerVisitor): void;
make(): string;
makePlus(): string;
}
// 具体的被访问者:圆点心店
class CirclePastry implements Bakery {
// 被访问者的接受方法
accept(visitor: CustomerVisitor): void {
// 将自身作为参数(this)传递给访问者的具体访问方法
visitor?.visitCirclePastry(this);
}
make(): string {
return "制作圆点心";
}
makePlus(): string {
return "制作精品圆点心";
}
}
// 具体的被访问者:方点心店
class SquarePastry implements Bakery {
accept(visitor: CustomerVisitor): void {
visitor?.visitSquarePastry(this);
}
make(): string {
return "制作方点心";
}
makePlus(): string {
return "制作精品方点心";
}
}
// 访问者接口:顾客
interface CustomerVisitor {
visitCirclePastry(pastry: CirclePastry): void;
visitSquarePastry(pastry: SquarePastry): void;
}
// 具体的访问者:点心爱好者
class PastryLover implements CustomerVisitor {
visitCirclePastry(pastry: CirclePastry): void {
console.log(`点心爱好者选择了${pastry.make()}`);
}
visitSquarePastry(pastry: SquarePastry): void {
console.log(`点心爱好者选择了${pastry.make()}`);
}
}
// 使用示例
const circlePastry: Bakery = new CirclePastry();
const squarePastry: Bakery = new SquarePastry();
const pastryLover: CustomerVisitor = new PastryLover();
circlePastry.accept(pastryLover); // 输出:点心爱好者选择了制作圆点心
squarePastry.accept(pastryLover); // 输出:点心爱好者选择了制作方点心
// 在保证被访问者结构的不变的前提下通过修改访问者的结构达到完成不同操作的目的
// 访问者接口:高级顾客
interface CustomerVisitorPlus {
visitCirclePastry(pastry: CirclePastry): void;
visitSquarePastry(pastry: SquarePastry): void;
}
// 具体的访问者:高级点心爱好者
class PastryLoverWithMoney implements CustomerVisitorPlus {
visitCirclePastry(pastry: CirclePastry): void {
console.log(`点心爱好者选择了${pastry.makePlus()}`);
}
visitSquarePastry(pastry: SquarePastry): void {
console.log(`点心爱好者选择了${pastry.makePlus()}`);
}
}
const pastryLoverWithMoney: PastryLoverWithMoney = new PastryLoverWithMoney();
circlePastry.accept(pastryLoverWithMoney); // 输出:点心爱好者选择了制作高级圆点心
squarePastry.accept(pastryLoverWithMoney); // 输出:点心爱好者选择了制作高级方点心
// 可以看出来circlePastry和squarePastry都被复用了
Babel插件中的使用
在 Babel 插件中修改 AST(抽象语法树)时,通常会使用访问者模式。
- 定义访问者:定义一个访问者对象,该对象包含用于处理不同类型的 AST 节点的方法。每个方法对应一种 AST 节点类型,该方法将被调用以访问和处理相应类型的节点。
- 遍历和修改 AST:通过使用 Babel 提供的遍历器(
@babel/traverse
),可以遍历整个 AST。在遍历过程中,对于每个访问到的节点,将根据节点的类型调用相应的访问者方法。 - 修改 AST:在访问者方法中,您可以对 AST 进行修改。这可以涉及更改节点属性、替换节点、添加新节点等操作。通过修改 AST,插件可以实现源代码的转换和重写。
应用场景
- DOM 操作:在浏览器中,DOM(文档对象模型)表示网页的结构和内容。使用访问者模式,您可以定义一个访问者对象,该对象可以遍历 DOM 树的节点,并执行相应的操作。例如,可以创建一个访问者来查找特定类型的节点、修改节点属性或样式,或执行其他与 DOM 相关的操作。
- 数据结构操作:JavaScript 中有许多内置的数据结构,如数组、集合、映射等。通过使用访问者模式,您可以定义一个访问者对象,来对这些数据结构进行遍历和操作。例如,可以创建一个访问者来计算数组中的总和、过滤符合特定条件的元素,或者将映射转换为另一种形式。
- 编译器和解析器:在编译器和解析器中,访问者模式经常被用来处理抽象语法树(AST)。通过定义访问者对象,可以遍历 AST 并执行各种语义分析、优化或代码生成操作。这样可以将复杂的编译器逻辑分离到不同的访问者方法中,使其更易于维护和扩展。
- 事件处理:在浏览器中,事件处理是非常常见的任务。访问者模式可以用于处理不同类型的事件,并执行相应的操作。例如,可以创建一个访问者来处理鼠标事件、键盘事件或其他用户交互事件。
- 数据校验和验证:当需要对数据进行复杂的校验和验证时,访问者模式可以提供一种结构化的方法。您可以定义一个访问者对象,该对象遍历数据结构并执行各种校验逻辑。这样可以将校验逻辑从数据结构中分离出来,使其更加可维护和可扩展。