Wzorce: Odwiedzający / Wizytator

Tak mnie wzięło na wzorce projektowe ostatnimi czasy, dzisiaj coś o o odwiedzającym (wizytatorze). Polska Wikipedia mówi o nim tak odwiedzający.
A teraz moimi słowami:
Wzorzec umożliwia przejście po strukturze danych, oraz zebranie jakichś informacji. Gdy zaistnieje potrzeba zaimplementowania nowej funkcjonalności, gdzie pobieranie danych jest zaimplementowane tak samo, ale rodzaj danych będzie się różnić, problem ten rozwiąże się tworząc nową klasę “odwiedzającą” strukturę danych, która zbierze nowe informacje.
Gdyby zastosować taki wzorzec np. do parsowania XML, pierwszy odwiedzający zbiera informacje o wszystkich linkach, które znajdują się w przeglądanym XML. Inna implementacja zlicza po prostu, wszystkie elementy, czy atrybuty takiego XML. W każdym przypadku, następuje przejście po całej strukturze, różnica jest natomiast w wyniku takiej operacji. Na tym właśnie polega “odseparowanie algorytmu od struktury obiektowej na której się operuje”.


Przykładowy kod:

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3.  
  4. namespace Visitor
  5. {
  6.     class SimpleExample
  7.     {
  8.         internal interface IElement
  9.         {
  10.             void Accept(IVisitor aVisitor);
  11.         }
  12.  
  13.         internal interface IVisitor
  14.         {
  15.             void Visit(IElement aElement);
  16.         }
  17.  
  18.         public class ElementClassA : IElement
  19.         {
  20.             public void Accept(IVisitor aVisitor)
  21.             {
  22.                 aVisitor.Visit(this);
  23.             }
  24.         }
  25.         public class ElementClassB : IElement
  26.         {
  27.             public void Accept(IVisitor aVisitor)
  28.             {
  29.                 aVisitor.Visit(this);
  30.             }
  31.         }
  32.  
  33.         public class ELementClassC : ElementClassB
  34.         {
  35.             new public void Accept(IVisitor aVisitor)
  36.             {
  37.                 aVisitor.Visit(this);
  38.             }
  39.         }
  40.  
  41.         public class RootElement : IElement
  42.         {
  43.             private ElementClassA _elementA;
  44.             private List<ElementClassB> _elementsBList;
  45.             private static ELementClassC _elementc = new ELementClassC();
  46.             public void Accept(IVisitor aVisitor)
  47.             {
  48.                 aVisitor.Visit(this);
  49.                 _elementA.Accept(aVisitor);
  50.                 foreach (var elementClassB in _elementsBList)
  51.                 {
  52.                     elementClassB.Accept(aVisitor);
  53.                 }
  54.                 RootElement._elementc.Accept(aVisitor);
  55.             }
  56.  
  57.             public RootElement()
  58.             {
  59.                 _elementA = new ElementClassA();
  60.                 _elementsBList = new List<ElementClassB>();
  61.                 for (int i = 0; i < 10; ++i)
  62.                 {
  63.                     _elementsBList.Add(new ElementClassB());
  64.                 }
  65.  
  66.             }
  67.         }
  68.  
  69.         public class VisitorToString : IVisitor
  70.         {
  71.             public int Count { get; set; }
  72.             public VisitorToString()
  73.             {
  74.                 Count = 0;
  75.             }
  76.  
  77.             public void Visit(IElement aElement)
  78.             {
  79.                 string s = string.Format(“Visited class {0}”, aElement.ToString());
  80.                 Count += s.Length;
  81.                 Console.WriteLine(s);
  82.             }
  83.         }
  84.  
  85.         public class VisitorHashString : IVisitor
  86.         {
  87.             public int Count { get; set; }
  88.             public VisitorHashString()
  89.             {
  90.                 Count = 0;
  91.             }
  92.             public void Visit(IElement aElement)
  93.             {
  94.                 string s = string.Format(“Visited class {0}, hash code {1}”, aElement.ToString(), aElement.GetHashCode());
  95.                 Count++;
  96.                 Console.WriteLine(s);
  97.             }
  98.         }
  99.         
  100.         public static void Main()
  101.         {
  102.             RootElement root = new RootElement();
  103.             VisitorToString vstring = new VisitorToString();
  104.             VisitorHashString vhstring = new VisitorHashString();
  105.             root.Accept(vstring);
  106.             Console.WriteLine(“Total written {0}”,vstring.Count);
  107.             root.Accept(vhstring);
  108.             Console.WriteLine(“Total written {0}”,vhstring.Count);
  109.         }
  110.     }
  111. }

W przykładzie jest dwóch odwiedzających. Jeden zlicza wszystkie literki, które zostały wykorzystane do wyświetlenia informacji o odwiedzanych elementach. Drugi zbiera informację o ilości odwiedzonych elementów. Oba wypisują jakieś tam informacje o odwiedzonych elementach, można to zignorować.
W przykładnie, klasy dziedziczące po IElement nie wykonują zbyt wiele poza akceptowaniem odwiedzającego, czy wysłaniem go do wszystkich swoich pól. IElement nie definiuje żadnej funkcjonalności, z której skorzystać mógłbym odwiedzający. Implementację odwiedzającego warto uodpornić na zmiany kolejności wywołać Accept (klasa agregująca), odwiedzający powinien poprawnie zadziałać bez względu na to czy przykładowy RootElement najpierw wyśle do odwiedzającego siebie, czy wcześniej przejdzie po wszystkich swoich polach i dopiero potem sama podda się odwiedzinom. Dlaczego warto tego przypilnować? Ponieważ nie zawsze mamy kontrolę na implementacją odwiedzanych elementów, np. zewnętrzna biblioteka. W nowszej wersji może się okazać, że zmieniono kolejność wywołania Accept ze względu na zwiększenie wydajności albo dodano nowe pola w odwiedzanej klasie.
Warto również zwrócić uwagę, że odwiedzający przyjmuje interfejs, nie będzie więc on w stanie wykonać innych operacji niż tam zdefiniowane. Można to rozwiązać na co najmniej dwa sposoby:
– Bardzo dobrze zdefiniować taki interfejs
– Dodać kolejne metody przeciążające metodę Accept, gdzie parametrem będą klasy na których odwiedzeniu nam zależy

Poniżej rozwiązanie drugie, troszkę rozbudowane:

Code Snippet
  1. using System;
  2.  
  3. namespace Visitor
  4. {
  5.     public interface IVisitor
  6.     {
  7.         void HandleVisit(IObject aObject);
  8.     }
  9.  
  10.     public interface IObject
  11.     {
  12.         void AcceptVisitor(IVisitor aVisitor);
  13.     }
  14.  
  15.     class GenericVisitor : IVisitor
  16.     {
  17.         void IVisitor.HandleVisit(IObject aObject)
  18.         {
  19.             throw new ArgumentException(
  20.                 string.Format(
  21.                     “Sorry but this visitor accepts only ConcreteObjectB types class. Please fix your code!. Error generated by instance of {0} class”,
  22.                     aObject));
  23.         }
  24.  
  25.         public void HandleVisit( ConcreteObjectB aObjectB)
  26.         {
  27.             Console.WriteLine(“Handle visit of class {0}”, aObjectB);
  28.         }
  29.     }
  30.  
  31.     class StronglyTypedVisitor : IVisitor
  32.     {
  33.         void IVisitor.HandleVisit(IObject aObject)
  34.         {
  35.             throw new ArgumentException(string.Format(“Just found new not properly handled visitor {0}- fix it”, aObject));
  36.         }
  37.  
  38.         public void HandleVisit(ConcreteObjectA aObject)
  39.         {
  40.             Console.WriteLine(“Visited by a strongly typed ObjectA instance”);
  41.         }
  42.  
  43.         public void HandleVisit(ConcreteObjectB aObject)
  44.         {
  45.             Console.WriteLine(“Visited by a strongly typed ObjectB instance”);
  46.         }
  47.     }
  48.  
  49.     class ConcreteObjectA : IObject
  50.     {
  51.         void IObject.AcceptVisitor(IVisitor aVisitor)
  52.         {
  53.             throw new ArgumentException(
  54.                 string.Format(
  55.                     “This object only accepts strongly typed objects. Fix your code. Class that generated exception {0}”,
  56.                     aVisitor));
  57.         }
  58.  
  59.         public void AcceptVisitor(StronglyTypedVisitor aVisitor)
  60.         {
  61.             aVisitor.HandleVisit(this);
  62.         }
  63.     }
  64.  
  65.     class StronglyTypedConcreteObject: IObject
  66.     {
  67.         public void AcceptVisitor(IVisitor aVisitor)
  68.         {
  69.             throw new ArgumentException(string.Format(“Consider using strongly typed methodsn. Please implement this method: public void AcceptVisitor({0} aVisitor)”, aVisitor));
  70.         }
  71.     }
  72.  
  73.     class ConcreteObjectB : IObject
  74.     {
  75.         void IObject.AcceptVisitor(IVisitor aVisitor)
  76.         {
  77.             throw new ArgumentException(
  78.                 string.Format(
  79.                     “ConcreteObjectB class accepts only StronglyTypedVisitor, please fix your code. Exception caused by instance of {0} class”,
  80.                     aVisitor));
  81.         }
  82.  
  83.         public void AcceptVisitor(StronglyTypedVisitor aVisitor)
  84.         {
  85.             aVisitor.HandleVisit(this);
  86.         }
  87.     }
  88.  
  89.     class Program
  90.     {
  91.         static void Main()
  92.         {
  93.             ConcreteObjectA a = new ConcreteObjectA();
  94.             ConcreteObjectB b = new ConcreteObjectB();
  95.             StronglyTypedVisitor sv = new StronglyTypedVisitor();
  96.  
  97.             //proper usage of code
  98.             a.AcceptVisitor(sv);
  99.             b.AcceptVisitor(sv);
  100.  
  101.             //now lets try to do some tricks
  102.             GenericVisitor gv = new GenericVisitor();
  103.  
  104.             try
  105.             {
  106.                 //a.AcceptVisitor(gv); this will generate compile error
  107.                 (a as IObject).AcceptVisitor(gv);       //oooo Im so great!
  108.             }
  109.             catch (ArgumentException ae)
  110.             {
  111.                 Console.WriteLine(“—————————————————————————————n{0}”,ae);
  112.             }
  113.  
  114.  
  115.             //example with synchronization lost!
  116.             try
  117.             {
  118.                 //Im sure it was agreed that the GeneralVisitor accepts ConcreteB, maybe its just a bug, I will try with the interface
  119.                 //b.AcceptVisitor(gv); //this will generate compile error
  120.                 (b as IObject).AcceptVisitor(gv);       //looks fine to me and the compilator, so it works!
  121.             }
  122.             catch (ArgumentException ae)
  123.             {
  124.                 Console.WriteLine(“—————————————————————————————n{0}”, ae);
  125.             }
  126.  
  127.         }
  128.     }
  129. }

C# oferuje możliwość zasłonięcia metody dziedziczonej po interfejsie, poprzez jej jawną implementacje (@17), oraz przeciążenie jej inną publiczną metodą, którą zadowoli kompilator (@25).
Wykorzystuje to do poinformowania użytkowników klas, że np. nie obsługuje ona innych typów klas, niż te dostępne publicznie (@19).
Skorzystałem z wyjątków, ze względu na czytelność kodu i przekazu dla użytkownika moich przykładowych klas. Innym sposobem jest np. zastosowanie asercji, które zadziałają jak należy, a w przypadku gdy aplikacja nie została dobrze przetestowana, nie wywalą wyjątku podczas oddawania aplikacji klientowi.
Niektórym może się nie spodobać takie przesłanianie metod, powiedzą że naruszona została zasada kontraktów i obiektowości. OK, ale! Zyskuje się na tym, wcześniejsze wykrywanie nie poprawnego korzystania z interfejsu. W dokumentacji można dodać odpowienie adnotacje, że dana implementacja odwiedzającego działa tylko z daną listą klas.
W części gdzie nie używam try/catch widać, że wszystko działa poprawnie, korzysta się z klas tak, jak osoba je pisząca sobie przewidziała. Nie potrzeba żadnych czarów, rzutowania etc. Wynikiem czego jest też poprawne działanie kodu. To akurat nie jest ciekawe.
Użytkownik decydując się na rzutowanie obiektów, korzysta w nie cywilizowany (@107, @120) sposób jest ciekawsza. Generowany jest czytelny komunikat i prośba o poprawienie kodu, oraz jedyny słuszny błąd w c# – wyjątek. I na koniec przykład z klasą GenericVisitor i ConcreteObjectB, widać jak można sobie rozsynchronizować kod. Co prawda przykład lekko naciągany, ale jednak.

Do czego się przydaje taki wzorzec, gdzie z niego skorzystać?
– Obiekty klas A i B można poddać walidacji, gdzie Wizytor będzie walidować ich poprawność. Warto też wtedy poprawniej nazwać poszczególne klasy.
– A i B zawierają jakieś zadania, które należy z nich wyciągnąć i zakolejkować, A pobiera informacje o zadaniach z sieciA a B pobiera z obserwowanego katalogu. Aplikacja tworzy odpowiednie klasy oczekujące na nowe zadanie, obiekty a lub b zgłaszają nadejście nowych zadań z odpowiednich źródeł, aplikacja przekazuje obiekt a lub b do obiektu zajmującego się zbieraniem takich zadań.
– A i B chcą uzyskać dostęp do strzeżonego obiektu, wcześniej muszą się zamedlować
u strażnika, aby sprawdzić czy mają odpowiednie uprawnienia dostępu
– Parsowanie skomplikowanych struktur

Bardziej życiowe porównanie, choć nie wiem czy najlepsze: organizujecie imprezę, wyklejacie plakaty: “Zapraszamy do nas na mega piwo”, przychodzą różne obiekty, klasa walidator przepuszcza część z nich, a innym grzecznie odmawia.

Cechy takiego wzorca:
– Trzeba napisać więcej kodu
– Może dojść do niesynchronizowania się klas odwiedzanych i odwiedzających, choć trzeba się o to porządnie postarać, dodałem go do przykładu (@120)
+ Czytelniejszy kod
+ Jasne określenie co jest wspierane przez klasy
+ Znika potrzeba rzutowania obiektów podczas ich obsługi
+ Jesteśmy zajebiści bo korzystamy ze wzorców projektowych

To tylko część z sytuacji, w których możliwe jest skorzystanie z tego wzorca. Jedynym ograniczeniem jest tutaj nasza wyobraźnia i zdrowy rozsądek.

JS

2 thoughts on “Wzorce: Odwiedzający / Wizytator

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.