我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
当前回答
这里的每个人都是对的,但我认为它没有解决“何时”这个问题。首先,从设计模式:
Visitor允许您定义一个新的 操作而不改变类 它所作用的元素。
现在,让我们考虑一个简单的类层次结构。我有类1、2、3和4,方法A、B、C和d。把它们像电子表格一样放出来:类是行,方法是列。
现在,面向对象设计假设您更有可能增长新类而不是新方法,因此添加更多行,可以说,更容易。您只需添加一个新类,指定该类中的不同之处,并继承其余部分。
但是,有时类是相对静态的,但是您需要频繁地添加更多的方法——添加列。面向对象设计的标准方法是将这样的方法添加到所有类中,这可能成本很高。访问者模式使这变得很容易。
顺便说一下,这就是Scala模式匹配想要解决的问题。
其他回答
双重分派只是使用此模式的众多原因之一。 但请注意,这是在使用单一分派范式的语言中实现双分派或多分派的唯一方法。
以下是使用该模式的原因:
1)我们希望定义新的操作,而不需要每次都改变模型,因为模型不会经常变化,而操作则经常变化。
2)我们不想把模型和行为结合起来,因为我们想要在多个应用程序中有一个可重用的模型,或者我们想要一个可扩展的模型,允许客户端类用它们自己的类定义它们的行为。
3)我们有依赖于模型的具体类型的通用操作,但我们不想在每个子类中实现逻辑,因为这会在多个类中爆炸通用逻辑,因此在多个地方。
4)我们正在使用领域模型设计,相同层次结构的模型类执行太多不同的事情,而这些事情可能会聚集在其他地方。
5) We need a double dispatch. We have variables declared with interface types and we want to be able to process them according their runtime type … of course without using if (myObj instanceof Foo) {} or any trick. The idea is for example to pass these variables to methods that declares a concrete type of the interface as parameter to apply a specific processing. This way of doing is not possible out of the box with languages relies on a single-dispatch because the chosen invoked at runtime depends only on the runtime type of the receiver. Note that in Java, the method (signature) to call is chosen at compile time and it depends on the declared type of the parameters, not their runtime type.
最后一点是使用访问器的一个原因,也是一个后果,因为当您实现访问器时(当然对于不支持多分派的语言),您必然需要引入双分派实现。
请注意,遍历元素(迭代)以在每个元素上应用访问者并不是使用该模式的原因。 您使用模式是因为您分离了模型和处理。 通过使用该模式,您还可以从迭代器功能中获益。 这种能力非常强大,并且超越了使用特定方法对普通类型进行迭代的能力,因为accept()是一个泛型方法。 这是一个特殊的用例。我先把这个放到一边。
Java示例
我将用一个国际象棋的例子来说明该模式的附加价值,在这个例子中,我们想要定义处理,即玩家要求棋子移动。
如果不使用访问者模式,我们可以直接在pieces子类中定义块移动行为。 例如,我们可以有一个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()最终都会将当前的Piece设置到特定的位置,并可能获取对手的Piece。 将相关的行为拆分到多个类中,而不是将它们聚集在一起,在某种程度上挫败了单一责任模式。使它们的可维护性更加困难。
处理checkMoveValidity()不应该是Piece子类可以看到或改变的东西。 它是超越人类或计算机行为的检查。该检查在玩家请求的每个动作中执行,以确保所请求的棋子移动是有效的。 我们甚至不想在Piece界面中提供这个。
-在对机器人开发者具有挑战性的国际象棋游戏中,应用程序通常提供标准API(棋子接口、子类、棋盘、常见行为等),并让开发者丰富他们的机器人策略。 为了做到这一点,我们必须提出一个模型,其中数据和行为在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接收器的调用。 在编译时,该方法被绑定到Piece接口的accept()方法,在运行时,该绑定方法将在运行时的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的坐标,piece接口必须提供一种方法来做到这一点:
void setCoordinates(Coordinates coordinates);
The responsibility of Piece coordinates changes is now open to other classes than Piece subclasses. Moving the processing performed by the visitor in the Piece subclasses is not an option either. It will indeed create another issue as the Piece.accept() accepts any visitor implementation. It doesn't know what the visitor performs and so no idea about whether and how to change the Piece state. A way to identify the visitor would be to perform a post processing in Piece.accept() according to the visitor implementation. It would be a very bad idea as it would create a high coupling between Visitor implementations and Piece subclasses and besides it would probably require to use trick as getClass(), instanceof or any marker identifying the Visitor implementation.
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)
我不理解这种模式,直到我看到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();
}
}
虽然它看起来很好,因为它确认了单一责任,但它违反了开放/封闭原则。每次你有新的员工类型,你将不得不添加如果与类型检查。如果你不知道,在编译时你永远也不会知道。
使用访问者模式,你可以让你的代码更干净,因为它不违反开放/关闭原则,也不违反单一责任。如果你忘记实现visit,它将不会编译:
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)看起来一样,但实际上是不同的,因为它调用不同的访问者重载。
我不太熟悉来客模式。看看我做得对不对。假设你有一个动物等级
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);
}
Visitor设计模式非常适用于目录树、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访问算法将确保每个TreeNode都被用作Visitor的arrivedAt方法的参数。
这里的每个人都是对的,但我认为它没有解决“何时”这个问题。首先,从设计模式:
Visitor允许您定义一个新的 操作而不改变类 它所作用的元素。
现在,让我们考虑一个简单的类层次结构。我有类1、2、3和4,方法A、B、C和d。把它们像电子表格一样放出来:类是行,方法是列。
现在,面向对象设计假设您更有可能增长新类而不是新方法,因此添加更多行,可以说,更容易。您只需添加一个新类,指定该类中的不同之处,并继承其余部分。
但是,有时类是相对静态的,但是您需要频繁地添加更多的方法——添加列。面向对象设计的标准方法是将这样的方法添加到所有类中,这可能成本很高。访问者模式使这变得很容易。
顺便说一下,这就是Scala模式匹配想要解决的问题。