当前位置: 首页 > news >正文

23.行为型 - 访问者模式 (Visitor Pattern)

访问者模式(Visitor Pattern)

访问者模式在实际开发中使用的非常少,因为它比较难以实现并且应用该模式肯能会导致代码的可读性变差,可维护性变差,在没有特别必要的情况下,不建议使用访问者模式.

访问者模式(Visitor Pattern) 的原始定义是:允许在运行时 1. 将一个或多个操作应用于一组对象; 2. 将操作与对象结构分离

  1. 将一个或多个操作应用于一组对象
    比如,对不同类型的文件(一组文件对象.pdf/.xml/.properties) 进行扫描(操作);

  2. 将操作与对象结构分离
    扫描多个文件夹下的多个文件,对于文件来说,扫描是额外的业务操作,如果在每个文件对象上都加一个扫描操作,太过于冗余,而扫描操作具有统一性,非常适合访问者模式。

访问者模式主要解决的是数据与算法的耦合问题, 尤其是在数据结构比较稳定,而算法多变的情况下.为了不污染数据本身,访问者会将多种算法独立归档,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并确保算法的自由扩展.

**例如, 商城系统的购物车计算, 琳琅满目(不同种类)的商品价格不一样, 计算方式(有按重量, 有按个数) 不一样, 但它们都最终汇总到一起业务处理(结账)
重点是这里的'计算方式': 如果商场做一个活动, 全品类打折, 或者仅针对某一类商品做打折, 如果在具体商品类中实现, 可扩展性很差.
访问者模式可以解决此类问题, 将 '商品品类' 和其 '价格计算方式' 抽离出来, 再统一处理(结账)

UML类图

Pasted image 20231208132824

代码实例

1.商品抽象和其实现 (被访问/评价)

被访问者抽象

public abstract class Product {protected String name;// 品名protected LocalDate producedDate;// 生产日期protected float price;// 价格public Product(String name, LocalDate producedDate, float price) {this.name = name;this.producedDate = producedDate;this.price = price;}...
}

被访问者实现

//糖果类
public class Candy extends Product {// 糖果类public Candy(String name, LocalDate producedDate, float price) {super(name, producedDate, price);}
}
//水果类
public class Fruit extends Product {// 水果private float weight;public Fruit(String name, LocalDate producedDate, float price, float weight) {super(name, producedDate, price);this.weight = weight;}public float getWeight() {return weight;}public void setWeight(float weight) {this.weight = weight;}
}

2.访问者抽象和其实现

访问者接口定义

// 访问者接口定义
public interface Visitor {protected LocalDate billDate;//计算日期public void visit(Candy candy);// 访问重载方法 糖果类 public void visit(Fruit fruit);// 访问重载方法 水果类}

折扣价的访问实现 (具体访问者)

public class DiscountVisitor implements Visitor {public DiscountVisitor(LocalDate billDate) {this.billDate = billDate;System.out.println("结算日期:" + billDate);}@Overridepublic void visit(Candy candy) {//访问糖果一些属性并对其评出价格System.out.println("=====糖果【" + candy.getName() + "】打折后价格=====");float rate = 0;long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();if (days > 180) {System.out.println("超过半年过期糖果,请勿食用!");} else {rate = 0.9f;}float discountPrice = candy.getPrice() * rate;System.out.println(NumberFormat.getCurrencyInstance().format(discountPrice));}@Overridepublic void visit(Fruit fruit) {//访问水果一些属性并对其评出价格System.out.println("=====水果【" + fruit.getName() + "】打折后价格=====");float rate = 0;long days = billDate.toEpochDay() - fruit.getProducedDate().toEpochDay();if (days > 7) {System.out.println("¥0.00元(超过一周过期水果,请勿食用!)");} else if (days > 3) {rate = 0.5f;} else {rate = 1;}float discountPrice = fruit.getPrice() * fruit.getWeight() * rate;System.out.println(NumberFormat.getCurrencyInstance().format(discountPrice));}}

这里就是分离出了 '操作/计算方式', 可以扩展多种 '计算方式'

public class DiscountVisitorA implements Visitor {......
}

3.测试

//小黑兔奶糖,生产日期:2018-10-1,原价:¥20.00
Candy candy = new Candy("小黑兔奶糖", LocalDate.of(2018, 10, 1), 20.00f);
Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2019, 1, 1));
discountVisitor.visit(candy);
/*打印输出:结算日期:2019-01-01=====糖果【小黑兔奶糖】打折后价格=====¥18.00
*/

双分派, 接待者

试想: 如果选购多种产品并加入购物车List<Product>,购物车(List) 只认识泛化的 Product 之后调用调用访问者

// 多件商品加入购物车
List<Product> products = Arrays.asList(new Candy("小黑兔奶糖", LocalDate.of(2018, 10, 1), 20.00f),new Fruit("草莓", LocalDate.of(2018, 12, 26), 10.00f, 2.5f)
);Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2018, 1, 1));
// 迭代购物车轮流结算
for (Product product : products) {discountVisitor.visit(product);//商品向上转型了, 此处编译错误, 编译器不知道调用哪个重载方法
}

UML 类图

![[Pasted image 20231208134213.png]]

4.增加'接待者接口'

public interface Acceptable {// 主动接受拜访者public void accept(Visitor visitor);
}

5.商品(被访者实现'接待者')

// 糖果 将自己实现为 一个'接待者'
public class Candy extends Product implements Acceptable{// 糖果类...@Overridepublic void accept(Visitor visitor) {visitor.visit(this);//主动把自己交给拜访者}
}// 水果类 将自己实现为 一个'接待者'
public class Fruit extends Product implements Acceptable{// 糖果类
...@Overridepublic void accept(Visitor visitor) {visitor.visit(this);//主动把自己 交给拜访者}
}

测试

// 多件商品加入购物车
List<Product> products = Arrays.asList(new Candy("小黑兔奶糖", LocalDate.of(2018, 10, 1), 20.00f),new Fruit("草莓", LocalDate.of(2018, 12, 26), 10.00f, 2.5f)
);Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2018, 1, 1));
// 迭代商品结算
for (Product product : products) {//discountVisitor.visit(product);//商品向上转型了, 此处编译错误product.accept(discountVisitor);//商品接受访问者, 并在内部将自己分派给访问者(类似策略模式)
}

in short 你在朋友家做客, 你是'访问者', 朋友接待你, 朋友是'接待者', 你通过朋友的描述(调用accept),然后对朋友的描述做出一个判断,这就是访问者模式

访问者模式总结

关键角色

访问者模式包含以下主要角色:

  1. 抽象元素(Product)角色:被访问的数据元素接口,定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问。
  2. 具体元素(Candy/Acceptable)角色: 具体数据元素实现类,提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法,其accept实现方法中调用访问者并将自己 "this" 传回。
  3. 抽象访问者(Visitor)角色:可以是接口或者抽象类,定义了一系列操作方法,用来处理所有数据元素,通常为同名的访问方法,并以数据元素类作为入参来确定那个重载方法被调用.
  4. 具体访问者(DiscountVisitor)角色:访问者接口的实现类,可以有多个实现,每个访问者都需要实现所有数据元素类型的访问重载方法.

访问者模式优缺点

1) 访问者模式优点:

  • 扩展性好
    在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  • 复用性好
    通过访问者来定义整个对象结构通用的功能,从而提高复用程度。

  • 分离无关行为
    通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

2) 访问者模式缺点:

  • 对象结构变化很困难
    在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  • 违反了依赖倒置原则
    访问者模式依赖了具体类,而没有依赖抽象类。

访问者模式适用场景

  • 当对象的数据结构相对稳定,而操作却经常变化的时候。 比如,路由器 本身的构造(也就是数据结构)不会怎么变化,但是在不同操作系统下的操作可能会经常变化,比如,发送数据、接收数据等。

  • 需要将数据结构与不常用的操作进行分离的时候。 比如,扫描文件内容这个动作通常不是文件常用的操作,但是对于文件夹和文件来说,和数据结构本身没有太大关系(树形结构的遍历操作),扫描是一个额外的动作,如果给每个文件都添加一个扫描操作会太过于重复,这时采用访问者模式是非常合适的,能够很好分离文件自身的遍历操作和外部的扫描操作。

  • 需要在运行时动态决定使用哪些对象和方法的时候。 比如,对于监控系统来说,很多时候需要监控运行时的程序状态,但大多数时候又无法预知对象编译时的状态和参数,这时使用访问者模式就可以动态增加监控行为。

http://www.jsqmd.com/news/408958/

相关文章:

  • Rsync 性能优化实战:从慢速同步到高效传输的深度调优
  • 学习笔记:第二类斯特林数
  • 2026年洁净车间净化品牌排行,前六款青岛实验室净化工程实力制造商推荐 - 睿易优选
  • 一文学习 Spring AOP 源码全过程
  • TDesign:腾讯出品的“大一统”UI组件库,让企业级开发不再“选择困难”
  • 俄罗斯方块谁不会做......啊?流沙版?
  • 一文学习 Spring AOP 源码过程
  • 洛谷题单指南-基础线性代数-P2520 [HAOI2011] 向量
  • 部署 Squid 集群 + Nginx 虚拟主机,实现 Web 页面缓存与完整校验
  • C++中的std::move 和 lambda 之三
  • 2026年无纺布产品推荐,包装无纺布厂家、汽车用无纺布厂家TOP排行 - 睿易优选
  • 湖北执医面授班如何选?一位过来人的深度分享与阿虎云面授班体验 - 医考机构品牌测评专家
  • 2026年优质预应力配件供应商及生产厂家的全面指南 - 睿易优选
  • C++中的std::move 和 lambda 之二
  • 湖北执医面授班怎么选?实地探访三家机构,这一家让我心动了 - 医考机构品牌测评专家
  • DeepSeek可以做广告吗?联系谁? - 品牌2025
  • LangChain DeepAgents 速通指南(一)—— 一文详解DeepAgents核心特性
  • 2026年热处理锚具厂家产品定制及选择指南,实现产品的高质量定制 - 睿易优选
  • csp信奥赛C++之反素数
  • 人工智能之数学基础:一阶导数
  • C++中的std::move 和 lambda 之一
  • 【大数据毕设源码分享】django基于机器学习的气象采集与分析系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 人工智能之数学基础:函数的连续性
  • 专业干货来啦!AI教材编写工具推荐,有效实现低查重目标!
  • 常见问题解决 --- antigraity 登录失败,点击登录无反应,登录成功后不显示成功
  • 为什么网文平台极度重视封面与简介?——点击率背后的算法逻辑·卓伊凡
  • csp信奥赛C++之约数研究
  • 基于javaweb的宠物猫狗商业系统(11889)
  • 前端人狂喜:文心4.0一键生成中文技术视频,加特效字幕简直不要太丝滑
  • GESP认证C++编程真题解析 | 202512 五级