Przejdź do głównej zawartości

[OOP] Na czym polega wzorzec projektowy obserwator?


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:
  • 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