设计模式——2_A 访问者(Visitor)

ops/2024/10/10 11:00:47/

文章目录

  • 定义
  • 图纸
  • 一个例子:如何给好奇宝宝提供他想知道的内容
    • 菜单、菜品和配方
          • Menu(菜单) & Cuisine(菜品)
          • Material(物料、食材)
    • 产地、有机蔬菜和卡路里
          • Cuisine & Material
    • 访问者
          • Visitor
          • Cuisine & Material
  • 碎碎念
    • 访问者和双分派
    • 访问者和代理
    • 写在最后的碎碎念

定义

表示一个作用于某对象结构中的个元素的操作。他使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作


访问器和其他的设计模式一样,致力于将程序中的 变化不变 的部分剥离,至于是谁被独立出来,这不好说。像策略状态 这种模式是将变化独立出来;而也有像 迭代器模板方法 这样将不变的部分独立出来的。总之袋子里只有两种苹果,你拿走青色的,剩下的都是红色的,反之亦然

可访问器又是设计模式中的异类。在其他的设计模式中,我们总是强调 隐藏细节、依赖抽象。但访问器反其道而行之,他是23种基础设计模式中唯一一个要求 被作用方,也就是 被访问者,必须对 访问者 公开自己的细节,而且访问者会依赖具体类,也就是说访问者的复杂程度是会随着你对被访问者类簇的拓展而复杂化的




图纸

在这里插入图片描述




一个例子:如何给好奇宝宝提供他想知道的内容

某天,你发现的出生点居然是大洋彼岸的美利坚,正当你准备掐掐自己人中看看是不是还没醒的时候,肚子却提醒你该补充能量了。你坚信有一技傍身的人总是饿不死的,于是准备靠着祖传的川菜手艺在唐人街创出一片天地。摸爬滚打几年后,随着一串鞭炮被点燃,属于你的川菜馆终于开张,可是当你准备做一个电子菜单的时候却犯了愁

客人们恨不得了解自己将点的菜的全部信息,而你却不能公开自己赖以生存的秘方,这就是我们这次的例子(没错,前面那个浪迹美国的感人故事跟正文毫无关联)

准备好了吗?四人组圣经里的最后一个设计模式的例子也开始了:



菜单、菜品和配方

为了展示菜单,无论如何你需要一个和菜品相关的类簇,就像这样:

在这里插入图片描述

Menu(菜单) & Cuisine(菜品)
java">/*** 菜品*/
public class Cuisine {/*** 菜品名*/private String name;/*** 配料表*/private List<Material> burdenSheet;public Cuisine(String name, List<Material> burdenSheet) {this.name = name;this.burdenSheet = burdenSheet;}public String getName() {return name;}public void setName(String name) {this.name = name;}public void setBurdenSheet(List<Material> burdenSheet) {this.burdenSheet = burdenSheet;}
}/*** 菜单*/
public class Menu {/*** 菜品列表*/private List<Cuisine> cuisineList;public static Menu createMenu(){Menu menu = new Menu();//初始化cuisineList的动作return menu;}private Menu() {}
}
Material(物料、食材)
java">/*** 食材*/
public class Material {/*** 食材名*/private String name;/*** 辛辣度*/private int spicyDegree;/*** 咸度*/private int saltyDegree;public String getName() {return name;}public void setName(String name) {this.name = name;}public int getSpicyDegree() {return spicyDegree;}public void setSpicyDegree(int spicyDegree) {this.spicyDegree = spicyDegree;}public int getSaltyDegree() {return saltyDegree;}public void setSaltyDegree(int saltyDegree) {this.saltyDegree = saltyDegree;}
}/*** 肉类*/
public class Meat extends Material{}/*** 蔬菜*/
public class Vegetable extends Material {}/*** 调料*/
public class Flavour extends Material {}

这个实现简单到不能称之为设计,只能说我们通过 Cuisine(菜品) 来表示一个菜品里面必须有的内容,比如配料表

配料表里面的食材我们通过 Material(食材) 类来表示,并根据类型给 Material 创建了三个子类,分别是 Meat(肉)Vegetable(蔬菜)Flavour(调料)。你可能会问,这仨子类有存在的必要吗?这不是仨空类吗?别着急,后面会用到他们

client 是通过 菜单 点菜的,为了让所有的 client 都可以在程序的任意位置都获取到正确的菜单。我们将川菜馆里面所有的菜品都集中到了 Menu(菜单) 中,并只允许 client 通过静态方法获取 Menu 对象


值得注意的是在 Cuisine 中,我只提供了 burdenSheet(配料表) 的 setter,因为将来调用这个模块的未必都是内部的系统,我不可能允许外部系统获取到我的配料表。别人学会了,我喝西北风去?



产地、有机蔬菜和卡路里

开张后第一个问题来了,客户们需要了解自己吃的牛肉是不是从大洋彼岸打飞的来的餐桌、送进嘴里的青椒是不是有机的 以及 咽下去的食物到底含有多少卡路里。也就是说,要求你在电子菜单上提供食材的 生产日期产地热量情况

上帝都发话了,那肯定要开搞,就像这样:

在这里插入图片描述

Cuisine & Material
java">/*** 菜品*/
public class Cuisine {……/*** 提供这道菜的热量*/public int getCalorie() {//菜品的热量=食材的热量和int result = 0;for (Material material : burdenSheet) {//只有在食材是肉和蔬菜时才计算他的热量if (material instanceof Meat) {Meat meat = (Meat) material;result += meat.getCalorie();} else if (material instanceof Vegetable) {Vegetable meat = (Vegetable) material;result += meat.getCalorie();}}return result;}/*** 是否包含有机蔬菜*/public boolean haveOrganicVegetable(){for (Material material : burdenSheet) {if(material instanceof Vegetable && ((Vegetable)material).isOrganic()){return true;}}return false;}
}/*** 肉类*/
public class Meat extends Material {/*** 卡路里*/private int calorie;/*** 产地*/private String productionPlace;public int getCalorie() {return calorie;}public void setCalorie(int calorie) {this.calorie = calorie;}public String getProductionPlace() {return productionPlace;}public void setProductionPlace(String productionPlace) {this.productionPlace = productionPlace;}
}/*** 蔬菜*/
public class Vegetable extends Material{/*** 卡路里*/private int calorie;/*** 是否是有机蔬菜*/private boolean isOrganic;public int getCalorie() {return calorie;}public void setCalorie(int calorie) {this.calorie = calorie;}public boolean isOrganic() {return isOrganic;}public void setOrganic(boolean organic) {isOrganic = organic;}
}

Flavour(调料) 的卡路里是忽略不计的,只有 Meat(肉) 是需要提供产地的,只有 Vegetable(蔬菜) 是区分有机和无机的

如果你将这些带有特殊性的属性全部都写到 Material 根类中,那么随着你对食材的描述越来越完善,这个根类也会复杂到让人害怕,而且有很多属性是没有任何意义的,所以你只能把他们分配到特定的子类中去

但是这种做法带来另一个问题,由于我不能直接公开菜品里的配料表,那就意味着客户的所有定制要求我都需要在 Cuisine 中实现对应的方法。如果只是简单的迭代获取信息倒是也无所谓,但是现在的状况是很多属性依赖的是具体子类的实现,而不是食材的根类,这就让我们必须对实例去做类型判断,才能决定执行什么逻辑


所以虽然上述实现可以完成需求,但是你已经预见到这将是一场噩梦

总有一天会有人希望你添加一个 是否包含香菜 这样的提示;又或者有位穆斯林大哥就要吃鱼香肉丝,你要怎么跟人家解释鱼香肉丝里没有鱼只有猪

至少,我们要找到一种实现可以把这些变化独立出来



访问者

如果你采用访问者改造上面的代码,那么就会得到这样的结果:

在这里插入图片描述

Visitor
java">/*** 访问者*/
public interface Visitor<E> {/*** 菜品执行的内容*/E doForCuisine(Cuisine cuisine);/*** 食材执行的内容*/E doForMaterial(Material material);/*** 肉类执行的内容*/E doForMeat(Meat meat);/*** 蔬菜执行的内容*/E doForVegetable(Vegetable vegetable);/*** 调料执行的内容*/E doForFlavour(Flavour flavour);
}/*** 卡路里访问器*/
public class CalorieVisitor implements Visitor<Integer>{@Overridepublic Integer doForCuisine(Cuisine cuisine) {int result = 0;for (Material material : cuisine.getBurdenSheet()) {result += material.accept(this);}return result;}@Overridepublic Integer doForMaterial(Material material) {return 0;}@Overridepublic Integer doForMeat(Meat meat) {return meat.getCalorie();}@Overridepublic Integer doForVegetable(Vegetable vegetable) {return vegetable.getCalorie();}@Overridepublic Integer doForFlavour(Flavour flavour) {return 0;}
}/*** 有机属性访问者*/
public class OrganicVisitor implements Visitor<Boolean> {@Overridepublic Boolean doForCuisine(Cuisine cuisine) {for (Material material : cuisine.getBurdenSheet()) {if(material.accept(this)){return true;}}return false;}@Overridepublic Boolean doForMaterial(Material material) {return false;}@Overridepublic Boolean doForMeat(Meat meat) {return false;}@Overridepublic Boolean doForVegetable(Vegetable vegetable) {return vegetable.isOrganic();}@Overridepublic Boolean doForFlavour(Flavour flavour) {return false;}
}
Cuisine & Material
java">/*** 菜品*/
public class Cuisine {//……protected List<Material> getBurdenSheet() {return burdenSheet;}public <E> E accept(Visitor<E> v){return v.doForCuisine(this);}
}/*** 食材*/
public class Material {//……public <E> E accept(Visitor<E> v){return v.doForMaterial(this);}
}/*** 肉类*/
public class Meat extends Material {//……public <E> E accept(Visitor<E> v){return v.doForMeat(this);}
}/*** 蔬菜*/
public class Vegetable extends Material{// ……public <E> E accept(Visitor<E> v){return v.doForVegetable(this);}
}/*** 调料*/
public class Flavour extends Material{public <E> E accept(Visitor<E> v){return v.doForFlavour(this);}
}

我们创建了一个全新的Visitor(访问者)类簇,让Visitor去和菜品相关的所有类打交道,并获取其中的信息(这就是一开始说的被访问者必须向访问者公开自己的属性),为此我们还特地在Cuisine中添加了一个受保护的getBurdenSheet,以便访问者获取Cuisine内的信息

那访问者要怎么跟被访问者交互呢?还记得观察者模式吗,在观察者模式里我们给观察者和被观察者都做了修改。访问者是一样的,他不能也不应该直接访问被访问者内的信息,而是需要被访问者对他授权,也就是 accept 方法。但是和观察者模式不同的是,所有的被访问者子类都需要针对accept做出自己的特殊操作


这种实现方式堪称惊艳,就像变魔术一样,让所有的类型判断都消失了

其实仔细想想这些类型判断并不是消失了,而是 重写 帮我们代劳了。因为 MeatVegetableFlavour都是Material的子类,所以当我们在这三者中写入accept动作时,其实是在重写他们从Material中继承的方法。也就是说,如果到时候访问者的那个对象是属于下级子类的实例,那他就会优先调用被重写的accept方法

这写法可比if-else优雅多了,而且就算将来真的需要判断有没有香菜,或者有没有猪肉,只需要添加对应的Visitor子类就可以实现


而这正是一个标准的访问者实现




碎碎念

访问者和双分派

笔者读的书少,第一次看到访问者的实现时真的当场拍案叫绝,这种通过子类重写来避开类型判断的写法真的是太妙了

但是这种写法不是访问者的原创,他的行话叫 双分派(double-dispatch)。这是一种很著名的技术,有些编程语言甚至直接支持这种技术,但不包括Java

我们习惯了通过对象/类去点他里面的属性或者方法,就像这样:

java">a.b(c);

这时候a和b一定是确定的,只有c是动态变化的。这种模式就叫 单分派(single-dispatch)

而双分派实现的效果是可以让a都变得不确定,这是可能的,上例的accept就实现了这种效果

你有没有想过为什么上例的 dofor…accept 中都会出现调用 this,其实这就是在指定执行对象啊。我没有静态的指定谁调用谁,而是在程序执行到那里是才最终确定是谁调用了谁



访问者和代理

从实现上来看,访问者其实是一种变相的代理模式,说得更具体一点是 保护代理

就像上例我们使用访问者的契机其实是为了保护菜品里的配料表,访问者可以减少外部代码和被访问者之间的交互,特别是被访问者的结构错综复杂的时候,可以简化很多工作



写在最后的碎碎念

《庄子·养生主》中讲了一个叫庖丁的人给梁惠王表演杀牛。梁惠王惊讶于庖丁的杀牛技术,于是问他要怎么学才能像他一样。庖丁说:“因为我学习的是道,而不只是技巧。我刚开始杀牛的时候看到什么都是牛,都想用杀牛的方法去操作。三年后,我眼里就没有牛了,连牛在我眼里都不是牛了。因为我不觉得我是在杀牛,而是在解开他的经络,不是因为别人教我要怎么做,而是我的刀划到那里后自然而然应该这样去做,顺着刀势牛就已经被解了。”

这就是庖丁解牛的典故,我们也常用这个程序来形容某人的技术高超

在实战中使用设计模式和庖丁说的是一样的,23种基础设计模式只是”形“而已,他可能是某种情况下的最优解,但绝不是规则。实战中会遇到各种各样的情形,设计模式未必是正确答案,要不然也不会有反模式了

那你会说,用不上那我还学他干嘛?

你要学形而上的东西,你要学模式里的”道“。不是把模型生搬硬套到自己的实现中,而是去思考以前设计这些模式的人为什么要这样做,是什么思路让他做出这样的选择

直到将来的某一天,我相信一定有这样的某一天,道友你在不考虑设计模式的情况下,也会做出和设计模式一样的选择





万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容


http://www.ppmy.cn/ops/13668.html

相关文章

如何安装cuda和cudnn

https://www.bilibili.com/video/BV1sY411c7JS/?spm_id_from333.999.0.0 https://www.bilibili.com/video/BV1q5411d7GD/?spm_id_from333.999.0.0 //下面分别是10.1和11.1的安装包 链接&#xff1a;https://pan.baidu.com/s/1B7ky5b_bb5eqYv2UI1AJXw?pwdbcko 提取码&…

【LeetCode刷题记录】24. 两两交换链表中的节点

24 两两交换链表中的节点 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&#xff1a;head [1,2,…

卷积神经网络(CNN)

一、什么是卷积神经网络 二、结构 卷积神经网络的基本结构由以下几个部分组成&#xff1a;输入层&#xff08;input layer&#xff09;&#xff0c;卷积层&#xff08;convolution layer&#xff09;&#xff0c;池化层&#xff08;pooling layer&#xff09;&#xff0c;激活…

IDEA中添加servlet模板

官方代码链接 #if (${PACKAGE_NAME} && ${PACKAGE_NAME} ! "")package ${PACKAGE_NAME};#end #parse("File Header.java")import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.*; import java.io.IOException…

CX5120-0125倍福CPU模块维修PLC控制器CX5120全系列主机电脑深圳捷达工控维修

工业计算机服务与维修 为任何品牌的工业计算机、显示器、人机界面 (HMI) 和面板计算设备提供服务、维修和改造。您可以信赖我们延长生命周期&#xff0c;包括不再投放市场的产品。 工业计算机维修与保养 您的工业计算机专家 工业计算机维修服务 深圳捷达工控维修深知做好工业…

FreeRTOS学习 -- 任务

一、什么是任务系统 单片机裸跑的时候一般都是在main函数里面用 while (1) 做一个大循环来完成所有的处理&#xff0c;即应用程序是一个无限的循环&#xff0c;循环中调用相应的函数完成所需的处理。这个就是单任务系统&#xff0c;也称为前后台系统&#xff0c;中断服务函数作…

UniApp状态管理:从深入理解到灵活运用

在UniApp开发中&#xff0c;状态管理是一个至关重要的概念&#xff0c;它决定了应用程序的结构、性能和可维护性。本文将深入探讨UniApp中的状态管理&#xff0c;从基础知识到高级技巧&#xff0c;帮助开发者更好地理解和应用状态管理。 1. 什么是状态管理&#xff1f; 在Uni…

golang学习笔记(defer基础知识)

什么是defer defer语句用于golang程序中延迟函数的调用&#xff0c; 每次defer都会把一个函数压入栈中&#xff0c; 函数返回前再把延迟的函数取出并执行。 为了方便描述&#xff0c; 我们把创建defer的函数称为主函数&#xff0c; defer语句后面的函数称为延迟函数。延迟函数…