访客模式的替代方案?

Ste*_*teg 51 oop design-patterns visitor

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

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

你阅读有关访客模式的大多数地方表明第5点几乎是模式工作的主要标准,我完全同意.如果修改了IShape派生类的数量,那么这可能是一种非常优雅的方法.

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

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

Dir*_*mar 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

对象是应该将自己写入文件,还是应该使用其他对象来执行I/O?

  • 我认为这里存在一个根本性的冲突 - 如果你有很多东西和一堆可以对这些东西执行的动作,那么添加一个新东西意味着你必须定义所有动作对它的影响,反之亦然 - 那里没有逃避这一点.访问者提供了一种非常简单,优雅的方式来添加新操作,但代价是难以添加新内容.如果必须放宽这种约束,你必须付钱.我希望可能有一个解决方案具有访客的优雅和简洁,但正如我所怀疑的,我不认为一个存在...续... (3认同)

Dan*_*tin 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);
}
Run Code Online (Sandbox Code Playgroud)

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

不幸的是,没有办法避免做一些事情来指定访问者对新课程的行为方式 - 要么你可以提前设置默认,要么你不能.(另见本卡通的第二个小组)


RS *_*ley 6

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

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

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

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

  • 表单实现各种表单接口.这些表单是将事件传递给UI层的shell.
  • 通过Form界面接收事件和操作表单的UI层.
  • 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
Run Code Online (Sandbox Code Playgroud)

要么

   CuttingTableScreen.Toolbar("Edit Path").AddButton Application.Icons("MirrorPath"),"Mirror Path", New MirrorPath
Run Code Online (Sandbox Code Playgroud)

在这两个实例中,Command对象MirrorPath都与所需的UI元素相关联.在MirrorPath的execute方法中,镜像特定轴中的路径所需的所有代码.可能该命令将拥有它自己的对话框或使用其中一个UI元素来询问用户镜像哪个轴.这些都不是制作访问者,也不是在路径中添加方法.

您会发现通过将操作捆绑到命令中可以处理很多事情.但是我提醒说这不是黑色或白色的情况.您仍然会发现某些东西在原始对象上的方法更好.在可能的经验中,我发现可能80%的方法在方法中被移动到命令中.最后的20%只是在对象上更好地工作.

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

将操作捆绑到Command对象有助于实现这一目标,而不是对封装理想的盲目奉献.镜像路径命令对象中捆绑了镜像路径所需的一切.