我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
当前回答
Cay Horstmann在他的OO设计和模式书中有一个很好的例子,说明了在哪里应用Visitor。他总结了这个问题:
复合对象通常具有复杂的结构,由单个元素组成。有些元素可能也有子元素. ...元素上的操作访问它的子元素,对它们应用该操作,并将结果组合在一起. ...然而,向这样的设计中添加新的操作并不容易。
不容易的原因是,操作是在结构类本身中添加的。例如,假设你有一个文件系统:
下面是一些我们可能想用这个结构实现的操作(功能):
显示节点元素的名称(一个文件列表) 显示计算出的节点元素大小(其中目录的大小包括其所有子元素的大小) 等。
You could add functions to each class in the FileSystem to implement the operations (and people have done this in the past as it's very obvious how to do it). The problem is that whenever you add a new functionality (the "etc." line above), you might need to add more and more methods to the structure classes. At some point, after some number of operations you've added to your software, the methods in those classes don't make sense anymore in terms of the classes' functional cohesion. For example, you have a FileNode that has a method calculateFileColorForFunctionABC() in order to implement the latest visualization functionality on the file system.
The Visitor Pattern (like many design patterns) was born from the pain and suffering of developers who knew there was a better way to allow their code to change without requiring a lot of changes everywhere and also respecting good design principles (high cohesion, low coupling). It's my opinion that it's hard to understand the usefulness of a lot of patterns until you've felt that pain. Explaining the pain (like we attempt to do above with the "etc." functionalities that get added) takes up space in the explanation and is a distraction. Understanding patterns is hard for this reason.
Visitor allows us to decouple the functionalities on the data structure (e.g., FileSystemNodes) from the data structures themselves. The pattern allows the design to respect cohesion -- data structure classes are simpler (they have fewer methods) and also the functionalities are encapsulated into Visitor implementations. This is done via double-dispatching (which is the complicated part of the pattern): using accept() methods in the structure classes and visitX() methods in the Visitor (the functionality) classes:
这个结构允许我们添加新的功能,这些功能作为具体的访问者在结构上工作(不需要改变结构类)。
例如,PrintNameVisitor实现目录列表功能,PrintSizeVisitor实现具有大小的版本。我们可以想象有一天有一个以XML生成数据的“ExportXMLVisitor”,或者另一个以JSON生成数据的访问者,等等。我们甚至可以让一个访问者使用图形化语言(如DOT)显示我的目录树,然后用另一个程序进行可视化。
最后要注意的是:Visitor的双重分派的复杂性意味着它更难以理解、编码和调试。简而言之,它有很高的极客因素,违背了KISS原则。在研究人员进行的一项调查中,访问者被证明是一个有争议的模式(关于它的有用性没有达成共识)。一些实验甚至表明,它并没有使代码更容易维护。
其他回答
我发现下面的链接更容易:
在 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对的数量。此外,你可能会注意到瓶使用单位是不同的数据类型,需要强制转换。
所以在主要方法中,您可以使用以下代码计算药丸计数:
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;
}
}
注意,上面的代码违反了单一责任原则。这意味着如果添加新类型的容器,则必须更改主方法代码。同时,延长开关时间也是不好的做法。
通过引入以下代码:
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
}
您将计数药丸数量的责任转移到名为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访问者查看他们的药片计数。他知道怎么数你的药。
看着来访者。伯爵有药丸的价值。
在 http://butunclebob.com/ArticleS.UncleBob.IuseVisitor你看到了真实的场景,你不能使用多态性(答案)来遵循单一责任原则。事实上在:
public class HourlyEmployee extends Employee {
public String reportQtdHoursAndPay() {
//generate the line for this hourly employee
}
}
reportQtdHoursAndPay方法用于报告和表示,这违反了单一责任原则。因此,最好利用访问者模式来解决这一问题。
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方法的参数。
感谢@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");
}
}
使用访问者模式至少有三个很好的理由:
减少代码的增殖,当数据结构发生变化时,代码只会略有不同。 将相同的计算应用于多个数据结构,而不改变实现计算的代码。 在不更改遗留代码的情况下向遗留库添加信息。
请看看我写的一篇关于这方面的文章。
虽然我知道如何做,何时做,但我一直不明白为什么。为了对有c++等语言背景的人有所帮助,请仔细阅读本文。
对于懒惰的人,我们使用访问者模式,因为“虽然在c++中虚函数是动态分派的,但函数重载是静态完成的”。
或者,换句话说,当你传递一个实际绑定到ApolloSpacecraft对象的飞船引用时,确保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;
}
}