访客模式的替代方法?

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

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

大多数你读到关于访客模式的地方指出,第5点几乎是模式的主要标准,我完全同意。 如果IShape派生类的数量是固定的,那么这可能是一个相当优雅的方法。

所以,问题是新的IShape派生类被添加 – 每个访问者实现需要添加一个新的方法来处理该类。 这至多是不愉快的,最坏的情况是不可能的,并且表明这种模式并不是真正用来应对这种变化的。

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

你可能想看看战略模式 。 这仍然给你一个关注的分离,同时仍然能够添加新的function,而不必改变你的层次结构中的每个类。

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?

有“访问者模式与默认”,在其中你正常访问的访问者模式,但然后定义一个抽象类,实现您的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); } 

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

不幸的是,没有办法避免做一些事情来指定访问者如何使用新的类 – 或者你可以提前设置默认值,或者你不能。 (另见这个漫画的第二个小组)

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

当我们第一次将我们的软件(它是在1985年首次发布的!)转换为面向对象的devise时,我只是做了你不喜欢的东西。 对象和接口有Draw,WriteToFile等等。通过转换中途发现和阅读devise模式帮助了很多,但是仍然有很多不好的代码味道。

最终我意识到,这些types的操作都不是这个对象所关心的。 而是需要做各种操作的各种子系统。 我通过使用现在称为被动视图命令对象的处理方式来处理这个问题,并且在软件层之间定义了良好的接口。

我们的软件结构基本如此

  • 实现各种表单接口的表单。 这些forms是将事件传递给UI层的事情。
  • 接收事件并通过Form接口操作表单的UI层。
  • UI层将执行所有实现Command接口的命令
  • UI对象具有该命令可以与之交互的自己的接口。
  • 命令获取他们需要的信息,处理它,操作模型,然后回报给UI对象,然后用这些对象做任何forms的事情。
  • 最后是包含我们系统的各种对象的模型。 像形状程序,切割path,切割台和金属片。

所以绘图是在UI层中处理的。 我们针对不同的机器有不同的软件。 所以,虽然我们所有的软件共享相同的模型,并重复使用许多相同的命令。 他们处理的东西像绘画非常不同。 例如,对于路由器机器而言,切割台与使用等离子体炬的机器是不同的,尽pipe它们都是巨大的XY平台。 这是因为像汽车一样,两台机器的build造方式不同,以至于给客户带来视觉上的差异。

至于形状,我们做的是如下

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

这种devise使我们能够将切割path的devise与形状的devise分开,而这些devise并不总是一回事。 在你的情况下,你可能需要包装的是绘制形状所需的信息。

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

我们有客户喜欢以不同方式input形状的多个视图的原因。 我们的客户基础在那些喜欢以表格formsinput形状参数的用户和喜欢用graphics表示形状的用户之间进行分割。 我们还需要通过最小对话框来访问参数,而不是我们的完整形状input屏幕。 因此有多个视图。

操作形状的命令属于两个类别之一。 他们要么操纵切割path,要么操纵形状参数。 为了操纵形状参数,我们通常要么把它们扔回形状input屏幕,要么显示最小的对话框。 重新计算形状,并将其显示在相同的位置。

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

ResizePath RotatePath,MovePath SplitPath等。

当我们需要添加新的function时,我们添加另一个命令对象,在右侧UI屏幕中find菜单,键盘短或工具栏button槽,并设置UI对象来执行该命令。

例如

  CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath 

要么

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

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

你会发现很多可以通过将命令捆绑到一起来处理。 不过,我提醒说,这不是一个黑色或白色的情况。 你仍然会发现某些东西作为原始对象的方法效果更好。 在可能的经验中,我发现我以前在方法中做的80%都能够被转移到命令中。 最后的20%只是简单的工作更好的对象。

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

将命令绑定到Command对象有助于实现这个目标,而不是对封装理想的奴隶般的奉献。 镜像path命令对象中捆绑了所有需要完成的操作。

不pipe采取什么样的path,Visitor模式当前提供的替代function的实现将不得不“知道”它正在处理的接口的具体实现。 所以没有必要为每个额外的实现添加“访问者”function。 这就是说你正在寻找的是一个更灵活和结构化的方法来创build这个function。

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

我会build议通过抽象工厂创造性的方法来创build访问者function的替代实现。

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

这仍然意味着您必须编写具体的实现来处理新版本的“形状”,但是因为它与形状的界面完全分离,所以您可以改进此解决scheme,而不会破坏与之交互的原始界面和软件。 它充当了围绕IShape实现的一种脚手架。

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

如果你使用Java:是的,它叫做instanceof 。 人们过分害怕使用它。 与访问者模式相比,它通常更快,更直接,而且不会受到第5点的困扰。

如果每个形状的n个形状和行为都不相同,则需要n * m个单独的函数。 把这些都放在同一个class上,对我来说,这似乎是一个可怕的主意,给你一些上帝的对象。 所以它们应该被IShape分组,通过在IShape接口中放置m个函数,每个操作一个,或者通过操作(通过使用访问者模式)分组,每个操作/访问者放置n个函数,每个IShape一个类。

当您添加新的IShape或添加新的操作时,您必须更新多个类,但是没有办法绕过它。


如果你正在寻找每一个操作来实现一个默认的IShape函数,那么这将解决你的问题,在丹尼尔马丁的答案: 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); } 
Interesting Posts