Использование шаблона посетителя с большой иерархией объектов

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

Я в основном вижу два варианта:

  • Сохраняйте тот же дизайн, добавляя новый метод для моей операции к каждому производному классу, за счет удобства обслуживания (не вариант, ИМХО)
  • Используйте «истинный» шаблон Visitor за счет расширяемости (не вариант, так как я ожидаю, что в будущем будет реализовано больше реализаций ...), с более чем 50-ю перегрузками метода Visit, каждая из которых соответствует определенной реализации ?

Вопрос

Вы бы порекомендовали использовать шаблон Visitor? Есть ли другой шаблон, который может помочь решить эту проблему?

Т. Фабр
источник
1
Возможно, цепь декораторов будет более подходящей?
MattDavey
некоторые вопросы: как эти реализации отличаются? какова структура иерархии? и всегда ли это одна и та же структура? вам всегда нужно пройти через структуру в том же порядке?
JK.
@MattDavey: так что вы бы порекомендовали иметь один декоратор для реализации и операции?
Т. Фабр
2
@ T.Fabre трудно сказать. Существует более 50 разработчиков MyInterface. Все ли эти классы имеют уникальную реализацию DoSomethingи DoSomethingElse? Я не вижу, где ваш класс посетителей фактически пересекает иерархию - facadeв данный момент это больше похоже на ...
MattDavey
и какая версия C # это. у тебя есть лямбды? или linq? в вашем распоряжении
JK.

Ответы:

13

В течение более 10 лет я использовал шаблон посетителей для представления деревьев выражений в шести крупномасштабных проектах на трех языках программирования, и я очень доволен результатом. Я нашел несколько вещей, которые сделали применение шаблона намного проще:

Не используйте перегрузки в интерфейсе посетителя

Поместите тип в имя метода, т.е. используйте

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

Это позволило бы им создавать свои собственные реализации IExpressionи IVisitor"понимать" их выражения, используя информацию о типах во время выполнения при реализации их VisitExpressionметода catch-all .

Обеспечить реализацию IVisitorинтерфейса по умолчанию, ничего не делающую

Это позволило бы пользователям, которым необходимо иметь дело с подмножеством типов выражений, быстрее создавать своих посетителей и сделать свой код невосприимчивым к добавлению вами новых методов IVisitor. Например, написание посетителя, который собирает все имена переменных из ваших выражений, становится легкой задачей, и код не сломается, даже если вы добавите несколько новых типов выражений к себе IVisitorпозже.

dasblinkenlight
источник
2
Можете ли вы уточнить, почему вы говорите Do not use overloads in the interface of the visitor?
Стивен Эверс
1
Можете ли вы объяснить, почему вы не рекомендуете использовать перегрузки? Я где-то читал (на oodesign.com, на самом деле), что на самом деле не имеет значения, использую ли я перегрузки или нет. Есть ли какая-то конкретная причина, почему вы предпочитаете этот дизайн?
Т. Фабр
2
@ T.Fabre Это не имеет значения с точки зрения скорости, но это имеет значение с точки зрения читабельности. Разрешение метода на двух из трех языков, на которых я это реализовал ( Java и C #), требует шага во время выполнения для выбора среди возможных перегрузок, что делает код с огромным количеством перегрузок немного сложнее для чтения. Рефакторинг кода также становится проще, потому что выбор метода, который вы хотите изменить, становится тривиальной задачей.
dasblinkenlight
@SnOrfus Пожалуйста, смотрите мой ответ T.Fabre выше.
dasblinkenlight
@dasblinkenlight C # теперь предлагает динамическое решение, позволяющее среде выполнения решать, какой перегруженный метод следует использовать (не во время компиляции). Есть ли еще причина, почему бы не использовать перегрузку?
Tintenfiisch