2009-06-12 5 views
44

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

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

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

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

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

+0

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

+6

Большой вопрос. Я просто хотел предоставить контрапункт. Иногда ваша проблема с (5) может быть хорошей. Я использую шаблон посетителя, когда у меня есть некоторые функции, которые необходимо обновить при определении нового подтипа IShape. У меня есть интерфейс IShapeVisitor, который определяет, какие методы необходимы. Пока этот интерфейс обновляется новым подтипом, мой код не создается до тех пор, пока не будет обновлена ​​критическая функциональность. В некоторых ситуациях это может быть очень полезно. – oillio

+1

Я согласен с @oillio, но тогда вы также можете применить его как абстрактный метод на IShape. То, что шаблон Visitor покупает вас на чистом языке OO, - это локальность функции (по сравнению с локальностью класса) и тем самым разделение проблем.В любом случае, использование шаблона посетителя должно явно нарушаться во время компиляции, когда вы хотите принудительно добавить новые типы, которые необходимо тщательно пересмотреть! –

ответ

13

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

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

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

Should an object write itself out to a file, or should another object act on it to perform I/O?

+0

Я отметил это как ответ на свой вопрос, поскольку я думаю, что это или некоторые незначительные вариации на нем, вероятно, вписываются в то, что я хочу делать. Для кого-то, кого интересует, я добавил «ответ», который описывает некоторые из моих мыслей о проблеме. – Steg

+0

ok - изменил свое мнение об ответе. Я попытаюсь сконденсировать это в комментарий (следующий) – Steg

+2

Я думаю, что там является фундаментальным конфликтом здесь - если у вас есть куча вещей и множество действий, которые могут быть выполнены над этими вещами, то добавление новой вещи означает, что вы должны определить влияние всех действий на нее и наоборот - нет выхода это. Посетитель предоставляет простой, элегантный способ добавления новых действий за счет затруднения добавления новых вещей. Если это ограничение должно быть расслабленным, вам придется заплатить. Я надеялся, что может быть решение, в котором есть элегантность и простота посетителя, но, как я подозревал, я не думаю, что существует один ... cont'd ... – Steg

13

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

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

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

6

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

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

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

Наша программа построена в основном так

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

Так что чертеж обрабатывается в слое пользовательского интерфейса. У нас есть разные программы для разных машин. Поэтому, хотя все наше программное обеспечение использует одну и ту же модель и повторно используют многие из тех же команд. Они обрабатывают такие вещи, как рисование очень разных. Например, стол для резки является разным для машины маршрутизатора по сравнению с машиной с использованием плазменного факела, несмотря на то, что оба они представляют собой гигантский плоский стол 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 

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

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

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

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

+0

Решение кажется интересным, но я был бы признателен, если бы вы могли сослаться на пример кода для такого решения, чтобы лучше понять концепцию. –

2

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

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

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

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.

+0

в AbstractWorkerFactory вам еще нужно делать экземпляр –

1

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

+0

быстрее? Проверьте [это] (http://alexshabanov.com/2011/12/03/instanceof-vs-visitor/). – ntohl

+0

@ntohl В тестах, которые я сделал (на Java 8, обратите внимание, что тест использовал Java 6) instanceof был быстрее, поэтому я предполагаю, что скорость относительной скорости этих двух должна варьироваться в зависимости от тонких деталей. – Andy

1

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

Вам необходимо обновить несколько классов при добавлении нового 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); 
} 
3

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

Смежные вопросы