Obserwator (ang. observer) jest wzorcem projektowym, który służy do powiadamiana pewnej grupy obiektów (obserwatorów) o zmianie w innym obiekcie (obserwowanym).
Obserwator należy do grupy wzorców czynnościowych. Więcej o podziale wzorców można przeczytać tutaj [under construction].
Wyobraźmy sobie sklep internetowy. W sklepie tym dostępne są różne towary, jednak zdarza się, że danego przedmiotu chwilowo nie ma w magazynie. Użytkownicy sklepu mają jednak możliwość podania swojego adresu email, by zostać powiadomionymi, gdy towar znów będzie dostępny. W tym przykładzie:
- konkretny przedmiot w sklepie internetowym będzie obserwowanym
- użytkownicy, którzy podali swój email i wyrazili chęć bycia informowanymi o dostępności, będą obserwatorami
- pojawienie się towaru w magazynie będzie zdarzeniem, które sprawi, że obserwatorzy zostaną poinformowani o dostępności towaru
Implementacje tego wzorca mogą się od siebie nieco różnić, jednak podstawowy schemat jest zazwyczaj dość podobny i polega on na tym, że:
- posiadamy klasę reprezentującą obserwującego, która posiada specjalną funkcję - nazwijmy ją OnNotify - która będzie wołana przez obserwowanego w momencie, gdy nadejdzie moment informowania obserwujących o zmianie
- posiadamy klasę reprezentującą obserwowanego, który to przechowuje w sobie listę obserwujących. W momencie, gdy pojawia się potrzeba poinformowania ich o zmianie w obiekcie iterujemy po tej liście i dla każdego obserwującego wołamy metodę OnNotify
Zacznijmy od zdefiniowania dwóch interfejsów - IObservable reprezentującego obserwowanego, i IObserver reprezentujący obserwującego.
Zanim to jednak zrobimy, musimy sobie zadać pytanie: czy gdy obserwowany będzie wysyłał obserwatorom informację o zmianie swojego stanu, będzie on musiał załączyć do tej notyfikacji jakieś dodatkowe dane? Innymi słowy: czy obserwatorowi wystarczy informacja "zaszła zmiana w obserwowanym obiekcie", czy potrzebna będzie wiedza "jaka to zmiana?", "co dokładnie się zmieniło"?
Jeśli potrzebne będą jakieś dodatkowe dane, warto zaimplementować możliwość przesłania ich między obserwowanym, a obserwatorami. Dlatego dobrze uczynić interfejsy IObservable i IObserver generycznymi - i utworzyć nowy typ danych, reprezentujący przesyłane między nimi informacje. Ustalmy, że strukturę danych przechowującą te informacje nazwiemy NotificationData.
IObservable posiadać będzie trzy metody:
Zanim to jednak zrobimy, musimy sobie zadać pytanie: czy gdy obserwowany będzie wysyłał obserwatorom informację o zmianie swojego stanu, będzie on musiał załączyć do tej notyfikacji jakieś dodatkowe dane? Innymi słowy: czy obserwatorowi wystarczy informacja "zaszła zmiana w obserwowanym obiekcie", czy potrzebna będzie wiedza "jaka to zmiana?", "co dokładnie się zmieniło"?
Jeśli potrzebne będą jakieś dodatkowe dane, warto zaimplementować możliwość przesłania ich między obserwowanym, a obserwatorami. Dlatego dobrze uczynić interfejsy IObservable i IObserver generycznymi - i utworzyć nowy typ danych, reprezentujący przesyłane między nimi informacje. Ustalmy, że strukturę danych przechowującą te informacje nazwiemy NotificationData.
IObservable posiadać będzie trzy metody:
- AddObserver dodającą obserwatora do listy obiektów, które mają zostać poinformowane o zmianie w obiekcie
- RemoveObserver usuwające obserwatora z tej listy
- NotifyAllObservers, która będzie informować wszytskich obserwujących, że nastąpiła jakaś zmiana w obiekcie
1 2 3 4 5 6 7 8 9 | namespace Observer { public interface IObservable<TNotificationData> { void AddObserver(IObserver<TNotificationData> observer); void RemoveObserver(IObserver<TNotificationData> observer); void NotifyAllObservers(); } } |
IObserver zaś będzie posiadał dwie metody:
- OnNotify - będzie to metoda wywołująca się, gdy obserwator zostanie poinformowany o zmianie w obiekcie obserwowanym
- StopObserving - metoda, za pomocą której obiekt będzie mógł się wypisać z listy obserwatorów
1 2 3 4 5 6 7 8 | namespace Observer { public interface IObserver<TNotificationData> { void OnNotify(TNotificationData notificationData); void StopObserving(); } } |
Utwórzmy klasy abstrakcyjne implementujące te interfejsy. Zacznijmy od obserwowanego. Będzie on przechowywał w sobie listę obserwujących. W momencie gdy potrzebne będzie poinformowanie ich o zmianie stanu obiektu, po prostu przeiterujemy po niej i dla każdego obserwującego zawołamy metodę OnNotify. Dodałam też metodę abstrakcyjną SetupNotificationData, której zadaniem będzie przygotować dane, jakie chcemy przesłać do obserwujących.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | using System.Collections.Generic; namespace Observer { public abstract class Observable<TNotificationData> : IObservable<TNotificationData> { private List<IObserver<TNotificationData>> _observers = new List<IObserver<TNotificationData>>(); public void AddObserver(IObserver<TNotificationData> observer) { _observers.Add(observer); } public void RemoveObserver(IObserver<TNotificationData> observer) { _observers.Remove(observer); } public virtual void NotifyAllObservers() { var notificationData = SetupNotificationData(); foreach (var o in _observers) { o.OnNotify(notificationData); } } protected abstract TNotificationData SetupNotificationData(); } } |
Skoro już przy obserwowanym jesteśmy, przyjrzyjmy się implementacji klasy Item, która reprezentuje przedmiot w sklepie internetowym.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; namespace Observer { public class Item : Observable<ItemAvailabilityNotificationData> { public Item(string name) { Name = name; } public string Name { get; set; } public override void NotifyAllObservers() { Console.WriteLine($"Przedmiot {Name} staje się dostępny."); base.NotifyAllObservers(); } protected override ItemAvailabilityNotificationData SetupNotificationData() { return new ItemAvailabilityNotificationData { ItemName = Name }; } } } |
Niewiele się tu zmieniło względem klasy bazowej. Dodałam property Name reprezentujące nazwę przedmiotu w sklepie. Utworzyłam też strukturę ItemAvailabilityNotificationData, która będzie przesyłana z obserwowanego do obserwujących w czasie informowania ich o zmianie w obiekcie.
1 2 3 4 5 6 7 | namespace Observer { public struct ItemAvailabilityNotificationData { public string ItemName { get; set; } } } |
Przejdźmy teraz do obserwujących, a więc do implementacji klasy Observer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | namespace Observer { public abstract class Observer<TNotificationData> : IObserver<TNotificationData> { private IObservable<TNotificationData> _observable; public Observer(IObservable<TNotificationData> observable) { _observable = observable; } public abstract void OnNotify(TNotificationData notificationData); public void StopObserving() { _observable.RemoveObserver(this); } } } |
Jak widać, przechowujemy tu referencję do obserwowanego. Gdybyśmy zrezygnowali z posiadania metody StopObserving, nie byłaby ona potrzebna. Metoda StopObserving nie jest kluczowa dla tego wzorca, i nie pojawia się w każdej implementacji. Musimy sami podjąć decyzję, czy chcemy, by obserwator mógł sam decydować o tym, by wypisać się z listy obserwujących.
Z klasy tej dziedziczy Customer, klasa reprezentującą klienta w sklepie internetowym:
using System; namespace Observer { public class Customer : Observer<ItemAvailabilityNotificationData> { public Customer(string name, IObservable<ItemAvailabilityNotificationData> observable) : base(observable) { Name = name; } public string Name { get; set; } public override void OnNotify(ItemAvailabilityNotificationData notificationData) { Console.WriteLine($"Ja, {Name}, zostałem poinformowany o zmianie dostępności towaru {notificationData.ItemName}."); } } }
Jak widać w tym przypadku Customer robi użytek z danych przesłanych przez obserwowanego.
Zerknijmy jeszcze na funkcję Main:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System; namespace Observer { class Program { static void Main(string[] args) { var observedItem = new Item("Bagietka z camembertem"); var customer1 = new Customer("Jean Pierre", observedItem); var customer2 = new Customer("Jean Paul", observedItem); observedItem.AddObserver(customer1); observedItem.AddObserver(customer2); observedItem.NotifyAllObservers(); Console.ReadKey(); } } } |
Jak można się domyślić, wynik działania programu to:
Przedmiot Bagietka z camembertem staje się dostępny. Ja, Jean Pierre, zostałem poinformowany o zmianie dostępności towaru Bagietka z camembertem. Ja, Jean Paul, zostałem poinformowany o zmianie dostępności towaru Bagietka z ca membertem.
Spróbujmy teraz wypisać obserwatorów z listy na dwa sposoby: przez obserwowanego, i na życzenie samego obserwującego.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using System; namespace Observer { class Program { static void Main(string[] args) { var observedItem = new Item("Bagietka z camembertem"); var customer1 = new Customer("Jean Pierre", observedItem); var customer2 = new Customer("Jean Paul", observedItem); observedItem.AddObserver(customer1); observedItem.AddObserver(customer2); observedItem.RemoveObserver(customer1); customer2.StopObserving(); observedItem.NotifyAllObservers(); Console.ReadKey(); } } } |
Jak widzimy, zadziałało, i obserwujący nie są już informowani:
Przedmiot Bagietka z camembertem staje się dostępny.
W niektórych językach wysokiego poziomu wzorzec Obserwator jest już zaimplementowany. W C# realizuje go mechanizm eventów, o których więcej można poczytać tutaj.
Kod z tego posta, wraz z innymi przykładami wzorców projektowych, można pobrać z tego repozytorium [under construction]
Komentarze
Prześlij komentarz