什么时候应该使用访问者设计模式? [关闭]
Posted
技术标签:
【中文标题】什么时候应该使用访问者设计模式? [关闭]【英文标题】:When should I use the Visitor Design Pattern? [closed] 【发布时间】:2010-09-20 06:59:59 【问题描述】:我一直在博客中看到对访问者模式的引用,但我不得不承认,我就是不明白。我阅读了wikipedia article for the pattern 并了解它的机制,但我仍然对何时使用它感到困惑。
作为一个最近才真正了解装饰器模式并且现在在任何地方都看到它的用途的人,我希望能够真正直观地理解这个看似方便的模式。
【问题讨论】:
在我的黑莓手机上阅读了 Jermey Miller 的 this article 后,在大厅里等了两个小时,终于明白了。它很长,但是很好地解释了双重调度、访问者和复合,以及你可以用这些做什么。 这是一篇不错的文章:codeproject.com/Articles/186185/Visitor-Design-Pattern 示例here 和here。 【参考方案1】:我对访客模式不是很熟悉。让我们看看我是否正确。假设你有一个动物等级
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);
最后,我们实现实际操作,既不修改猫也不修改狗:
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);
【讨论】:
@Knownasilya - 这不是真的。 &-Operator 给出了接口所需的 Sound-Object 的地址。letsDo(Operation *v)
需要一个指针。
为了清楚起见,这个访问者设计模式示例正确吗?
@Federico A. Ramponi,这听起来像是简单地将复杂性转移到了操作类型上。假设您添加了数十个和数百个其他操作,然后您添加了另一种动物,我们称之为绵羊。那么这将迫使您修改所有这些为绵羊创建的操作并在此处添加IsASheep(Sheep *d);用于所有操作。
经过深思熟虑,我想知道您为什么调用了hereIsADog和hereIsACat这两个方法,尽管您已经将Dog和Cat传递给了方法。我更喜欢一个简单的 performTask(Object *obj) 并且你在 Operation 类中转换这个对象。 (并且在支持覆盖的语言中,不需要强制转换)
在最后的“主要”示例中:theSound.hereIsACat(c)
会完成这项工作,您如何证明该模式引入的所有开销是合理的? double dispatching 是理由。【参考方案2】:
您感到困惑的原因可能是访客是一个致命的误称。许多(著名的1!)程序员都偶然发现了这个问题。它实际上做的是用原生不支持它的语言(大多数不支持)实现double dispatching。
1) 我最喜欢的例子是著名的“Effective C++”作者 Scott Meyers,他称这是他的most important C++ aha! moments ever 之一。
【讨论】:
+1 “没有模式” - 完美的答案。最受好评的答案证明许多 c++ 程序员尚未意识到使用类型枚举和 switch case(c 方式)的虚拟函数对“adhoc”多态性的限制。使用 virtual 可能会更整洁且不可见,但仍仅限于单次调度。在我个人看来,这是c++最大的缺陷。 @user3125280 我现在已经阅读了 4/5 篇文章和关于访问者模式的设计模式章节,但没有一篇文章解释了使用这种晦涩的模式而不是 case stmt 的优势,或者当你可能使用其中一个。谢谢你至少提出来! @sam 我很确定他们会解释它——the same advantage 你总是得到from subclassing / runtime polymorphism 而不是switch
:switch
硬编码在客户端进行决策(代码重复)并且不提供静态类型检查(检查案例的完整性和独特性等)。访问者模式由类型检查器验证,通常使客户端代码更简单。
@KonradRudolph 对此表示感谢。但请注意,它没有在 Patterns 或 wikipedia 文章中明确提及。我不同意你的观点,但你可能会争辩说使用 case stmt 也有好处,所以它很奇怪,它通常没有对比:1.你不需要对你的集合对象使用 accept() 方法。 2. ~visitor 可以处理未知类型的对象。因此,案例 stmt 似乎更适合对涉及可变类型集合的对象结构进行操作。 Patterns 确实承认访问者模式不太适合这种情况(p333)。
@SamPinkus konrad 的观点 - 这就是为什么 virtual
之类的功能在现代编程语言中如此有用 - 它们是可扩展程序的基本构建块 - 在我看来是 c 方式(嵌套开关或模式匹配等,具体取决于您选择的语言)在不需要可扩展的代码中要干净得多,我很惊喜地在复杂的软件(如 prover 9)中看到这种风格。更重要的是,任何想要提供可扩展性的语言都应该可能比递归单次调度(即访问者)更好地适应调度模式。【参考方案3】:
这里的每个人都是正确的,但我认为它无法解决“何时”。首先,来自设计模式:
Visitor 让您定义一个新的 不改变类的操作 它操作的元素。
现在,让我们考虑一个简单的类层次结构。我有 1、2、3 和 4 类以及方法 A、B、C 和 D。像在电子表格中一样布置它们:类是行,方法是列。
现在,面向对象的设计假定您更可能开发新类而不是新方法,因此可以说添加更多行更容易。您只需添加一个新类,指定该类的不同之处,然后继承其余部分。
虽然有时类是相对静态的,但您需要经常添加更多方法——添加列。 OO 设计中的标准方法是将此类方法添加到所有类中,这可能会很昂贵。访问者模式让这一切变得简单。
顺便说一句,这就是Scala的模式匹配要解决的问题。
【讨论】:
为什么我会在一个实用类上使用访问者模式。我可以这样调用我的实用程序类:AnalyticsManger.visit(someObjectToVisit) vs AnalyticsVisitor.visit(someOjbectToVisit)。有什么不同 ?他们都做分离关注吧?希望你能帮忙。 @j2emanue 因为访问者模式在运行时使用正确的访问者重载。虽然您的代码需要类型转换才能调用正确的重载。 这样做有效率提升吗?我想它避免了一个好主意 @j2emanue 的想法是编写符合开放/封闭原则的代码,而不是性能原因。见在 Bob 叔叔butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod@【参考方案4】:Visitor 设计模式非常适用于“递归”结构,如目录树、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
方法调用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 都用作访问者的 arrivedAt
方法的参数。
【讨论】:
正如其他人所说,这是“分层访问者模式”。 @PPC-Coder “分层访问者模式”和访问者模式有什么区别? 分层访问者模式比经典访问者模式更灵活。例如,使用分层模式,您可以跟踪遍历的深度并决定要遍历哪个分支或一起停止遍历。经典访问者没有这个概念,会访问所有节点。【参考方案5】:一种看待它的方式是,访问者模式是一种让您的客户向特定类层次结构中的所有类添加其他方法的方法。
当您有一个相当稳定的类层次结构时,它很有用,但您对该层次结构的需求不断变化。
经典的例子是编译器等。抽象语法树 (AST) 可以准确地定义编程语言的结构,但是您可能希望在 AST 上执行的操作会随着项目的进展而改变:代码生成器、漂亮打印机、调试器、复杂性度量分析。
如果没有访问者模式,开发人员每次想要添加新功能时,都需要将该方法添加到基类中的每个功能中。当基类出现在单独的库中或由单独的团队生产时,这尤其困难。
(我听说访问者模式与良好的 OO 实践相冲突,因为它将数据的操作从数据中移开。访问者模式恰好在正常的 OO 实践失败的情况下很有用。 )
【讨论】:
我还想听听您对以下问题的看法:为什么我要使用访问者模式而不是一个实用程序类。我可以这样调用我的实用程序类:AnalyticsManger.visit(someObjectToVisit) vs AnalyticsVisitor.visit(someOjbectToVisit)。有什么不同 ?他们都做分离关注吧?希望你能帮忙。 @j2emanue:我不明白这个问题。我建议您充实它并将其作为一个完整的问题发布,供任何人回答。 我在这里发布了一个新问题:***.com/questions/52068876/…【参考方案6】:双重调度只是使用这种模式的一个原因。 但请注意,这是在使用单一调度范式的语言中实现双重或多重调度的单一方式。
以下是使用该模式的原因:
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
接口中提供它。
– 在对 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);
片段现在定义了:
public interface Piece
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
它的关键方法是:
void accept(PieceMovingVisitor pieceVisitor);
它提供了第一个调度:基于Piece
接收器的调用。
在编译时,该方法绑定到 Piece 接口的 accept()
方法,在运行时,将在运行时 Piece
类上调用有界方法。 accept()
方法实现将执行第二次分派。
实际上,每个希望被PieceMovingVisitor
对象访问的Piece
子类都会通过作为参数本身传递来调用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()
中执行后处理。这将是一个非常糟糕的主意,因为它会在访问者实现和 Piece 子类之间创建高度耦合,此外它可能需要使用技巧作为 getClass()
、instanceof
或任何标识访问者实现的标记。
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)
【讨论】:
【参考方案7】:使用访问者模式至少有三个很好的理由:
减少仅在数据结构发生变化时略有不同的代码的扩散。
将相同的计算应用于多个数据结构,而无需更改实现计算的代码。
在不更改旧代码的情况下向旧库添加信息。
请查看an article I've written about this。
【讨论】:
我对您的文章发表了评论,这是我为访问者看到的最大用途。想法?【参考方案8】:正如 Konrad Rudolph 已经指出的,它适用于我们需要双重调度
的情况这是一个示例,展示了我们需要双重调度的情况以及访问者如何帮助我们这样做。
示例:
假设我有 3 种类型的移动设备 - iPhone、android、Windows Mobile。
这三个设备都安装了蓝牙无线电。
让我们假设蓝牙收音机可以来自 2 个独立的 OEM——英特尔和博通。
为了使示例与我们的讨论相关,我们还假设 Intel 无线电公开的 API 与 Broadcom 无线电公开的 API 不同。
这就是我的课程的样子 –
现在,我要介绍一个操作——打开移动设备上的蓝牙。
它的函数签名应该是这样的——
void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
因此,根据正确的设备类型和取决于正确的蓝牙无线电类型,可以通过调用适当的步骤或算法来打开它>。
原则上,它变成了一个 3 x 2 矩阵,其中我试图根据所涉及的正确类型的对象来向量化正确的操作。
取决于两个参数的类型的多态行为。
现在,访问者模式可以应用于这个问题。灵感来自 Wikipedia 页面声明 – “本质上,访问者允许在不修改类本身的情况下将新的虚函数添加到类族中;相反,我们创建了一个访问者类,它实现了虚函数的所有适当的特化。访问者将实例引用作为输入,通过双重调度实现目标。”
由于 3x2 矩阵,此处需要双重分派
这是设置的样子 -
我写了这个例子来回答另一个问题,代码及其解释提到了here。
【讨论】:
【参考方案9】:我发现以下链接更容易:
在
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
包含成对的 Pills,因此您需要将成对的数量乘以 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
您将计算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 你看到了不能使用polymorphism(答案)来遵循单一责任原则的真实场景。事实上:
public class HourlyEmployee extends Employee
public String reportQtdHoursAndPay()
//generate the line for this hourly employee
reportQtdHoursAndPay
方法用于报告和表示,这违反了单一职责原则。所以最好使用访问者模式来解决这个问题。
【讨论】:
嗨,赛义德,您能否编辑您的答案以添加您认为最有启发性的部分。 SO 通常不鼓励仅提供链接的答案,因为目标是成为知识数据库并且链接会失效。【参考方案10】:Cay Horstmann 有一个很好的例子来说明在哪里申请 Visitor in his OO Design and patterns book。他总结了这个问题:
复合对象通常具有复杂的结构,由单个元素组成。某些元素可能再次具有子元素。 ...对元素的操作访问其子元素,将操作应用于它们,并组合结果。 ... 但是,要在这样的设计中添加新的操作并不容易。
之所以不容易,是因为操作是在结构类本身中添加的。例如,假设您有一个文件系统:
以下是我们可能希望使用此结构实现的一些操作(功能):
显示节点元素的名称(文件列表) 显示计算出的节点元素的大小(其中目录的大小包括其所有子元素的大小) 等您可以向 FileSystem 中的每个类添加函数来实现操作(过去人们已经这样做了,因为这样做非常明显)。问题是,每当您添加新功能(上面的“等”行)时,您可能需要向结构类添加越来越多的方法。在某些时候,在您添加到软件中的一些操作之后,这些类中的方法就类的功能内聚而言不再有意义。例如,您有一个 FileNode
,它有一个方法 calculateFileColorForFunctionABC()
,以便在文件系统上实现最新的可视化功能。
访问者模式(像许多设计模式一样)诞生于开发人员的痛苦和痛苦,他们知道有更好的方法可以让他们的代码更改而不需要到处进行大量更改,而且尊重良好的设计原则(高内聚、低耦合)。我的观点是,在你感受到痛苦之前,很难理解很多模式的用处。解释痛苦(就像我们试图在上面添加的“等”功能一样)在解释中占用空间并且会分散注意力。因此很难理解模式。
Visitor 允许我们将数据结构(例如,FileSystemNodes
)上的功能与数据结构本身分离。该模式允许设计尊重内聚——数据结构类更简单(它们有更少的方法),并且功能被封装到Visitor
实现中。这是通过双重调度完成的(这是模式的复杂部分):在结构类中使用accept()
方法,在访问者(功能)类中使用visitX()
方法:
这个结构允许我们添加新的功能,作为具体的访问者在结构上工作(不改变结构类)。
例如,实现目录列表功能的PrintNameVisitor
和实现具有大小的版本的PrintSizeVisitor
。我们可以想象有一天会有一个“ExportXMLVisitor”以 XML 格式生成数据,或者另一个以 JSON 格式生成数据的访问者,等等。我们甚至可以让一个访问者使用 graphical language such as DOT 显示我的目录树,并与另一个访问者一起可视化程序。
最后一点:Visitor 的复杂性及其双重调度意味着它更难理解、编码和调试。简而言之,它具有很高的极客因子,并且违背了 KISS 原则。 In a survey done by researchers, Visitor was shown to be a controversial pattern (there wasn't a consensus about its usefulness). Some experiments even showed it didn't make code easier to maintain.
【讨论】:
目录结构我认为是一个很好的复合模式,但同意你的最后一段。【参考方案11】:在我看来,使用Visitor Pattern
或直接修改每个元素结构,添加新操作的工作量或多或少是相同的。另外,如果我要添加新的元素类,比如Cow
,操作接口将受到影响,这会传播到所有现有的元素类,因此需要重新编译所有元素类。那么有什么意义呢?
【讨论】:
几乎每次我使用 Visitor 时,您都在处理对象层次结构的遍历。考虑一个嵌套的树形菜单。您想要折叠所有节点。如果您不实现访问者,则必须编写图遍历代码。或与访客:rootElement.visit (node) -> node.collapse()
。有了visitor,每个节点都会为其所有子节点实现图遍历,这样就完成了。
@GeorgeMauer,双重调度的概念为我清除了动机:依赖于类型的逻辑与类型或痛苦的世界有关。分布遍历逻辑的想法仍然让我停下来。是不是更有效率?它更易于维护吗?如果添加“折叠到第 N 级”作为要求怎么办?
@nik.shornikov 效率在这里真的不应该是一个问题。在几乎任何语言中,一些函数调用的开销都可以忽略不计。除此之外的任何事情都是微优化。它更易于维护吗?这得看情况。我想大多数时候是,有时不是。至于“折叠到N级”。轻松传入levelsRemaining
计数器作为参数。在调用下一级子级之前将其递减。在您的访客内部if(levelsRemaining == 0) return
。
@GeorgeMauer,完全同意效率是一个小问题。但是可维护性,例如接受签名的覆盖,正是我认为应该归结为的决定。【参考方案12】:
访问者模式与 Aspect Object 编程的地下实现相同。
例如,如果您定义了一个新操作而不更改它所操作的元素的类
【讨论】:
提到Aspect Object Programming【参考方案13】:访问者模式的简要说明。 需要修改的类都必须实现 'accept' 方法。客户端调用此接受方法来对该类族执行一些新操作,从而扩展它们的功能。通过为每个特定操作传入不同的访问者类,客户端可以使用这个接受方法来执行范围广泛的新操作。访问者类包含多个重写的访问方法,这些方法定义了如何为家庭中的每个类实现相同的特定操作。这些访问方法被传递给一个可以工作的实例。
什么时候可以考虑使用它
-
当您拥有一个类族时,您知道您将不得不向它们全部添加许多新操作,但由于某种原因,您将来无法更改或重新编译该类族。
当您想要添加一个新操作并将该新操作完全定义在一个访问者类中而不是分布在多个类中时。
当您的老板说您必须生成一系列必须立即做某事的类时!...但实际上还没有人确切知道那是什么。
【讨论】:
【参考方案14】:直到遇到uncle bob article 并阅读了 cmets,我才理解这种模式。 考虑以下代码:
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();
虽然它看起来不错,因为它与Single Responsibility 确认它违反了Open/Closed 原则。每次您有新的 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)
看起来一样,但实际上却不同,因为它调用了不同的访问者重载。
【讨论】:
是的,我发现它在处理树结构时特别有用,而不仅仅是平面列表(平面列表是树的一种特殊情况)。正如您所注意到的,它仅在列表上并不是非常混乱,但是随着节点之间的导航变得更加复杂,访问者可以成为救星【参考方案15】:基于@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);
【讨论】:
走路,吃饭不是合适的例子,因为它们对Dog
和Cat
都很常见。您可以在基类中制作它们,以便它们被继承或选择合适的示例。
声音不同,很好的示例,但不确定它是否与访问者模式有关
@AbhinavGauniyal 狗和猫的走路方式可能不同。在您看来,他们可能会使用相同的逻辑/代码“Walk()”,但情况可能并非如此。我对这个例子的批评不是猫和狗都可以在现实世界中“行走”,而是这个例子并不能真正帮助解释模式,因为你永远不会编写实现这种情况的软件取自现实世界。【参考方案16】:
我真的很喜欢 http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html 的描述和示例。
假设您有一个固定的主要类层次结构;也许它来自另一个供应商,您无法更改该层次结构。但是,您的意图是您想向该层次结构添加新的多态方法,这意味着通常您必须向基类接口添加一些东西。所以困境是你需要在基类中添加方法,但你不能触及基类。你如何解决这个问题?
解决此类问题的设计模式称为“访问者”(设计模式一书中的最后一个),它建立在上一节中显示的双重调度方案之上。
访问者模式允许您通过创建一个单独的访问者类型的类层次结构来虚拟化对主要类型执行的操作,从而扩展主要类型的接口。主要类型的对象只是简单地“接受”访问者,然后调用访问者的动态绑定成员函数。
【讨论】:
虽然从技术上讲是访问者模式,但这实际上只是他们示例中的基本双重调度。我认为仅凭这一点并不能特别看出其用处。 由于需要在基类中实现某种Accept
方法,并且这可能需要更改主类层次结构(除非它已经被设计为与“访问者”一起使用)似乎写引用部分的人是不正确的。【参考方案17】:
Visitor
Visitor 允许在不修改类本身的情况下向类族中添加新的虚函数;相反,我们创建了一个访问者类,它实现了虚函数的所有适当的特化
访客结构:
在以下情况下使用访问者模式:
-
必须对结构中分组的不同类型的对象执行类似的操作
您需要执行许多不同且不相关的操作。 它将操作与对象结构分开
必须在不改变对象结构的情况下添加新操作
将相关操作集中到单个类中,而不是强迫您更改或派生类
向您没有源或无法更改源的类库添加函数
尽管 Visitor 模式提供了在不更改 Object 中现有代码的情况下添加新操作的灵活性,但这种灵活性也有一个缺点。
如果添加了新的 Visitable 对象,则需要更改 Visitor 和 ConcreteVisitor 类中的代码。有一个解决方法可以解决这个问题:使用反射,这会对性能产生影响。
代码sn-p:
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
接口(@987654336@)。
每个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
参考
oodesign article
sourcemaking文章
更多详情
Decorator
模式允许将行为静态或动态添加到单个对象,而不会影响同一类中其他对象的行为
相关帖子:
Decorator Pattern for IO
When to Use the Decorator Pattern?
【讨论】:
【参考方案18】:虽然我了解了如何以及何时,但我从未了解过原因。如果它对任何具有 C++ 等语言背景的人有所帮助,您需要非常小心地read this。
对于懒惰的人,我们使用访问者模式,因为“虽然虚函数在 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;
【讨论】:
在访问者模式中使用动态调度完全让我感到困惑。该模式的建议用途描述了可以在编译时完成的分支。使用函数模板似乎会更好地处理这些情况。【参考方案19】:感谢@Federico A. Ramponi 的精彩解释,我刚刚在 java 版本中做了这个。希望它可能会有所帮助。
也正如@Konrad Rudolph 指出的那样,它实际上是一个双重调度,使用两个具体实例一起确定运行时方法。
所以实际上,只要我们正确定义了 operation 接口,就不需要为 operation 执行器创建 common 接口。
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");
如您所料,common 界面会让我们更加清晰,尽管它实际上并不是此模式中的必需 部分。
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");
【讨论】:
【参考方案20】:你的问题是什么时候知道:
我不首先使用访问者模式编写代码。我编码标准并等待需要发生然后重构。因此,假设您有多个支付系统,一次安装一个。在结帐时,您可能有许多 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()
您可以从这里的示例数量中了解如何实现它,我只是向您展示了一个用例。
【讨论】:
以上是关于什么时候应该使用访问者设计模式? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章