我一直在博客中看到对访问者模式的引用,但我不得不承认,我就是不明白。我阅读了该模式的维基百科文章,并且了解它的机制,但我仍然对何时使用它感到困惑。
作为一个最近才真正获得装饰器模式并且现在在任何地方都看到它的用途的人,我希望能够真正直观地理解这个看似方便的模式。
我一直在博客中看到对访问者模式的引用,但我不得不承认,我就是不明白。我阅读了该模式的维基百科文章,并且了解它的机制,但我仍然对何时使用它感到困惑。
作为一个最近才真正获得装饰器模式并且现在在任何地方都看到它的用途的人,我希望能够真正直观地理解这个看似方便的模式。
我对访客模式不是很熟悉。让我们看看我是否正确。假设你有一个动物等级
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);
}
您感到困惑的原因可能是访客是一个致命的误称。许多(突出的1!)程序员都偶然发现了这个问题。它实际上做的是在本机不支持它的语言中实现双重调度(大多数不支持)。
1)我最喜欢的例子是著名的“Effective C++”作者 Scott Meyers,他称这是他最重要的 C++ 之一啊哈!永远的时刻。
这里的每个人都是正确的,但我认为它无法解决“何时”。首先,从设计模式:
Visitor 允许您定义一个新的操作,而无需更改它所操作的元素的类。
现在,让我们考虑一个简单的类层次结构。我有 1、2、3 和 4 类以及方法 A、B、C 和 D。像在电子表格中一样布置它们:类是行,方法是列。
现在,面向对象的设计假定您更可能生成新类而不是新方法,因此可以说添加更多行更容易。您只需添加一个新类,指定该类中的不同之处,然后继承其余部分。
不过,有时类是相对静态的,但您需要经常添加更多方法——添加列。OO 设计中的标准方法是将此类方法添加到所有类中,这可能会很昂贵。访问者模式使这很容易。
顺便说一句,这就是 Scala 的模式匹配想要解决的问题。
访问者设计模式非常适用于“递归”结构,如目录树、XML 结构或文档大纲。
访问者对象访问递归结构中的每个节点:每个目录、每个 XML 标记等等。访问者对象不会循环遍历结构。相反,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方法将一个访问者对象应用于结构中的每个节点。在这种情况下,它是一个自上而下的访问者。您可以更改visit方法的结构以进行自下而上或其他一些排序。
这是访问者的超类。它被visit方法使用。它“到达”结构中的每个节点。由于该visit方法调用upand 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 )
TreeNodevisit算法将确保每个 TreeNode 都用作访问者arrivedAt方法的参数。
一种看待它的方法是,访问者模式是一种让您的客户向特定类层次结构中的所有类添加附加方法的方法。
当您有一个相当稳定的类层次结构时,它很有用,但是您对该层次结构需要做什么的要求不断变化。
经典的例子是编译器等。抽象语法树 (AST) 可以准确地定义编程语言的结构,但是您可能希望在 AST 上执行的操作会随着项目的进展而改变:代码生成器、漂亮打印机、调试器、复杂性度量分析。
如果没有访问者模式,每次开发人员想要添加新功能时,他们都需要将该方法添加到基类中的每个功能中。当基类出现在单独的库中或由单独的团队生产时,这尤其困难。
(我听说访问者模式与良好的 OO 实践相冲突,因为它将数据的操作从数据中移开。访问者模式恰好在正常的 OO 实践失败的情况下很有用。)
双分派只是使用这种模式的原因之一。
但请注意,这是在使用单一分派范式的语言中实现双重或多重分派的单一方法。
以下是使用该模式的原因:
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()最终都会将当前棋子设置到特定位置,并可能拿走对手棋子。
在多个类中拆分相关行为而不是收集它们在某种程度上破坏了单一责任模式。使它们的可维护性更难。
– 处理 ascheckMoveValidity()不应该是Piece子类可能看到或改变的东西。
它是超越人类或计算机行为的检查。该检查在玩家请求的每个动作中执行,以确保请求的棋子移动有效。
所以我们甚至不想在Piece界面中提供它。
– 在对 bot 开发者具有挑战性的国际象棋游戏中,通常应用程序提供标准的 API(Piece接口、子类、Board、常见行为等),并让开发者丰富他们的 bot 策略。
为了能够做到这一点,我们必须提出一个模型,其中数据和行为在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);
}
现在定义了 Piece:
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 子类之间创建高度耦合,此外它可能需要使用技巧 asgetClass()或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)
使用访问者模式至少有三个很好的理由:
减少仅在数据结构发生变化时略有不同的代码的扩散。
将相同的计算应用于多个数据结构,而无需更改实现计算的代码。
在不更改遗留代码的情况下向遗留库添加信息。
请看一下我写的一篇关于这个的文章。
我发现以下链接更容易:
在
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使用unit不同的数据类型并且需要强制转换。
因此,在 main 方法中,您可以使用以下代码计算药丸数:
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. 这意味着如果添加新类型的容器,则必须更改 main 方法代码。延长开关时间也是不好的做法。
所以通过引入以下代码:
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
}
您将计算 s 数量的责任转移Pill到了名为的类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方法用于报告和表示,这违反了单一责任原则。所以最好使用访问者模式来解决这个问题。
Cay Horstmann在他的 OO 设计和模式一书中有一个很好的例子来说明在哪里应用访问者。他总结了这个问题:
复合对象通常具有复杂的结构,由单个元素组成。某些元素可能再次具有子元素。...对元素的操作访问其子元素,将操作应用于它们,并组合结果。... 但是,要在这样的设计中添加新的操作并不容易。
之所以不容易,是因为操作是在结构类本身中添加的。例如,假设您有一个文件系统:
以下是我们可能希望使用此结构实现的一些操作(功能):
您可以向 FileSystem 中的每个类添加函数来实现操作(过去人们已经这样做了,因为它很明显如何做到这一点)。问题是,每当您添加新功能(上面的“等”行)时,您可能需要向结构类添加越来越多的方法。在某些时候,在您添加到软件中的一些操作之后,这些类中的方法就类的功能内聚而言不再有意义。例如,您有FileNode一个方法calculateFileColorForFunctionABC(),以便在文件系统上实现最新的可视化功能。
访问者模式(像许多设计模式一样)源于开发人员的痛苦和痛苦,他们知道有更好的方法可以让他们的代码更改而不需要到处进行大量更改,并且还尊重良好的设计原则(高内聚,低耦合)。我的观点是,在你感受到痛苦之前,很难理解很多模式的用处。解释痛苦(就像我们试图在上面添加的“等”功能一样)在解释中占用空间并且会分散注意力。出于这个原因,理解模式很困难。
访问者允许我们将数据结构(例如FileSystemNodes)上的功能与数据结构本身分离。该模式允许设计尊重内聚——数据结构类更简单(它们具有更少的方法),并且功能被封装到Visitor实现中。这是通过双重调度完成的(这是模式的复杂部分):使用accept()结构类visitX()中的方法和访问者(功能)类中的方法:
这种结构允许我们添加在结构上作为具体访问者工作的新功能(无需更改结构类)。
例如,aPrintNameVisitor实现目录列表功能,aPrintSizeVisitor实现具有大小的版本。我们可以想象有一天有一个以 XML 格式生成数据的“ExportXMLVisitor”,或者另一个以 JSON 格式生成数据的访问者,等等。我们甚至可以让一个访问者使用诸如 DOT 之类的图形语言来显示我的目录树,以进行可视化与另一个程序。
最后一点:Visitor 的双重调度的复杂性意味着它更难理解、编码和调试。简而言之,它具有很高的极客因子,并且违背了 KISS 原则。在研究人员进行的一项调查中,Visitor 被证明是一种有争议的模式(对其有用性没有达成共识)。一些实验甚至表明它并没有使代码更易于维护。
在我看来,添加新操作的工作量或多或少与使用Visitor Pattern或直接修改每个元素结构相同。此外,如果我要添加新的元素类,例如Cow,Operation 接口将受到影响,这会传播到所有现有的元素类,因此需要重新编译所有元素类。那么有什么意义呢?
访问者模式与 Aspect Object 编程的地下实现相同。
例如,如果您定义一个新操作而不更改它所操作的元素的类
访客模式的快速描述。需要修改的类都必须实现 'accept' 方法。客户端调用此接受方法来对该类族执行一些新操作,从而扩展它们的功能。通过为每个特定操作传入不同的访问者类,客户端可以使用这个接受方法来执行范围广泛的新操作。访问者类包含多个重写的访问方法,这些方法定义了如何为家庭中的每个类实现相同的特定操作。这些访问方法通过一个实例来工作。
什么时候可以考虑使用它
直到我遇到鲍勃叔叔的文章并阅读评论时,我才理解这种模式。考虑以下代码:
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 类型时,您都必须添加 if 类型检查。如果你不这样做,你将永远不会在编译时知道这一点。
使用访问者模式,您可以使代码更简洁,因为它不违反开放/封闭原则,也不违反单一职责。如果您忘记执行访问,它将无法编译:
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");
}
}
如果你需要在这里添加一个“Walk”方法会发生什么?这对整个设计来说是痛苦的。
同时,加入“Walk”方法会产生新的问题。“吃”还是“睡”呢?我们真的必须为我们想要添加的每个新动作或操作添加一个新方法到 Animal 层次结构吗?这很丑陋,最重要的是,我们永远无法关闭 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);
}
}
}
访问者允许在不修改类本身的情况下向类族中添加新的虚函数;相反,我们创建了一个访问者类,它实现了虚函数的所有适当的特化
访客结构:
在以下情况下使用访客模式:
尽管访问者模式提供了在不更改 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接口的相关方法。GameasElement和具体游戏Chess,Checkers and Ludo视为 as 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中的描述和示例。
假设您有一个固定的主要类层次结构;也许它来自另一个供应商,您无法更改该层次结构。但是,您的意图是您想向该层次结构添加新的多态方法,这意味着通常您必须向基类接口添加一些东西。所以困境是你需要在基类中添加方法,但你不能触及基类。你如何解决这个问题?
解决此类问题的设计模式称为“访问者”(设计模式一书中的最后一个),它建立在上一节所示的双重调度方案之上。
访问者模式允许您通过创建一个单独的访问者类型的类层次结构来扩展主要类型的接口,以虚拟化对主要类型执行的操作。主要类型的对象只是简单地“接受”访问者,然后调用访问者的动态绑定成员函数。
虽然我了解了如何以及何时,但我从未了解过原因。如果它对任何具有 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()
您可以从此处的示例数量中了解如何实现它,我只是向您展示了一个用例。