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:
- using System;
- using System.Collections.Generic;
- namespace Visitor
- {
- class SimpleExample
- {
- internal interface IElement
- {
- void Accept(IVisitor aVisitor);
- }
- internal interface IVisitor
- {
- void Visit(IElement aElement);
- }
- public class ElementClassA : IElement
- {
- public void Accept(IVisitor aVisitor)
- {
- aVisitor.Visit(this);
- }
- }
- public class ElementClassB : IElement
- {
- public void Accept(IVisitor aVisitor)
- {
- aVisitor.Visit(this);
- }
- }
- public class ELementClassC : ElementClassB
- {
- new public void Accept(IVisitor aVisitor)
- {
- aVisitor.Visit(this);
- }
- }
- public class RootElement : IElement
- {
- private ElementClassA _elementA;
- private List<ElementClassB> _elementsBList;
- private static ELementClassC _elementc = new ELementClassC();
- public void Accept(IVisitor aVisitor)
- {
- aVisitor.Visit(this);
- _elementA.Accept(aVisitor);
- foreach (var elementClassB in _elementsBList)
- {
- elementClassB.Accept(aVisitor);
- }
- RootElement._elementc.Accept(aVisitor);
- }
- public RootElement()
- {
- _elementA = new ElementClassA();
- _elementsBList = new List<ElementClassB>();
- for (int i = 0; i < 10; ++i)
- {
- _elementsBList.Add(new ElementClassB());
- }
- }
- }
- public class VisitorToString : IVisitor
- {
- public int Count { get; set; }
- public VisitorToString()
- {
- Count = 0;
- }
- public void Visit(IElement aElement)
- {
- string s = string.Format(“Visited class {0}”, aElement.ToString());
- Count += s.Length;
- Console.WriteLine(s);
- }
- }
- public class VisitorHashString : IVisitor
- {
- public int Count { get; set; }
- public VisitorHashString()
- {
- Count = 0;
- }
- public void Visit(IElement aElement)
- {
- string s = string.Format(“Visited class {0}, hash code {1}”, aElement.ToString(), aElement.GetHashCode());
- Count++;
- Console.WriteLine(s);
- }
- }
- public static void Main()
- {
- RootElement root = new RootElement();
- VisitorToString vstring = new VisitorToString();
- VisitorHashString vhstring = new VisitorHashString();
- root.Accept(vstring);
- Console.WriteLine(“Total written {0}”,vstring.Count);
- root.Accept(vhstring);
- Console.WriteLine(“Total written {0}”,vhstring.Count);
- }
- }
- }
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:
- using System;
- namespace Visitor
- {
- public interface IVisitor
- {
- void HandleVisit(IObject aObject);
- }
- public interface IObject
- {
- void AcceptVisitor(IVisitor aVisitor);
- }
- class GenericVisitor : IVisitor
- {
- void IVisitor.HandleVisit(IObject aObject)
- {
- throw new ArgumentException(
- string.Format(
- “Sorry but this visitor accepts only ConcreteObjectB types class. Please fix your code!. Error generated by instance of {0} class”,
- aObject));
- }
- public void HandleVisit( ConcreteObjectB aObjectB)
- {
- Console.WriteLine(“Handle visit of class {0}”, aObjectB);
- }
- }
- class StronglyTypedVisitor : IVisitor
- {
- void IVisitor.HandleVisit(IObject aObject)
- {
- throw new ArgumentException(string.Format(“Just found new not properly handled visitor {0}- fix it”, aObject));
- }
- public void HandleVisit(ConcreteObjectA aObject)
- {
- Console.WriteLine(“Visited by a strongly typed ObjectA instance”);
- }
- public void HandleVisit(ConcreteObjectB aObject)
- {
- Console.WriteLine(“Visited by a strongly typed ObjectB instance”);
- }
- }
- class ConcreteObjectA : IObject
- {
- void IObject.AcceptVisitor(IVisitor aVisitor)
- {
- throw new ArgumentException(
- string.Format(
- “This object only accepts strongly typed objects. Fix your code. Class that generated exception {0}”,
- aVisitor));
- }
- public void AcceptVisitor(StronglyTypedVisitor aVisitor)
- {
- aVisitor.HandleVisit(this);
- }
- }
- class StronglyTypedConcreteObject: IObject
- {
- public void AcceptVisitor(IVisitor aVisitor)
- {
- throw new ArgumentException(string.Format(“Consider using strongly typed methodsn. Please implement this method: public void AcceptVisitor({0} aVisitor)”, aVisitor));
- }
- }
- class ConcreteObjectB : IObject
- {
- void IObject.AcceptVisitor(IVisitor aVisitor)
- {
- throw new ArgumentException(
- string.Format(
- “ConcreteObjectB class accepts only StronglyTypedVisitor, please fix your code. Exception caused by instance of {0} class”,
- aVisitor));
- }
- public void AcceptVisitor(StronglyTypedVisitor aVisitor)
- {
- aVisitor.HandleVisit(this);
- }
- }
- class Program
- {
- static void Main()
- {
- ConcreteObjectA a = new ConcreteObjectA();
- ConcreteObjectB b = new ConcreteObjectB();
- StronglyTypedVisitor sv = new StronglyTypedVisitor();
- //proper usage of code
- a.AcceptVisitor(sv);
- b.AcceptVisitor(sv);
- //now lets try to do some tricks
- GenericVisitor gv = new GenericVisitor();
- try
- {
- //a.AcceptVisitor(gv); this will generate compile error
- (a as IObject).AcceptVisitor(gv); //oooo Im so great!
- }
- catch (ArgumentException ae)
- {
- Console.WriteLine(“—————————————————————————————n{0}”,ae);
- }
- //example with synchronization lost!
- try
- {
- //Im sure it was agreed that the GeneralVisitor accepts ConcreteB, maybe its just a bug, I will try with the interface
- //b.AcceptVisitor(gv); //this will generate compile error
- (b as IObject).AcceptVisitor(gv); //looks fine to me and the compilator, so it works!
- }
- catch (ArgumentException ae)
- {
- Console.WriteLine(“—————————————————————————————n{0}”, ae);
- }
- }
- }
- }
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
Kluczowy argument:
+ Jesteśmy zajebiści bo korzystamy ze wzorców projektowych
😀
Przemawia do ludzi 🙂