56

我正在寻找访问者模式的替代方案。让我只关注模式的几个相关方面,同时跳过不重要的细节。我将使用 Shape 示例(对不起!):

  1. 您有一个实现 IShape 接口的对象层次结构
  2. 您有许多要对层次结构中的所有对象执行的全局操作,例如 Draw、WriteToXml 等...
  3. 直接潜入并将 Draw() 和 WriteToXml() 方法添加到 IShape 接口是很诱人的。这不一定是好事 - 每当您希望添加要对所有形状执行的新操作时,都必须更改每个 IShape 派生类
  4. 为每个操作实现一个访问者,即一个 Draw 访问者或一个 WirteToXml 访问者,将该操作的所有代码封装在一个类中。然后添加一个新操作就是创建一个新的访问者类,该类对所有类型的 IShape 执行操作
  5. 当您需要添加一个新的 IShape 派生类时,您基本上遇到了与 3 中相同的问题 - 必须更改所有访问者类以添加一个方法来处理新的 IShape 派生类型

您阅读访问者模式的大多数地方都指出,第 5 点几乎是该模式工作的主要标准,我完全同意。如果 IShape 派生类的数量是固定的,那么这可能是一种非常优雅的方法。

因此,问题在于添加新的 IShape 派生类时 - 每个访问者实现都需要添加一个新方法来处理该类。这充其量是令人不快的,最坏的情况是不可能的,并且表明这种模式并不是真正设计用于应对此类变化。

所以,问题是有没有人遇到过处理这种情况的替代方法?

4

8 回答 8

15

您可能想看看策略模式。这仍然为您提供了关注点分离,同时仍然能够添加新功能而无需更改层次结构中的每个类。

class AbstractShape
{
    IXmlWriter _xmlWriter = null;
    IShapeDrawer _shapeDrawer = null;

    public AbstractShape(IXmlWriter xmlWriter, 
                IShapeDrawer drawer)
    {
        _xmlWriter = xmlWriter;
        _shapeDrawer = drawer;
    }

    //...
    public void WriteToXml(IStream stream)
    {
        _xmlWriter.Write(this, stream);

    }

    public void Draw()
    {
        _drawer.Draw(this);
    }

    // any operation could easily be injected and executed 
    // on this object at run-time
    public void Execute(IGeneralStrategy generalOperation)
    {
        generalOperation.Execute(this);
    }
}

更多信息在此相关讨论中:

一个对象应该将自己写到一个文件中,还是应该另一个对象对其进行操作以执行 I/O?

于 2009-06-12T10:11:51.810 回答
13

有“默认访问者模式”,您可以正常执行访问者模式,然后定义一个抽象类,IShapeVisitor通过将所有内容委托给具有签名的抽象方法来实现您的类visitDefault(IShape)

然后,当你定义一个访问者时,扩展这个抽象类而不是直接实现接口。您可以覆盖visit您当时知道的 * 方法,并提供一个合理的默认值。但是,如果真的没有办法提前找出合理的默认行为,您应该直接实现接口。

然后,当您添加一个新的IShape子类时,您修复了抽象类以委托给它的visitDefault方法,并且每个指定默认行为的访问者都会为新的IShape.

如果您的IShape类自然地落入层次结构,则对此的一种变体是通过几种不同的方法使抽象类委托;例如,一个DefaultAnimalVisitor可能会:

public abstract class DefaultAnimalVisitor implements IAnimalVisitor {
  // The concrete animal classes we have so far: Lion, Tiger, Bear, Snake
  public void visitLion(Lion l)   { visitFeline(l); }
  public void visitTiger(Tiger t) { visitFeline(t); }
  public void visitBear(Bear b)   { visitMammal(b); }
  public void visitSnake(Snake s) { visitDefault(s); }

  // Up the class hierarchy
  public void visitFeline(Feline f) { visitMammal(f); }
  public void visitMammal(Mammal m) { visitDefault(m); }

  public abstract void visitDefault(Animal a);
}

这使您可以定义访问者,以您希望的任何特定级别指定他们的行为。

不幸的是,没有办法避免做一些事情来指定访问者对新类的行为方式 - 您可以提前设置默认值,或者您不能。(另见该漫画的第二个面板)

于 2009-06-12T10:35:47.710 回答
6

我维护一个用于金属切割机的 CAD/CAM 软件。所以我对这个问题有一些经验。

当我们第一次将我们的软件(它于 1985 年首次发布!)转换为面向对象设计时,我做了你不喜欢的事情。对象和接口有 Draw、WriteToFile 等。在转换过程中发现和阅读设计模式有很大帮助,但仍然有很多不好的代码气味。

最终我意识到这些类型的操作都不是对象真正关心的。而是需要执行各种操作的各种子系统。我通过使用现在所谓的被动视图命令对象以及软件层之间定义良好的接口来处理这个问题。

我们的软件结构基本上是这样的

  • 实现各种表单接口的表单。这些表单是将事件传递给 UI 层的事物外壳。
  • UI 层,通过 Form 接口接收事件和操作表单。
  • UI层将执行所有实现Command接口的命令
  • UI 对象有自己的接口,命令可以与之交互。
  • 命令获取他们需要的信息,对其进行处理,操作模型,然后向 UI 对象报告,然后 UI 对象对表单执行任何所需的操作。
  • 最后是包含我们系统的各种对象的模型。像形状程序、切割路径、切割台和金属板。

所以绘图是在 UI 层中处理的。我们为不同的机器提供不同的软件。因此,尽管我们所有的软件都共享相同的模型并重复使用许多相同的命令。他们处理绘画之类的事情非常不同。例如,对于路由器机器和使用等离子炬的机器来说,切割台的绘制是不同的,尽管它们本质上都是一个巨大的 XY 平板。这是因为就像汽车一样,这两种机器的制造方式不同,因此给客户带来了视觉上的差异。

至于形状我们做的如下

我们有通过输入参数生成切割路径的形状程序。切割路径知道产生了哪个形状程序。然而,切割路径不是形状。它只是在屏幕上绘制和切割形状所需的信息。这种设计的一个原因是当从外部应用程序导入切割路径时,可以在没有形状程序的情况下创建切割路径。

这种设计使我们能够将切割路径的设计与形状的设计分开,这并不总是相同的东西。在您的情况下,您可能需要打包的只是绘制形状所需的信息。

每个形状程序都有许多实现 IShapeView 接口的视图。通过 IShapeView 接口,形状程序可以告诉我们如何设置自己的通用形状表单以显示该形状的参数。通用形状表单实现一个 IShapeForm 接口并将其自身注册到 ShapeScreen 对象。ShapeScreen 对象向我们的应用程序对象注册自身。形状视图使用向应用程序注册自身的任何形状屏幕。

我们有客户喜欢以不同方式输入形状的多重视图的原因。我们的客户群分为喜欢以表格形式输入形状参数的人和喜欢在他们面前以图形表示形式输入的人。我们有时还需要通过最小的对话框而不是完整的形状输入屏幕来访问参数。因此,多个视图。

操纵形状的命令属于两种类别之一。他们要么操纵切割路径,要么操纵形状参数。为了操纵形状参数,我们通常要么将它们放回形状输入屏幕,要么显示最小对话框。重新计算形状,并将其显示在同一位置。

对于切割路径,我们将每个操作捆绑在一个单独的命令对象中。例如我们有命令对象

ResizePath RotatePath MovePath SplitPath 等等。

当我们需要添加新功能时,我们添加另一个命令对象,在右侧 UI 屏幕中找到菜单、键盘短键或工具栏按钮槽,并设置 UI 对象来执行该命令。

例如

   CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath

或者

   CuttingTableScreen.Toolbar("Edit Path").AddButton Application.Icons("MirrorPath"),"Mirror Path", New MirrorPath

在这两种情况下,Command 对象 MirrorPath 都与所需的 UI 元素相关联。在 MirrorPath 的执行方法中是镜像特定轴上的路径所需的所有代码。该命令可能会有它自己的对话框或使用其中一个 UI 元素来询问用户要镜像哪个轴。这些都不是访问者,也不是向路径添加方法。

您会发现通过将操作捆绑到命令中可以处理很多事情。但是我警告说,这不是非黑即白的情况。您仍然会发现某些东西作为原始对象的方法效果更好。在可能的经验中,我发现我过去在方法中所做的事情可能有 80% 可以移到命令中。最后 20% 只是简单地在对象上工作得更好。

现在有些人可能不喜欢这样,因为它似乎违反了封装。从过去十年将我们的软件维护为面向对象的系统,我不得不说,从长远来看,您可以做的最重要的事情是清楚地记录软件不同层之间以及不同对象之间的交互。

将动作捆绑到 Command 对象中有助于实现这一目标,而不是盲目地致力于封装的理想。镜像路径所需的一切都捆绑在镜像路径命令对象中。

于 2009-06-12T13:16:55.593 回答
3

访问者设计模式是一种变通方法,而不是问题的解决方案。简短的回答是模式匹配

于 2015-12-23T09:43:42.327 回答
2

无论您采用哪种方式,访问者模式当前提供的替代功能的实现都必须“了解”它正在处理的接口的具体实现。因此,您必须为每个额外的实现编写额外的“访问者”功能,这是无法回避的事实。也就是说,您正在寻找一种更灵活和结构化的方法来创建此功能。

您需要从形状的界面中分离出访问者功能。

我建议的是通过抽象工厂创建访问者功能的替代实现的创造论方法。

public interface IShape {
  // .. common shape interfaces
}

//
// This is an interface of a factory product that performs 'work' on the shape.
//
public interface IShapeWorker {
     void process(IShape shape);
}

//
// This is the abstract factory that caters for all implementations of
// shape.
//
public interface IShapeWorkerFactory {
    IShapeWorker build(IShape shape);
    ...
}

//
// In order to assemble a correct worker we need to create
// and implementation of the factory that links the Class of
// shape to an IShapeWorker implementation.
// To do this we implement an abstract class that implements IShapeWorkerFactory
//
public AbsractWorkerFactory implements IShapeWorkerFactory {

    protected Hashtable map_ = null;

    protected AbstractWorkerFactory() {
          map_ = new Hashtable();
          CreateWorkerMappings();
    }

    protected void AddMapping(Class c, IShapeWorker worker) {
           map_.put(c, worker);
    }

    //
    // Implement this method to add IShape implementations to IShapeWorker
    // implementations.
    //
    protected abstract void CreateWorkerMappings();

    public IShapeWorker build(IShape shape) {
         return (IShapeWorker)map_.get(shape.getClass())
    }
}

//
// An implementation that draws circles on graphics
//
public GraphicsCircleWorker implements IShapeWorker {

     Graphics graphics_ = null;

     public GraphicsCircleWorker(Graphics g) {
        graphics_ = g;
     }

     public void process(IShape s) {
       Circle circle = (Circle)s;
       if( circle != null) {
          // do something with it.
          graphics_.doSomething();
       }
     }

}

//
// To replace the previous graphics visitor you create
// a GraphicsWorkderFactory that implements AbstractShapeFactory 
// Adding mappings for those implementations of IShape that you are interested in.
//
public class GraphicsWorkerFactory implements AbstractShapeFactory {

   Graphics graphics_ = null;
   public GraphicsWorkerFactory(Graphics g) {
      graphics_ = g;
   }

   protected void CreateWorkerMappings() {
      AddMapping(Circle.class, new GraphicCircleWorker(graphics_)); 
   }
}


//
// Now in your code you could do the following.
//
IShapeWorkerFactory factory = SelectAppropriateFactory();

//
// for each IShape in the heirarchy
//
for(IShape shape : shapeTreeFlattened) {
    IShapeWorker worker = factory.build(shape);
    if(worker != null)
       worker.process(shape);
}

这仍然意味着您必须编写具体的实现来处理新版本的“形状”,但由于它与形状的接口完全分离,您可以在不破坏与它交互的原始接口和软件的情况下改进这个解决方案。它充当了围绕 IShape 实现的一种脚手架。

于 2009-06-12T15:47:52.127 回答
1

如果您使用的是 Java:是的,它被称为instanceof. 人们过于害怕使用它。与访问者模式相比,它通常更快、更直接,并且不受第 5 点的困扰。

于 2013-09-13T18:02:35.320 回答
1

如果您有IShape对每个形状表现不同的 n s 和 m 操作,那么您需要 n*m 个单独的函数。把这些都放在同一个班级对我来说似乎是一个可怕的想法,给你某种上帝的对象。因此,它们应该通过IShape在界面中放置 m 个函数(每个操作一个)来IShape分组,或者按操作分组(通过使用访问者模式),通过放置 n 个函数(IShape每个操作/访问者类中的每个函数一个)来分组。

您要么在添加新类时更新多个类,IShape要么在添加新操作时更新多个类,这是没有办法的。


如果您正在寻找每个操作来实现默认IShape功能,那么这将解决您的问题,如 Daniel Martin 的回答:https ://stackoverflow.com/a/986034/1969638 ,尽管我可能会使用重载:

interface IVisitor
{
    void visit(IShape shape);
    void visit(Rectangle shape);
    void visit(Circle shape);
}

interface IShape
{
    //...
    void accept(IVisitor visitor);
}
于 2015-06-26T13:57:31.107 回答
1

我实际上已经使用以下模式解决了这个问题。不知道有没有名字!

public interface IShape
{
}

public interface ICircleShape : IShape
{
}

public interface ILineShape : IShape
{
}

public interface IShapeDrawer
{
    void Draw(IShape shape);

    /// <summary>
    /// Returns the type of the shape this drawer is able to draw!
    /// </summary>
    Type SourceType { get; }
}

public sealed class LineShapeDrawer : IShapeDrawer
{
    public Type SourceType => typeof(ILineShape);
    public void Draw(IShape drawing)
    {
        if (drawing is ILineShape)
        {
            // Code to draw the line
        }
    }
}

public sealed class CircleShapeDrawer : IShapeDrawer
{
    public Type SourceType => typeof(ICircleShape);
    public void Draw(IShape drawing)
    {
        if (drawing is ICircleShape)
        {
            // Code to draw the circle
        }
    }
}

public sealed class ShapeDrawingClient
{
    private readonly IDictionary<Type, IShapeDrawer> m_shapeDrawers =
        new Dictionary<Type, IShapeDrawer>();

    public void Add(IShapeDrawer shapeDrawer)
    {
        m_shapeDrawers[shapeDrawer.SourceType] = shapeDrawer;
    }

    public void Draw(IShape shape)
    {
        Type[] interfaces = shape.GetType().GetInterfaces();
        foreach (Type @interface in interfaces)
        {
            if (m_shapeDrawers.TryGetValue(@interface, out IShapeDrawer drawer))
              {
                drawer.Draw(drawing); 
                return;
              }

        }
    }
}

用法:

        LineShapeDrawer lineShapeDrawer = new LineShapeDrawer();
        CircleShapeDrawer circleShapeDrawer = new CircleShapeDrawer();

        ShapeDrawingClient client = new ShapeDrawingClient ();
        client.Add(lineShapeDrawer);
        client.Add(circleShapeDrawer);

        foreach (IShape shape in shapes)
        {
            client.Draw(shape);
        }

现在,如果有人作为我的库的用户定义IRectangleShape并想要绘制它,他们可以简单地定义IRectangleShapeDrawer并将其添加到ShapeDrawingClient抽屉列表中!

于 2018-11-09T17:59:27.293 回答