访客模式的目的与示例

时间:2010-04-08 23:41:27

标签: java design-patterns visitor

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

6 个答案:

答案 0 :(得分:184)

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

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

abstract class Fruit { }
class Orange : Fruit { }
class Apple : Fruit { }
class Banana : Fruit { }

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

var fruits = new Fruit[]
    { new Orange(), new Apple(), new Banana(),
      new Banana(), new Banana(), new Orange() };

我想将列表分成三个列表,每个列表包含橙子,苹果或香蕉。你会怎么做?那么, easy 解决方案将是一种类型测试:

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);
}

它有效,但此代码存在许多问题:

  • 首先,它的丑陋。
  • 它不是类型安全的,我们不会在运行时之前捕获类型错误。
  • 不可维护。如果我们添加一个新的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); } }

看起来我们是复制粘贴代码,但请注意派生类都调用不同的重载(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); }
}

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

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);

这有以下优点:

  • 相对干净,易于阅读的代码。
  • 类型安全,类型错误在编译时捕获。
  • 可维护性。如果我添加或删除一个具体的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; }
}

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

abstract class Fruit
{
    public abstract void Accept(IFruitVisitor visitor);
    public abstract double Price { get; }
}

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

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

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

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

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

  • 您支持对事先不知道派生类型的对象类的操作。

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

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

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

  • 您需要一个相对简单的API。

答案 1 :(得分:63)

很久以前......

class MusicLibrary {
    private Set<Music> collection ...
    public Set<Music> getPopMusic() { ... }
    public Set<Music> getRockMusic() { ... }
    public Set<Music> getElectronicaMusic() { ... }
}

然后你意识到你希望能够通过其他类型过滤图书馆的馆藏。您可以继续添加新的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; }
}

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

答案 2 :(得分:6)

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

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

希望有助于清除它。

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

答案 3 :(得分:3)

访客模式的示例。书,水果&amp;蔬菜是“可访问”类型的基本元素 有两个“访客” BillingVisitor&amp; OfferVisitor 每个访问者都有自己的目的。计算账单和算法来计算这些元素的报价被封装在相应的访问者中,并且可访问者(元素)保持不变。

import java.util.ArrayList;
import java.util.List;


public class VisitorPattern {

    public static void main(String[] args) {
        List<Visitable> visitableElements = new ArrayList<Visitable>();
        visitableElements.add(new Book("I123",10,2.0));
        visitableElements.add(new Fruit(5,7.0));
        visitableElements.add(new Vegetable(25,8.0));
        BillingVisitor billingVisitor = new BillingVisitor();
        for(Visitable visitableElement : visitableElements){
            visitableElement.accept(billingVisitor);
        }

        OfferVisitor offerVisitor = new OfferVisitor();
        for(Visitable visitableElement : visitableElements){
            visitableElement.accept(offerVisitor);
        }
        System.out.println("Total bill " + billingVisitor.totalPrice);
        System.out.println("Offer  " + offerVisitor.offer);

    }

    interface Visitor {
        void visit(Book book);
        void visit(Vegetable vegetable);
        void visit(Fruit fruit);
    }

    //Element
    interface Visitable{
        public void accept(Visitor visitor);
    }


    static class OfferVisitor implements Visitor{
        StringBuilder offer = new StringBuilder();

        @Override
        public void visit(Book book) {
            offer.append("Book " +  book.isbn +  " discount 10 %" + " \n");
        }

        @Override
        public void visit(Vegetable vegetable) {
            offer.append("Vegetable  No discount \n");
        }

        @Override
        public void visit(Fruit fruit) {
            offer.append("Fruits  No discount \n");
        }

    }

    static class BillingVisitor implements Visitor{
        double totalPrice = 0.0;

        @Override
        public void visit(Book book) {
            totalPrice += (book.quantity * book.price);
        }

        @Override
        public void visit(Vegetable vegetable) {
            totalPrice += (vegetable.weight * vegetable.price);
        }

        @Override
        public void visit(Fruit fruit) {
            totalPrice += (fruit.quantity * fruit.price);
        }

    }

    static class Book implements Visitable{
        private String isbn;
        private double quantity;
        private double price;

        public Book(String isbn, double quantity, double price) {
            this.isbn = isbn;
            this.quantity = quantity;
            this.price = price;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    }

    static class Fruit implements Visitable{
        private double quantity;
        private double price;

        public Fruit(double quantity, double price) {
            this.quantity = quantity;
            this.price = price;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    }

    static class Vegetable implements Visitable{
        private double weight;
        private double price;

        public Vegetable(double weight, double price) {
            this.weight = weight;
            this.price = price;
        }


        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);            
        }
    }


}

答案 4 :(得分:3)

我认为访客模式的主要目的是它具有很高的可扩展性。直觉是你买了一个机器人。机器人已经完全实现了基本功能,如前进,左转,右转,返回,选择一个阶段,说一个阶段,......

有一天,你希望你的机器人可以为你去邮局。有了所有这些基本功能,它可以做到,但你需要把机器人带到商店并“更新”你的机器人。商店销售商不需要修改机器人,只需将新的更新芯片放到您的机器人上,它就可以做您想要的。

有一天,您希望您的机器人去超级市场。同样的过程,您必须将您的机器人带到商店并更新这个“高级”功能。无需修改机器人本身。

依旧......

因此,访问者模式的概念是,在所有实现的基本功能的基础上,您可以使用访问者模式添加无限数量的复杂功能。在示例中,机器人是您的工人类,“更新芯片”是访客。每次都需要新的功能“更新”时,不要修改工人类,而是添加访问者。

答案 5 :(得分:1)

将数据操作与实际数据分开。作为奖励,您可以为类的整个层次结构重用相同的访问者类,这再次使您无需携带与实际对象无关的数据操作算法。