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

news2024/9/20 1:11:28

文章目录

  • 定义
  • 图纸
  • 一个例子:如何给好奇宝宝提供他想知道的内容
    • 菜单、菜品和配方
          • 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>{

    @Override
    public Integer doForCuisine(Cuisine cuisine) {
        int result = 0;
        for (Material material : cuisine.getBurdenSheet()) {
            result += material.accept(this);
        }

        return result;
    }

    @Override
    public Integer doForMaterial(Material material) {
        return 0;
    }

    @Override
    public Integer doForMeat(Meat meat) {
        return meat.getCalorie();
    }

    @Override
    public Integer doForVegetable(Vegetable vegetable) {
        return vegetable.getCalorie();
    }

    @Override
    public Integer doForFlavour(Flavour flavour) {
        return 0;
    }
}

/**
 * 有机属性访问者
 */
public class OrganicVisitor implements Visitor<Boolean> {

    @Override
    public Boolean doForCuisine(Cuisine cuisine) {
        for (Material material : cuisine.getBurdenSheet()) {
            if(material.accept(this)){
                return true;
            }
        }

        return false;
    }

    @Override
    public Boolean doForMaterial(Material material) {
        return false;
    }

    @Override
    public Boolean doForMeat(Meat meat) {
        return false;
    }

    @Override
    public Boolean doForVegetable(Vegetable vegetable) {
        return vegetable.isOrganic();
    }

    @Override
    public 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种基础设计模式只是”形“而已,他可能是某种情况下的最优解,但绝不是规则。实战中会遇到各种各样的情形,设计模式未必是正确答案,要不然也不会有反模式了

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

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

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





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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1613042.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C#基础|StringBuilder字符串如何高效处理。

哈喽&#xff0c;你好&#xff0c;我是雷工。 字符串处理在C#程序开发中是使用频率比较高的&#xff0c;但常规的字符串处理方式对内存占用比较多&#xff0c;为了优化内存&#xff0c;减少不必要的内存浪费&#xff0c;引入了StringBuilder类。 下面学习下StringBuilder类的使…

插入排序动态展示3(Python可视化源代码)

修改了“开始”命令按钮&#xff0c;每次单击“开始”&#xff0c;都重新排序。 Python代码 import tkinter as tk import random import timeclass InsertionSortVisualizer:def __init__(self, root, canvas_width800, canvas_height400, num_bars10):self.root rootself.…

wasm 系列之 WebAssembly 和 emscripten 暴力上手

wasm 是什么&#xff1f; wasm 是 WebAssembly 的缩写。wasm 不是传统意义上的汇编语言&#xff0c;而是一种编译的中间字节码&#xff0c;可以在浏览器和其他 wasm runtime 上运行非 JavaScript 类型的语言&#xff0c;只要能被编译成 wasm&#xff0c;譬如 kotlin/wasm、Rus…

鸿蒙OpenHarmony【轻量系统编写“Hello World”程序】 (基于Hi3861开发板)

编写“Hello World”程序 下方将通过修改源码的方式展示如何编写简单程序&#xff0c;输出“Hello world”。请在下载的源码目录中进行下述操作。 前提条件 已参考鸿蒙开发指导文档&#xff1a;gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到…

编写函数fun,它的功能是:根据以下公式求P的值,结果由函数值带回。m与n为两个正整数且要求m>n

本文收录于专栏:算法之翼 https://blog.csdn.net/weixin_52908342/category_10943144.html 订阅后本专栏全部文章可见。 本文含有题目的题干、解题思路、解题思路、解题代码、代码解析。本文分别包含C语言、C++、Java、Python四种语言的解法完整代码和详细的解析。 题干 编写…

【JavaEE初阶系列】——网络层IP协议(地址管理和路由选择)

目录 &#x1f6a9;网络层 &#x1f388;IP协议 &#x1f469;&#x1f3fb;‍&#x1f4bb;IP协议"拆包组包"功能 &#x1f388;地址管理 &#x1f469;&#x1f3fb;‍&#x1f4bb;IP地址的分类 &#x1f469;&#x1f3fb;‍&#x1f4bb;NAT机制如何工作的…

记录:阿里云服务器网站搭建(2)

Docker安装Mysql mysql版本 查看开发环境中mysql版本 &#xff1a;select version()&#xff1b;安装时版本尽量保证一致&#xff0c;最低要求大版本要一致 docker 拉取mysql镜像 docker pull mysql:8.0.36 docker启动mysql容器 docker run -d \ # 创建并运行一个容器&…

下班族张亮的副业赚钱故事

张亮是一个普通的上班族&#xff0c;每天过着朝九晚五的生活。他渴望改变现状&#xff0c;却又觉得生活缺乏突破口。直到有一天&#xff0c;他在网络上偶然发现了水牛社这个平台&#xff0c;这为他打开了一扇新的大门。 张亮开始利用下班后的空闲时间&#xff0c;认真浏览水牛社…

IDEA下载与安装

1.下载 链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;7v5q 2.安装

综合案例(前端代码练习):猜数字和表白墙

目录 一、猜数字 html代码&#xff1a; 点击 猜 按钮的js代码&#xff1a; 点击 重开游戏 按钮的js代码&#xff1a; 整体代码&#xff1a; 页面效果&#xff1a; 二、留言板 css代码&#xff1a; html代码&#xff1a; js代码&#xff08;主逻辑在这&#xff09;&am…

JAVA高阶私房菜:JVM虚拟机核心概念及参数微调实验

目录 基础快速掌握 什么是JVM虚拟机 JVM的的实现 操作系统-虚拟机-JRE-JDK的关系 生产环境部署JDK还是JRE JVM内存组成部分和堆空间分布 内存组成 堆空间内存分布 内存分布 堆空间分配 JVM堆空间垃圾回收流程及JVM参数 垃圾回收流程 JVM参数分类 JVM参数格式分类 …

山东大学操作系统实验一(Linux虚拟机实现)

目录 实验题目 实验要求 示例程序 主程序 头文件 重点代码解析 一、main函数的参数 参数介绍 参数输入方式 本块代码 二、信号处理 本块代码 原理介绍 实现效果 三、kill函数 功能介绍 使用方式 本块代码 四、头文件处理 本块代码 代码作用 实验程序 …

Python数据可视化:频率统计条形图countplot()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 Python数据可视化&#xff1a; 频率统计条形图 countplot() [太阳]选择题 请问关于以下代码表述正确的选项是&#xff1f; import seaborn as sns import matplotlib.pyplot as plt data { …

二、python+前端 实现MinIO分片上传

python前端 实现MinIO分片上传 一、背景二、流程图三、代码 一、背景 问题一&#xff1a;前端 -> 后端 ->对象存储 的上传流程&#xff0c;耗费带宽。 解决方案&#xff1a;上传流程需要转化为 前端 -> 对象存储&#xff0c;节省上传带宽 问题二&#xff1a;如果使用…

Leetcode 第394场周赛 问题和解法

题目 统计特殊字母的数量 I 给你一个字符串word。如果word中同时存在某个字母的小写形式和大写形式&#xff0c;则称这个字母为特殊字母。 返回word中特殊字母的数量。 示例 1: 输入&#xff1a;word "aaAbcBC"输出&#xff1a;3解释&#xff1a;word 中的特殊…

【Entity Framework】聊一聊EF如何使用数据库函数

【Entity Framework】聊一聊EF如何使用数据库函数 文章目录 【Entity Framework】聊一聊EF如何使用数据库函数一、数据库函数的类型二、内置函数与用户定义的函数四、聚合函数、标量函数和表值函数五、Niladic函数六、EF Core 中的数据库函数映射6.1 内置函数映射6.2 EF.Functi…

【iOS开发】(四)react Native第三方组件五个20240419-20

react native 外的 第三方组件 目录标题 react native 外的 第三方组件&#xff08;一&#xff09;与rn核心组件的使用步骤区别&#xff1a;&#xff08;二&#xff09;第三方组件概览1 WebView2 Picker3 Swiper4 AsyncStorage5 Geolocation6 Camera (三)详细学习1 WebViewCoco…

ROS1快速入门学习笔记 - 01Linux基础

目录 一、Linux极简基础 二、C与Python极简基础 1. for循环 2. while循环 3. 面向对象 一、Linux极简基础 终端快捷键&#xff1a;ctrlaltt 命令行的操作方式 查看当前终端所在路径&#xff1a;pwd切换路径cd&#xff1b;例如cd /home/ 进入home文件夹&#xff1b;cd …

Oracle Hint 语法详解

什么是Hint Hint 是 Oracle 提供的一种 SQL 语法&#xff0c;它允许用户在 SQL 语句中插入相关的语法&#xff0c;从而影响 SQL 的执行方式。 因为 Hint 的特殊作用&#xff0c;所以对于开发人员不应该在代码中使用它&#xff0c;Hint 更像是 Oracle 提供给 DBA 用来分析诊断问…

2024数学建模时间汇总与竞赛攻略

目录 2024数学建模汇总&#xff08;时间、报名费、获奖率、竞赛级别、是否可跨校&#xff09; 中国高校大数据挑战赛 “华数杯”国际大学生数学建模竞赛 美国大学生数学建模竞赛&#xff08;美赛&#xff09; 数学中国&#xff08;认证杯&#xff09;数学建模网络挑战赛 …