Альтернатива шаблону посетителя? - PullRequest
50 голосов
/ 12 июня 2009

Я ищу альтернативу шаблону посетителя. Позвольте мне сосредоточиться на паре подходящих аспектов шаблона, пропуская при этом неважные детали. Я буду использовать пример формы (извините!):

  1. У вас есть иерархия объектов, которые реализуют интерфейс IShape
  2. У вас есть ряд глобальных операций, которые должны выполняться над всеми объектами в иерархии, например, Draw, WriteToXml и т. Д. ...
  3. Соблазнительно погрузиться и добавить методы Draw () и WriteToXml () в интерфейс IShape. Это не обязательно хорошо - всякий раз, когда вы хотите добавить новую операцию, которая должна быть выполнена для всех фигур, каждый класс, полученный из IShape, должен быть изменен
  4. Реализация посетителя для каждой операции, то есть посетителя Draw или посетителя WirteToXml, инкапсулирует весь код этой операции в одном классе. Добавление новой операции - это вопрос создания нового класса посетителя, который выполняет эту операцию для всех типов IShape
  5. Когда вам нужно добавить новый производный от IShape класс, у вас, по сути, возникает та же проблема, что и в 3 - все классы посетителей должны быть изменены, чтобы добавить метод для обработки нового производного от IShape типа

В большинстве мест, где вы читаете о шаблоне посетителя, указывается, что пункт 5 является главным критерием для работы шаблона, и я полностью согласен. Если число производных от IShape классов фиксировано, то это может быть довольно элегантным подходом.

Итак, проблема в том, что добавляется новый производный от IShape класс - каждая реализация посетителя должна добавлять новый метод для обработки этого класса. Это, в лучшем случае, неприятно, а в худшем - невозможно, и показывает, что эта схема на самом деле не рассчитана на такие изменения.

Итак, вопрос в том, сталкивался ли кто-нибудь с альтернативными подходами к решению этой ситуации?

Ответы [ 8 ]

15 голосов
/ 12 июня 2009

Возможно, вы захотите взглянуть на шаблон стратегии . Это по-прежнему дает вам разделение проблем, и в то же время вы можете добавлять новые функции без необходимости изменения каждого класса в вашей иерархии.

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

Более подробная информация в этой связанной дискуссии:

Должен ли объект записывать себя в файл или другой объект воздействует на него для выполнения операций ввода-вывода?

13 голосов
/ 12 июня 2009

Существует «Шаблон посетителя со значением по умолчанию», в котором вы делаете шаблон посетителя как обычно, но затем определяете абстрактный класс, который реализует ваш класс 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);
}

Это позволяет вам определять посетителей, которые определяют их поведение на любом уровне специфичности, который вы пожелаете.

К сожалению, невозможно избежать каких-либо действий, чтобы указать, как посетители будут вести себя с новым классом - либо вы можете заранее установить значение по умолчанию, либо нет. (См. Также вторую панель этого мультфильма )

6 голосов
/ 12 июня 2009

Я поддерживаю программное обеспечение CAD / CAM для металлорежущего станка. Так что у меня есть некоторый опыт с этими проблемами.

Когда мы впервые преобразовали наше программное обеспечение (оно было впервые выпущено в 1985 году!) В объектно-ориентированный дизайн, я сделал то, что вам не нравится. Объекты и интерфейсы имели Draw, WriteToFile и т. Д. Обнаружение и чтение шаблонов проектирования в середине процесса преобразования очень помогло, но все еще было много неприятных запахов кода.

В конце концов я понял, что ни один из этих типов операций на самом деле не касался объекта. Скорее, различные подсистемы, необходимые для выполнения различных операций. Я справился с этим с помощью так называемого объекта Passive View Command и четко определенного интерфейса между уровнями программного обеспечения.

Наше программное обеспечение в основном имеет такую ​​структуру

  • Формы, реализующие различные Формы Интерфейс. Эти формы являются оболочкой, передающей события на уровень пользовательского интерфейса.
  • Уровень пользовательского интерфейса, который получает события и манипулирует формами через интерфейс Form.
  • Уровень пользовательского интерфейса будет выполнять команды, которые все реализуют интерфейс команд
  • Объект пользовательского интерфейса имеет свои собственные интерфейсы, с которыми команда может взаимодействовать.
  • Команды получают необходимую им информацию, обрабатывают ее, манипулируют моделью и затем отчитываются перед объектами пользовательского интерфейса, которые затем делают все необходимое с формами.
  • Наконец, модели, которые содержат различные объекты нашей системы. Как программы форм, траектории резки, раскройный стол и листы металла.

Таким образом, рисование обрабатывается на уровне пользовательского интерфейса. У нас разное программное обеспечение для разных машин. Таким образом, в то время как все наши программы используют одну и ту же модель и используют много одинаковых команд. Они занимаются такими вещами, как рисование совсем по-другому. Например, раскройный стол ничем не отличается от станка с маршрутизатором от станка с плазменной горелкой, несмотря на то, что оба они по сути являются гигантским плоским столом X-Y. Это потому, что, как и в случае с автомобилями, эти две машины построены достаточно по-разному, так что для клиента существует визуальная разница.

Что касается фигур, мы делаем следующее

У нас есть программы форм, которые создают траектории резки по введенным параметрам. Путь резки знает, какая программа обработки формы. Однако траектория резки не является формой. Это просто информация, необходимая для рисования на экране и разрезания фигуры. Одна из причин такого дизайна заключается в том, что траектории резки могут быть созданы без программы фигур, когда они импортируются из внешнего приложения.

Этот дизайн позволяет нам отделить дизайн траектории резания от дизайна формы, которые не всегда одно и то же. В вашем случае, вероятно, все, что вам нужно, - это информация, необходимая для рисования фигуры.

Каждая программа-фигура имеет несколько представлений, реализующих интерфейс IShapeView. Через интерфейс IShapeView программа shape может сообщить общей форме формы, которая у нас есть, как настроить себя, чтобы показать параметры этой формы. Универсальная форма формы реализует интерфейс IShapeForm и регистрируется в объекте ShapeScreen. Объект ShapeScreen регистрируется в нашем объекте приложения. Представления формы используют любой экран формы, который регистрируется в приложении.

Причина множества просмотров, которые есть у наших клиентов, которые любят вводить фигуры по-разному. Наша клиентская база разделена пополам между теми, кто любит вводить параметры формы в форме таблицы, и теми, кто любит вводить с графическим представлением формы перед ними. Нам также необходимо время от времени получать доступ к параметрам через минимальное диалоговое окно, а не через экран ввода полной формы. Отсюда и несколько просмотров.

Команды, управляющие формами, попадают в одну из двух категорий. Либо они управляют траекторией резки, либо они управляют параметрами формы. Чтобы манипулировать параметрами формы в целом, мы либо возвращаем их обратно на экран ввода формы, либо показываем минимальный диалог. Пересчитайте фигуру и отобразите ее в том же месте.

Для пути резки мы объединяли каждую операцию в отдельный объект команды. Например, у нас есть объекты команды

ResizePath RotatePath MovePath SplitPath и т. д.

Когда нам нужно добавить новую функциональность, мы добавляем еще один объект команды, на правой стороне экрана пользовательского интерфейса находим меню, короткую клавиатуру или слот для кнопки панели инструментов и настраиваем объект интерфейса для выполнения этой команды.

Например

   CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath

или

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

В обоих случаях объект Command MirrorPath связан с требуемым элементом пользовательского интерфейса. В методе выполнения MirrorPath есть весь код, необходимый для зеркального отображения пути на определенной оси. Скорее всего, команда будет иметь собственный диалог или использовать один из элементов пользовательского интерфейса, чтобы спросить пользователя, какую ось отразить. Ничто из этого не делает посетителя или добавляет метод к пути.

Вы обнаружите, что многое можно обработать путем объединения действий в команды. Однако я предупреждаю, что это не черно-белая ситуация. Вы по-прежнему обнаружите, что определенные вещи лучше работают как методы для исходного объекта. По своему опыту я обнаружил, что, возможно, 80% того, что я делал в методах, можно было перенести в команду. Последние 20% просто работают лучше на объекте.

Теперь некоторым это может не понравиться, потому что кажется, что оно нарушает инкапсуляцию. От сохранения нашего программного обеспечения в качестве объектно-ориентированной системы в течение последнего десятилетия я должен сказать, что САМОЕ важное долгосрочное действие, которое вы можете сделать, это четко задокументировать взаимодействия между различными уровнями вашего программного обеспечения и между различными объектами.

Объединение действий в объекты Command помогает достичь этой цели лучше, чем рабская преданность идеалам инкапсуляции. Все, что нужно сделать, чтобы отразить путь, объединено в объекте команды Mirror Path.

4 голосов
/ 23 декабря 2015

Шаблон дизайна посетителя - это обходной путь, а не решение проблемы. Короткий ответ будет сопоставление с образцом .

2 голосов
/ 12 июня 2009

Независимо от того, по какому пути вы идете, реализация альтернативной функциональности, которая в настоящее время предоставляется шаблоном Visitor, должна будет «кое-что знать» о конкретной реализации интерфейса, над которым он работает. Таким образом, нельзя обойти стороной тот факт, что вам придется писать дополнительные функции «посетитель» для каждой дополнительной реализации. Тем не менее, то, что вы ищете, является более гибким и структурированным подходом к созданию этой функциональности.

Вам необходимо отделить функциональность посетителя от интерфейса фигуры.

Я бы предложил креационистский подход через абстрактную фабрику для создания замещающих реализаций для функциональности посетителей.

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.

1 голос
/ 26 июня 2015

Если у вас есть n IShape s и m операций, которые ведут себя по-разному для каждой фигуры, то вам требуется n * m отдельных функций. Поместить все это в один класс кажется мне ужасной идеей, давая вам какой-то объект Бога. Поэтому они должны быть сгруппированы либо по IShape, поместив m функций, по одной для каждой операции, в интерфейс IShape, либо сгруппированы по операциям (используя шаблон посетителя), добавив n функций, по одной для каждой * 1004. * в каждой операции / класс посетителя.

Вам нужно либо обновить несколько классов, когда вы добавляете новую 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);
}
1 голос
/ 13 сентября 2013

Если вы используете Java: Да, он называется instanceof. Люди слишком напуганы, чтобы использовать это. По сравнению с шаблоном посетителей, он, как правило, быстрее, проще и не страдает от пункта № 5.

0 голосов
/ 09 ноября 2018

Я на самом деле решил эту проблему, используя следующую схему. Я не знаю, есть ли у него имя или нет!

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 и добавить его в список ящиков ShapeDrawingClient!

...