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

2024-04-22 06:44

本文主要是介绍设计模式——2_A 访问者(Visitor),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

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

定义

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


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

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




图纸

在这里插入图片描述




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

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

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

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



菜单、菜品和配方

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

在这里插入图片描述

Menu(菜单) & Cuisine(菜品)
/*** 菜品*/
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(物料、食材)
/*** 食材*/
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
/*** 菜品*/
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
/*** 访问者*/
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
/*** 菜品*/
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

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

a.b(c);

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

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

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



访问者和代理

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

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



写在最后的碎碎念

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

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

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

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

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

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





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

这篇关于设计模式——2_A 访问者(Visitor)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/925061

相关文章

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

小技巧绕过Sina Visitor System(新浪访客系统)

0x00 前言 一直以来,爬虫与反爬虫技术都时刻进行着博弈,而新浪微博作为一个数据大户更是在反爬虫上不遗余力。常规手段如验证码、封IP等等相信很多人都见识过…… 当然确实有需要的话可以通过新浪开放平台提供的API进行数据采集,但是普通开发者的权限比较低,限制也比较多。所以如果只是做一些简单的功能还是爬虫比较方便~ 应该是今年的早些时候,新浪引入了一个Sina Visitor Syst

设计模式之工厂模式(通俗易懂--代码辅助理解【Java版】)

文章目录 1、工厂模式概述1)特点:2)主要角色:3)工作流程:4)优点5)缺点6)适用场景 2、简单工厂模式(静态工厂模式)1) 在简单工厂模式中,有三个主要角色:2) 简单工厂模式的优点包括:3) 简单工厂模式也有一些限制和考虑因素:4) 简单工厂模式适用场景:5) 简单工厂UML类图:6) 代码示例: 3、工厂方法模式1) 在工厂方法模式中,有4个主要角色:2) 工厂方法模式的工作流程

C#设计模式(1)——单例模式(讲解非常清楚)

一、引言 最近在学设计模式的一些内容,主要的参考书籍是《Head First 设计模式》,同时在学习过程中也查看了很多博客园中关于设计模式的一些文章的,在这里记录下我的一些学习笔记,一是为了帮助我更深入地理解设计模式,二同时可以给一些初学设计模式的朋友一些参考。首先我介绍的是设计模式中比较简单的一个模式——单例模式(因为这里只牵涉到一个类) 二、单例模式的介绍 说到单例模式,大家第一

漫谈设计模式 [12]:模板方法模式

引导性开场 菜鸟:老大,我最近在做一个项目,遇到了点麻烦。我们有很多相似的操作流程,但每个流程的细节又有些不同。我写了很多重复的代码,感觉很乱。你有啥好办法吗? 老鸟:嗯,听起来你遇到了典型的代码复用和维护问题。你有没有听说过“模板方法模式”? 菜鸟:模板方法模式?没听过。这是什么? 老鸟:简单来说,模板方法模式让你在一个方法中定义一个算法的骨架,而将一些步骤的实现延迟到子类中。这样,你可

漫谈设计模式 [9]:外观模式

引导性开场 菜鸟:老鸟,我最近在做一个项目,感觉代码越来越复杂,我都快看不懂了。尤其是有好几个子系统,它们之间的调用关系让我头疼。 老鸟:复杂的代码确实让人头疼。你有没有考虑过使用设计模式来简化你的代码结构? 菜鸟:设计模式?我听说过一些,但不太了解。你觉得我应该用哪个模式呢? 老鸟:听起来你的问题可能适合用**外观模式(Facade Pattern)**来解决。我们可以一起探讨一下。

设计模式大全和详解,含Python代码例子

若有不理解,可以问一下这几个免费的AI网站 https://ai-to.cn/chathttp://m6z.cn/6arKdNhttp://m6z.cn/6b1quhhttp://m6z.cn/6wVAQGhttp://m6z.cn/63vlPw 下面是设计模式的简要介绍和 Python 代码示例,涵盖主要的创建型、结构型和行为型模式。 一、创建型模式 1. 单例模式 (Singleton

漫谈设计模式 [6]:适配器模式

引导性开场 菜鸟:老鸟,我最近在项目中遇到一个问题,我们的系统需要集成一个新的第三方库,但这个库的接口和我们现有的代码完全不兼容。我该怎么办? 老鸟:这是个常见的问题,很多开发者都会遇到这种情况。你有没有听说过适配器模式? 菜鸟:适配器模式?没有,能详细说说吗? 老鸟:当然可以!这就是我们今天要讨论的主题。适配器模式是一个设计模式,可以帮助我们解决你现在遇到的问题。 渐进式介绍概念 老

2 观察者模式(设计模式笔记)

2 观察者模式(别名:发布-订阅) 概念 定义对象间的一种一对多的依赖关系,当一个对象状态发生变化时,所以依赖于它的对象都得到通知并被自动更新。 模式的结构与使用 角色 主题(Subject)观察者(Observer)具体主题(ConcreteSubject)具体观察者(ConcreteObserver) 结构 Subject依赖于Observer最重要!!! package

1 单例模式(设计模式笔记)

1 单例模式 概述:使得一个类的对象成为系统中的唯一实例。 具体实现: 构造函数私有化 限制实例的个数 懒汉式(时间换空间) public class Singleton2 {public static Singleton2 singleton2;private Singleton2(){}public static Singleton2 getInstance() throws I