访问者模式

实现访问者模式,分离数据结构与操作

问题

什么是访问者模式?如何在 JavaScript 中实现?

解答

访问者模式将数据结构与作用于结构上的操作分离,使得可以在不修改数据结构的前提下添加新的操作。

基本实现

// 元素接口 - 接受访问者
class Element {
  accept(visitor) {
    throw new Error('子类必须实现 accept 方法');
  }
}

// 具体元素:文章
class Article extends Element {
  constructor(title, wordCount) {
    super();
    this.title = title;
    this.wordCount = wordCount;
  }

  accept(visitor) {
    visitor.visitArticle(this);
  }
}

// 具体元素:视频
class Video extends Element {
  constructor(title, duration) {
    super();
    this.title = title;
    this.duration = duration; // 分钟
  }

  accept(visitor) {
    visitor.visitVideo(this);
  }
}

// 访问者接口
class Visitor {
  visitArticle(article) {}
  visitVideo(video) {}
}

// 具体访问者:统计访问者
class StatsVisitor extends Visitor {
  constructor() {
    super();
    this.totalWords = 0;
    this.totalDuration = 0;
  }

  visitArticle(article) {
    this.totalWords += article.wordCount;
    console.log(`统计文章: ${article.title}, ${article.wordCount} 字`);
  }

  visitVideo(video) {
    this.totalDuration += video.duration;
    console.log(`统计视频: ${video.title}, ${video.duration} 分钟`);
  }

  getStats() {
    return {
      totalWords: this.totalWords,
      totalDuration: this.totalDuration
    };
  }
}

// 具体访问者:导出访问者
class ExportVisitor extends Visitor {
  constructor() {
    super();
    this.result = [];
  }

  visitArticle(article) {
    this.result.push({
      type: 'article',
      title: article.title,
      readTime: Math.ceil(article.wordCount / 300) + ' 分钟'
    });
  }

  visitVideo(video) {
    this.result.push({
      type: 'video',
      title: video.title,
      duration: video.duration + ' 分钟'
    });
  }

  getResult() {
    return this.result;
  }
}

// 对象结构 - 管理元素集合
class ContentLibrary {
  constructor() {
    this.elements = [];
  }

  add(element) {
    this.elements.push(element);
  }

  // 接受访问者遍历所有元素
  accept(visitor) {
    this.elements.forEach(element => element.accept(visitor));
  }
}

// 使用示例
const library = new ContentLibrary();
library.add(new Article('JavaScript 基础', 1500));
library.add(new Article('React 入门', 2000));
library.add(new Video('Vue 教程', 30));
library.add(new Video('Node.js 实战', 45));

// 使用统计访问者
const statsVisitor = new StatsVisitor();
library.accept(statsVisitor);
console.log(statsVisitor.getStats());
// { totalWords: 3500, totalDuration: 75 }

// 使用导出访问者 - 无需修改元素类
const exportVisitor = new ExportVisitor();
library.accept(exportVisitor);
console.log(exportVisitor.getResult());

实际应用:AST 遍历

// 简化的 AST 节点
class NumberNode {
  constructor(value) {
    this.value = value;
  }

  accept(visitor) {
    return visitor.visitNumber(this);
  }
}

class BinaryNode {
  constructor(operator, left, right) {
    this.operator = operator;
    this.left = left;
    this.right = right;
  }

  accept(visitor) {
    return visitor.visitBinary(this);
  }
}

// 计算访问者
class CalculateVisitor {
  visitNumber(node) {
    return node.value;
  }

  visitBinary(node) {
    const left = node.left.accept(this);
    const right = node.right.accept(this);

    switch (node.operator) {
      case '+': return left + right;
      case '-': return left - right;
      case '*': return left * right;
      case '/': return left / right;
    }
  }
}

// 打印访问者
class PrintVisitor {
  visitNumber(node) {
    return String(node.value);
  }

  visitBinary(node) {
    const left = node.left.accept(this);
    const right = node.right.accept(this);
    return `(${left} ${node.operator} ${right})`;
  }
}

// 构建 AST: (1 + 2) * 3
const ast = new BinaryNode(
  '*',
  new BinaryNode('+', new NumberNode(1), new NumberNode(2)),
  new NumberNode(3)
);

const calculator = new CalculateVisitor();
const printer = new PrintVisitor();

console.log(ast.accept(printer));     // ((1 + 2) * 3)
console.log(ast.accept(calculator));  // 9

函数式实现

// 更简洁的函数式写法
const createVisitor = (handlers) => ({
  visit(node) {
    const handler = handlers[node.type];
    if (!handler) {
      throw new Error(`未知节点类型: ${node.type}`);
    }
    return handler(node, this);
  }
});

// 节点定义
const num = (value) => ({ type: 'number', value });
const binary = (op, left, right) => ({ type: 'binary', op, left, right });

// 计算访问者
const calcVisitor = createVisitor({
  number: (node) => node.value,
  binary: (node, visitor) => {
    const l = visitor.visit(node.left);
    const r = visitor.visit(node.right);
    const ops = { '+': (a, b) => a + b, '*': (a, b) => a * b };
    return ops[node.op](l, r);
  }
});

// 使用
const expr = binary('+', num(1), binary('*', num(2), num(3)));
console.log(calcVisitor.visit(expr)); // 7

关键点

  • 双重分派:通过 acceptvisit 两次调用确定具体操作
  • 开闭原则:添加新操作只需新增访问者,无需修改元素类
  • 适用场景:对象结构稳定但操作经常变化,如 AST 处理、DOM 遍历
  • 缺点:添加新元素类型需要修改所有访问者
  • 前端应用:Babel 插件、ESLint 规则、编译器实现