我一直在博客中看到对访客模式的引用,但我必须承认,我只是不明白。我阅读了有关该模式的Wikipedia文章,并且了解了该模式的原理,但是对于何时使用它仍然感到困惑。
作为刚刚真正获得装饰器模式并且现在在任何地方都可以看到其用途的人,我也希望能够真正直观地理解这种看似方便的模式。
我一直在博客中看到对访客模式的引用,但我必须承认,我只是不明白。我阅读了有关该模式的Wikipedia文章,并且了解了该模式的原理,但是对于何时使用它仍然感到困惑。
作为刚刚真正获得装饰器模式并且现在在任何地方都可以看到其用途的人,我也希望能够真正直观地理解这种看似方便的模式。
Answers:
我对访客模式不是很熟悉。让我们看看我是否正确。假设您有动物的等级制度
class Animal { };
class Dog: public Animal { };
class Cat: public Animal { };
(假设它是一个具有完善接口的复杂层次结构。)
现在,我们要向层次结构添加一个新操作,即我们希望每个动物发出声音。只要层次结构很简单,您就可以使用直接多态性来实现:
class Animal
{ public: virtual void makeSound() = 0; };
class Dog : public Animal
{ public: void makeSound(); };
void Dog::makeSound()
{ std::cout << "woof!\n"; }
class Cat : public Animal
{ public: void makeSound(); };
void Cat::makeSound()
{ std::cout << "meow!\n"; }
但是以这种方式进行操作,每次要添加操作时,都必须将接口修改为层次结构的每个单个类。现在,假设您对原始接口感到满意,并且想要对其进行最少的修改。
访客模式允许您将每个新操作移入合适的类,并且只需要扩展层次结构的界面一次。我们开始做吧。首先,我们定义一个抽象操作(GoF中的“ Visitor”类),该操作为层次结构中的每个类都提供一个方法:
class Operation
{
public:
virtual void hereIsADog(Dog *d) = 0;
virtual void hereIsACat(Cat *c) = 0;
};
然后,我们修改层次结构以接受新操作:
class Animal
{ public: virtual void letsDo(Operation *v) = 0; };
class Dog : public Animal
{ public: void letsDo(Operation *v); };
void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }
class Cat : public Animal
{ public: void letsDo(Operation *v); };
void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }
最后,我们实现了实际操作,而不修改Cat和Dog:
class Sound : public Operation
{
public:
void hereIsADog(Dog *d);
void hereIsACat(Cat *c);
};
void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }
void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }
现在,您无需添加层次结构即可添加操作。下面是它的工作原理:
int main()
{
Cat c;
Sound theSound;
c.letsDo(&theSound);
}
letsDo(Operation *v)
需要一个指针。
theSound.hereIsACat(c)
本来可以完成工作,您如何证明该模式所带来的所有开销呢?双重调度是合理的。
您感到困惑的原因可能是访客是致命的误称。许多(突出显示1!)程序员都偶然发现了这个问题。它实际上所做的是用本机不支持(大多数都不支持)的语言实现双重调度。
1)我最喜欢的例子是“有效C ++”的著名作者Scott Meyers,他称这是他最重要的C ++之一!曾经的时刻。
switch
:switch
硬编码使得在客户端(代码重复)的决定,并没有提供静态类型检查(检查案件的完整性和独特性等)。访问者模式由类型检查器验证,通常使客户端代码更简单。
virtual
类似的功能在现代编程语言中如此有用的原因-它们是可扩展程序的基本构建块-我认为c方式(嵌套开关或模式匹配等,取决于您选择的语言)是不需要扩展的代码中的代码更简洁,而令我惊讶的是,在证明者9之类的复杂软件中看到了这种风格。更重要的是,任何想要提供扩展性的语言都应该比递归单一调度(例如,游客)。
这里的每个人都是正确的,但我认为它无法解决“何时”问题。首先,从设计模式:
通过Visitor,您可以定义新操作,而无需更改其所操作元素的类。
现在,让我们考虑一个简单的类层次结构。我有1、2、3和4类,以及方法A,B,C和D。像在电子表格中一样对它们进行布局:类是线,方法是列。
现在,面向对象的设计假定您比新方法更可能增加新类,因此可以说添加更多行变得更加容易。您只需添加一个新类,指定该类中的不同之处,然后继承其余部分即可。
尽管有时这些类是相对静态的,但是您需要经常添加更多方法-添加列。OO设计中的标准方法是将这样的方法添加到所有类中,这可能会很昂贵。访客模式使此操作变得容易。
顺便说一下,这就是Scala模式匹配要解决的问题。
该游客设计模式可以很好地表现就像目录树,XML结构,或文档大纲“递归”结构。
一个Visitor对象访问递归结构中的每个节点:每个目录,每个XML标签,等等。Visitor对象不会在结构中循环。相反,将Visitor方法应用于该结构的每个节点。
这是典型的递归节点结构。可以是目录或XML标记。[如果您是Java的人,请想象一下有许多其他方法来构建和维护子级列表。]
class TreeNode( object ):
def __init__( self, name, *children ):
self.name= name
self.children= children
def visit( self, someVisitor ):
someVisitor.arrivedAt( self )
someVisitor.down()
for c in self.children:
c.visit( someVisitor )
someVisitor.up()
该visit
方法将Visitor对象应用于结构中的每个节点。在这种情况下,它是自上而下的访客。您可以更改visit
方法的结构以自下而上或进行其他排序。
这是给游客的超类。该visit
方法使用了它。它“到达”结构中的每个节点。由于该visit
方法调用up
和down
,因此访问者可以跟踪深度。
class Visitor( object ):
def __init__( self ):
self.depth= 0
def down( self ):
self.depth += 1
def up( self ):
self.depth -= 1
def arrivedAt( self, aTreeNode ):
print self.depth, aTreeNode.name
子类可以执行诸如在每个级别上计数节点并累积节点列表的操作,从而生成一个不错的路径层次结构段号。
这是一个应用程序。它建立一个树结构someTree
。它创建了一个Visitor
,dumpNodes
。
然后将应用于dumpNodes
树。该dumpNode
对象将“访问”树中的每个节点。
someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )
TreeNode visit
算法将确保将每个TreeNode用作Visitor arrivedAt
方法的参数。
一种查看方式是,访问者模式是一种让您的客户向特定类层次结构中的所有类添加其他方法的方法。
当您拥有一个相当稳定的类层次结构,但是对于该层次结构需要完成的操作的需求不断变化时,这将很有用。
经典示例适用于编译器等。抽象语法树(AST)可以准确地定义编程语言的结构,但是您可能希望对AST进行的操作会随着项目的进展而改变:代码生成器,漂亮打印机,调试器,复杂性指标分析。
如果没有访问者模式,开发人员每次想要添加新功能时,都需要将该方法添加到基类的每个功能中。当基类出现在单独的库中或由单独的团队产生时,这尤其困难。
(我听说它辩称,Visitor模式与良好的OO实践相冲突,因为它使数据的操作远离数据。在正常的OO实践失败的情况下,Visitor模式非常有用。)
使用访问者模式至少有三个非常好的理由:
减少代码的扩散,当数据结构更改时,代码的扩散仅略有不同。
将相同的计算应用于几个数据结构,而无需更改实现该计算的代码。
将信息添加到旧版库,而无需更改旧版代码。
请看我写的关于这个的文章。
正如Konrad Rudolph指出的那样,它适合需要双重派遣的情况
这是一个示例,显示了我们需要双重调度以及访客如何帮助我们的情况。
范例:
可以说我有3种类型的移动设备-iPhone,Android,Windows Mobile。
所有这三个设备都安装了蓝牙无线电。
假设蓝牙无线电可以来自两个独立的OEM:英特尔和Broadcom。
只是为了使示例与我们的讨论相关,我们还假设英特尔无线电公开的API与Broadcom无线电公开的API不同。
这是我班级的样子–
现在,我要介绍一个操作–在移动设备上打开蓝牙。
它的功能签名应该像这样–
void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
所以,这取决于权类型的设备,并根据不同类型正确蓝牙无线电的,它可以通过接通调用适当的步骤或算法。
原则上,它变成一个3 x 2的矩阵,其中,我试图根据所涉及对象的正确类型对正确的操作进行矢量化处理。
多态行为取决于两个参数的类型。
现在,访问者模式可以应用于此问题。灵感来自维基百科页面:“本质上,访问者允许一个人向一类类添加新的虚函数,而无需修改类本身;而是创建一个访问者类,该类实现虚拟功能的所有适当专业化。访问者将实例引用作为输入,并通过双重调度实现目标。”
由于采用3x2矩阵,因此必须进行双重调度
我编写示例以回答另一个问题,此处提到了代码及其解释。
我发现通过以下链接更容易:
在
http://www.remondo.net/visitor-pattern-example-csharp/中,我找到了一个示例,该示例显示了一个模拟示例,该示例显示了访问者模式的好处。在这里,您有以下不同的容器类Pill
:
namespace DesignPatterns
{
public class BlisterPack
{
// Pairs so x2
public int TabletPairs { get; set; }
}
public class Bottle
{
// Unsigned
public uint Items { get; set; }
}
public class Jar
{
// Signed
public int Pieces { get; set; }
}
}
如上所示,您BilsterPack
包含成对的药丸,因此您需要将成对的药丸数乘以2。另外,您可能会注意到Bottle
use unit
是不同的数据类型,需要强制转换。
因此,在主要方法中,您可以使用以下代码来计算药丸计数:
foreach (var item in packageList)
{
if (item.GetType() == typeof (BlisterPack))
{
pillCount += ((BlisterPack) item).TabletPairs * 2;
}
else if (item.GetType() == typeof (Bottle))
{
pillCount += (int) ((Bottle) item).Items;
}
else if (item.GetType() == typeof (Jar))
{
pillCount += ((Jar) item).Pieces;
}
}
请注意,以上代码违反Single Responsibility Principle
。这意味着,如果添加新类型的容器,则必须更改主方法代码。延长开关时间也是一个坏习惯。
因此,通过引入以下代码:
public class PillCountVisitor : IVisitor
{
public int Count { get; private set; }
#region IVisitor Members
public void Visit(BlisterPack blisterPack)
{
Count += blisterPack.TabletPairs * 2;
}
public void Visit(Bottle bottle)
{
Count += (int)bottle.Items;
}
public void Visit(Jar jar)
{
Count += jar.Pieces;
}
#endregion
}
您将计数Pill
s的责任移到了称为的类PillCountVisitor
(并且我们删除了switch case语句)。就是说,每当您需要添加新型药丸容器时,都应该只更改PillCountVisitor
类。通知IVisitor
界面也很一般,可用于其他场景。
通过向药丸容器类添加Accept方法:
public class BlisterPack : IAcceptor
{
public int TabletPairs { get; set; }
#region IAcceptor Members
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
#endregion
}
我们允许访问者访问药丸容器类。
最后,我们使用以下代码计算药丸计数:
var visitor = new PillCountVisitor();
foreach (IAcceptor item in packageList)
{
item.Accept(visitor);
}
意思是:每个药丸容器都允许PillCountVisitor
游客看到他们的药丸计数。他知道如何计算您的药丸。
在visitor.Count
具有丸的价值。
在 http://butunclebob.com/ArticleS.UncleBob.IuseVisitor中,您看到了不能使用多态性(答案)遵循单一责任原则的真实情况。实际上在:
public class HourlyEmployee extends Employee {
public String reportQtdHoursAndPay() {
//generate the line for this hourly employee
}
}
该reportQtdHoursAndPay
方法用于报告和表示,这违反了单一职责原则。因此最好使用访问者模式来克服该问题。
双重调度只是使用此模式的原因之一。
但是请注意,这是使用单一调度范式在语言中实现双重或更多调度的唯一方法。
这是使用此模式的原因:
1)我们希望在不每次更改模型的情况下定义新操作,因为模型不会经常更改,而操作会经常更改。
2)我们不想将模型和行为耦合在一起,因为我们想要在多个应用程序中使用可重用的模型,或者希望使用可扩展的模型在一起,,允许客户端类使用自己的类来定义其行为。
3)我们有依赖于模型的具体类型的通用操作,但是我们不想在每个子类中实现逻辑,因为那样会在多个类中以及因此在多个地方爆炸通用逻辑。
4)我们正在使用领域模型设计,并且相同层次结构的模型类执行了太多不同的事情,这些事情可能在其他地方收集到。
5)我们需要双重派遣。
我们有使用接口类型声明的变量,我们希望能够根据其运行时类型来处理它们……当然,无需使用if (myObj instanceof Foo) {}
任何技巧。
例如,想法是将这些变量传递给方法,这些方法将接口的具体类型声明为参数以应用特定处理。对于语言而言,这种方式不可能开箱即用,因为它依赖于单调度,因为在运行时调用的所选内容仅取决于接收器的运行时类型。
请注意,在Java中,要调用的方法(签名)是在编译时选择的,它取决于参数的声明类型,而不是参数的运行时类型。
最后一个使用访问者的原因也是一个结果,因为在实现访问者时(当然,对于不支持多调度的语言),您必须引入双重调度实现。
请注意,遍历要在每个对象上应用访问者的元素(迭代)并不是使用模式的原因。
使用模式是因为您拆分了模型和处理。
通过使用该模式,您还可以从迭代器功能中受益。
这种能力非常强大,并且超越accept()
了普通方法的通用类型的迭代。
这是一个特殊的用例。因此,我将其放在一边。
Java范例
我将通过一个国际象棋示例来说明该模式的附加值,在该示例中,我们希望在玩家要求移动棋子时定义处理。
如果没有使用访客模式,我们可以直接在件子类中定义件移动行为。
例如,我们可以有一个Piece
接口,例如:
public interface Piece{
boolean checkMoveValidity(Coordinates coord);
void performMove(Coordinates coord);
Piece computeIfKingCheck();
}
每个Piece子类都会实现它,例如:
public class Pawn implements Piece{
@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}
@Override
public void performMove(Coordinates coord) {
...
}
@Override
public Piece computeIfKingCheck() {
...
}
}
对于所有Piece子类来说都是一样的。
这是一个说明该设计的图类:
这种方法存在三个重要的缺点:
–诸如performMove()
或的行为computeIfKingCheck()
很可能会使用通用逻辑。
例如,无论哪种混凝土Piece
,performMove()
最终都会将当前棋子设置到特定位置,并有可能拿走对手棋子。
将相关行为拆分为多个类,而不是收集它们,以某种方式击败了单一责任模式。使其难以维护。
– 子类可能checkMoveValidity()
不会Piece
看到或更改的处理内容。
这是超越人为或计算机行为的检查。该检查是在玩家要求的每个动作下执行的,以确保所要求的棋子移动有效。
所以我们甚至不想在Piece
界面中提供它。
–在对机器人开发人员具有挑战性的国际象棋游戏中,通常该应用程序提供标准的API(Piece
接口,子类,开发板,常见行为等),并使开发人员丰富其机器人策略。
为了做到这一点,我们必须提出一个模型,在该模型中,数据和行为在Piece
实现中并不紧密耦合。
因此,让我们使用访问者模式!
我们有两种结构:
–接受访问的模型类(片段)
–访问他们的访客(移动操作)
这是说明该模式的类图:
在上部,我们有访问者,在下部,我们有模型类。
这是PieceMovingVisitor
接口(为的每种行为指定的行为Piece
):
public interface PieceMovingVisitor {
void visitPawn(Pawn pawn);
void visitKing(King king);
void visitQueen(Queen queen);
void visitKnight(Knight knight);
void visitRook(Rook rook);
void visitBishop(Bishop bishop);
}
现在定义了作品:
public interface Piece {
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
}
其关键方法是:
void accept(PieceMovingVisitor pieceVisitor);
它提供了第一个调度:基于Piece
接收方的调用。
在编译时,该方法绑定到accept()
Piece接口的方法,并且在运行时,将在运行时Piece
类上调用有界方法。
它是accept()
将执行第二调度方法实现。
确实,每个Piece
要由PieceMovingVisitor
对象访问的子类都PieceMovingVisitor.visit()
通过传递为参数本身来调用该方法。
这样,编译器会在编译时尽快将声明的参数的类型与具体类型绑定在一起。
有第二次派遣。
这是Bishop
说明这一点的子类:
public class Bishop implements Piece {
private Coordinates coord;
public Bishop(Coordinates coord) {
super(coord);
}
@Override
public void accept(PieceMovingVisitor pieceVisitor) {
pieceVisitor.visitBishop(this);
}
@Override
public Coordinates getCoordinates() {
return coordinates;
}
@Override
public void setCoordinates(Coordinates coordinates) {
this.coordinates = coordinates;
}
}
这是一个用法示例:
// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();
// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);
// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
piece.accept(new MovePerformingVisitor(coord));
}
访客的缺点
访客模式是一种非常强大的模式,但是它也有一些重要的限制,您在使用它之前应该考虑这些限制。
1)减少/破坏封装的风险
在某些类型的操作中,访问者模式可能会减少或破坏域对象的封装。
例如,由于MovePerformingVisitor
类需要设置实际块的坐标,因此Piece
界面必须提供一种方法:
void setCoordinates(Coordinates coordinates);
Piece
坐标更改的责任现在对子类以外的其他类开放Piece
。
移动访问者在Piece
子类中执行的处理也不是一种选择。
的确,由于Piece.accept()
接受任何访客实现,这确实会带来另一个问题。它不知道访问者执行什么操作,因此不知道是否以及如何更改Piece状态。
识别访客的一种方法是Piece.accept()
根据访客的实现方式执行后处理。这将是一个非常糟糕的主意,因为它会在Visitor实现和Piece子类之间建立高度的耦合,此外,可能还需要使用rick作为getClass()
,instanceof
或使用任何标记来标识Visitor实现。
2)更改型号的要求
与其他一些行为设计模式相反Decorator
,例如,访客模式是侵入式的。
我们确实需要修改初始接收者类,以提供一种accept()
接受访问的方法。
我们Piece
及其子类没有任何问题,因为它们是我们的类。
在内置或第三方课程中,事情并不是那么容易。
我们需要包装或继承(如果可以的话)以添加accept()
方法。
3)间接
该模式创建多个间接。
双重调度意味着两次调用,而不是一次调用:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
当访问者更改被访问对象的状态时,我们可能会有其他间接方式。
它看起来像一个周期:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)
Cay Horstmann 在他的OO设计和模式书中有一个很好的例子,说明了将访问者应用到何处。他总结了问题:
复合对象通常具有由单个元素组成的复杂结构。一些元素可能再次具有子元素。...对元素的操作将访问其子元素,将其应用于该元素,然后合并结果。...但是,要在这种设计中添加新的操作并不容易。
之所以不容易,是因为操作是在结构类本身中添加的。例如,假设您有一个文件系统:
这是我们可能要使用此结构实现的一些操作(功能):
您可以向FileSystem中的每个类添加函数以实现操作(并且过去人们很容易做到这一点,因此人们过去已经这样做了)。问题在于,无论何时添加新功能(上面的“ etc.”行),都可能需要向结构类添加越来越多的方法。在某些时候,在您向软件中添加了一些操作之后,就类的功能衔接而言,这些类中的方法不再有意义。例如,您有一个FileNode
具有方法的calculateFileColorForFunctionABC()
,在文件系统上实现最新的可视化功能。
访客模式(就像许多设计模式一样)源于开发人员的痛苦和苦难。他们知道,有一种更好的方法可以允许对其代码进行更改,而不需要在各处进行大量更改,并且尊重良好的设计原则(高内聚,低耦合)。 )。我认为,直到您感到那种痛苦,才很难理解许多模式的用处。解释痛苦(就像我们上面试图对添加的“ etc.”功能所做的那样)占用了解释空间,这是分心的。因此,很难理解模式。
访问者使我们能够将数据结构(例如FileSystemNodes
)上的功能与数据结构本身分离开来。该模式允许设计尊重内聚性-数据结构类更简单(它们的方法较少),并且功能也封装在Visitor
实现中。这是通过双重调度(这是模式的复杂部分)完成的:使用accept()
结构类中的visitX()
方法和Visitor(功能)类中的方法:
这种结构使我们可以添加新功能,以具体的访客身份在结构上工作(无需更改结构类)。
例如,一个PrintNameVisitor
实现目录列表功能的,一个PrintSizeVisitor
实现具有大小的版本的。我们可以想象1天具有“ExportXMLVisitor`生成XML中的数据,或者产生它JSON一位来访者,等等。我们甚至可以有客人,其中显示使用我的目录树的图形语言,如DOT,以可视化与另一个程序。
最后要注意的是:Visitor具有双重调度功能的复杂性意味着很难理解,编码和调试。简而言之,它具有极高的怪胎因素,并且再次采用了KISS原则。在研究人员进行的一项调查中,“访客”被证明是一种有争议的模式(关于其实用性尚未达成共识)。一些实验甚至表明它并没有使代码更易于维护。
我认为,使用Visitor Pattern
或直接修改每个元素结构来添加新操作的工作量几乎相同。另外,如果我要添加新的元素类,例如Cow
,Operation接口将受到影响,并且会传播到所有现有的元素类,因此需要重新编译所有元素类。那有什么意义呢?
rootElement.visit (node) -> node.collapse()
。使用visitor,每个节点都为其所有子节点实现图遍历,因此您已完成。
levelsRemaining
计数器作为参数。在呼叫下一级孩子之前将其减小。在您的访客内部if(levelsRemaining == 0) return
。
访客模式的快速描述。需要修改的类必须全部实现'accept'方法。客户调用此accept方法对该类系列执行一些新操作,从而扩展其功能。通过为每个特定操作传递不同的访问者类,客户可以使用这一接受方法来执行各种新操作。访客类包含多个重写的访问方法,这些方法定义如何为家庭中的每个类实现相同的特定操作。这些访问方法传递了一个可以在其上工作的实例。
当您考虑使用它时
在碰到bob叔叔的文章并阅读评论之前,我不了解这种模式。考虑以下代码:
public class Employee
{
}
public class SalariedEmployee : Employee
{
}
public class HourlyEmployee : Employee
{
}
public class QtdHoursAndPayReport
{
public void PrintReport()
{
var employees = new List<Employee>
{
new SalariedEmployee(),
new HourlyEmployee()
};
foreach (Employee e in employees)
{
if (e is HourlyEmployee he)
PrintReportLine(he);
if (e is SalariedEmployee se)
PrintReportLine(se);
}
}
public void PrintReportLine(HourlyEmployee he)
{
System.Diagnostics.Debug.WriteLine("hours");
}
public void PrintReportLine(SalariedEmployee se)
{
System.Diagnostics.Debug.WriteLine("fix");
}
}
class Program
{
static void Main(string[] args)
{
new QtdHoursAndPayReport().PrintReport();
}
}
尽管它看起来很不错,因为它确认了“ 单一责任”,但它违反了“ 开放/封闭”原则。每次使用新的Employee类型时,如果要进行类型检查,则必须添加。如果您不这样做,那么在编译时就永远不会知道这一点。
使用访问者模式,您可以使代码更整洁,因为它不违反打开/关闭原则,也不违反单一职责。而且,如果您忘记实现访问,它将无法编译:
public abstract class Employee
{
public abstract void Accept(EmployeeVisitor v);
}
public class SalariedEmployee : Employee
{
public override void Accept(EmployeeVisitor v)
{
v.Visit(this);
}
}
public class HourlyEmployee:Employee
{
public override void Accept(EmployeeVisitor v)
{
v.Visit(this);
}
}
public interface EmployeeVisitor
{
void Visit(HourlyEmployee he);
void Visit(SalariedEmployee se);
}
public class QtdHoursAndPayReport : EmployeeVisitor
{
public void Visit(HourlyEmployee he)
{
System.Diagnostics.Debug.WriteLine("hourly");
// generate the line of the report.
}
public void Visit(SalariedEmployee se)
{
System.Diagnostics.Debug.WriteLine("fix");
} // do nothing
public void PrintReport()
{
var employees = new List<Employee>
{
new SalariedEmployee(),
new HourlyEmployee()
};
QtdHoursAndPayReport v = new QtdHoursAndPayReport();
foreach (var emp in employees)
{
emp.Accept(v);
}
}
}
class Program
{
public static void Main(string[] args)
{
new QtdHoursAndPayReport().PrintReport();
}
}
}
魔术是,尽管v.Visit(this)
外观相同,但实际上却有所不同,因为它调用了不同的访问者重载。
基于@Federico A. Ramponi的出色回答。
试想一下,您具有以下层次结构:
public interface IAnimal
{
void DoSound();
}
public class Dog : IAnimal
{
public void DoSound()
{
Console.WriteLine("Woof");
}
}
public class Cat : IAnimal
{
public void DoSound(IOperation o)
{
Console.WriteLine("Meaw");
}
}
如果您需要在此处添加“步行”方法会怎样?这将给整个设计带来痛苦。
同时,添加“步行”方法会产生新的问题。那“吃”或“睡觉”呢?我们是否真的必须为要添加的每个新动作或操作向动物层次结构中添加新方法?这很丑陋,最重要的是,我们将永远无法关闭Animal接口。因此,通过访问者模式,我们可以在不修改层次结构的情况下向层次结构添加新方法!
因此,只需检查并运行以下C#示例:
using System;
using System.Collections.Generic;
namespace VisitorPattern
{
class Program
{
static void Main(string[] args)
{
var animals = new List<IAnimal>
{
new Cat(), new Cat(), new Dog(), new Cat(),
new Dog(), new Dog(), new Cat(), new Dog()
};
foreach (var animal in animals)
{
animal.DoOperation(new Walk());
animal.DoOperation(new Sound());
}
Console.ReadLine();
}
}
public interface IOperation
{
void PerformOperation(Dog dog);
void PerformOperation(Cat cat);
}
public class Walk : IOperation
{
public void PerformOperation(Dog dog)
{
Console.WriteLine("Dog walking");
}
public void PerformOperation(Cat cat)
{
Console.WriteLine("Cat Walking");
}
}
public class Sound : IOperation
{
public void PerformOperation(Dog dog)
{
Console.WriteLine("Woof");
}
public void PerformOperation(Cat cat)
{
Console.WriteLine("Meaw");
}
}
public interface IAnimal
{
void DoOperation(IOperation o);
}
public class Dog : IAnimal
{
public void DoOperation(IOperation o)
{
o.PerformOperation(this);
}
}
public class Cat : IAnimal
{
public void DoOperation(IOperation o)
{
o.PerformOperation(this);
}
}
}
Dog
,以及Cat
。您可以在基类中创建它们,以便它们被继承或选择合适的示例。
访问者允许在不修改类本身的情况下将新的虚函数添加到类家族中。而是创建一个访问者类,该类实现虚拟函数的所有适当专业化
访客结构:
在以下情况下使用访问者模式:
即使Visitor模式提供了在不更改Object中现有代码的情况下添加新操作的灵活性,但这种灵活性也存在缺点。
如果添加了新的Visitable对象,则需要更改Visitor和ConcreteVisitor类中的代码。有一个解决方法,可以解决此问题:使用反射,这将对性能产生影响。
程式码片段:
import java.util.HashMap;
interface Visitable{
void accept(Visitor visitor);
}
interface Visitor{
void logGameStatistics(Chess chess);
void logGameStatistics(Checkers checkers);
void logGameStatistics(Ludo ludo);
}
class GameVisitor implements Visitor{
public void logGameStatistics(Chess chess){
System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");
}
public void logGameStatistics(Checkers checkers){
System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");
}
public void logGameStatistics(Ludo ludo){
System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");
}
}
abstract class Game{
// Add game related attributes and methods here
public Game(){
}
public void getNextMove(){};
public void makeNextMove(){}
public abstract String getName();
}
class Chess extends Game implements Visitable{
public String getName(){
return Chess.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
class Checkers extends Game implements Visitable{
public String getName(){
return Checkers.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
class Ludo extends Game implements Visitable{
public String getName(){
return Ludo.class.getName();
}
public void accept(Visitor visitor){
visitor.logGameStatistics(this);
}
}
public class VisitorPattern{
public static void main(String args[]){
Visitor visitor = new GameVisitor();
Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
for (Visitable v : games){
v.accept(visitor);
}
}
}
说明:
Visitable
(Element
)是一个接口,此接口方法必须添加到一组类中。Visitor
是一个接口,其中包含在其上执行操作的方法 Visitable
元素。GameVisitor
是一个类,它实现 Visitor
接口(ConcreteVisitor
)的类。Visitable
元素都接受Visitor
并调用相关的方法Visitor
接口。Game
视为Element
Chess,Checkers and Ludo
的ConcreteElements
。在上面的示例中, Chess, Checkers and Ludo
是三个不同的游戏(和Visitable
类)。有一天,我遇到了一个记录每个游戏统计信息的方案。因此,您无需修改单个类即可实现统计功能,就可以将职责集中在GameVisitor
类中,从而在不修改每个游戏结构的情况下为您解决了问题。
输出:
Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser
参考
来源制作文章
更多细节
模式允许将行为静态或动态地添加到单个对象,而不会影响同一类中其他对象的行为
相关文章:
我真的很喜欢http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html中的描述和示例。
假设您拥有固定的主类层次结构;也许是来自其他供应商的,您无法更改该层次结构。但是,您的意图是要向该层次结构添加新的多态方法,这意味着通常必须向基类接口添加一些内容。因此,难题是您需要向基类中添加方法,但是您不能碰到基类。您如何解决这个问题?
解决此类问题的设计模式称为“访问者”(“设计模式”书中的最后一个),它基于上一节中所示的双重调度方案。
访问者模式允许您通过创建类型为Visitor的单独的类层次结构来虚拟化对主要类型执行的操作,从而扩展主要类型的接口。主要类型的对象只是简单地“接受”访问者,然后调用访问者的动态绑定成员函数。
虽然我了解了方式和时间,但我从未理解为什么。如果它可以帮助具有C ++等语言背景的任何人,请阅读此内容非常仔细地。
对于懒惰者,我们使用访问者模式,因为“尽管在C ++中动态分配虚拟函数,但函数重载是静态完成的”。
或者,换一种说法,以确保当您传入实际上绑定到ApolloSpacecraft对象的SpaceShip引用时,确保调用CollideWith(ApolloSpacecraft&)。
class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
virtual void CollideWith(SpaceShip&) {
cout << "ExplodingAsteroid hit a SpaceShip" << endl;
}
virtual void CollideWith(ApolloSpacecraft&) {
cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
}
}
感谢@Federico A. Ramponi的出色解释,我只是用Java版本制作的。希望对您有所帮助。
就像@Konrad Rudolph指出的那样,它实际上是使用两个具体实例共同确定运行时方法的双重调度。
因此,实际上,只要我们正确定义了操作接口,就无需为操作执行程序创建公共接口。
import static java.lang.System.out;
public class Visitor_2 {
public static void main(String...args) {
Hearen hearen = new Hearen();
FoodImpl food = new FoodImpl();
hearen.showTheHobby(food);
Katherine katherine = new Katherine();
katherine.presentHobby(food);
}
}
interface Hobby {
void insert(Hearen hearen);
void embed(Katherine katherine);
}
class Hearen {
String name = "Hearen";
void showTheHobby(Hobby hobby) {
hobby.insert(this);
}
}
class Katherine {
String name = "Katherine";
void presentHobby(Hobby hobby) {
hobby.embed(this);
}
}
class FoodImpl implements Hobby {
public void insert(Hearen hearen) {
out.println(hearen.name + " start to eat bread");
}
public void embed(Katherine katherine) {
out.println(katherine.name + " start to eat mango");
}
}
正如您所期望的,尽管通用接口实际上并不是此模式中的必要部分,但它将为我们带来更多的清晰度。
import static java.lang.System.out;
public class Visitor_2 {
public static void main(String...args) {
Hearen hearen = new Hearen();
FoodImpl food = new FoodImpl();
hearen.showHobby(food);
Katherine katherine = new Katherine();
katherine.showHobby(food);
}
}
interface Hobby {
void insert(Hearen hearen);
void insert(Katherine katherine);
}
abstract class Person {
String name;
protected Person(String n) {
this.name = n;
}
abstract void showHobby(Hobby hobby);
}
class Hearen extends Person {
public Hearen() {
super("Hearen");
}
@Override
void showHobby(Hobby hobby) {
hobby.insert(this);
}
}
class Katherine extends Person {
public Katherine() {
super("Katherine");
}
@Override
void showHobby(Hobby hobby) {
hobby.insert(this);
}
}
class FoodImpl implements Hobby {
public void insert(Hearen hearen) {
out.println(hearen.name + " start to eat bread");
}
public void insert(Katherine katherine) {
out.println(katherine.name + " start to eat mango");
}
}
您的问题是何时知道:
我不首先使用访客模式进行编码。我编码标准,并等待需要发生然后重构。因此,假设您一次安装了多个支付系统。在结帐时,您可能有许多if条件(或instanceOf),例如:
//psuedo code
if(payPal)
do paypal checkout
if(stripe)
do strip stuff checkout
if(payoneer)
do payoneer checkout
现在想象我有10种付款方式,这有点丑陋。因此,当您看到这种模式发生时,访问者会派上用场来将所有内容分离出来,然后您最终会调用以下内容:
new PaymentCheckoutVistor(paymentType).visit()
您可以从此处的许多示例中看到如何实现它,只是向您展示了一个用例。