访客模式的目的与示例

Vic*_*tor 83 java design-patterns visitor

我对访客模式及其用途感到困惑.我似乎无法想象使用这种模式或其目的的好处.如果有人可以用例子解释,如果可能的话会很好.

Jul*_*iet 191

所以你可能已经阅读了大量关于访客模式的不同解释,你可能仍在说"但你什么时候会用它!"

传统上,访问者习惯于在不牺牲类型安全的情况下实施类型测试,只要您的类型预先明确定义并提前知道即可.假设我们有几个类如下:

abstract class Fruit { }
class Orange : Fruit { }
class Apple : Fruit { }
class Banana : Fruit { }
Run Code Online (Sandbox Code Playgroud)

让我们说我们创建一个Fruit[]:

var fruits = new Fruit[]
    { new Orange(), new Apple(), new Banana(),
      new Banana(), new Banana(), new Orange() };
Run Code Online (Sandbox Code Playgroud)

我想将列表分成三个列表,每个列表包含橙子,苹果或香蕉.你会怎么做?嗯,简单的解决方案是类型测试:

List<Orange> oranges = new List<Orange>();
List<Apple> apples = new List<Apple>();
List<Banana> bananas = new List<Banana>();
foreach (Fruit fruit in fruits)
{
    if (fruit is Orange)
        oranges.Add((Orange)fruit);
    else if (fruit is Apple)
        apples.Add((Apple)fruit);
    else if (fruit is Banana)
        bananas.Add((Banana)fruit);
}
Run Code Online (Sandbox Code Playgroud)

它有效,但这段代码有很多问题:

  • 一开始,它的丑陋.
  • 它不是类型安全的,我们不会在运行时之前捕获类型错误.
  • 它不可维护.如果我们添加一个新的Fruit派生实例,我们需要对执行水果类型测试的每个地方进行全局搜索,否则我们可能会错过类型.

访客模式优雅地解决了问题.首先修改我们的基础Fruit类:

interface IFruitVisitor
{
    void Visit(Orange fruit);
    void Visit(Apple fruit);
    void Visit(Banana fruit);
}

abstract class Fruit { public abstract void Accept(IFruitVisitor visitor); }
class Orange : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Apple : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Banana : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
Run Code Online (Sandbox Code Playgroud)

看起来我们是复制粘贴代码,但请注意派生类都调用不同的重载(Apple调用Visit(Apple),Banana调用Visit(Banana)等).

实施访客:

class FruitPartitioner : IFruitVisitor
{
    public List<Orange> Oranges { get; private set; }
    public List<Apple> Apples { get; private set; }
    public List<Banana> Bananas { get; private set; }

    public FruitPartitioner()
    {
        Oranges = new List<Orange>();
        Apples = new List<Apple>();
        Bananas = new List<Banana>();
    }

    public void Visit(Orange fruit) { Oranges.Add(fruit); }
    public void Visit(Apple fruit) { Apples.Add(fruit); }
    public void Visit(Banana fruit) { Bananas.Add(fruit); }
}
Run Code Online (Sandbox Code Playgroud)

现在你可以在没有类型测试的情况下对水果进行分区:

FruitPartitioner partitioner = new FruitPartitioner();
foreach (Fruit fruit in fruits)
{
    fruit.Accept(partitioner);
}
Console.WriteLine("Oranges.Count: {0}", partitioner.Oranges.Count);
Console.WriteLine("Apples.Count: {0}", partitioner.Apples.Count);
Console.WriteLine("Bananas.Count: {0}", partitioner.Bananas.Count);
Run Code Online (Sandbox Code Playgroud)

这具有以下优点:

  • 相对干净,易于阅读的代码.
  • 类型安全,类型错误在编译时捕获.
  • 可维护性.如果我添加或删除一个具体的Fruit类,我可以修改我的IFruitVisitor接口以相应地处理类型,编译器将立即找到我们实现接口的所有位置,以便我们进行适当的修改.

话虽如此,访问者通常过度使用,并且他们倾向于使API严重复杂化,并且为每种新行为定义新访问者可能非常麻烦.

通常,应该使用更简单的模式(如继承)来代替访问者.例如,原则上我可以写一个类,如:

class FruitPricer : IFruitVisitor
{
    public double Price { get; private set; }
    public void Visit(Orange fruit) { Price = 0.69; }
    public void Visit(Apple fruit) { Price = 0.89; }
    public void Visit(Banana fruit) { Price = 1.11; }
}
Run Code Online (Sandbox Code Playgroud)

它有效,但与这个微不足道的修改相比有什么优势:

abstract class Fruit
{
    public abstract void Accept(IFruitVisitor visitor);
    public abstract double Price { get; }
}
Run Code Online (Sandbox Code Playgroud)

因此,在满足以下条件时,您应该使用访问者:

  • 您将访问一组明确定义的已知类.

  • 对所述类的操作没有明确定义或事先已知.例如,如果某人正在使用您的API,并且您希望为消费者提供向对象添加新的临时功能的方法.它们也是使用ad-hoc功能扩展密封类的便捷方式.

  • 您执行一类对象的操作,并希望避免运行时类型测试.当您遍历具有不同属性的不同对象的层次结构时,通常会出现这种情况.

在以下情况下不要使用访客

  • 您支持对一类对象的操作,这些对象的派生类型事先是未知的.

  • 对象的操作是事先明确定义的,特别是如果它们可以从基类继承或在接口中定义.

  • 客户端更容易使用继承向类添加新功能.

  • 您正在遍历具有相同属性或接口的对象层次结构.

  • 你想要一个相对简单的API.

  • 除了访客未因修改而关闭(开放/封闭原则).任何新的水果必须有一个新的方法.最好是访问者只有一个"访问(Fruit fruit)"方法和一个具体实现,它将每个水果映射到一个特定的方法.(这样其他访客类可以扩展具体基础) (6认同)
  • @jgauffin访问者模式的正式结果之一是,每当您添加一个可以访问的新对象时,都会创建一个新方法,因此这种模式的后果隐含了违反Open/Closed的行为.使用类型解析的Visit()方法提供了许多缺点,包括不允许IDE,编译器,RTE等验证实现. (6认同)

Noe*_*Ang 68

很久以前...

class MusicLibrary {
    private Set<Music> collection ...
    public Set<Music> getPopMusic() { ... }
    public Set<Music> getRockMusic() { ... }
    public Set<Music> getElectronicaMusic() { ... }
}
Run Code Online (Sandbox Code Playgroud)

然后你意识到你希望能够通过其他类型过滤图书馆的馆藏.您可以继续添加新的getter方法.或者您可以使用访客.

interface Visitor<T> {
    visit(Set<T> items);
}

interface MusicVisitor extends Visitor<Music>;

class MusicLibrary {
    private Set<Music> collection ...
    public void accept(MusicVisitor visitor) {
       visitor.visit( this.collection );
    }
}

class RockMusicVisitor implements MusicVisitor {
    private final Set<Music> picks = ...
    public visit(Set<Music> items) { ... }
    public Set<Music> getRockMusic() { return this.picks; }
}
class AmbientMusicVisitor implements MusicVisitor {
    private final Set<Music> picks = ...
    public visit(Set<Music> items) { ... }
    public Set<Music> getAmbientMusic() { return this.picks; }
}
Run Code Online (Sandbox Code Playgroud)

您将数据与算法分开.您将算法卸载到访问者实现.您可以通过创建更多访问者来添加功能,而不是不断修改(和膨胀)包含数据的类.

  • 对不起,这对于访客模式来说并不是一个很好的例子.访问者模式的主要机制之一,通过访问元素的类型(双重调度)选择功能未显示-1 (36认同)
  • @HaraldScheirich访客可能会或可能不会选择按类型选择功能.我发现即使没有它,访客也非常有用. (3认同)
  • @NoelAng这不是战略设计模式吗? (2认同)
  • 借用@HaraldScheirich - 访问者的正式表示(来自“设计模式 - 可重用面向对象软件的元素”,1995 年)表明每个访问者都应该代表一个操作,并且访问者应该为每种类型的应该可操作的类型提供一个方法. (2认同)

Kai*_*ili 6

它提供了另一层抽象.降低对象的复杂性并使其更加模块化.排序就像使用一个接口(实现是完全独立的,没有人关心如何完成它只是它完成.)

现在我从来没有使用它,但它对于实现需要在不同子类中完成的特定函数是有用的,因为每个子类需要以不同的方式实现它,而另一个类将实现所有函数.有点像一个模块,但只适用于一组类.维基百科有一个很好的解释:http://en.wikipedia.org/wiki/Visitor_pattern 他们的例子有助于解释我想说的内容.

希望有助于清除它.

编辑**对不起我链接到维基百科你的答案,但他们确实有一个不错的例子:)不想成为那个说去的人自己去找.