当前位置: 首页 > news >正文

设计模式之访问者模式详解(Visitor Pattern)

最复杂的设计模式,并且使用频率不高,《设计模式》的作者评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。
访问者模式是一种将数据操作和数据结构分离的设计模式。(觉得太抽象,可以看下面的例子)。

模式的定义与特点

访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。

访问者(Visitor)模式是一种对象行为型模式,其主要优点如下:

  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

访问者(Visitor)模式的主要缺点如下。

  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

模式的原理类图与角色说明

访问者模式的原理类图

角色说明

  • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
  • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  • 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。

模式的应用实例

年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量。

由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。

抽象元素(Element)角色
//员工基类
public abstract class Staff {
    public String name;
    public int kpi;

    public Staff(String name) {
        this.name = name;
        kpi = new Random().nextInt(10);
    }
    //核心方法,接受Visitor的访问
    public abstract void accept(Visitor visitor);
}
具体元素(ConcreteElement)角色
//经理
public class Manager extends Staff {
    public Manager(String name) {
        super(name);
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // 一年做的产品数量
    public int getProducts() {
        return new Random().nextInt(10);
    }
}
//工程师
public class Engineer extends Staff {
    public Engineer(String name) {
        super(name);
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // 工程师一年的代码数量
    public int getCodeLines() {
        return new Random().nextInt(10 * 10000);
    }
}

工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构这些类型相对稳定,不会发生变化。

对象结构(Object Structure)角色

将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的 showReport 方法查看所有员工的业绩,具体代码如下:

// 员工业务报表类
public class BusinessReport {

    private List<Staff> mStaffs = new LinkedList<>();

    public BusinessReport() {
        mStaffs.add(new Manager("经理-A"));
        mStaffs.add(new Engineer("工程师-A"));
        mStaffs.add(new Engineer("工程师-B"));
        mStaffs.add(new Engineer("工程师-C"));
        mStaffs.add(new Manager("经理-B"));
        mStaffs.add(new Engineer("工程师-D"));
    }

    /**
     * 为访问者展示报表
     * @param visitor 公司高层,如CEO、CTO
     */
    public void showReport(Visitor visitor) {
        for (Staff staff : mStaffs) {
            staff.accept(visitor);
        }
    }
}
抽象访问者(Visitor)角色
public interface Visitor {
    // 访问工程师类型
    void visit(Engineer engineer);

    // 访问经理类型
    void visit(Manager manager);
}

该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。具体实现类为 CEOVisitor、CTOVisitor类

具体访问者(ConcreteVisitor)角色
// CEO访问者
public class CEOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
        System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
                ", 新产品数量: " + manager.getProducts());
    }
}
//CTO访问者
public class CTOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
        System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines());
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts());
    }
}

在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。

重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码。

客户端使用
public class Client {

    public static void main(String[] args) {
        // 构建报表
        BusinessReport report = new BusinessReport();
        System.out.println("=========== CEO看报表 ===========");
        report.showReport(new CEOVisitor());
        System.out.println("=========== CTO看报表 ===========");
        report.showReport(new CTOVisitor());
    }
}

具体输出如下:

=========== CEO看报表 ===========
经理: 经理-A, KPI: 0, 新产品数量: 2
工程师: 工程师-A, KPI: 5
工程师: 工程师-B, KPI: 4
工程师: 工程师-C, KPI: 2
经理: 经理-B, KPI: 2, 新产品数量: 0
工程师: 工程师-D, KPI: 4
=========== CTO看报表 ===========
经理: 经理-A, 产品数量: 9
工程师: 工程师-A, 代码行数: 14704
工程师: 工程师-B, 代码行数: 41123
工程师: 工程师-C, 代码行数: 64598
经理: 经理-B, 产品数量: 5
工程师: 工程师-D, 代码行数: 42490

访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。

模式的应用场景

通常在以下情况可以考虑使用访问者(Visitor)模式。

  1. 对象结构相对稳定,但其操作算法经常变化的程序。
  2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。

参考

访问者模式一篇就够了
访问者模式(Visitor模式)详解

相关文章:

  • GCC for Win32开发环境介绍(2)
  • 设计模式之观察者模式详解(Observer Pattern)
  • 设计模式之迭代器模式详解(Iterator Pattern)
  • asp.net部分优化
  • 设计模式之中介者模式详解(Mediator Pattern)
  • ASP.NET中利用存储过程实现模糊查询
  • 设计模式之备忘录模式详解(Memento Pattern)
  • 从asp转asp.net的相关
  • 设计模式之状态模式详解(State Pattern)
  • 浅谈DataSet 的用法
  • 多维数组和矩阵之顺时针打印二维数组
  • 各种数据库连接代码(JSP)
  • 多维数组与矩阵之0所在的行列清零
  • 多维数组与矩阵之之字形打印矩阵
  • 生姜有味的调色板
  • ES学习笔记(12)--Symbol
  • export和import的用法总结
  • Javascripit类型转换比较那点事儿,双等号(==)
  • JavaScript 基础知识 - 入门篇(一)
  • JS学习笔记——闭包
  • laravel 用artisan创建自己的模板
  • Laravel核心解读--Facades
  • Linux快速复制或删除大量小文件
  • Mithril.js 入门介绍
  • SpringBoot 实战 (三) | 配置文件详解
  • Spring核心 Bean的高级装配
  • Sublime text 3 3103 注册码
  • SwizzleMethod 黑魔法
  • v-if和v-for连用出现的问题
  • 第13期 DApp 榜单 :来,吃我这波安利
  • 第2章 网络文档
  • 码农张的Bug人生 - 初来乍到
  • 前端js -- this指向总结。
  • 入门级的git使用指北
  • 适配mpvue平台的的微信小程序日历组件mpvue-calendar
  • 小而合理的前端理论:rscss和rsjs
  • [地铁译]使用SSD缓存应用数据——Moneta项目: 低成本优化的下一代EVCache ...
  • Hibernate主键生成策略及选择
  • ​LeetCode解法汇总2182. 构造限制重复的字符串
  • # 透过事物看本质的能力怎么培养?
  • $.type 怎么精确判断对象类型的 --(源码学习2)
  • (1综述)从零开始的嵌入式图像图像处理(PI+QT+OpenCV)实战演练
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (十)T检验-第一部分
  • (四)c52学习之旅-流水LED灯
  • (轉貼) 寄發紅帖基本原則(教育部禮儀司頒布) (雜項)
  • .bat批处理(三):变量声明、设置、拼接、截取
  • .net core 依赖注入的基本用发
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .NET牛人应该知道些什么(2):中级.NET开发人员
  • .NET使用HttpClient以multipart/form-data形式post上传文件及其相关参数
  • .Net中的集合
  • .NET中统一的存储过程调用方法(收藏)
  • @GlobalLock注解作用与原理解析
  • @Validated和@Valid校验参数区别