
文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本篇精读来自 前端精读周刊 设计模式系列第 189 篇讲解行为型模式中的Visitor访问者模式。文章以「城市建造游戏资源系统」为贯穿案例剖析访问者模式如何把对象的操作权移交出去让你在不修改基础元素类的前提下为元素不断叠加新的操作并给出完整的 TypeScript 实现、调用链拆解与适用边界判断帮助你判断「何时该用、何时千万别用」。意图把操作权从元素手里移交出去访问者模式属于行为型模式官方意图如下表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。拆成两句话理解“作用于某对象结构中的各元素的操作”——Visitor 存在的意义就是去操作对象结构里的元素“不改变各元素的类的前提下定义新操作”——新增操作时只需要修改 Visitor 本身原本的元素类一行都不用动。这初看很反直觉给对象定义新操作竟然不用改对象自己而是靠改另一个对象这正是访问者模式的设计妙处——它把对象的操作权移交给了 Visitor让“操作”与“被操作对象”解耦。举例子城市建造游戏中的资源设计访问者模式能落地的场景很少本文只举一个例子但足够有代表性。假设你在制作一款城市建造游戏基础资源只有四种毛皮、木材、铜矿、铁矿。你需要用这些资源造出楼房、衣服、家具、门、空调、锅、健身房、游泳馆等各种各样的东西。关键在于游戏要做得非常逼真每种资源的用法都高度定制——不是简单地“消耗 N 个数量”就能完成比如制作家具时要同时用到毛皮和木材而毛皮、木材对环境、制作人、资金又有各自不同的要求。最自然的想法是把资源的所有使用方法都枚举在资源类里用到哪个场景就调用哪个方法。但问题立刻出现资源本身是相对固定的每增加一种用途就要改一次木材类、铁矿类会非常麻烦。能不能做到“增加新用途时不修改原始资源类”答案就是访问者模式。结构图与四个关键角色访问者模式由以下四个角色构成角色职责Visitor访问者接口定义访问元素的操作visit方法族ConcreteVisitor具体的访问者实现针对各具体元素的操作逻辑Element可被访问的元素必须定义一个accept方法接收 visitor 对象——这是实现访问者模式的关键ObjectStructure对象结构存储多个 Element利用 Visitor 进行批量操作要实现“操作权转让到 Visitor”核心是元素必须实现一个accept函数把自己抛给 Visitorclass ConcreteElement implements Element { public accept(visitor: Visitor) { visitor.visit(this) } }由此形成一条清晰的调用链路Element 通过accept函数接收 Visitor 对象并将自己的实例抛给 Visitor 的visit函数。这样我们就能在 Visitor 的visit方法中拿到对象实例完成对对象的操作——操作逻辑从此住在 Visitor 里而不是元素里。代码例子双访问者操作同一元素下面例子使用 TypeScript 编写。先定义两个具体的访问者class ConcreteVisitorX implements Visitor{ public visit(element: Element) { element.accept(this); } public visit(concreteElementA: ConcreteElementA) { console.log(X 操作 A) } public visit(concreteElementB: ConcreteElementB) { console.log(X 操作 B) } } class ConcreteVisitorY implements Visitor{ public visit(element: Element) { element.accept(this); } public visit(concreteElementA: ConcreteElementA) { console.log(Y 操作 A) } public visit(concreteElementB: ConcreteElementB) { console.log(Y 操作 B) } }配合上文已经写好的Element整个调用过程如下// 先创建元素 const element new ConcreteElement() // 访问者 X const visitorX new ConcreteVisitorX() // 访问者 Y const visitorY new ConcreteVisitorY() // 然后让访问者 visit 观察一下元素 visitorX.visit(element as Element) visitorY.visit(element as Element)调用链逐步拆解第一步注意入参类型。访问者观察的 Element 一定要是通用类型Element而不是具体类型ConcreteElement。因为 Visitor 可以访问任何类型的 Element先传接口进去访问者模式的抽象性才能体现出来——如果传具体类型模式就直接退化成普通方法调用。第二步双重分发Double Dispatch。整个执行过程分两次跳转Visitor 定义的visit被调用由于入参符合Element通用类型因此会调用 Element 接口定义的accept函数——这是所有元素共有的方法每个具体元素都重写了accept方法public accept(visitor: Visitor) { visitor.visit(this) }this在这里是具体元素类型所以再次调用 Visitor 的visit时参数已经变成了具体类型于是命中到 Visitor 中针对具体元素的重载方法例如public visit(concreteElementA: ConcreteElementA) { console.log(X 操作 A) }最终控制台输出 “X 操作 A”。整个链路的本质是类型信息在两次调用之间被“护送”到了 Visitor 一侧——第一次以通用类型触发accept第二次以具体类型触发精准的visit重载。这就是访问者模式能“不改元素类而新增操作”的底层机制。由此获得的三种拓展性从上面这套机制里可以提炼出访问者模式的三个拓展性收益Element 元素的所有子类都不用频繁修改只要修改 Visitor 即可——元素的类变得极其稳定一个 Visitor 可以按需选择操作任何类型的 Element 子类——只要申明了处理函数就会命中不申明就不会命中非常方便。回到城市建造的例子锅需要用铁制作、但不消耗木材那就干脆不定义木材的visit方法可以定义多种 Visitor对同一种 Element 子类做不同的操作——城市建造中门和窗户对铁矿的使用方式完全不同各自定义一个 Visitor 即可互不干扰。由此我们就可以在城市建造的例子中拓展出任意多种使用资源的场景而无需让资源本身感知到这些场景的存在。弊端使用场景非常有限访问者模式使用场景非常有限请先确认你的场景同时满足“元素稳定、操作多变”再使用如果资源本身并不需要频繁修改和拓展那么就没必要使用访问者模式引入 Visitor 会带来额外的抽象层与双重分发复杂度元素数量少、操作固定的场景只会徒增理解成本。一个合理的判断标准是基础元素的数量基本不变而对元素的“使用方式”或“组合使用”在不断更新——此时把变化更快的部分打包提取到 Visitor 中才真正划算。总结防止基础元素代码不断膨胀访问者模式的精髓就是在不断拓展的业务场景中防止基础元素代码不断膨胀。假设城市建造游戏由 20 人团队开发每周发布 2 个版本每个版本都会新增几种资源的组合使用方式。由于资源一共就木材、铁矿、铜矿那么几种如果你作为团队负责人放任大家随意修改这些资源基础类过不了半年就会发现木材类的成员方法突破了 100 种而且还在以每天新增 2 种的速度增长你精心打造的程序正在变成一堆屎山更要命的是你根本搞不清哪些场景的用法是打包的当一种使用场景下线时已经存在的成员方法还不敢删除。而如果用了访问者模式每次迭代新增的方法都会放进一个新的 Visitor 文件。比如一种纳米材料的门板在游戏 V1.5 版本被引进它对材料的使用就体现在新增一个 Visitor 文件上资源本身的类完全不会被修改。这既不会引发协同问题也让功能代码按照场景聚合无论维护还是删除心智负担都非常小。访问者模式背后的思考本质是基础的元素数量一般不会随程序迭代产生太大变化而对这些基础元素的使用方式或组合使用会随迭代不断更新——将变化更快的部分通过 Visitor 打包提取出来自然更利于维护。延伸访问者模式在前端与编译领域的真实身影访问者模式在日常业务里虽然用得少但在前端工程化与编译原理领域却无处不在——凡是需要“遍历一棵稳定的结构、不断叠加新处理逻辑”的地方几乎都是 Visitor 的影子这也印证了本文总结的适用条件。Babel 插件体系AST 的 visitor 遍历器在 《用 Babel 创造自定义 JS 语法》 一文中周刊明确提到遍历 AST 树常采用的方案就是做一个遍历器 visitor在遍历过程中进行拓展常采用 Babel 这种写法return { visitor: { FunctionDeclaration(path) { if (path.get(curry).node) { // const foo curry(function () { ... }); path.node.curry false; path.replaceWith( t.variableDeclaration(const, [ t.variableDeclarator( t.identifier(path.get(id.name).node), t.callExpression(t.identifier(currying), [ t.toExpression(path.node), ]) ), ]) ); } }, }, };visitor下的每一个 key 名都是遍历过程中的拓展点相当于访问者模式里的visit钩子。AST 节点本身是稳定不变的而 Babel 插件生态却可以无限叠加——每种插件都是一个 Visitor只处理自己关心的节点类型不关心也不修改其他节点这与访问者模式“元素稳定、操作多变”的结构完全同构。手写 SQL 编译器中的语法树遍历在 《手写 SQL 编译器 - 语法树》 系列中语法树同样是一棵结构相对固定的树而编译器后续要做的类型检查、代码生成、智能提示等操作都是针对这棵树不断叠加的“访问者式”处理——元素节点不频繁变化操作遍历逻辑却持续增长正是访问者模式的适用土壤。与同系列模式的关系在学习访问者模式时还可以与同系列的两篇对照阅读体会行为型模式在“变化封装”上的不同取舍《设计模式 - Strategy 策略模式》策略模式把“算法”整体提取出来相互替换改变的是算法的全部访问者模式则把“作用于元素的操作”提取出来配合双重分发按元素类型精确分派《设计模式 - Template Method 模版模式》模板模式固定算法骨架、只允许子类重写部分步骤改变的是算法的一部分访问者模式则完全不触碰元素类把所有操作变化收敛到 Visitor 侧。三者共同回答了同一个问题当变化不可避免时把变化关进哪个笼子才能让其余代码保持稳定。访问者模式的答案就是把“操作的变化”全部关进 Visitor 这一个笼子里。结语访问者模式是典型的“少数场景、极高价值”模式日常业务代码里不必强求但当出现“基础元素稳定、对元素的操作高速迭代”的结构时它能有效阻止元素类膨胀为无法维护的屎山。判断标准记住一句即可——元素的数量不变对元素的使用方式在变就用访问者模式。更多设计模式精读与实战内容可回到 前端精读周刊 的 设计模式 目录继续阅读。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Java 设计模式Acyclic Visitor无环访问者模式实战——在不破坏类层次结构的前提下为对象添加新操作Java 设计模式Acyclic Visitor无环访问者模式实战——在不破坏类层次结构的前提下为对象添加新操作 导读 本文以 java design p示例工程教程Java 非循环访问者模式Acyclic Visitor实战指南在不修改类层次结构的前提下安全扩展功能Java 非循环访问者模式Acyclic Visitor实战指南在不修改类层次结构的前提下安全扩展功能 非循环访问者模式Acyclic Visitor示例工程教程Java Design Patterns 之 Acyclic Visitor无环访问者模式在不破坏类层次结构的前提下实现开放扩展Java Design Patterns 之 Acyclic Visitor无环访问者模式在不破坏类层次结构的前提下实现开放扩展 Acyclic Visi示例工程教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考