JAVA设计模式之访问模式详解

news2024/10/6 6:44:34

访问者模式

1 访问者模式介绍

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

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

这个定义会比较抽象,但是我们依然能看出两个关键点:

  • 一个是: 运行时使用一组对象的一个或多个操作,比如,对不同类型的文件(.pdf、.xml、.properties)进行扫描;

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

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

2 访问者模式原理

在这里插入图片描述

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

  • 抽象访问者(Visitor)角色:可以是接口或者抽象类,定义了一系列操作方法,用来处理所有数据元素,通常为同名的访问方法,并以数据元素类作为入参来确定那个重载方法被调用.
  • 具体访问者(ConcreteVisitor)角色:访问者接口的实现类,可以有多个实现,每个访问者都需要实现所有数据元素类型的访问重载方法.
  • 抽象元素(Element)角色:被访问的数据元素接口,定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素(ConcreteElement)角色: 具体数据元素实现类,提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法,其accept实现方法中调用访问者并将自己 “this” 传回。
  • 对象结构(Object Structure)角色:包含所有可能被访问的数据对象的容器,可以提供数据对象的迭代功能,可以是任意类型的数据结构.
  • 客户端 ( Client ) : 使用容器并初始化其中各类数据元素,并选择合适的访问者处理容器中的所有数据对象.

3 访问者模式实现

我们以超市购物为例,假设超市中的三类商品: 水果,糖果,酒水进行售卖. 我们可以忽略每种商品的计价方法,因为最终结账时由收银员统一集中处理,在商品类中添加计价方法是不合理的设计.我们先来定义糖果类和酒类、水果类.

/**
 * 抽象商品父类
 **/
public abstract class Product {

    private String name;  //商品名
    private LocalDate producedDate;  // 生产日期
    private double price;  //单品价格

    public Product(String name, LocalDate producedDate, double price) {
        this.name = name;
        this.producedDate = producedDate;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public LocalDate getProducedDate() {
        return producedDate;
    }

    public void setProducedDate(LocalDate producedDate) {
        this.producedDate = producedDate;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

/**
 * 糖果类
 **/
public class Candy extends Product{
    public Candy(String name, LocalDate producedDate, double price) {
        super(name, producedDate, price);
    }
}

/**
 * 酒水类
 **/
public class Wine extends Product{

    public Wine(String name, LocalDate producedDate, double price) {
        super(name, producedDate, price);
    }
}

/**
 * 水果类
 **/
public class Fruit extends Product{
    
    //重量
    private float weight;

    public Fruit(String name, LocalDate producedDate, double price, float weight) {
        super(name, producedDate, price);
        this.weight = weight;
    }

    public float getWeight() {
        return weight;
    }

    public void setWeight(float weight) {
        this.weight = weight;
    }
}

访问者接口

  • 收银员就类似于访问者,访问用户选择的商品,我们假设根据生产日期进行打折,过期商品不能够出售. 注意这种计价策略不适用于酒类,作为收银员要对不同商品应用不同的计价方法.
/**
 * 访问者接口-根据入参不同调用对应的重载方法
 **/
public interface Visitor {

    public void visit(Candy candy);  //糖果重载方法
    
    public void visit(Wine wine);  //酒类重载方法
    
    public void visit(Fruit fruit);  //水果重载方法
}

具体访问者

  • 创建计价业务类,对三类商品进行折扣计价,折扣计价访问者的三个重载方法分别实现了3类商品的计价方法,体现了visit() 方法的多态性.
/**
 * 折扣计价访问者类
 **/
public class DiscountVisitor implements Visitor {

    private LocalDate billDate;

    public DiscountVisitor(LocalDate billDate) {
        this.billDate = billDate;
        System.out.println("结算日期: " + billDate);
    }

    @Override
    public void visit(Candy candy) {
        System.out.println("糖果: " + candy.getName());

        //获取产品生产天数
        long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();

        if(days > 180){
            System.out.println("超过半年的糖果,请勿食用!");
        }else{
            double rate = 0.9;
            double discountPrice = candy.getPrice() * rate;
            System.out.println("糖果打折后的价格"+NumberFormat.getCurrencyInstance().format(discountPrice));
        }
    }

    @Override
    public void visit(Wine wine) {
        System.out.println("酒类: " + wine.getName()+",无折扣价格!");
        System.out.println("原价: "+NumberFormat.getCurrencyInstance().format(wine.getPrice()));
    }

    @Override
    public void visit(Fruit fruit) {
        System.out.println("水果: " + fruit.getName());
        //获取产品生产天数
        long days = billDate.toEpochDay() - fruit.getProducedDate().toEpochDay();

        double rate = 0;

        if(days > 7){
            System.out.println("超过七天的水果,请勿食用!");
        }else if(days > 3){
            rate = 0.5;
        }else{
            rate = 1;
        }

        double discountPrice = fruit.getPrice() * fruit.getWeight() * rate;
        System.out.println("水果价格: "+NumberFormat.getCurrencyInstance().format(discountPrice));
    }

    public static void main(String[] args) {

        LocalDate billDate = LocalDate.now();

        Candy candy = new Candy("徐福记",LocalDate.of(2022,10,1),10.0);
        System.out.println("糖果: " + candy.getName());

        double rate = 0.0;

        long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();
        System.out.println(days);

        if(days > 180){
            System.out.println("超过半年的糖果,请勿食用!");
        }else{
            rate = 0.9;
            double discountPrice = candy.getPrice() * rate;
            System.out.println("打折后的价格"+NumberFormat.getCurrencyInstance().format(discountPrice));
        }
    }
}

客户端

public class Client {

    public static void main(String[] args) {

        //德芙巧克力,生产日期2002-5-1 ,原价 10元
        Candy candy = new Candy("德芙巧克力",LocalDate.of(2022,5,1),10.0);

        Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,11));
        visitor.visit(candy);
    }
}

上面的代码虽然可以完成当前的需求,但是设想一下这样一个场景: 由于访问者的重载方法只能对当个的具体商品进行计价,如果顾客选择了多件商品来结账时,就可能会引起重载方法的派发问题(到底该由谁来计算的问题).

首先我们定义一个接待访问者的类 Acceptable,其中定义了一个accept(Visitor visitor)方法, 只要是visitor的子类都可以接收.

/**
 * 接待者接口(抽象元素角色)
 **/
public interface Acceptable {

    //接收所有的Visitor访问者的子类实现类
    public void accept(Visitor visitor);
}

/**
 * 糖果类
 **/
public class Candy extends Product implements Acceptable{
    public Candy(String name, LocalDate producedDate, double price) {
        super(name, producedDate, price);
    }

    @Override
    public void accept(Visitor visitor) {
        //accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型
        visitor.visit(this);
    }
}

//酒水与水果类同样实现Acceptable接口,重写accept方法

测试

public class Client {

    public static void main(String[] args) {

//        //德芙巧克力,生产日期2002-5-1 ,原价 10元
        Candy candy = new Candy("德芙巧克力",LocalDate.of(2022,5,1),10.0);

        Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,11));
        visitor.visit(candy);

        //模拟添加多个商品的操作
        List<Acceptable> products = Arrays.asList(
                new Candy("金丝猴奶糖",LocalDate.of(2022,6,10),10.00),
                new Wine("衡水老白干",LocalDate.of(2020,6,10),100.00),
                new Fruit("草莓",LocalDate.of(2022,10,12),50.00,1)
        );

        Visitor visitor = new DiscountVisitor(LocalDate.of(2022,10,17));
        for (Acceptable product : products) {
            product.accept(visitor);
        }
    }
}

代码编写到此出,就可以应对计价方式或者业务逻辑的变化了,访问者模式成功地将数据资源(需实现接待者接口)与数据算法 (需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,数据则保持相对固定,最终形成⼀个算法类对应⼀套数据。

4 访问者模式总结

  1. 访问者模式优点:
  • 扩展性好

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

  • 复用性好

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

  • 分离无关行为

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

  1. 访问者模式缺点:
  • 对象结构变化很困难

    在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  • 违反了依赖倒置原则

    访问者模式依赖了具体类,而没有依赖抽象类。

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

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

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

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

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

相关文章

华为 huawei 交换机 接口 MAC 地址学习限制接入用户数量 配置示例

目录 组网需求: 配置思路&#xff1a; 操作步骤&#xff1a; 配置文件&#xff1a; 组网需求: 如 图 2-14 所示&#xff0c;用户网络 1 和用户网络 2 通过 LSW 与 Switch 相连&#xff0c; Switch 连接 LSW 的接口为GE0/0/1 。用户网络 1 和用户网络 2 分别属于 VLAN10 和 V…

第三节 zookeeper基础应用与实战2

目录 1. Watch事件监听 1.1 一次性监听方式&#xff1a;Watcher 1.2 Curator事件监听机制 2. 事务&异步操作演示 2.1 事务演示 2.2 异步操作 3. Zookeeper权限控制 3.1 zk权限控制介绍 3.2 Scheme 权限模式 3.3 ID 授权对象 3.4 Permission权限类型 3.5 在控制台…

antdpro框架npm install 报错,切换tyarn安装成功。

报错日志 有时间补 当前版本 解决办法 进入工作目录 安装官方推荐的tyarn工具&#xff1a;npm install yarn tyarn -g 进行依赖安装&#xff1a;tyarn 启动项目 &#xff1a;tyarn start 注意&#xff1a; 技术迭代较快&#xff0c;建议查询官网后实践&#xff0c;以上作为…

Hive窗口函数详解

一、 窗口函数知识点 1.1 窗户函数的定义 窗口函数可以拆分为【窗口函数】。窗口函数官网指路&#xff1a; LanguageManual WindowingAndAnalytics - Apache Hive - Apache Software Foundationhttps://cwiki.apache.org/confluence/display/Hive/LanguageManual%20Windowing…

并行计算导论 笔记 1

目录 并行编程平台隐式并行超标量执行/指令流水线超长指令字处理器 VLIW 内存性能系统的局限避免内存延迟的方法 并行计算平台控制结构通信模型共享地址空间平台消息传递平台对比 物理组织理想并行计算机并行计算机互联网络网络拓朴结构基于总线的网络交叉开关网络多级网络全连…

微服务架构RabbitMQ实现CQRS模式

在现代软件开发中,微服务架构和CQRS模式都是备受关注的技术趋势。微服务架构通过将应用程序拆分为一系列小型、自治的服务,提供了更好的可伸缩性和灵活性。而CQRS模式则通过将读操作和写操作分离,优化了系统的性能和可维护性。本文小编将为大家介绍如何在ASP.NET Core微服务…

树莓派4B(Raspberry Pi 4B)使用docker搭建阿里巴巴sentinel服务

树莓派4B&#xff08;Raspberry Pi 4B&#xff09;使用docker搭建阿里巴巴sentinel服务 由于国内访问不了docker hub&#xff0c;而国内镜像仓库又没有适配树莓派ARM架构的sentinel镜像&#xff0c;所以我们只能退而求其次——自己动手构建镜像。本文基于Ubuntu&#xff0c;Jav…

Netty应用(八) 之 ByteBuf 半包粘包问题 半包粘包解决方案-封帧解码器

目录 19.ByteBuf 19.1 ByteBuf的基本使用 19.2 ByteBuf的扩容机制 19.3 ByteBuf与内存的关系 19.4 ByteBuf的内存结构 19.5 ByteBuf的API 19.5.1 ByteBuf的写操作 19.5.2 ByteBuf的读操作 19.5.3 ByteBuf的slice 19.6 ByteBuf的内存释放 19.6.1 实现API 19.6.2 如何…

前沿重器[42] | self-RAG-大模型决策的典型案例探究

前沿重器 栏目主要给大家分享各种大厂、顶会的论文和分享&#xff0c;从中抽取关键精华的部分和大家分享&#xff0c;和大家一起把握前沿技术。具体介绍&#xff1a;仓颉专项&#xff1a;飞机大炮我都会&#xff0c;利器心法我还有。&#xff08;算起来&#xff0c;专项启动已经…

Junit5基础教程

文章目录 一&#xff0c;导入依赖二&#xff0c;基本功能一、常用断言二、执行顺序和常用注解1、通过BeforeAll类的注解来保证顺序2、通过order注解来保证执行顺序 三、依赖测试四、参数化测试五、测试套件SelectPackages、IncludePackages、SelectClasses、IncludeTags等注解的…

javaweb物业管理系统jsp项目

文章目录 物业管理系统一、系统演示二、项目介绍三、系统部分功能截图四、部分代码展示五、底部获取项目源码&#xff08;9.9&#xffe5;带走&#xff09; 物业管理系统 可用作javaweb项目、servlet项目、jsp项目的项目设计 一、系统演示 物业管理系统 二、项目介绍 语言&a…

春晚魔术和约瑟夫问题

春晚的魔术实际上是一个约瑟夫问题&#xff0c;最终的结果是魔术开始时确定的几个变量确定好的&#xff0c;扑克牌只是道具和障眼法。网上一查这个问题发现颇有历史渊源&#xff0c;17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事&#xff1a;15个教徒和15 个…

酷开科技荣获“消费者服务之星”称号后的未来展望

恭喜酷开科技荣获2023年第四季度黑猫平台“消费者服务之星”称号&#xff01;这是对酷开科技长期以来坚持用户至上、用心服务的肯定和认可。作为OTT行业的佼佼者&#xff0c;酷开科技一直秉承着“以用户为中心”的服务理念&#xff0c;不断追求卓越品质&#xff0c;为用户提供更…

《CSS 简易速速上手小册》第6章:高级 CSS 技巧(2024 最新版)

文章目录 6.1 使用 CSS 变量进行设计&#xff1a;魔法配方的调配6.1.1 基础知识6.1.2 重点案例&#xff1a;创建可定制的主题6.1.3 拓展案例 1&#xff1a;响应式字体大小6.1.4 拓展案例 2&#xff1a;使用 CSS 变量创建动态阴影效果 6.2 calc(), min(), max() 等函数的应用&am…

Java 基于微信小程序的电子商城购物系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

使用R语言fifer包进行分层采样

使用R语言fifer包中的stratified()函数用来进行分层采样非常方便&#xff0c;但fifer包已经从CRAN存储库中删除&#xff0c;需要从存档中下载可用的历史版本&#xff0c;下载链接&#xff1a;Index of /src/contrib/Archive/fifer (r-project.org)https://cran.r-project.org/s…

架构篇34:深入理解微服务架构 - 银弹 or 焦油坑?

文章目录 微服务与 SOA 的关系微服务的陷阱小结 微服务是近几年非常火热的架构设计理念&#xff0c;大部分人认为是 Martin Fowler 提出了微服务概念&#xff0c;但事实上微服务概念的历史要早得多&#xff0c;也不是 Martin Fowler 创造出来的&#xff0c;Martin 只是将微服务…

Github 2024-02-11 开源项目日报Top10

根据Github Trendings的统计&#xff0c;今日(2024-02-11统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Python项目4非开发语言项目2C项目1C项目1Solidity项目1JavaScript项目1Rust项目1HTML项目1 免费服务列表 | f…

KingSCADA实现按钮点击效果

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 在做SCADA项目的时候&#xff0c;按钮是不可缺少的功能&#xff0c;但软件自带的按钮太丑&#xff0c;已经无法满足现如今客户对界面美观度的要求。 这时候就需要UI小姐姐设计美观大气的SCADA界面&#xff0c;但UI设计…

内网安全-内网穿透

目录 内网渗透 Nc使用详解 Nc监听和探测 Nc传文件 termite内网穿透工具 ssh代理内网穿透 ssh配置socket代理 MSF多级网络穿透 内网渗透 Nc使用详解 Nc监听和探测 Nc传文件 termite内网穿透工具 1、termite 之前叫ew &#xff08;可以进行正向连接&#xff0c;可以…