【发布时间】:2010-04-08 23:41:27
【问题描述】:
我对访问者模式及其用途感到非常困惑。我似乎无法想象使用这种模式的好处或其目的。如果有人能在可能的情况下举例说明,那就太好了。
【问题讨论】:
-
您是否查看过有关该主题的维基百科? en.wikipedia.org/wiki/Visitor_pattern具体哪些部分不清楚?还是您在寻找真实世界的示例?
标签: java design-patterns visitor-pattern
我对访问者模式及其用途感到非常困惑。我似乎无法想象使用这种模式的好处或其目的。如果有人能在可能的情况下举例说明,那就太好了。
【问题讨论】:
标签: java design-patterns visitor-pattern
因此,您可能已经阅读了无数种关于访问者模式的不同解释,并且您可能仍在说“但是您什么时候使用它!”
传统上,访问者习惯于在不牺牲类型安全性的情况下实施类型测试,只要您的类型预先定义好并且事先知道。假设我们有如下几个类:
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 基类:
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);
这样做的好处是:
话虽如此,访问者通常是矫枉过正,而且他们倾向于使 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。
【讨论】:
Visit(Fruit fruit) 方法和一个将每个水果映射到特定方法的具体实现。 (以便其他访问者类可以扩展具体基础)
从前...
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; }
}
您将数据与算法分开。您将算法卸载到访问者实现。您可以通过创建更多个访问者来添加功能,而不是不断修改(和膨胀)保存数据的类。
【讨论】:
它提供了另一层抽象。降低对象的复杂性并使其更加模块化。有点像使用接口(实现完全独立,没有人关心它是如何完成的。)
现在我从未使用过它,但它对以下方面很有用:实现需要在不同子类中完成的特定功能,因为每个子类都需要以不同的方式实现它,另一个类将实现所有功能。有点像模块,但仅适用于类的集合。维基百科有一个很好的解释:http://en.wikipedia.org/wiki/Visitor_pattern 他们的例子有助于解释我想说的话。
希望这有助于澄清一点。
编辑**对不起,我链接到维基百科以获得您的答案,但他们确实有一个不错的例子 :) 不要试图成为那个说自己去寻找的人。
【讨论】:
访客模式示例。书籍、水果和蔬菜是 “Visitable” 类型的基本元素 并且有两个“Visitor”,BillingVisitor & OfferVisitor每个访问者都有自己的目的。Algo计算账单和algo计算offer对这些元素进行了封装在相应的访问者和可访问对象(元素)中保持不变。
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);
}
}
}
【讨论】:
我认为访问者模式的主要目的是它具有很高的可扩展性。直觉是你买了一个机器人。机器人已经完全实现了基本功能,例如前进、左转、右转、返回、挑选东西、说出一个阶段……
有一天,您希望您的机器人可以为您去邮局。有了所有这些基本功能,它就可以做到,但您需要将机器人带到商店并“更新”您的机器人。店家不需要修改机器人,只要给你的机器人装一个新的更新芯片,它就可以做你想做的事了。
有一天,你想让你的机器人去超市。同样的过程,您必须将您的机器人带到商店并更新此“高级”功能。无需修改机器人本身。
等等……
所以访问者模式的想法是,给定所有已实现的基本功能,您可以使用访问者模式添加无限数量的复杂功能。在示例中,机器人是您的工人阶级,“更新芯片”是访客。每次需要新的“更新”功能时,您都不会修改您的工人阶级,而是添加一个访问者。
【讨论】:
是将数据操作与实际数据分开。作为奖励,您可以在类的整个层次结构中重复使用相同的访问者类,这再次使您免于携带与实际对象无关的数据操作算法。
【讨论】: