Cześć!

Dawno mnie tu nie było… Który raz to już mówię… Ale teraz takie czasy, że już nie ma migania się 😉 Mam teraz całą listę tematów, o których chciałbym Ci opowiedzieć, więc nie przedłużajmy i zaczynamy.

Wstęp

Temat wzorców projektowych był wałkowany na każdy możliwy sposób. W tym wpisie oczywiście zostanie przedstawione co to jest za wzorzec ten cały Obserwator, jaki problem adresuje, z czym się wiąże jego stosowanie. Pokażę Ci także podstawowy sposób implementacji. Na koniec zostawię Cię z pewnego rodzaju przemyśleniem związanym z tym wzorcem oraz nowoczesną architekturą aplikacji.

W swoim wpisie będę się bardzo mocno opierał o książkę Bandy Czworga, więc wiesz czym jest Obserwator i nie chcesz czytać po raz kolejny, to przewiń proszę do akapitu Jak i dlaczego Obserwator tak mocno wniknął w architekturę nowoczesnych aplikacji?.

Zachęcam Cię także do zostawienia komentarza i podzielenia się tym wpisem w swoich social media!

Problem

Problem, który adresuje obserwator, określiłbym w następujący sposób:

Chcę być powiadomiony o zmianach w innej części systemu

A teraz spójrz na to tak:

Jestę YouTuberę, daj łapkę w górę, zasubskrybuj i kliknij dzwoneczek, żeby dostawać powiadomienia o najnowszych filmach na kanale!

Instagram: #f4f #follow4follow

Albo kiedy aktualizujesz sobie jakieś pole w arkuszu kalkulacyjnym i automatycznie odświeżają Ci się jakieś kalkulacje, wykresy w raportach itd.

Bardziej inżyniersko proszę.

Cytując Bandę Czworga:

Określa zależność jeden do wielu między obiektami. Kiedy zmieni się stan jednego z obiektów, to wszystkie obiekty zależne od niego są automatycznie powiadamiane i aktualizowane.

“Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku”, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides

Osobiście rozszerzyłbym zależność jeden do wielu, na wiele do wielu. Niekoniecznie przecież obserwator musi obserwować tylko jeden obiekt główny. Dalej bardzo ważny jest fragment Wszystkie obiekty zależne od niego są automatycznie powiadamiane i aktualizowane. Jest to pewnego rodzaju odwrócenie odpowiedzialności – nie chcę sprawdzać, czy coś się zmieniło, powiadom mnie, kiedy to się zdarzy. Gdybyśmy nie mieli tej zależności, a chcielibyśmy zmienić stan jakiegoś obiektu w zależności od innego, to byśmy musieli co jakiś czas sprawdzać, czy coś się zmieniło. Dużo niepotrzebnego kodziku. A to rozwiązanie jest genialne w swojej prostocie.

Wzorzec ten jest też nazywany inaczej Publish/Subscribe. Obiekty subskrybują się do innego, a ten publikuje zmiany. Czy teraz nie jest to jakieś takie bardziej znane? 🙂

Rachunek zysków i strat

Co nam daje takie podejście? Gdzie zyskujemy, a gdzie tracimy? Spróbujmy przez to przejść.

Dużą zaletą zastosowania tego wzorca jest fakt, że zdecydowanie rozluźniamy powiązanie między obiektami. Mamy tylko dwa interfejsy – podmiotu i obserwatora – to, jak je wepniemy w pozostałą część systemu to zależy tylko od nas.

Idąc dalej, jeśli konkretny dany fragment aplikacji chce powiadomić inne obiekty o zmianie swojego stanu, to “nie musi się zastanawiać w jaki sposób to zrobić”, tzn. po prostu zainteresowani “zgłosili się do niego” z chęcią otrzymywania powiadomień o zaistniałej zmianie.

A gdzie możemy stracić? Największy ból jest w tym, że nie wiemy, kiedy powiadomienie o zmianie przyjdzie… Co się stanie, jeśli nowe dane wpadną, kiedy obserwatorzy dokonują jakiegoś procesowania danych, które aktualnie mieli? Powinni przerwać? Kontynuować i zaraz rozpocząć pracę z nowymi danymi? To zależy oczywiście, a odpowiedź powinniśmy wyciągnąć od biznesu 🙂 Tylko bez rękoczynów proszę!

Kolejnym wąskim gardłem może się okazać obsłużenie dużej ilości obserwatorów. Co się stanie, jeśli np. na milionie obiektów będziemy chcieli wywołać metodę Notify()? Powiesz no gdzie milion obiektów… A np. kanały na YouTube, które mają tyle subskrybentów :)? Oczywiście tam to jest dużo lepiej rozwiązane niż List<IObserver> i iteracja po kolejnych elementach 😉 Warto jednak mieć to na uwadze przy projektowaniu.

Dalej, załóżmy, że w jakiś sposób usuniemy obiekt z pamięci, ale zanim to zostanie zrobione, to nie odsubskrybujemy się w podmiocie. Zostanie nam wisząca referencja, którą w jakiś sposób trzeba obsłużyć.

To jest wierzchołek góry lodowej. Nie zaczynam nawet tematu tego, jak przerzucić dane z podmiotu do obserwatorów, bo też można zastosować tutaj kilka podejść. Po więcej informacji, czy też bardziej formalny opis, odsyłam Cię do książki Bandy Czworga – tam znajdziesz dużo szerszy opis tego, o czym tutaj wspomniałem. Ewentualnie jeśli chcesz, abym ja się nad tym dłużej porozwodził – zostaw komentarz!

Implementacja

Na poniższym obrazku znajduje się diagram opisujący w jaki sposób możemy zaimplementować ten wzorzec.

Diagram implementacji wzorca Obserwator
Diagram implementacji wzorca Obserwator

Dwa główne elementy z tego diagramu to interfejsy Subject oraz Observer dostarczające odpowiednie metody. Kolejne dwa elementy to klasy implementujące owe interfejsy. Oczywiście nie ma przeszkody, żeby klas, które implementują owe interfejsy było więcej. Loosely coupled 😉 Jeśli znajdą się klasy implementujące oba interfejsy, to komunikacja pomiędzy obiektami będzie mogła być zestawiona.

Spójrz proszę na parę kawałków kodów poniżej, które dla Ciebie przygotowałem. Oczywiście wszystko znajdziesz na moim koncie Github.

Kodzik został napisany w C#/.NET Core 3.1. Tak przy okazji ostatnio zostałem korzystać z Rider od JetBrains. I jak na razie jest miazga jeśli chodzi o szybkość. To IDE myśli szybciej ode mnie hahaha 😀

Do brzegu!

Na początek wrzucamy sobie na tapetę dwa interfejsy – ISubject oraz IObserver.

namespace BaseImplementation
{
    public interface IObserver
    {
        void Update(ISubject subject);
    }
}
namespace BaseImplementation
{
    public interface ISubject
    {
        void Attach(IObserver observer);
        void Detach(IObserver observer);
        void Notify();
    }
}

W pierwszym mamy metodę Update(), która przyjmuje obiekt typu ISubject. Po prostu tak będzie łatwiej wyciągnąć nowe dane ;). W drugim zaś mamy trzy metody – dwie pierwsze obsłużą podłączanie i odłączanie obserwatorów, ostatnia będzie wywoływana przez podmiot wtedy, kiedy podmiot będzie chciał powiadomić obserwatory o zmianach.

Rzućmy okiem teraz na dwie klasy, które zaimplementują powyższe interfejsy:

using System;

namespace BaseImplementation
{
    public class ConcreteObserver : IObserver
    {
        private readonly string _observerName;

        public ConcreteObserver(string observerName) => _observerName = observerName;

        public void Update(ISubject subject)
        {
            Console.WriteLine($"{_observerName}: {(subject as ConcreteSubject)?.ConcreteSubjectState}");
        }
    }
}

Klasa ta ma jedno pole, w którym przechowamy sobie jakąś nazwę naszego obserwatora, żebyśmy mogli w konsoli łatwo zidentyfikować, gdzie metoda została wywołana. No i oczywiście dalej zaimplementowałem metodę Update() z interfejsu.

using System.Collections.Generic;

namespace BaseImplementation
{
    public class ConcreteSubject : ISubject
    {
        public int ConcreteSubjectState
        {
            get => _concreteSubjectState;
            set
            {
                _concreteSubjectState = value;
                Notify();
            }
        }
        private int _concreteSubjectState;

        private readonly List<IObserver> _observers = new List<IObserver>();
        
        public void Attach(IObserver observer)
        {
            _observers.Add(observer);
        }

        public void Detach(IObserver observer)
        {
            _observers.Remove(observer);
        }

        public void Notify()
        {
            foreach (var observer in _observers)
            {
                observer.Update(this);
            }
        }
    }
}

Ta klasa jest nieco bardziej rozbudowana, ale też bez strachu 😉 W pierwszej części przygotowałem sobie zmienną, która zasymuluje nam stan obiektu, żebyśmy mieli co zmienić 😉 Co ciekawe w setterze od razu wrzuciłem wywołanie metody Notify() – z automatu po wpisaniu nowej wartości zostanie uruchomiona aktualizacja obserwatorów. W drugiej części mamy obsługę listy obserwatorów – odpowiednio dołączanie i odłączanie oraz fala powiadomień.

Na koniec program, który tworzy obiekty i dokonuje odpowiednich wywołań.

namespace BaseImplementation
{
    class Program
    {
        static void Main(string[] args)
        {
            var concreteSubject = new ConcreteSubject();
            var observer1 = new ConcreteObserver("Observer 1");
            var observer2 = new ConcreteObserver("Observer 2");

            concreteSubject.ConcreteSubjectState = 2;

            concreteSubject.Attach(observer1);
            observer1.Update(concreteSubject);

            concreteSubject.Attach(observer2);
            observer2.Update(concreteSubject);

            concreteSubject.ConcreteSubjectState = 3;

            concreteSubject.Detach(observer1);

            concreteSubject.ConcreteSubjectState = 4;
        }
    }
}

I wisienka na torcie, tzn. efekt uruchomienia.

/usr/share/dotnet/dotnet /home/piotr/Storage/CodingTime-Examples/ObserverPattern/BaseImplementation/bin/Debug/netcoreapp3.1/BaseImplementation.dll
Observer 1: 2
Observer 2: 2
Observer 1: 3
Observer 2: 3
Observer 2: 4

Process finished with exit code 0.

Myślę, że powyższy kodzik jest prosty i rozumiesz co się w nim dzieje 😉 Jeśli nie – dawaj znać w komentarzu!

Jak widzisz implementacja tego wzorca jest w podstawowej wersji banalna 😉 Brać i korzystać 😉 Oczywiście -tylko jeśli rzeczywiście jest to potrzebne!

Jak i dlaczego Obserwator tak mocno wniknął w architekturę nowoczesnych aplikacji?

Uff… W końcu… Dotarłem do tego, o czym chciałem na początku powiedzieć… 🙂 Jednak wiem, że nie każdy, kto będzie czytał ten post, sobie przepracował ten temat, dlatego też chciałem zrobić dobre podstawy, żeby mieć dalej o czym gadać 🙂 To będzie stosunkowo krótka i bardzo subiektywna część. Mam nadzieję, że może u kogoś wywoła jakieś przemyślenie, może chwilę skupienia. Może dla kogoś jest to oczywiste – i też dobrze!

W mojej ocenie ten wzorzec jest bardzo ważną częścią “nowoczesnej architektury”. O to stwierdzenie też można by się pokłócić, co znaczy, że jest nowoczesna itd., ale to może kiedy indziej 😉

UWAGA, TERAZ POPŁYNĘ! Pomyślmy sobie o tym wzorcu nie jako o hacku na pewien problem w kodzie, ale warstwę albo dwie wyżej, tzn. jako po prostu o modelu komunikacji. Ja nie chcę chodzić na pocztę raz dziennie i sprawdzać, czy jest dla mnie paczka, tylko chcę dostać powiadomienie, kiedy ona będzie. Chodzi mi o to, że dzisiaj wszystko chcemy mieć w modelu “Powiadom mnie”. Powiem więcej, specjalnie aplikacje są modelowane tak, by powiadamiać użytkownika, by zaskarbiać jego uwagę. Raz, że na koniec dwa wiąże się to z piniążkami (Gdzie jest moje pisiont groszy?), dwa, że to w moim odczuciu poprawia UX.

Teraz trochę z innej beczki. Event Sourcing. I tak naprawdę cały system opieramy o to, że poszczególne części aplikacji powiadamiają się o zmianach. OrderPlaced, OrderShipped, TicketSold… Przykłady można mnożyć. Jedne części powiadamiają o zmianach, a inne się na nie subskrybują odpowiednimi handlerami (wybaczcie, ale nie będę świadomie stosował ponglish, nie trawię większości tłumaczeń angielskich nazw na polski). Wchodzimy, patrzymy są zdarzenia, CQRS, mikroserwisy, reactive programming, wszystko stoi na chmurze i mówimy “Ooo… A kto to Panu… Tak ładnie zrobił”? Czyż nie? Czyżbyśmy nie dogonili właśnie króliczka? Nie odkryli konferencyjnego świętego grala?

Tak jak napisałem wcześniej – z podejściem w tym modelu wiążą się różnego rodzaju problemy. Jak ze wszystkim. NIE MA ROZWIĄZAŃ IDEALNYCH. Ale z jakiegoś powodu ten model ostatnio przoduje. Po prostu ma wiele zalet, ułatwia skalowanie, rozluźnia więzy itd…

Do brzegu Panie Kolego, do brzegu!

No ładnie. Kurczę dawno nie pisałem. Nie powiem, troszkę mnie to zmęczyło, ale to z tych fajnych zmęczeń 🙂 Do tego miejsca 1500+ słów. Troszkę też to czasu zajęło, ale no jak się nie piszę, to i nie ma takiej wprawy.

W każdym razie dziękuję Ci, że dotarłeś/dotarłaś do tego miejsca. Tak jak pisałem, mam nadzieję, że ten wpis wywoła u Ciebie jakieś przemyślenia, może na temat tego wzorca, może na jakikolwiek inny. Śmiało podziel się w komentarzu! Tym czasem kończymy na dzisiaj 🙂 Ofiara spełniona 😉

Życzę Ci dużo zdrowia w tym szalonym czasie. Wypoczywaj i pamiętaj – #zostań w domu

Piotrek

Literatura

  1. “Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku”, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
  2. https://refactoring.guru/design-patterns/observer/csharp/example

A może…?

Dziękuję Ci, że dotartłeś aż do tego miejsca. Mam nadzieję, że podobał Ci się mój wpis i dowiedziałeś się z niego czegoś wartościowego. Jeżeli chcesz być poinformowanym o moich kolejnych wpisach bądź od czasu do czasu dostać wiadomość ze zbiorem jakiś ciekawych linków, zapisz się proszę do mojego newslettera. Będzie mi niesamowicie miło, jeśli to zrobisz i dołączysz do mojej społeczności!


Piotr Wachulec

Student, konsultant IT/programista, bloger, w wolnych chwilach uczy się nowych rzeczy, tańczy bachatę lub pije pyszną kawę ze znajomymi (chętnie się jej napije także z Tobą! :)). Często można go spotkać na konferencjach, meetupach lub po prostu biegnącego po stolicy na tramwaj. Jego piątka w "teście" Gallupa: Learner, Achiver, Intellection, Relator, Harmony.