访客模式的替代方案?

发布于 2024-07-24 08:50:07 字数 731 浏览 8 评论 0原文

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

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

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

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

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

I am looking for an alternative to the visitor pattern. Let me just focus on a couple of pertinent aspects of the pattern, while skipping over unimportant details. I'll use a Shape example (sorry!):

  1. You have a hierarchy of objects that implement the IShape interface
  2. You have a number of global operations that are to be performed on all objects in the hierarchy, e.g. Draw, WriteToXml etc...
  3. It is tempting to dive straight in and add a Draw() and WriteToXml() method to the IShape interface. This is not necessarily a good thing - whenever you wish to add a new operation that is to be performed on all shapes, each IShape-derived class must be changed
  4. Implementing a visitor for each operation i.e. a Draw visitor or a WirteToXml visitor encapsulates all the code for that operation in one class. Adding a new operation is then a matter of creating a new visitor class that performs the operation on all types of IShape
  5. When you need to add a new IShape-derived class, you essentially have the same problem as you did in 3 - all visitor classes must be changed to add a method to handle the new IShape-derived type

Most places where you read about the visitor pattern state that point 5 is pretty much the main criteria for the pattern to work and I totally agree. If the number of IShape-derived classes is fixed, then this can be a quite elegant approach.

So, the problem is when a new IShape-derived class is added - each visitor implementation needs to add a new method to handle that class. This is, at best, unpleasant and, at worst, not possible and shows that this pattern is not really designed to cope with such changes.

So, the question is has anybody come across alterative approaches to handling this situation?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

尛丟丟 2024-07-31 08:50:07

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

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?

You might want to have a look at the Strategy pattern. This still gives you a separation of concerns while still being able to add new functionality without having to change each class in your hierarchy.

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

More information is in this related discussion:

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

时光礼记 2024-07-31 08:50:07

有一种“默认访问者模式”,您可以像平常一样执行访问者模式,但然后定义一个抽象类,通过将所有内容委托给带有签名 的抽象方法来实现您的 IShapeVisitor 类访问默认(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);
}

这使您可以定义访问者,以您希望的任何具体级别指定其行为。

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

There is the "Visitor Pattern With Default", in which you do the visitor pattern as normal but then define an abstract class that implements your IShapeVisitor class by delegating everything to an abstract method with the signature visitDefault(IShape).

Then, when you define a visitor, extend this abstract class instead of implementing the interface directly. You can override the visit* methods you know about at that time, and provide for a sensible default. However, if there really isn't any way to figure out sensible default behavior ahead of time, you should just implement the interface directly.

When you add a new IShape subclass, then, you fix the abstract class to delegate to its visitDefault method, and every visitor that specified a default behavior gets that behavior for the new IShape.

A variation on this if your IShape classes fall naturally into a hierarchy is to make the abstract class delegate through several different methods; for example, an DefaultAnimalVisitor might do:

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 lets you define visitors that specify their behavior at whatever level of specificity you wish.

Unfortunately, there is no way to avoid doing something to specify how visitors will behave with a new class - either you can set up a default ahead of time, or you can't. (See also the second panel of this cartoon )

征﹌骨岁月お 2024-07-31 08:50:07

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

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

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

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

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

所以绘图是在UI层处理的。 我们针对不同的机器有不同的软件。 因此,虽然我们所有的软件共享相同的模型并重用许多相同的命令。 他们处理绘画之类的事情非常不同。 例如,刳刨机与使用等离子炬的机器的切割台绘制方式不同,尽管它们本质上都是一个巨大的 XY 平板。 这是因为就像汽车一样,这两台机器的构造差异很大,因此客户会产生视觉差异。

至于形状,我们所做的如下:

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

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

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

之所以有多种视图,是因为我们的客户喜欢以不同的方式输入形状。 我们的客户群分为两部分:喜欢以表格形式输入形状参数的人和喜欢以面前形状的图形表示形式输入的人。 有时我们还需要通过最小的对话框而不是完整的形状输入屏幕来访问参数。 因此出现了多种观点。

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

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

ResizePath
旋转路径
移动路径
分割路径
等等。

当我们需要添加新功能时,我们添加另一个命令对象,在右侧 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 元素来询问用户要镜像哪个轴。 这些都不是创建访问者,也不是向路径添加方法。

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

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

将操作捆绑到 Command 对象中比盲目地追求封装理想更有助于实现这一目标。 镜像路径所需执行的所有操作都捆绑在镜像路径命令对象中。

I maintain a CAD/CAM software for metal cutting machine. So I have some experience with this issues.

When we first converted our software (it was first released in 1985!) to a object oriented designed I did just what you don't like. Objects and Interfaces had Draw, WriteToFile, etc. Discovering and reading about Design Patterns midway through the conversion helped a lot but there were still a lot of bad code smells.

Eventually I realized that none of these types of operations were really the concern of the object. But rather the various subsystems that needed to do the various operations. I handled this by using what is now called a Passive View Command object, and well defined Interface between the layers of software.

Our software is structured basically like this

  • The Forms implementing various Form
    Interface. These forms are a thing shell passing events to the UI Layer.
  • UI layer that receives Events and manipulate forms through the Form interface.
  • The UI Layer will execute commands that all implement the Command interface
  • The UI Object have interfaces of their own that the command can interact with.
  • The Commands get the information they need, process it, manipulates the model and then report back to the UI Objects which then does anything needed with the forms.
  • Finally the models which contains the various objects of our system. Like Shape Programs, Cutting Paths, Cutting Table, and Metal Sheets.

So Drawing is handled in the UI Layer. We have different software for different machines. So while all of our software share the same model and reuse many of the same commands. They handle things like drawing very different. For a example a cutting table is draw different for a router machine versus a machine using a plasma torch despite them both being esstentially a giant X-Y flat table. This because like cars the two machines are built differently enough so that there is a visual difference to the customer.

As for shapes what we do is as follows

We have shape programs that produce cutting paths through the entered parameters. The cutting path knows which shape program produced. However a cutting path isn't a shape. It just the information needed to draw on the screen and to cut the shape. One reason for this design is that cutting paths can be created without a shape program when they are imported from a external app.

This design allows us to separate the design of the cutting path from the design of the shape which are not always the same thing. In your case likely all you need to package is the information needed to draw the shape.

Each shape program has a number of views implementing a IShapeView Interface. Through the IShapeView interface the shape program can tell the generic shape form we have how to setup itself up to show the parameters of that shape. The generic shape form implements a IShapeForm interface and registers itself with the ShapeScreen Object. The ShapeScreen Object registers itself with our application object. The shape views use whatever shapescreen that registers itself with the application.

The reason for the multiple views that we have customers that like to enter shapes in different ways. Our customer base is split in half between those who like to enter shape parameters in a table form and those who like to enter with a graphical representation of the shape in front of them. We also need to access the parameters at times through a minimal dialog rather than our full shape entry screen. Hence the multiple views.

Commands that manipulate shapes fall in one of two catagories. Either they manipulate the cutting path or they manipulate the shape parameters. To manipulate the shape parameters generally we either throw them back into the shape entry screen or show the minimal dialog. Recalculate the shape, and display it in the same location.

For the cutting path we bundled up each operation in a separate command object. For example we have command objects

ResizePath
RotatePath
MovePath
SplitPath
and so on.

When we need to add new functionality we add another command object, find a menu, keyboard short or toolbar button slot in the right UI screen and setup the UI object to ececute that command.

For example

   CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath

or

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

In both instances the Command object MirrorPath is being associated with a desired UI element. In the execute method of MirrorPath is all the code needed to mirror the path in a particular axis. Likely the command will have it's own dialog or use one of the UI elements to ask the user which axis to mirror. None of this is making a visitor, or adding a method to the path.

You will find that a lot can be handled through bundling actions into commands. However I caution that is not a black or white situation. You will still find that certain things work better as methods on the original object. In may experience I found that perhaps 80% of what I used to do in methods were able to be moved into the command. The last 20% just plain work better on the object.

Now some may not like this because it seems to violate encapsulations. From maintaining our software as a object oriented system for the last decade I have to say the MOST important long term thing you can do is clearly document the interactions between the different layers of your software and between the different objects.

Bundling actions into Command objects helps with this goal way better than a slavish devotion to the ideals of encapsulation. Everything that is needs to be done to Mirror a Path is bundled in the Mirror Path Command Object.

留一抹残留的笑 2024-07-31 08:50:07

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

Visitor design pattern is a workaround, not a solution to the problem. Short answer would be pattern matching.

江城子 2024-07-31 08:50:07

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

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

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

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

Regardless of what path you take, the implementation of alternate functionality that is currently provided by the Visitor pattern will have to 'know' something about the concrete implementation of the interface that it is working on. So there is no getting around the fact that you are going to have to write addition 'visitor' functionality for each additional implementation. That said what you are looking for is a more flexible and structured approach to creating this functionality.

You need to separate out the visitor functionality from the interface of the shape.

What I would propose is a creationist approach via an abstract factory to create replacement implementations for visitor functionality.

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

It still means that you have to write concrete implementations to work on new versions of 'shape' but because it is completely separated from the interface of shape, you can retrofit this solution without breaking the original interface and software that interacts with it. It acts as a sort of scaffolding around the implementations of IShape.

殤城〤 2024-07-31 08:50:07

如果您有 n 个 IShape 和 m 个操作,每个形状的行为都不同,那么您需要 n*m 个单独的函数。 对我来说,将所有这些都放在同一个类中似乎是一个糟糕的主意,给你某种上帝对象。 因此,它们应该按 IShape 进行分组,通过在 IShape 接口中放置 m 个函数(每个操作一个),或者按操作分组(通过使用访问者模式),通过在每个操作/访问者类中放置 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);
}

If you have n IShapes and m operations that behave differently for each shape, then you require n*m individual functions. Putting these all in the same class seems like a terrible idea to me, giving you some sort of God object. So they should be grouped either by IShape, by putting m functions, one for each operation, in the IShape interface, or grouped by operation (by using the visitor pattern), by putting n functions, one for each IShape in each operation/visitor class.

You either have to update multiple classes when you add a new IShape or when you add a new operation, there is no way around it.


If you are looking for each operation to implement a default IShape function, then that would solve your problem, as in Daniel Martin's answer: https://stackoverflow.com/a/986034/1969638, although I would probably use overloading:

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

interface IShape
{
    //...
    void accept(IVisitor visitor);
}
一世旳自豪 2024-07-31 08:50:07

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

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 code> 并将其添加到 ShapeDrawingClient 的抽屉列表中!

I have actually solved this problem using the following pattern. I do not know if it has a name or not!

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

        }
    }
}

Usage:

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

Now if someone as the user of my library defines IRectangleShape and wants to draw it, they can simply define IRectangleShapeDrawer and add it to ShapeDrawingClient's list of drawers!

与他有关 2024-07-31 08:50:07

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

更新:“每当您希望添加要对所有形状执行的新操作时,必须更改每个 IShape 派生类” - 这是某些语言的缺点。 Java 管理员意识到了这一点,并添加了对接口方法默认实现的支持,因此这不是一个问题:

interface IShape {
    default void someNewOperation() {
        // some code...
    }
}

在 TypeScript 中实现此目的的类似方法是

interface IShape {
  someNewOperation?: () => void;
}

function defaultSomeNewOperation(shape: IShape) {
  // some code...
}

function performSomeNewOperation(shape: IShape) {
  if (shape.someNewOperation) {
    shape.someNewOperation()
  } else {
    defaultSomeNewOperation(shape)
  }
}

If you're using Java: Yes, it's called instanceof. People are overly scared to use it. Compared to the visitor pattern, it's generally faster, more straightforward, and not plagued by point #5.

Update: "whenever you wish to add a new operation that is to be performed on all shapes, each IShape-derived class must be changed" - this is a shortcoming of some languages. Java stewards realized this and added support for interface method default implementations, so this is less of an issue:

interface IShape {
    default void someNewOperation() {
        // some code...
    }
}

A similar way of accomplishing this in TypeScript would be

interface IShape {
  someNewOperation?: () => void;
}

function defaultSomeNewOperation(shape: IShape) {
  // some code...
}

function performSomeNewOperation(shape: IShape) {
  if (shape.someNewOperation) {
    shape.someNewOperation()
  } else {
    defaultSomeNewOperation(shape)
  }
}
一城柳絮吹成雪 2024-07-31 08:50:07

我认为你在谈论“表达问题”。 有关此问题的更多信息,请参阅此链接
对象代数是解决这个问题的一个很好的方法。 它已在名为 大众可扩展性 - 实用可扩展性的论文中进行了描述与对象代数。 对象代数适用于对泛型具有基本支持的各种编程语言。

I think you are talking about the 'Expression problem'. See this link for more information about this problem.
Object algebras are a great solution for this problem. It has been described in a paper called Extensibility for the masses - Practical Extensibility with Object Algebras. Object algebras are applicable to a wide range of programming languages that have basic support for generics.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文