草庐IT

访问者模式(学习笔记)

慕仙白 2023-03-28 原文

  1. 意图

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

  2. 动机   

  假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市),也能代表更精细的对象(例如工业区和旅游景点等)。如果节点代表的真实对象之间存在公路,那么这些节点就会相互连接。在程序内部,每个节点的类型都由其所属的类来表示,每个特定的节点则是一个对象。

         

  一段时间后,接到了实现将图像导出到XML文件中的任务。这些工作最初看上去非常简单。你计划为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数。解决方案简单且优雅:使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。但s是系统架构师拒绝批准对已有节点类进行修改。他认为这些代码已经是产品了,不想冒险对其进行修改,因为修改可能会引入潜在的缺陷。

 

  此外,他还质疑在节点类中包含导出XML文件的代码是否有意义。这些类的主要工作是处理地理数据,导出XML文件的代码放在这里并不合适。还有另一个原因,那就是在此项任务完成后,营销部门很有可能会要求程序提供导出其他类型文件的功能,或者提出其他奇怪的要求。这样你很可能会被迫再次修改这些重要但脆弱的类。

  访问者模式建议将新行为放入一个名为访问者的独立类中,而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。  

  如果现在该操作能在不同类的对象上执行会怎么样呢?比如在我们的示例中,各节点类导出XML文件的实际实现很可能会稍有不同。因此,访问者类可以定义一组(而不是一个)方法,且每个方法可接收不同类型的参数,如下所示:  

class ExportVisitor implements Visitor is
    method doForCity(City c) { ... }
    method doForIndustry(Industry f) { ... }
    method doForSightSeeing(SightSeeing ss) { ... }
    // ...

  问题是,我们该如何调用这些方法呢?可以发现,这些方法的签名各不相同,因此不能使用多态机制。为了可以挑选出能够处理特定对象的访问者方法我们需要对它的类进行检查,如下面代码所示:

foreach (Node node in graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node)
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node)
    // ...
}

  可否使用方法地重载呢?依然不行,因为我们无法提前知晓节点对象所属的类,所以重载机制无法执行正确的方法

  访问者模式可以解决这个问题。它使用了一种名为双分派(在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时型别(Run time type),还要根据参数的运行时型别(Run time type)。这里的消息接收者其实就是方法的调用者。具体来讲就是,对于消息表达式a.m(b),双分派能够按照a和b的实际类型为其绑定对应方法体)的技巧,在不使用累赘的条件语句地情况下,也可以执行正确的方法。与其让客户端来选择调用正确版本的方法,不如将对象放在各个节点中,并将访问者对象作为参数传给该对象。由于该对象知晓其自身的类,因此能更自然地在访问者中选出正确的方法。它们会 “接收” 一个访问者并告诉其应执行的访问者方法

// 客户端代码
foreach (Node node in graph)
    node.accept(exportVisitor)

// 城市
class City is
    method accept(Visitor v) is
        v.doForCity(this)
    // ...

// 工业区
class Industry is
    method accept(Visitor v) is
        v.doForIndustry(this)
    // ...

  我们最终还是修改了节点类,但毕竟改动很小,而且在后续进一步添加行为时无需再次修改代码。现在,如果我们抽取出所有访问者的通用接口,所有已有的节点都能与我们在程序中引入的任何访问者交互。如果需要引入与节点相关的某个行为,你只需要实现一个新的访问者类即可  

  3. 适用性

  • 一个对象结构(如对象树)包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作
  • 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作污染这些类对象。Visitor使得你可以将相关操作集中起来定义在一个类中。当对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作
  • 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用该模式

  4. 结构

                                       

  5. 效果

  1. 访问者模式使得易于增加新的操作(开闭原则)

  2. 访问者集中相关的操作而分离无关的操作(单一职责原则)

  3. 增加新的ConcreteElement类很困难    Visitor模式使得难以增加新的Element的子类。每添加一个新的ConcreteElement都要在Visitor中添加一个新的抽象操作,并在每一个ConcreteVisitor类中实现相应的操作。所以在使用访问者模式时考虑的关键问题是系统的哪个部分会经常变化,是作用于对象结构上的算法还是构成该结构的给个对象的类。如果总是有新的ConcreteElement类加进来,Visitor类层次将变得难以维护

  4. 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限

  6. 代码实现    

  在本例中 我们希望将一系列几何形状导出为 XML 文件 重点在于我们不希望直接修改形状代码 或者至少能确保最小程度的修改

  shapes/Shape.java: 通用形状接口

package visitor.shapes;

import visitor.visitor.Visitor;

/**
 * @author GaoMing
 * @date 2021/7/26 - 17:23
 */
public interface Shape {
    void move(int x, int y);
    void draw();
    String accept(Visitor visitor);
}

  shapes/Dot.java: 点

package visitor.shapes;

import visitor.visitor.Visitor;

/**
 * @author GaoMing
 * @date 2021/7/26 - 17:24
 */
public class Dot implements Shape{
    private int id;
    private int x;
    private int y;

    public Dot() {
    }

    public Dot(int id, int x, int y) {
        this.id = id;
        this.x = x;
        this.y = y;
    }

    @Override
    public void move(int x, int y) {
        // move shape
    }

    @Override
    public void draw() {
        // draw shape
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitDot(this);
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getId() {
        return id;
    }
}

  shapes/Circle.java: 圆形

package visitor.shapes;

import visitor.visitor.Visitor;

/**
 * @author GaoMing
 * @date 2021/7/26 - 17:25
 */
public class Circle extends Dot{
    private int radius;

    public Circle(int id, int x, int y, int radius) {
        super(id, x, y);
        this.radius = radius;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitCircle(this);
    }

    public int getRadius() {
        return radius;
    }
}

  shapes/Rectangle.java: 矩形

package visitor.shapes;

import visitor.visitor.Visitor;

/**
 * @author GaoMing
 * @date 2021/7/26 - 17:26
 */
public class Rectangle implements Shape{
    private int id;
    private int x;
    private int y;
    private int width;
    private int height;

    public Rectangle(int id, int x, int y, int width, int height) {
        this.id = id;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitRectangle(this);
    }

    @Override
    public void move(int x, int y) {
        // move shape
    }

    @Override
    public void draw() {
        // draw shape
    }

    public int getId() {
        return id;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}

  shapes/CompoundShape.java: 组合形状

package visitor.shapes;

import visitor.visitor.Visitor;

import java.util.ArrayList;
import java.util.List;

/**
 * @author GaoMing
 * @date 2021/7/26 - 17:27
 */
public class CompoundShape implements Shape{
    public int id;
    public List<Shape> children = new ArrayList<>();

    public CompoundShape(int id) {
        this.id = id;
    }

    @Override
    public void move(int x, int y) {
        // move shape
    }

    @Override
    public void draw() {
        // draw shape
    }

    public int getId() {
        return id;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitCompoundGraphic(this);
    }

    public void add(Shape shape) {
        children.add(shape);
    }
}

  visitor/Visitor.java: 通用访问者接口

package visitor.visitor;

import visitor.shapes.Circle;
import visitor.shapes.CompoundShape;
import visitor.shapes.Dot;
import visitor.shapes.Rectangle;

/**
 * @author GaoMing
 * @date 2021/7/26 - 17:24
 */
public interface Visitor {
    String visitDot(Dot dot);

    String visitCircle(Circle circle);

    String visitRectangle(Rectangle rectangle);

    String visitCompoundGraphic(CompoundShape cg);
}

  visitor/XMLExportVisitor.java: 具体访问者,将所有形状导出为 XML 文件

package visitor.visitor;

import visitor.shapes.*;

/**
 * @author GaoMing
 * @date 2021/7/26 - 17:28
 */
public class XMLExportVisitor implements Visitor{
    public String export(Shape... args) {
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>" + "\n");
        for (Shape shape : args) {
            sb.append(shape.accept(this)).append("\n");
        }
        return sb.toString();
    }

    public String visitDot(Dot d) {
        return "<dot>" + "\n" +
                "    <id>" + d.getId() + "</id>" + "\n" +
                "    <x>" + d.getX() + "</x>" + "\n" +
                "    <y>" + d.getY() + "</y>" + "\n" +
                "</dot>";
    }

    public String visitCircle(Circle c) {
        return "<circle>" + "\n" +
                "    <id>" + c.getId() + "</id>" + "\n" +
                "    <x>" + c.getX() + "</x>" + "\n" +
                "    <y>" + c.getY() + "</y>" + "\n" +
                "    <radius>" + c.getRadius() + "</radius>" + "\n" +
                "</circle>";
    }

    public String visitRectangle(Rectangle r) {
        return "<rectangle>" + "\n" +
                "    <id>" + r.getId() + "</id>" + "\n" +
                "    <x>" + r.getX() + "</x>" + "\n" +
                "    <y>" + r.getY() + "</y>" + "\n" +
                "    <width>" + r.getWidth() + "</width>" + "\n" +
                "    <height>" + r.getHeight() + "</height>" + "\n" +
                "</rectangle>";
    }

    public String visitCompoundGraphic(CompoundShape cg) {
        return "<compound_graphic>" + "\n" +
                "   <id>" + cg.getId() + "</id>" + "\n" +
                _visitCompoundGraphic(cg) +
                "</compound_graphic>";
    }

    private String _visitCompoundGraphic(CompoundShape cg) {
        StringBuilder sb = new StringBuilder();
        for (Shape shape : cg.children) {
            String obj = shape.accept(this);
            // Proper indentation for sub-objects.
            obj = "    " + obj.replace("\n", "\n    ") + "\n";
            sb.append(obj);
        }
        return sb.toString();
    }

}

  Demo.java: 客户端代码

package visitor;

import visitor.shapes.*;
import visitor.visitor.XMLExportVisitor;

/**
 * @author GaoMing
 * @date 2021/7/26 - 17:23
 */
public class Demo {
    public static void main(String[] args) {
        Dot dot = new Dot(1, 10, 55);
        Circle circle = new Circle(2, 23, 15, 10);
        Rectangle rectangle = new Rectangle(3, 10, 17, 20, 30);

        CompoundShape compoundShape = new CompoundShape(4);
        compoundShape.add(dot);
        compoundShape.add(circle);
        compoundShape.add(rectangle);

        CompoundShape c = new CompoundShape(5);
        c.add(dot);
        compoundShape.add(c);

        export(circle, compoundShape);
    }

    private static void export(Shape... shapes) {
        XMLExportVisitor exportVisitor = new XMLExportVisitor();
        System.out.println(exportVisitor.export(shapes));
    }
}

  运行结果

<?xml version="1.0" encoding="utf-8"?>
<circle>
    <id>2</id>
    <x>23</x>
    <y>15</y>
    <radius>10</radius>
</circle>

<?xml version="1.0" encoding="utf-8"?>
<compound_graphic>
   <id>4</id>
    <dot>
        <id>1</id>
        <x>10</x>
        <y>55</y>
    </dot>
    <circle>
        <id>2</id>
        <x>23</x>
        <y>15</y>
        <radius>10</radius>
    </circle>
    <rectangle>
        <id>3</id>
        <x>10</x>
        <y>17</y>
        <width>20</width>
        <height>30</height>
    </rectangle>
    <compound_graphic>
       <id>5</id>
        <dot>
            <id>1</id>
            <x>10</x>
            <y>55</y>
        </dot>
    </compound_graphic>
</compound_graphic>    

  7. 与其他模式的关系

  • 可以将访问者模式视为命令模式的加强版本,其对象可对不同类的多种对象执行操作
  • 可以使用访问者对整个组合模式树执行操作
  • 可以同时使用访问者和迭代器模式来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。而迭代器能够访问的所有元素都有一个共同的父类

  8. 已知应用  

  使用示例:访问者不是常用的设计模式,因为它不仅复杂,应用范围也比较狭窄

  这里是 Java 程序库代码中该模式的一些示例:
  javax.lang.model.element.AnnotationValue 和 Annotation­Value­Visitor
  javax.lang.model.element.Element 和 Element­Visitor
  javax.lang.model.type.TypeMirror 和 Type­Visitor
  java.nio.file.FileVisitor 和 Simple­File­Visitor
  javax.faces.component.visit.VisitContext 和 Visit­Callback

有关访问者模式(学习笔记)的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  3. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  4. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  5. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  6. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  7. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  8. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  9. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  10. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

随机推荐