Вариация по шаблону посетителя: почему бы не переместить 2-ую отправку в метод «посещения» посетителя? - PullRequest
3 голосов
/ 05 ноября 2010

Intro

Судя по всему, я всю свою жизнь программиста делал "неортодоксальный" шаблон Visitor.

Да, я отправляю в метод посещения конкретного составного элемента из Visitor's Visit method.

Я думаю, что так я это и выучил, но сейчас я не могу найти никаких примеров, и источник, из которого я узнал, пропал.

Теперь, столкнувшись сНеопровержимым доказательством того, что отправка конкретного элемента входит в метод составного элемента Accept, мне интересно, имеет ли способ, которым я это делал, хоть какое-то преимущество.Два преимущества, которые мне кажутся:

  1. У меня есть единственное место, чтобы принять решение о том, как отправлять: базовый посетитель.
  2. Я могу добавлять новые типы составных элементов,и пусть основной посетитель игнорирует их, однако производный посетитель может переопределить Visit для их обработки.

Примеры

Вот базовая модель Composite / Visitor:

// "Unorthodox" version
public class BaseVisitor 
{
    public virtual void Visit(CompositeElement e)
    {
         if(e is Foo)
         {
             VisitFoo((Foo)e);
         }
         else if(e is Bar)
         {             
             VisitBar((Bar)e);
         }
         else
         {
             VisitUnknown(e);
         }
    }

    protected virtual void VisitFoo(Foo foo) { }
    protected virtual void VisitBar(Bar bar) { }
    protected virtual void VisitUnknown(CompositeElement e) { }
} 

public class CompositeElement 
{
    public virtual void Accept(BaseVisitor visitor) { } 
}

public class Foo : CompositeElement { }
public class Bar : CompositeElement { }

Обратите внимание, что класс посетителя теперь отвечает за 2-ю типовую отправку, а не каноническую версию, где, например, за нее будет отвечать Foo и будет иметь:

// Canonical visitor pattern 2nd dispatch
public override void Accept(BaseVisitor visitor)
{
    visitor.VisitFoo(this);
}

Теперь для защиты ...

Преимущество 1

Допустим, мы хотим добавить новый тип CompositeElement:

public class Baz : CompositeElement { }

Для размещения этого новоготип элемента в модели посетителя, мне просто нужно внести изменения в класс BaseVisitor:

public class BaseVisitor 
{  
    public virtual void Visit(CompositeElement e)
    {
        // Existing cases elided...
        else if(e is Baz)
        {
            VisitBaz((Baz)e);
        }
    }

    protected virtual void VisitBaz(Foo foo) { }
}

По общему признанию, это небольшая проблема, но она упрощает обслуживание (т. е. если вы неЯ думаю, что большой if или switch Statements).

Advantage 2

Допустим, мы хотим расширить составную часть в отдельный пакет.Мы можем приспособить это, не изменяя BaseVisitor:

public class ExtendedVisitor : BaseVisitor
{
    public override Visit(CompositeElement e)
    {
        if(e is ExtendedElement)
        {
            VisitExtended((ExtendedElement)e);
        }
        else
        {
            base.Visit(e);
        }            
    }

    protected virtual void VisitExtended(ExtendedElement e) { }
}

public class ExtendedCompositeElement : CompositeElement { }

Наличие этой структуры позволяет нам сломать зависимость BaseVisitor, нам нужно иметь VisitExtended для размещения расширенных типов CompositeElement.

Заключение

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

Пожалуйста, взвесите свои мысли о минусах.

Ответы [ 5 ]

11 голосов
/ 05 ноября 2010

Основная причина, по которой шаблон посетителя определяется в книге GoF как таковой, заключается в том, что в C ++ не было никакой формы идентификации типов во время выполнения (RTTI). Они использовали «двойную диспетчеризацию», чтобы заставить целевые объекты сказать им, какой у них был тип. Довольно круто, но невероятно сложно описать трюк.

Основное различие между тем, что вы описываете, и шаблоном GoF Visitor (как вы упоминаете) заключается в том, что у вас есть явный метод "dispatch" - метод "visit", который проверяет тип аргумента и отправляет его явному visitFoo методы, visitBar и др.

Шаблон GoF Visitor использует объекты данных для выполнения диспетчеризации, предоставляя метод «accept», который поворачивает и передает «this» обратно посетителю, разрешая правильный метод.

Чтобы сложить все это в одном месте, базовый шаблон GoF выглядит так (я парень по Java, поэтому, пожалуйста, извините код Java вместо C # здесь)

public interface Visitor {
    void visit(Type1 value1);
    void visit(Type2 value2);
    void visit(Type3 value3);
}

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

и ваши объекты данных все необходимо реализовать метод "accept":

public class Type1 {
    public void accept(Visitor v) {
        v.visit(this);
    }
}

Примечание. Большая разница между этим и тем, что вы упомянули для версии GoF, заключается в том, что мы можем использовать перегрузку метода, поэтому имя метода "посещения" остается согласованным. Это позволяет каждому объекту данных иметь идентичную реализацию «accept», что снижает вероятность опечатки

Каждый тип нуждается в одинаковом коде метода. «This» в методе accept приводит к тому, что компилятор разрешает правильный метод посещения.

После этого вы можете реализовать интерфейс Visitor, как захотите.

Обратите внимание, что добавление нового типа (например, Type4) в тот же или другой пакет потребует меньше изменений, чем вы описали. Если в том же пакете мы добавим метод в интерфейс Visitor (и каждую реализацию), но вам не нужен метод dispatch.

Это говорит ...

  • Реализация GoF требует взаимодействия / модификации объектов данных. Это главное, что мне не нравится в этом (кроме попытки описать это кому-то, что может быть довольно болезненным. У многих людей возникают проблемы с концепцией «двойной отправки»). Я предпочитаю хранить свои данные и то, что я собираюсь делать с ними отдельно - подход типа MVC.
  • Как ваша реализация, так и реализация GoF требуют изменения кода для добавления новых типов - это может нарушить существующие реализации посетителей
  • Ваша реализация и реализация GoF являются статическими; «что делать» для определенных типов нельзя изменить во время выполнения
  • Теперь у нас есть RTTI на языках, которые используются наиболее часто

Кстати, я преподаю Шаблоны проектирования в Джонсе Хопкинсе, и я бы хотел порекомендовать вам приятный динамический подход.

Начните с более простого однообъектного интерфейса Visitor:

public interface Visitor<T> {
    void visit(T type);
}

Затем создайте VisitorRegistry

public class VisitorRegistry {
    private Map<Class<?>, Visitor<?>> visitors = new HashMap<Class<?>, Visitor<?>>();
    public <T> void register(Class<T> clazz, Visitor<T> visitor) {
        visitors.put(clazz, visitor);
    }
    public <T> void visit(T thing) {
        // needs error checks, and possibly "walk up" to check supertypes if direct type not found
        // also -- can provide default action to perform - maybe register using Void.class?
        @SuppressWarnings("unchecked")
        Visitor<T> visitor = (Visitor<T>) visitors.get(thing.getClass());
        visitor.visit(thing);
    }
}

Вы бы использовали это как

VisitorRegistry registry = new VisitorRegistry();
registry.register(Person.class, new Visitor<Person>() {
    @Override public void visit(Person person) {
        System.out.println("I see " + person.getName());
    }});
// register other types similarly

// walk the data however you would...
for (Object thing : things) {
    registry.visit(thing);
}

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

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

Надеюсь, это поможет!

3 голосов
/ 06 ноября 2010

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

// acyclic version 
public interface IBaseVisitor { }
public interface IBaseVisitor<T> : IBaseVisitor where T : CompositeElement {
  void Visit(T e) { }
}
public class CompositeElement {
  public virtual void Accept(IBaseVisitor visitor) { }
}
public class Foo : CompositeElement {
  public override void Accept(IBaseVisitor visitor) {
    if (visitor is IBaseVisitor<Foo>) {
      ((IBaseVisitor<Foo>)visitor).Visit(this);
    }
  }
}
public class Bar : CompositeElement {
  public override void Accept(IBaseVisitor visitor) {
    if (visitor is IBaseVisitor<Bar>) {
      ((IBaseVisitor<Bar>)visitor).Visit(this);
    }
  }
}

Ваши реальные посетители могут выбирать, какие подклассы они посещают:

public class MyVisitor : IBaseVisitor<Foo>, IBaseVisitor<Bar> {
  public void Visit(Foo e) { }
  public void Visit(Bar e) { }
}

Это "ациклический", потому что у него нет циклической зависимости между типами в вашей иерархии и методами в посетителе.

2 голосов
/ 05 ноября 2010

Помимо недостатков, о которых вы уже упоминали (производительность и необходимость поддерживать большой оператор switch), другая проблема заключается в том, что при использовании шаблона GoF Visitor добавление нового подкласса CompositeElement заставит вас написать обработчик для него или вашего код даже не скомпилируется. С вашим подходом, с другой стороны, было бы легко добавить новые подклассы CompositeElement и забыть обновить соответствующие операторы переключения посетителей.

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

1 голос
/ 05 ноября 2010

Некоторые языки также имеют ограничения, которые делают это очень непривлекательным. У Java нет множественного наследования, кроме как через интерфейсы. Требование, чтобы каждый составной элемент и посетитель происходили из одного и того же базового класса, привело бы к иерархии грубых типов.

т.е. ваш путь не позволяет Visitor и CompositeElement быть интерфейсами.

0 голосов
/ 30 января 2011

Мне не нравятся реализации с visitA, visitB, visitWh чем угодно, acceptA, acceptB, acceptWh независимо, потому что этот подход подразумевает, что вы будете нарушать интерфейсы каждый раз, когда добавляете класс в свою иерархию.

Пожалуйста, посмотрите на статью, которую я написал об этом .

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

...