【问题标题】:Alternative to the visitor pattern?访问者模式的替代方案?
【发布时间】:2010-11-02 10:13:24
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • 顺便说明一下,因为您不太可能更改您的语言:有些语言直接支持多个调度泛型函数。
  • 好问题。我只是想提供一个对位。有时,您对 (5) 的问题可能是件好事。当我有一些在定义新的 IShape 子类型时必须更新的功能时,我会使用访问者模式。我有一个 IShapeVisitor 接口,它定义了需要哪些方法。只要使用新的子类型更新该接口,我的代码就不会在关键功能更新之前构建。在某些情况下,这可能非常有用。
  • 我同意@oillio,但您也可以将其作为 IShape 上的抽象方法强制执行。访问者模式在纯 OO 语言中为您购买的是函数的局部性(与类的局部性),因此是关注点的分离。在任何情况下,当您想强制添加新类型时,使用访问者模式应该在编译时显式中断!
  • “必须更改所有访问者类以添加处理新 IShape 派生类型的方法”:我不会说这是一个“问题”。我会说这是您设计中非常好的安全性。它保证在您执行每种类型特定操作的每个地方都考虑新添加的类(即:在我们定义访问者的每个地方......如果您不考虑新添加的类型,编译器将不会放开你……)。

标签: oop design-patterns visitor-pattern


【解决方案1】:

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

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

更多信息在这个相关讨论中:

Should an object write itself out to a file, or should another object act on it to perform I/O?

【讨论】:

  • 我已将此标记为我的问题的答案,因为我认为这个或一些小的变化可能适合我想做的事情。对于任何感兴趣的人,我添加了一个“答案”,描述了我对这个问题的一些想法
  • 好的 - 改变了我对答案的想法 - 我将尝试将其浓缩为评论(以下)
  • 我认为这里有一个根本的冲突——如果你有一堆东西和一堆可以对这些东西执行的动作,那么添加一个新东西意味着你必须定义所有的效果对其采取行动,反之亦然 - 没有逃避这一点。访问者提供了一种非常简单、优雅的方式来添加新动作,但代价是难以添加新事物。如果这个限制必须放宽,你必须付出代价。我希望有一种解决方案可以像访问者那样优雅而简单,但正如我所怀疑的那样,我认为不存在......继续......
  • 所以,我认为这个答案可能适合我目前的需求,我觉得它很有吸引力,因为它比其他一些建议更简单。无论如何,感谢大家花时间回答
  • @jungle_mole 也许我的解决方案和你在这里描述的有点相似?
【解决方案2】:

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

然后,当你定义一个访问者时,扩展这个抽象类而不是直接实现接口。您可以覆盖您当时知道的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);
}

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

不幸的是,没有办法避免指定访问者在新课程中的行为方式 - 您可以提前设置默认值,也可以不设置。 (另见this cartoon的第二个面板)

【讨论】:

    【解决方案3】:

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

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

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

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

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

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

    对于形状我们做的如下

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

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

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

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

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

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

    调整路径大小 旋转路径 移动路径 分割路径 等等。

    当我们需要添加新功能时,我们会添加另一个命令对象,在右侧 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 对象中比盲目地追求封装的理想更有助于实现这一目标。镜像路径所需的所有操作都捆绑在镜像路径命令对象中。

    【讨论】:

    • 该解决方案似乎很有趣,但如果您能向我推荐此类解决方案的示例代码以更好地理解该概念,我将不胜感激。
    【解决方案4】:

    访问者设计模式是一种解决方法,而不是解决问题的方法。简短的回答是pattern matching

    【讨论】:

    • 你的答案可以通过解释为什么模式匹配是相关的,而不是仅仅提供一个外部网站的链接。
    【解决方案5】:

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

    您需要将访问者功能与形状的界面分开。

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

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

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

    【讨论】:

    • 在 AbstractWorkerFactory 你仍然需要做 instanceof
    【解决方案6】:

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

    【讨论】:

    • @ntohl 在我做过的测试中(在 Java 8 上,注意测试使用了 Java 6)instanceof 更快,所以我猜两者的相对速度的速度必须根据细微的细节而有所不同。
    【解决方案7】:

    如果您有 n 个 IShapes 和 m 个操作对每个形状表现不同,那么您需要 n*m 个单独的函数。把这些都放在同一个班级对我来说似乎是一个糟糕的主意,给你某种上帝的对象。所以它们应该按IShape分组,通过在IShape接口中放置m个函数,每个操作一个,或者按操作分组(通过使用访问者模式),通过放置n个函数,每个@987654325一个@ 在每个操作/访问者类中。

    您要么必须在添加新的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);
    }
    

    【讨论】:

      【解决方案8】:

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

      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 的抽屉列表中!

      【讨论】:

        猜你喜欢
        • 2015-05-19
        • 2011-07-07
        • 1970-01-01
        • 1970-01-01
        • 2013-12-11
        • 2019-09-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多