ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

访问者模式详解:分离数据和行为,不改原有类就能加新操作

访问者模式详解:分离数据和行为,不改原有类就能加新操作 一、什么是访问者模式定义GOF表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。一句话理解数据结构元素保持稳定但对这些数据要做什么却经常变化。与其每次改数据结构不如把操作抽象成一个Visitor接口新操作 新 Visitor 实现类原类一行都不用动。二、为什么需要访问者模式动机假设我们有一个公园系统ParkElement公园的各部分已经定义好并有ParkA、ParkB、Park等具体元素。某天需求来了要新增计算绿化覆盖率“安全检查”游客容量评估三个功能。不用访问者只能改ParkElement接口加 3 个方法再改ParkA/ParkB/Park全部实现类。类一多改动面爆炸还违反了对修改关闭。用访问者把操作抽成Visitor接口每个新功能就是一个新的Visitor实现类如GreenVisitor、SafetyVisitor。原有的元素类完全不动只新增类 → 符合开闭原则对扩展开放、对修改关闭。访问者模式最适合的场景就是元素类型基本稳定但作用在元素上的操作频繁变化。三、访问者模式的结构角色和职责角色职责Visitor抽象访问者为对象结构中每一种具体元素声明一个visit访问操作。方法名/参数决定了能访问哪类元素ConcreteVisitor具体访问者实现 Visitor 声明的每一个visit操作代表一种具体算法/行为Element抽象元素定义一个accept(Visitor)方法以访问者为参数把自己交给访问者ConcreteElement具体元素实现accept方法通常内部写visitor.visit(this)把自身回传ObjectStructure对象结构容纳多个元素如组合/集合提供接口让访问者遍历访问所有元素ObjectStructure 角色是使用访问者模式必备的——它负责枚举元素并驱动遍历。类结构图mermaidvisit(this)visit(this)visit(this)«interface»Visitorvisit(Park park)visit(ParkA parkA)visit(ParkB parkB)«interface»Elementaccept(Visitor visitor)«interface»ParkElementParkAaccept(Visitor)ParkBaccept(Visitor)Park-ParkA parkA-ParkB parkBaccept(Visitor)VisitorAVisitorBVisitorManager四、代码举例// 1. 抽象元素公园的每一部分 publicinterfaceParkElement{// 用来接纳访问者publicvoidaccept(Visitorvisitor);}// 2. 具体元素 A 部分 publicclassParkAimplementsParkElement{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.namename;}Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);// 把自己回传给访问者触发双分派}}// 2. 具体元素 B 部分 publicclassParkBimplementsParkElement{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.namename;}Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);}}// 2. 具体元素整个公园也是对象结构角色 publicclassParkimplementsParkElement{privateParkAparkA;privateParkBparkB;publicPark(){this.parkAnewParkA();this.parkBnewParkB();}Overridepublicvoidaccept(Visitorvisitor){visitor.visit(this);// 访问者先访问整体parkA.accept(visitor);// 再深入遍历各组成部分parkB.accept(visitor);}}// 3. 抽象访问者 publicinterfaceVisitor{publicvoidvisit(Parkpark);publicvoidvisit(ParkAparkA);publicvoidvisit(ParkBparkB);}// 4. 具体访问者 A只负责 ParkA 的卫生 publicclassVisitorAimplementsVisitor{Overridepublicvoidvisit(Parkpark){/* 不处理整体 */}Overridepublicvoidvisit(ParkAparkA){System.out.println(清洁工A:完成公园A的卫生);}Overridepublicvoidvisit(ParkBparkB){/* 不处理 B */}}// 4. 具体访问者 B只负责 ParkB 的卫生 publicclassVisitorBimplementsVisitor{Overridepublicvoidvisit(Parkpark){/* 不处理整体 */}Overridepublicvoidvisit(ParkAparkA){/* 不处理 A */}Overridepublicvoidvisit(ParkBparkB){System.out.println(清洁工B:完成公园B的卫生);}}// 4. 具体访问者管理员做全面检查 publicclassVisitorManagerimplementsVisitor{Overridepublicvoidvisit(Parkpark){System.out.println(管理员负责公园卫生检查);}Overridepublicvoidvisit(ParkAparkA){System.out.println(管理员负责公园A部分卫生检查);}Overridepublicvoidvisit(ParkBparkB){System.out.println(管理员负责公园B部分卫生检查);}}// 5. 测试 publicclassMainClass{publicstaticvoidmain(String[]args){ParkparknewPark();VisitorAvisitorAnewVisitorA();park.accept(visitorA);// 清洁工A 只扫 AVisitorBvisitorBnewVisitorB();park.accept(visitorB);// 清洁工B 只扫 BVisitorManagervisitorManagernewVisitorManager();park.accept(visitorManager);// 管理员全面检查}}运行结果清洁工A:完成公园A的卫生 清洁工B:完成公园B的卫生 管理员负责公园卫生检查 管理员负责公园A部分卫生检查 管理员负责公园B部分卫生检查可以看到同一个park.accept(visitor)调用传入不同访问者就自动分发到了对应的visit重载方法。五、核心原理双分派Double Dispatch这是访问者模式最灵魂、也最容易被面试追问的部分。Java 是单分派语言方法调用具体执行哪个版本在编译期就由调用者receiver的静态类型决定了重载overload是编译期绑定重写override是运行期绑定但都只盯一个维度。访问者模式用两次动态绑定绕开了这个限制第一次分派park.accept(visitor)accept具体执行哪个由元素的运行时类型Park/ParkA/ParkB决定 → 动态第二次分派accept内部调用visitor.visit(this)这里this把元素的真实类型传进去visit(this)调哪个重载由访问者的运行时类型 this 的类型共同决定 → 又是动态正是靠元素先accept把this回传才让visit的重载选择延迟到运行期、由元素类型 × 访问者类型两个维度共同决定。这就是Double Dispatch双分派。普通的方法重载如visit(Park)/visit(ParkA)在单分派语言里本无法按运行时类型自动选访问者模式通过这层回传 this把它实现了。六、开闭原则的权衡访问者模式的阿喀琉斯之踵访问者模式在开闭原则上是不对称的这点必须讲清变化点是否好扩展原因新增操作新 Visitor✅ 容易只加一个ConcreteVisitor类原有元素类一行都不用改新增元素新节点类型如ParkC❌ 困难必须改Visitor接口加visit(ParkC)所有ConcreteVisitor实现牵一发动全身所以一句话选型元素类型稳定、操作频繁变化 → 用访问者如果元素类型本身也天天变别用访问者改用其他方案或重新审视设计。七、真实世界中的访问者模式JDK / 框架java.nio.file.FileVisitor/SimpleFileVisitor遍历目录树时对文件和目录做不同操作如统计大小、查找特定文件。元素文件/目录稳定操作复制/删除/统计多变——访问者模式的教科书级落地。ASM 字节码框架ClassVisitor在解析.class文件时对字段、方法、注解、指令等不同类型节点做不同处理是访问者模式的经典应用。SpringBeanDefinitionVisitor用来访问并修改BeanDefinition中的占位符/值。编译器抽象语法树AST的遍历对不同类型的语法节点赋值、循环、函数调用做语义分析、优化、代码生成几乎都用访问者。八、应用场景与优缺点适用场景对象结构元素集合稳定但经常需要在其上定义新的操作。需要对一个对象结构中的多个元素做互不相干的操作希望把这些操作集中而不是散落在各个元素类里。元素类需要避免暴露内部状态给外部操作访问者可通过受控接口访问。优点易扩展新操作新增功能 新增 Visitor 类符合开闭原则的操作侧。操作集中相关的行为集中到一个 Visitor 里如所有卫生检查逻辑都在VisitorManager便于维护也方便做累加器式统计.visit 里累加结果。元素类保持纯净数据类只管数据不堆砌各种业务方法。缺点新增元素极难要改 Visitor 接口和所有实现违反开闭原则的元素侧见第六节。破坏封装访问者往往需要元素暴露内部细节才能操作元素通常得提供getXxx给 visitor 读。结构变复杂多了 Visitor 接口、双分派、ObjectStructure newcomer 理解成本高于简单 if-else。九、总结一句话访问者模式 把操作从数据里抽出来通过双分派让同一份数据被不同的访问者以不同方式处理元素不动、操作随便加。记忆口诀数据结构稳如山操作天天在变 → 上访问者元素也想常增减 → 访问者先靠边。
返回列表