圣诞大减价开始了!

游客

意图

游客是一种行为设计模式,它允许将算法与它们所操作的对象分离。

访客设计模式

问题

想象一下,您的团队开发了一个应用程序,该应用程序将地理信息结构成一个巨大的图表。图中的每个节点可能表示一个复杂的实体,如城市,也可能表示更细粒度的事物,如行业、观光区等。如果节点所代表的真实对象之间有路,节点就会与其他节点相连。在底层,每个节点类型都由它自己的类表示,而每个特定的节点都是一个对象。

将图形导出为XML

将图形导出为XML。

在某些时候,您需要执行将图形导出为XML格式的任务。起初,这项工作似乎很简单。您计划向每个节点类添加一个导出方法,然后利用递归遍历图的每个节点,执行导出方法。解决方案简单而优雅:由于多态性,您无需将调用导出方法的代码耦合到具体的节点类。

不幸的是,系统架构师拒绝允许您修改现有的节点类。他说代码已经投入生产,他不想因为您的更改中存在潜在的错误而冒险破坏它。

必须将XML导出方法添加到所有节点类中

必须将XML导出方法添加到所有节点类中,如果在更改过程中出现任何错误,则会有破坏整个应用程序的风险。

此外,他还质疑在节点类中包含XML导出代码是否有意义。这些类的主要工作是处理地理数据。XML导出行为在这里看起来很陌生。

拒绝还有另一个原因。在这个功能实现之后,营销部门的某些人很可能会要求您提供导出为不同格式的功能,或者要求其他一些奇怪的功能。这将迫使您再次更改那些宝贵而脆弱的类。

解决方案

Visitor模式建议将新行为放入名为游客,而不是试图将其集成到现有的类中。必须执行该行为的原始对象现在作为参数传递给访问者的一个方法,提供对对象中包含的所有必要数据的方法访问。

现在,如果该行为可以在不同类的对象上执行呢?例如,在我们使用XML导出的例子中,不同节点类之间的实际实现可能会略有不同。因此,visitor类可以定义不止一个方法,而是一组方法,每个方法可以接受不同类型的参数,如下所示:

类ExportVisitor实现了Visitor的方法doForCity(City c){…}方法doForIndustry(行业f){…}方法doForSightSeeing(SightSeeing ss){…} //…

但是我们该如何调用这些方法呢,特别是在处理整个图的时候?这些方法具有不同的签名,因此我们不能使用多态性。要选择一个能够处理给定对象的适当的访问者方法,我们需要检查它的类。这听起来不像是个噩梦吗?

foreach(图中的节点节点)if (City的节点实例)exportVisitor.doForCity((City)节点)if (Industry的节点实例)exportVisitor.doForIndustry((Industry)节点)//…}

你可能会问,为什么不用方法重载呢?也就是说,即使所有方法支持不同的参数集,也要赋予它们相同的名称。不幸的是,即使我们的编程语言完全支持它(如Java和c#),它也帮不了我们。由于节点对象的确切类是预先未知的,重载机制将无法确定要执行的正确方法。它将默认为接受base对象的方法节点类。

然而,访问者模式解决了这个问题。它使用一种叫做双重分发,这有助于在对象上执行正确的方法,而无需繁琐的条件。与其让客户端选择要调用的方法的适当版本,不如我们将这种选择委托给作为参数传递给访问者的对象。由于对象知道它们自己的类,它们将能够不那么笨拙地在访问者身上选择合适的方法。他们“接受”访问者,并告诉它应该执行什么访问方法。

//客户端代码(图中的节点节点)Node .accept(exportVisitor) //城市类城市是方法accept(访问者v)是v. doforcity (this) //…//行业类Industry is method accept(访问者v) is v. doforindustry (this) //…

我承认。我们最终不得不改变节点类。但至少这个改变是微不足道的,它让我们无需再次修改代码就可以添加更多的行为。

现在,如果我们为所有访问者提取一个公共接口,所有现有的节点都可以与你引入到应用程序中的任何访问者一起工作。如果你发现自己引入了一个与节点相关的新行为,你所要做的就是实现一个新的访问者类。

真实的模拟

保险代理人

一个好的保险代理人总是随时准备为各种类型的组织提供不同的政策。

想象一下,一位经验丰富的保险代理人渴望获得新客户。他可以拜访一个社区的每一栋建筑,试图向他遇到的每一个人推销保险。根据占用该建筑物的组织类型,他可以提供专门的保险政策:

  • 如果是住宅楼,他卖医疗保险。
  • 如果是银行,他卖盗窃保险。
  • 如果是咖啡店,他卖火灾和洪水保险。

结构

访问者设计模式的结构 访问者设计模式的结构
  1. 游客Interface声明了一组访问方法,这些方法可以将对象结构的具体元素作为参数。如果程序是用支持重载的语言编写的,这些方法可能具有相同的名称,但它们的参数类型必须不同。

  2. 每一个具体的游客实现相同行为的几个版本,为不同的具体元素类量身定制。

  3. 元素Interface声明了一个“接受”访问者的方法。这个方法应该有一个参数声明为访问者接口的类型。

  4. 每一个具体的元素必须实施验收方法。此方法的目的是将调用重定向到对应于当前元素类的适当访问者的方法。请注意,即使基元素类实现了此方法,所有子类仍然必须在自己的类中重写此方法,并在访问者对象上调用适当的方法。

  5. 客户端通常表示一个集合或其他复杂对象(例如复合树)。通常,客户端并不知道所有的具体元素类,因为它们通过一些抽象接口来处理该集合中的对象。

伪代码

在本例中,游客pattern将XML导出支持添加到几何形状的类层次结构中。

访问者模式示例的结构

通过访问者对象将各种类型的对象导出为XML格式。

//元素接口声明了一个' accept '方法,该方法以基本访问者接口作为参数。interface Shape是方法move(x, y)方法draw()方法accept(v: Visitor) //每个具体的元素类必须实现' accept ' //方法,以这样的方式调用访问者的方法,//对应于元素的类。class Dot实现Shape is //…//注意我们正在调用' visitDot ',它匹配//当前类名。通过这种方式,我们让访问者知道它使用的元素的//类。方法accept(v: Visitor)是v. visitdot (this)类Circle实现的形状是//…方法accept(v: Visitor)是v. visitcircle(这个)类矩形实现形状是//…方法accept(v: Visitor)是v. visitrectangle(这个)类CompoundShape实现的形状是//…//访问者接口声明了一组访问方法,这些方法对应于元素类。访问//方法的签名允许访问者识别它正在处理的//元素的确切类。 interface Visitor is method visitDot(d: Dot) method visitCircle(c: Circle) method visitRectangle(r: Rectangle) method visitCompoundShape(cs: CompoundShape) // Concrete visitors implement several versions of the same // algorithm, which can work with all concrete element classes. // // You can experience the biggest benefit of the Visitor pattern // when using it with a complex object structure such as a // Composite tree. In this case, it might be helpful to store // some intermediate state of the algorithm while executing the // visitor's methods over various objects of the structure. class XMLExportVisitor implements Visitor is method visitDot(d: Dot) is // Export the dot's ID and center coordinates. method visitCircle(c: Circle) is // Export the circle's ID, center coordinates and // radius. method visitRectangle(r: Rectangle) is // Export the rectangle's ID, left-top coordinates, // width and height. method visitCompoundShape(cs: CompoundShape) is // Export the shape's ID as well as the list of its // children's IDs. // The client code can run visitor operations over any set of // elements without figuring out their concrete classes. The // accept operation directs a call to the appropriate operation // in the visitor object. class Application is field allShapes: array of Shapes method export() is exportVisitor = new XMLExportVisitor() foreach (shape in allShapes) do shape.accept(exportVisitor)

如果你想知道为什么我们需要接受方法在这个例子中,我的文章访客和双重派遣详细阐述了这个问题。

适用性

当您需要对复杂对象结构(例如,对象树)的所有元素执行操作时,请使用Visitor。

访问者模式允许您通过一个访问者对象实现同一操作的多个变体(这些变体对应于所有目标类),对一组具有不同类的对象执行操作。

使用Visitor清理辅助行为的业务逻辑。

该模式允许你通过将所有其他行为提取到一组访问者类中,使应用程序的主要类更专注于它们的主要工作。

当一个行为只在类层次结构的某些类中有意义,而在其他类中没有意义时,请使用该模式。

您可以将此行为提取到一个单独的访问者类中,并只实现那些接受相关类对象的访问方法,其余的都为空。

如何实施

  1. 使用一组“访问”方法声明访问者接口,每个方法对应程序中存在的每个具体元素类。

  2. 声明元素接口。如果您正在使用现有的元素类层次结构,请将抽象的“acceptance”方法添加到层次结构的基类中。此方法应接受visitor对象作为参数。

  3. 在所有具体元素类中实现接受方法。这些方法必须简单地将调用重定向到传入访问者对象上与当前元素的类匹配的访问方法。

  4. 元素类只能通过访问者界面与访问者一起工作。然而,访问者必须知道所有具体的元素类,它们作为访问方法的参数类型引用。

  5. 对于每个不能在元素层次结构内部实现的行为,创建一个新的具体访问者类并实现所有的访问方法。

    您可能会遇到访问者需要访问元素类的一些私有成员的情况。在这种情况下,您可以将这些字段或方法设为公共(违反元素的封装),也可以将访问者类嵌套在元素类中。后者只有在您幸运地使用支持嵌套类的编程语言时才可能实现。

  6. 客户端必须创建访问者对象,并通过“接受”方法将它们传递到元素中。

利与弊

  • 打开/关闭原则.您可以引入一个新的行为,该行为可以处理不同类的对象,而无需更改这些类。
  • 单一责任原则.您可以将相同行为的多个版本移动到同一个类中。
  • 访问者对象可以在处理各种对象时积累一些有用的信息。当您希望遍历一些复杂的对象结构(如对象树)并将访问者应用到该结构的每个对象时,这可能很方便。
  • 每次将类添加到元素层次结构或从元素层次结构中删除类时,都需要更新所有访问者。
  • 访问者可能缺乏对他们要使用的元素的私有字段和方法的必要访问权。

与其他模式的关系

  • 你可以治疗游客作为一个强大的版本命令模式。它的对象可以对不同类的不同对象执行操作。

  • 你可以使用游客对整个系统执行操作复合树。

  • 你可以使用游客随着迭代器遍历一个复杂的数据结构并对其元素执行一些操作,即使它们都具有不同的类。

代码示例

c#中的访问者c++中的访问者Go的访客Java访问者PHP中的访问者Python中的访问者Ruby访客Rust的访客Swift访客TypeScript中的访问者

额外的内容

  • 为什么我们不能简单地用方法重载替换访问者模式?阅读我的文章访客和双重派遣去了解那些肮脏的细节。
Baidu
map