访客模式的目的与示例

2022-08-31 10:47:15

我对访客模式及其用途感到非常困惑。我似乎无法真正想象使用这种模式的好处或其目的。如果可能的话,如果有人可以用例子来解释,那就太好了。


答案 1

所以你可能已经读过一个关于访客模式的不同解释,你可能还在说“但你什么时候会使用它!

传统上,访问者习惯于在不牺牲类型安全性的情况下实现类型测试,只要您的类型预先定义良好并事先知道即可。假设我们有几个类,如下所示:

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

我想将列表划分为三个列表,每个列表包含橙子,苹果或香蕉。你会怎么做?好吧,简单的解决方案是进行型式试验:

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 派生实例,我们需要对执行水果类型测试的每个地方进行全局搜索,否则我们可能会错过类型。

访客模式优雅地解决了问题。首先修改我们的基本水果类:

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

看起来我们正在复制粘贴代码,但请注意,派生类都在调用不同的重载(调用 ,调用等)。AppleVisit(Apple)BananaVisit(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,而您希望为使用者提供一种向对象添加新的即席功能的方法。它们也是使用临时函数扩展密封类的便捷方法。

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

在以下情况下不要使用访问者:

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

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

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

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

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


答案 2

从前。。。

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

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


推荐