在大型对象层次结构中使用访问者模式


12

语境

我一直在使用对象层次结构(一个表达式树)使用“伪”访问者模式(伪,因为它不使用双调度):

 public interface MyInterface
 {
      void Accept(SomeClass operationClass);
 }

 public class MyImpl : MyInterface 
 {
      public void Accept(SomeClass operationClass)
      {   
           operationClass.DoSomething();
           operationClass.DoSomethingElse();
           // ... and so on ...
      }
 }

由于MyInterface的实现数量很多(约50个或更多),而且我不需要添加额外的操作,因此该设计非常舒适。

每个实现都是唯一的(它是一个不同的表达式或运算符),而某些实现是组合的(即,将包含其他运算符/叶节点的运算符节点)。

遍历当前是通过在树的根节点上调用Accept操作来执行的,后者依次在其每个子节点上调用Accept,依次类推...依次类推...

但是现在是时候需要添加一个新操作了,例如漂亮的打印:

 public class MyImpl : MyInterface 
 {
      // Property does not come from MyInterface
      public string SomeProperty { get; set; }

      public void Accept(SomeClass operationClass)
      {   
           operationClass.DoSomething();
           operationClass.DoSomethingElse();
           // ... and so on ...
      }

      public void Accept(SomePrettyPrinter printer)
      {
           printer.PrettyPrint(this.SomeProperty);
      }
 }    

我基本上看到两个选择:

  • 保持相同的设计,在每个派生类中为我的操作添加一个新方法,但以可维护性为代价(恕我直言,这不是一种选择)
  • 使用“真正的”访问者模式,但要牺牲可扩展性(这不是一个选择,我希望这会带来更多的实现...),并且Visit方法有大约50多个重载,每个重载都匹配一个特定的实现?

您会使用“访客”模式来建议使用吗?还有其他模式可以帮助解决此问题吗?


1
也许装饰连锁店会更合适?
MattDavey 2012年

一些问题:这些实现有何不同?层次结构是什么?它总是相同的结构吗?您是否总是需要以相同的顺序遍历结构?
jk。

@MattDavey:所以您会建议每个实现和操作使用一个装饰器?
T. Fabre 2012年

2
@ T.Fabre很难说。有50+实现者MyInterface..做所有这些类都有一个独特的实施DoSomethingDoSomethingElse?我没有看到你的访问者类实际上遍历层次-它看起来更像是facade此刻..
MattDavey

也是什么版本的C#。你有lambdas吗?还是linq?在您的处置
JK。

Answers:


13

在过去的十多年中,我一直使用访问者模式来代表三种树种编程语言的六个大型项目上的表达树,我对结果感到非常满意。我发现有几件事使应用模式更加容易:

不要在访问者的界面中使用重载

将类型放入方法名称,即使用

IExpressionVisitor {
    void VisitPrimitive(IPrimitiveExpression expr);
    void VisitComposite(ICompositeExpression expr);
}

而不是

IExpressionVisitor {
    void Visit(IPrimitiveExpression expr);
    void Visit(ICompositeExpression expr);
}

在您的访问者界面中添加“未知未知”方法。

这将使无法修改您代码的用户成为可能:

IExpressionVisitor {
    void VisitPrimitive(IPrimitiveExpression expr);
    void VisitComposite(ICompositeExpression expr);
    void VisitExpression(IExpression expr);
};

这将使他们构建自己的实现,IExpressionIVisitor通过在其包罗万象的VisitExpression方法的实现中使用运行时类型信息来“理解”其表达式。

提供默认的虚无IVisitor接口实现

这样一来,需要处理表达式类型子集的用户就可以更快地构建其访问者,并使他们的代码不受您的干扰,从而向中添加更多方法IVisitor。例如,编写访问者以从表达式中获取所有变量名称成为一件容易的事,即使以后添加一堆新的表达式类型,代码也不会中断IVisitor


2
你能说清楚为什么Do not use overloads in the interface of the visitor吗?
史蒂文·埃弗斯2012年

1
您能解释为什么不使用重载吗?我在某个地方(实际上在oodesign.com上)读到,无论是否使用重载都无关紧要。您偏爱该设计有任何特定原因吗?
T. Fabre 2012年

2
@ T.Fabre速度无关紧要,但可读性却很重要。我在其中实现了这三种语言(Java和C#)中的两种,方法解析需要一个运行时步骤来选择潜在的重载,这使得具有大量重载的代码更加难以阅读。重构代码也变得更加容易,因为选择要修改的方法变得很简单。
dasblinkenlight 2012年

@SnOrfus请在上方查看我对T.Fabre的回答。
dasblinkenlight 2012年

@dasblinkenlight C#现在提供了动态功能,使运行时可以决定应使用哪种重载方法(而不是在编译时)。还有什么理由不使用重载?
Tintenfiisch
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.