Używanie wzorca gościa z hierarchią dużych obiektów

12

Kontekst

Używałem z hierarchią obiektów (drzewa wyrażeń) wzorca „pseudo” użytkownika (pseudo, ponieważ w nim nie stosuje podwójnej wysyłki):

 public interface MyInterface
 {
      void Accept(SomeClass operationClass);
 }

 public class MyImpl : MyInterface 
 {
      public void Accept(SomeClass operationClass)
      {   
           operationClass.DoSomething();
           operationClass.DoSomethingElse();
           // ... and so on ...
      }
 }

Ten projekt był jednak wątpliwy, całkiem wygodny, ponieważ liczba implementacji MyInterface jest znacząca (~ 50 lub więcej) i nie musiałem dodawać dodatkowych operacji.

Każda implementacja jest unikalna (jest to inne wyrażenie lub operator), a niektóre są kompozytami (tj. Węzłami operatora, które będą zawierać inne węzły operatora / liści).

Przechodzenie jest obecnie wykonywane przez wywołanie operacji Accept w węźle głównym drzewa, który z kolei wywołuje Accept na każdym z jego węzłów potomnych, który z kolei ... i tak dalej ...

Ale nadszedł czas, aby dodać nową operację , na przykład ładny druk:

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

Zasadniczo widzę dwie opcje:

  • Zachowaj ten sam projekt, dodając nową metodę mojej operacji do każdej klasy pochodnej, kosztem konserwacji (nie opcja, IMHO)
  • Użyj „prawdziwego” wzorca gościa, kosztem rozszerzenia (nie jest to opcja, ponieważ spodziewam się, że po drodze pojawi się więcej implementacji ...), z około 50+ przeciążeniami metody Visit, z których każda odpowiada konkretnej implementacji ?

Pytanie

Czy poleciłbyś użycie wzorca gościa? Czy jest jakiś inny wzorzec, który mógłby pomóc rozwiązać ten problem?

T. Fabre
źródło
1
Może łańcuch dekoratorów byłby bardziej odpowiedni?
MattDavey
kilka pytań: czym różnią się te wdrożenia? Jaka jest struktura hierarchii? i czy zawsze ma taką samą strukturę? czy zawsze musisz przemierzać strukturę w tej samej kolejności?
jk.
@MattDavey: więc poleciłbyś posiadanie jednego dekoratora na wdrożenie i operację?
T. Fabre,
2
@ T.Fabre trudno powiedzieć. Istnieje ponad 50 implementatorów MyInterface… czy wszystkie te klasy mają unikalną implementację DoSomethingi DoSomethingElse? Nie wiem, gdzie faktycznie twoja klasa gości przemierza hierarchię - facadew tej chwili wygląda to bardziej jak
MattDavey
także jaka jest wersja C #. czy masz lambdy? czy linq? do Twojej dyspozycji
jk.

Odpowiedzi:

13

Używam wzorca odwiedzających do reprezentowania drzew wyrażeń przez ponad 10 lat w sześciu dużych projektach w trzech językach programowania i jestem bardzo zadowolony z rezultatu. Znalazłem kilka rzeczy, które znacznie ułatwiły stosowanie wzoru:

Nie używaj przeciążeń w interfejsie gościa

Wpisz typ do nazwy metody, tzn. Użyj

IExpressionVisitor {
    void VisitPrimitive(IPrimitiveExpression expr);
    void VisitComposite(ICompositeExpression expr);
}

zamiast

IExpressionVisitor {
    void Visit(IPrimitiveExpression expr);
    void Visit(ICompositeExpression expr);
}

Dodaj metodę „złap nieznaną” w interfejsie użytkownika.

Umożliwiłoby to użytkownikom, którzy nie mogą modyfikować kodu:

IExpressionVisitor {
    void VisitPrimitive(IPrimitiveExpression expr);
    void VisitComposite(ICompositeExpression expr);
    void VisitExpression(IExpression expr);
};

Pozwoliłoby im to na zbudowanie własnych implementacji IExpressioni IVisitor„zrozumienie” ich wyrażeń poprzez wykorzystanie informacji o typie wykonawczym przy implementacji metody catch-all VisitExpression.

Zapewnij domyślną implementację IVisitorinterfejsu „ nic nie rób”

Pozwoliłoby to użytkownikom, którzy muszą poradzić sobie z podzbiorem typów wyrażeń, szybciej budować użytkowników i uczynić ich kod odpornym na dodawanie kolejnych metod IVisitor. Na przykład napisanie użytkownika, który zbierze wszystkie nazwy zmiennych z wyrażeń, stanie się łatwym zadaniem, a kod się nie złamie, nawet jeśli dodasz kilka nowych typów wyrażeń do swojego IVisitorpóźniejszego.

dasblinkenlight
źródło
2
Czy możesz wyjaśnić, dlaczego mówisz Do not use overloads in the interface of the visitor?
Steven Evers
1
Czy możesz wyjaśnić, dlaczego nie zaleca się używania przeciążeń? Czytałem gdzieś (właściwie na oodesign.com), że tak naprawdę nie ma znaczenia, czy używam przeciążeń, czy nie. Czy jest jakiś konkretny powód, dla którego wolisz ten projekt?
T. Fabre,
2
@ T.Fabre Nie ma znaczenia pod względem prędkości, ale ma znaczenie pod względem czytelności. Rozwiązanie metody w dwóch z trzech języków, w których to zaimplementowałem ( Java i C #), wymaga wykonania w czasie wykonywania wyboru spośród potencjalnych przeciążeń, co sprawia, że ​​kod z ogromną liczbą przeciążeń jest nieco trudniejszy do odczytania. Refaktoryzacja kodu staje się również łatwiejsza, ponieważ wybranie metody, którą chcesz zmodyfikować, staje się banalnym zadaniem.
dasblinkenlight
@SnOrfus Proszę zobaczyć moją odpowiedź na T.Fabre powyżej.
dasblinkenlight
@dasblinkenlight C # oferuje teraz dynamikę pozwalającą środowisku wykonawczemu decydować, która metoda przeciążenia powinna zostać użyta (nie w czasie kompilacji). Czy nadal istnieje powód, dla którego nie należy stosować przeciążenia?
Tintenfiisch