设计模式——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

相关文章

十五.各设计模式总结与对比

1.各设计模式总结与对比 1.1.课程目标 1、 简要分析GoF 23种设计模式和设计原则,做整体认知。 2、 剖析Spirng的编程思想,启发思维,为之后深入学习Spring做铺垫。 3、 了解各设计模式之间的关联,解决设计模式混淆的问题。 1.2.内容定位 1、 掌握设计模式的"道" ,而不只是"术" 2、 道可道非常道,滴水石穿非一日之功,做好长期修炼的准备。 3、 不要为了

十四、观察者模式与访问者模式详解

21.观察者模式 21.1.课程目标 1、 掌握观察者模式和访问者模式的应用场景。 2、 掌握观察者模式在具体业务场景中的应用。 3、 了解访问者模式的双分派。 4、 观察者模式和访问者模式的优、缺点。 21.2.内容定位 1、 有 Swing开发经验的人群更容易理解观察者模式。 2、 访问者模式被称为最复杂的设计模式。 21.3.观察者模式 观 察 者 模 式 ( Obser

从《深入设计模式》一书中学到的编程智慧

软件设计原则   优秀设计的特征   在开始学习实际的模式前,让我们来看看软件架构的设计过程,了解一下需要达成目标与需要尽量避免的陷阱。 代码复用 无论是开发何种软件产品,成本和时间都最重要的两个维度。较短的开发时间意味着可比竞争对手更早进入市场; 较低的开发成本意味着能够留出更多营销资金,因此能更广泛地覆盖潜在客户。 代码复用是减少开发成本时最常用的方式之一。其意图

[最全]设计模式实战(一)UML六大原则

UML类图 UML类图是学习设计模式的基础,学习设计模式,主要关注六种关系。即:继承、实现、组合、聚合、依赖和关联。 UML类图基本用法 继承关系用空心三角形+实线来表示。实现接口用空心三角形+虚线来表示。eg:大雁是最能飞的,它实现了飞翔接口。 关联关系用实线箭头来表示。当一个类"知道"另一个类时,可以用关联。eg:企鹅需要"知道"气候的变化,需要"了解"气候规律。 聚合关

设计模式学习之中介者模式

我们平时写代码的过程,一个类必然会与其他类产生依赖关系,如果这种依赖关系如网状般错综复杂,那么必然会影响我们的代码逻辑以及执行效率,适当地使用中介者模式可以对这种依赖关系进行解耦使逻辑结构清晰,本篇博客,我们就一起学习中介者模式。 定义及使用场景 定义:中介者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。从而使它们可以松散耦合。当某些对象之间的作用发生改变时,不会立即影响其

设计模式学习之模版方法模式

模板方法模式是一种基于继承的代码复用的行为型模式;在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。本篇博客我们一起来学习模版方法模式。 定义与UML图 定义 模板方法模式:定义一个操作

Android设计模式学习之Builder模式

Android设计模式学习之观察者模式 建造者模式(Builder Pattern),是创造性模式之一,Builder 模式的目的则是为了将对象的构建与展示分离。Builder 模式是一步一步创建一个复杂对象的创建型模式,它允许用户在不知道内部构建细节的情况下,可以更精细地控制对象的构造流程。 模式的使用场景 1.相同的方法,不同的执行顺序,产生不同的事件结果时; 2.多个部件或零件,都可

【设计模式-04】原型模式

【设计模式-04】原型模式 1. 概述2. 结构3. 实现4. 案例5. 使用场景6. 优缺点6.1 原型模式的优点6.2 原型模式的缺点 7. 实现深克隆(深拷贝) 1. 概述 原型模式: 用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。 2. 结构 原型模式包含如下角色: 抽象原型类:规定了具体原型对象必须实现的 clone() 方法。

Java中常见的设计模式及应用场景

Java中常见的设计模式及应用场景 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将探讨Java中常见的设计模式及其应用场景,帮助大家更好地理解和应用这些设计模式,提高代码的可维护性和可扩展性。 设计模式概述 设计模式是一种被反复使用的、经过分类的、代码设计中被广泛认可的优秀代码设计经验。它不仅能解决常见的问题,还能

【设计模式之解释器模式 -- C++】

解释器模式 – 语法解析,执行操作 解释器模式是一种设计模式,用于为某个语言定义其语法表示,并提供一个解释器,这个解释器使用该表示来解释语言中的句子。这种模式通常用于开发专门的语言或脚本引擎,可以解析和执行用户定义的指令或表达式。 组成 抽象表达式(Abstract Expression):定义解释操作的接口,这个接口为解释特定的上下文提供了解释(interpret)方法。终结符表达式(Te