代码之家  ›  专栏  ›  技术社区  ›  Steg

替代访客模式?

  •  50
  • Steg  · 技术社区  · 15 年前

    我正在寻找一种访客模式的替代方案。让我只关注模式的一些相关方面,而跳过不重要的细节。我将使用形状示例(抱歉!)以下内容:

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

    大多数你读到关于访客模式的地方,第5点几乎是模式工作的主要标准,我完全同意。如果ishape派生类的数量是固定的,那么这是一种非常优雅的方法。

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

    所以,问题是有人遇到过处理这种情况的不同方法吗?

    8 回复  |  直到 5 年前
        1
  •  15
  •   Community Egal    7 年前

    你可能想看看 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?

        2
  •  13
  •   Daniel Martin    15 年前

    有一个“具有默认值的访问者模式”,在这个模式中,您将访问者模式作为普通模式进行操作,但随后定义一个抽象类来实现 IShapeVisitor 通过将所有内容委托给带有签名的抽象方法来初始化 visitDefault(IShape) .

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

    当您添加新的 IShape 子类,然后,修复抽象类以将其委托给 visitDefault 方法,并且每个指定默认行为的访问者都将获取新的 伊莎普 .

    如果您的 伊莎普 类自然地划分为一个层次结构是通过几个不同的方法使抽象类委托;例如, 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
  •  6
  •   RS Conley    15 年前

    我为金属切割机维护了一个CAD/CAM软件。所以我在这个问题上有一些经验。

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

    最终我意识到,这些类型的操作都不是对象真正关心的。而是需要执行各种操作的各种子系统。我用现在所说的 Passive View 命令对象,以及软件层之间定义良好的接口。

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

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

    所以图形是在用户界面层处理的。对于不同的机器,我们有不同的软件。因此,尽管我们所有的软件都共享同一个模型,并重用许多相同的命令。他们处理的事情,如绘画非常不同。例如,对于一台路由器机器和一台使用等离子焊炬的机器来说,切割台是不同的,尽管它们都是一个巨大的X-Y平板。这是因为像汽车一样,这两台机器的构造也完全不同,所以对客户来说,视觉上是不同的。

    至于形状,我们要做的是

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

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

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

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

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

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

    重筛 扶轮社 莫比佩斯 分裂路径 等等。

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

    例如

       CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath
    

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

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

    您会发现,通过将操作绑定到命令中,可以处理很多问题。不过,我警告说,这不是一种黑白相间的情况。您仍然会发现某些东西作为原始对象上的方法工作得更好。在5月份的经验中,我发现我在方法中所做的工作中,大约有80%能够被转移到命令中。最后20%只是简单地处理对象。

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

    将行为捆绑到命令对象中有助于实现这一目标,而不是对封装理想的盲目投入。镜像路径所需的所有操作都捆绑在mirror path命令对象中。

        4
  •  4
  •   Marko Tunjic    8 年前

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

        5
  •  2
  •   Jean-François Corbett    9 年前

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

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

    我所建议的是通过一个抽象工厂来创建访问者功能的替换实现的创造性方法。

    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实现的一种脚手架。

        6
  •  1
  •   Andy    11 年前

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

        7
  •  1
  •   Community Egal    7 年前

    如果你有N IShape S和M操作对每个形状的行为都不同,因此需要N*M单独的函数。把这些都放在同一节课上对我来说是一个可怕的想法,给你一些上帝的东西。所以它们应该按 伊莎普 ,通过在 伊莎普 接口,或按操作分组(使用访问者模式),通过放置n个函数,每个函数一个 伊莎普 在每个操作/访客类中。

    当您添加新的 伊莎普 或者,当您添加新的操作时,没有办法绕过它。


    如果要查找每个操作以实现默认值 伊莎普 函数,这样就可以解决您的问题,如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
  •  0
  •   Vahid    5 年前

    我已经使用下面的模式解决了这个问题。我不知道它有没有名字!

    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 抽屉清单!

    推荐文章