我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
我一直在博客中看到访客模式的参考,但我不得不承认,我就是不明白。我读了维基百科上关于这个模式的文章,我理解了它的机制,但我仍然不知道什么时候使用它。
作为一个最近才真正了解装饰器模式的人,现在看到它在任何地方都有使用,我希望能够真正直观地理解这个看似方便的模式。
当前回答
我不太熟悉来客模式。看看我做得对不对。假设你有一个动物等级
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);
}
其他回答
你的问题是什么时候知道:
我不首先编码访问者模式。我编写标准代码,等待需求的出现,然后重构。假设你有多个支付系统,一次安装一个。在签出时,你可以有很多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()
你可以看到如何实现它从这里的例子的数量,我只是向你展示一个用例。
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方法的参数。
我发现下面的链接更容易:
在 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方法用于报告和表示,这违反了单一责任原则。因此,最好利用访问者模式来解决这一问题。
我不理解这种模式,直到我看到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)看起来一样,但实际上是不同的,因为它调用不同的访问者重载。
正如Konrad Rudolph已经指出的,它适用于需要双重调度的情况
这里有一个例子,以显示我们需要双重调度&访客如何帮助我们这样做的情况。
例子:
假设我有三种类型的移动设备——iPhone, Android, Windows mobile。
这三种设备都安装了蓝牙收音机。
让我们假设蓝牙收音机可以来自2个独立的原始设备制造商-英特尔和博通。
为了使这个例子与我们的讨论相关,我们还假设Intel电台公开的api与Broadcom电台公开的api是不同的。
这是我的类的样子
现在,我想介绍一个操作——移动设备蓝牙开关。
它的函数特征应该是这样的
void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
因此,根据正确的设备类型和蓝牙收音机的正确类型,它可以通过调用适当的步骤或算法打开。
原则上,它变成了一个3 × 2的矩阵,在这里,我试图根据所涉及的对象的正确类型来进行正确的操作。
取决于两个参数类型的多态行为。
现在,访问者模式可以应用于这个问题。灵感来自维基百科页面上的说明——“本质上,访问者允许在不修改类本身的情况下向类族添加新的虚函数;相反,创建一个实现虚函数的所有适当专门化的访问者类。访问者将实例引用作为输入,通过双重调度实现目标。
由于3x2矩阵,双重调度是必要的
这是如何设置看起来像-
我写了一个例子来回答另一个问题,代码和它的解释在这里被提到。