Подтвердить что ты не робот

Альтернатива шаблону посетителя?

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Дополнительная информация находится в этом связанном обсуждении:

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

Ответ 2

Существует шаблон "Visitor Pattern With Default", в котором вы обычно используете шаблон посетителя, а затем определяете абстрактный класс, который реализует ваш класс 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);
}

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

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

Ответ 3

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

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

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

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

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

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

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

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

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

Каждая формальная программа имеет несколько представлений, реализующих интерфейс IShapeView. Через интерфейс IShapeView формальная программа может рассказать об общей форме формы, которую мы имеем, чтобы настроить себя, чтобы показать параметры этой формы. Форма формы формы реализует интерфейс 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 the Path, связано с объектом Command Mirror Path.

Ответ 4

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

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

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

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

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

Вам необходимо обновить несколько классов при добавлении нового IShape или при добавлении новой операции, нет никакого способа обойти это.


Если вы ищете каждую операцию для реализации функции IShape по умолчанию, это решит вашу проблему, как в ответе Даниэля Мартина: fooobar.com/info/128518/..., хотя я бы, вероятно, использовал перегрузку:

interface IVisitor
{
    void visit(IShape shape);
    void visit(Rectangle shape);
    void visit(Circle shape);
}

interface IShape
{
    //...
    void accept(IVisitor visitor);
}