Programowanie Obiektowe wykłady
Klasa i Obiekt
Klasa typ ogólny, abstrakcyjny, określa cechy a nie ich wartości
Obiekt, instancja klasy, egzemplarz klasy - konkretny byt z rzeczywistości
Podstawowe wyróżniki obiektowości
Abstrakcja
Hermetyzacja
Dziedziczenie
Polimorfizm
Abstrakcja
Proces uogólniania, tworzenia i definiowania klas na pewnym poziomie określenia szczegółów
Wyodrębnienie istotnych cech rozpatrywanych obiektów domenowych
Odpowiednie nazewnictwo pól i metod w klasach, tak aby łączyły poszczególne klasy domenowe w jeden byt
Ograniczenie liczby powiązań pomiędzy klasami, dobrze zdefiniowane odpowiedzialności klas
Pozwala na tworzenie systemów łatwiej rozszerzalnych
Przykład:
Załóżmy, że tworzymy aplikację dla muzyków. Rozważmy dwie klasy Pianino i Gitara. W drodze analizy doszliśmy do wniosku, że aby wydać dźwięk na Pianinie wywołujemy metodę NacisnijKlawisz(), a na Gitarze SzarpnijStrune()
Mamy także klasę Zespół, która zarządza graniem instrumentów. Jedyne co powinna ona wiedzieć to jak wydać dźwięk.
W obecnej postaci klasy Pianino i Gitara są zbyt szczegółowe, należałoby je uogólnić i zmienić metodę wywołującą dźwięk np. na Graj()
Dzięki temu dodanie kolejnego instrumentu np. Trąbka jest łatwiejsze, wystarczy zaimplementować metodę Graj()
Zapewnia utrzymanie właściwego stanu klasy. Pola klasy są inicjowane, modyfikowane i odczytywane w sposób przewidziany przez autora klasy.
Ukrycie wewnętrznego stanu klasy, a udostępnienie tylko tego co jest niezbędne do komunikacji z innymi klasami
Użycie odpowiednich modyfikatorów dostępu do pól i metod,
Innymi słowy, nie pozwalajcie innym mieszać w stanie waszej klasy, gdyż to utrudnia odnajdywanie błędów
Przykład:
Załóżmy, że w ramach dużego systemu piszemy moduł, który będzie wykorzystywany przez wielu programistów.
Zadanie polega na napisaniu klasy Licznik, ma być ona wykorzystywana w wielu modułach zliczających rozpoczęcie i zakończenie jakiegoś zdarzenia. Licznik nie może być mniejszy od 0.
Klasa Licznik ma 3 metody Zwiększ(), Zmniejsz(), WypiszStan()
Class
Licznik
{
private
int
Stan;
public
void
Zwieksz(){ this.Stan
+= 1; }
public
void
Zmniejsz(){
if(this.Stan>0)
this.Stan
-= 1;
}
public
void
WypiszStan() { Console.Write(this.Stan);
}
}
Dziedziczenie
Pozwala na tworzenie naturalnej struktury “bycia czymś” np. Samochód jest Pojazdem i Rower jest Pojazdem, dzięki temu Samochód i Rower mogą współdzielić zachowania zaimplementowane w klasie Pojazd.
Pozwala to rozszerzać klasy o nowe cechy, bez powielania kodu. Wspólny kod jest w klasie nadrzędnej (bazowej)
Pozwala lepiej oddać charakter relacji pomiędzy klasami rzeczywistymi i zamapować je w systemie informatycznym
Pozwala uwspólnić pewne cechy i wyciągnąć je do klasy bazowej np. każdy pojazd ma koła (wspiera abstrakcję)
Umożliwia korzystanie z własnej lub cudzej pracy poprzez rozszerzanie już zaimplementowanych elementów (np. klas, interfejsów)
Tworzenie klasy na podstawie już istniejącej (w drugą stronę - uogólnianie klasy) np. na podstawie klasy Istota powstanie klasa Człowiek
Elementy dziedziczone: pola, metody, właściwości, klasy
Dziedziczone są elementy public, protected - z zachowaniem modyfikatora dostępu
Umiejętnie zastosowane dziedzicznie tworzy hierarchię klas, z wyszczególnioną klasą bazową i klasami pochodnymi
Hierarchia dziedziczenia może być wielopoziomowa
W .net wszystkie klasy automatycznie dziedziczą po klasie Object
Polimorfizm
Poli - wiele, morf - postać, wielopostaciowość
Obiekt może być różnie traktowany w zależności od kontekstu np. Raz jako Samochód a raz jako Pojazd
W trakcie wykonywania programu automatycznie są odnajdywane odpowiednie implementacje metod w zależności jak chcemy traktować obiekt
Pojęcie związane z Dziedziczeniem
więcej na kolejnych wykładach
Związki Klas w diagramach URL
Zależność - zaznaczamy przerywana linią z strzałką, oznacza że jedna klasa używa innej klasy w niedługim czasie
Asocjacja - zaznaczamy za pomocą linii zakończonej strzałką, często występuje wraz z określeniem liczebności. Reprezentuje czasowe powiązanie pomiędzy obiektami dwóch klas. Obiekty są niezależne od siebie
Agregacja - zaznaczamy linią zakończoną pustym rombem. Oznacza relację część-całość, elementy części należą do większej całości. Np. obiekty Pracownik do obiektu Firma, obiekt części może istnieć samodzielnie
Kompozycja - zaznaczamy linią zakończoną wypełnionym rombem. Oznacza relację część-całość, części nie mogą istnieć bez nadzorcy
Generalizacja - na diagramie zaznaczamy linią zakończoną trójkątną niewypełnioną strzałką. Oznacza związek dziedziczenia, kierunek od klasy pochodnej do bazowej
Klasy Abstrakcyjne i Finalne
KLASY ABSTRAKCYJNE (modyfikator abstract)
Istnieje możliwość tworzenia klas bazowych, które nie będą posiadały żadnych obiektów
Można umieścić implementacje metod, zawierać pola i właściwości
Może zawierać stałe
Główny cel - można z niej dziedziczyć
Klasa potomna musi implementować wszystkie metody i właściwości
KLASY FINALNE (modyfikator sealed)
Klasy nie umożliwiają dziedziczenia (zapobiega przypadkowemu dziedziczeniu)
Interfejsy
Podobne do dziedziczenia, ale nie realizują zasady ‘bycia czymś’
Określają możliwe do wykonania czynności na często różnych ideowo klasach, które ciężko wpisać w spójną hierarchię dziedziczenia
wyobraźmy sobie że chcemy móc wywoływać metody pozwalające na podgrzanie czegoś np. kubka, akwarium, samochodu
Pozwalają spojrzeć na różne klasy z punktu widzenia ich wspólnego zachowania a nie wspólnego pochodzenia
Gdy klasa implementuje interfejs to musimy zaimplementować wszystkie metody interfejsu
Zaimplementowane metody interfejsu muszą dokładnie pasować do sygnatury
Interfejsy nie mogą zawierać stałych, pól, operatorów, konstruktorów, destruktorów
Często mówi się o interfejsach jako o kontraktach
Dziedziczenie pozwala przejąć implementację z klasy rodzica, interfejsy definiują tylko zachowanie/kontrakt/interfejs/nazwy metod
Wykorzystanie interfejsów
IComparable - pozwala na porównywanie dwóch obiektów ze sobą, przydatny przy sortowaniu
IEnumerable, ICollection, IList - interfejsy pozwalające na dostęp do elementów zagregowanych w ramach klasy, pozwala na przechodzenie po elementach, dostęp do nich za pomocą indeksatora itp
IClonable - pozwala na zdefiniowanie własnego sposobu kopiowania obiektów
IDisposable - pozwala na implementację mechanizmu zwalniania zasobów
Polimorfizm
Metody wirtualne
Metody, która nie została oznaczona jako wirtualna nie można przesłonić!
Metoda wirtualna, może ale nie musi być przesłonięta, zależy od autora klasy pochodnej
Gdy piszesz klasę, po której ktoś będzie dziedziczył to ułatw mu życie i oznacz metodę jako wirtualną, niech sam zdecyduje co z tym zrobić!
W ten sposób tworzysz elastyczny kod
Metody abstrakcyjne
Metody abstrakcyjne definiujemy w klasie bazowej
Klasę z metodą abstrakcyjną nazywamy klasą abstrakcyjną
Nie można utworzyć egzemplarza klasy abstrakcyjnej! Podobnie jak egzemplarza interfejsu!
Metody abstrakcyjne nie mają implementacji, nie definiujemy ciała metody.
Deklaracja podobna jak przy metodach z interfejsu
Klasy dziedziczące muszą zaimplementować metodę abstrakcyjną
Abstrakcja vs wirtualna
Virtual możemy stosować gdy chcemy dostarczyć bazowej implementacji, gdy ma ona sens także dla innych obiektów
Przykład metoda ToString() z klasy Object
Abstract stosujemy gdy tylko zależy nam na samym kontrakcie, implementacja zazwyczaj nie ma sensu
Klasa bazowa Figura, klasy pochodne Okrąg, Kwadrat
Metoda abstrakcyjna Pole()
Reguły projektowania obiektowego S.O.L.I.D.
Single Responsibility Principle - Zasada jednej odpowiedzialności
Open Closed Principle - Zasada otwarte-zamknięte
Liskow Substitution Principle - Zasada podstawienia Liskov
Interface Segragation Principle -Zasada segregacji interfejsów
Dependency Inversion Principle - Zasada odwrócenia zależności
Single Responsibility Principle
Klasa powinna mieć tylko jedną odpowiedzialność, nigdy nie powinien istnieć więcej niż jeden powód do modyfikacji klasy.
Należy dobrze się zastanowić co dana klasa ma robić, jeżeli widzimy więcej odpowiedzialności klasy, to należy ją podzielić
Gdy już musimy zmienić zachowanie klasy nie powinno to wymuszać zmian w innych częściach systemu: GUI, raportach, schemacie bazy danych itp.
Sytuacje, które powinny nas zastanowić:
mieszanie logiki z prezentacją
mieszanie logiki z działaniem innych zewnętrznych komponentów: baz danych, api, bibliotek
Mniejsze klasy,
łatwiejsze testowanie,
łatwiejsze ponowne wykorzystanie poprzez kompozycję,
łatwiejsze utrzymanie kodu
Open Closed Principle
Klasy powinny być otwarte na rozbudowę, ale zamknięte na modyfikacje.
Przy zmianie wymagań nie powinien być zmieniany stary działający kod, ale dodawany nowy, który rozszerza zachowania.
Najczęściej realizujemy to poprzez
dziedziczenie - rozbudowujemy klasę o nowe funkcjonalności
ponowne użycie kodu poprzez kompozycje
hermetyzację i wydzielenie tego co może się zmienić w przyszłości
Wydzielamy to co może być rozbudowywane do oddzielnego interfejsu
Nie polegamy na szczegółach implementacyjnych klas
Unikamy if, switch w zależności do wartości pól lub typów wykorzystywanych klas
Przeciwdziała to wprowadzaniu nowych błędów w istniejącym kodzie.
Liskov Substitution Principle
Korzystanie z funkcji klasy bazowej musi być możliwe również w przypadku podstawienia instancji klas pochodnych.
Test: czy w twoim programie w każdym wystąpieniu typu bazowego możemy podstawić typ pochodny bez konsekwencji dla działania aplikacji
Używaj dziedziczenia tylko wtedy, gdy będziesz korzystał z polimorfizmu. A nie w celu „wyciągania przed nawias” wspólnych części – do tego służy agregacja
Interface Segragation Principle
Klienci nie powinni zależeć od interfejsów, których nie używają.
Zależność dwóch klas od siebie powinna być zdefiniowana najmniejszym możliwym spójnym interfejsem.
Nie pakujmy za dużo do pojedynczego interfejsu!
Obiekt nie powinien implementować metod, które nie są mu potrzebne.
Rozbijajmy interfejsy na mniejsze, lepiej aby klasa implementowała ich kilka niż jeden duży
Dependency Inversion Principle
Wysokopoziomowe moduły nie powinny zależeć od modułów niskopoziomowych - zależności między nimi powinny wynikać z abstrakcji.
Należy pamiętać o tym aby klasy były na tym samym poziomie, czyli np. klasa abstrakcyjne nie powinna operować na klasie konkretnej.
Pisząć kod mamy do czynienia z klasami niskopoziomowymi: komunikacja z bazą, z systemem plików, wywołania remote API, algorytmy liczące coś itp
Mamy także klasy wysokopoziomowe: logika biznesowa opisująca pewien proces
Bardzo często się zdarza, że klasy niskopoziomowe się zmieniają, np.
nie chcemy zapisywać do zwykłego pliku tylko do xml
chcemy zmienić bazę danych z mysql na oracl
chcemy użyć innego protokołu przy API nie WSDL a REST
zewnętrzna usługa, z której dostawaliśmy jsona, zmieniła format pliku
zmienił się sposób autentykacji w jakiejś usłudze, zamiast hasła wymaga certyfikatu
zmieniło się kodowanie odczytywanych plików wideo
Obsługa wyjątków
Wyjątkiem nazywamy mechanizm kontroli przepływu występujący w językach programowania i służący do obsługi zdarzeń wyjątkowych. Zdarzenia wyjątkowe to w szczególności błędy, jak np. Brak pliku wejściowego, zbyt duży indeks.
Dodanie instrukcji, metod, które zostaną wywołane w momencie wystąpienia sytuacji wyjątkowej
Czy zawsze obsługa wyjątku jest potrzebna?
Jakie są minusy obsługi wyjątków?
Możliwość zapobiegania nieoczekiwanemu zakończeniu działania aplikacji
Aplikacja musi radzić sobie z błędami pojawiającymi się w czasie jej wykonywania, niezależnie od przyczyny tych błędów (usterek w kodzie, niedociągnięć w bibliotece klas platformy .NET, awarii sprzętowych)
Możliwość poznania przyczyny nieprawidłowego funkcjonowania aplikacji (np. poprzez prowadzenie logów przy obsłudze wyjątków)
Finally pozwala na posprzątanie w sytuacji gdy po wystąpieniu wyjątku coś musimy jeszcze zrobić
zamknąć uchwyt do pliku
zamknąć połączenie do bazy danych
zatrzymać uruchomione wątki
Zgłaszać czy nie ?
Wyjątki zgłaszamy w sytuacjach, hmm, wyjątkowych
Parsowanie na liczbę do takich nie należy
Brak rekordów w wyniku zapytania do takich nie należy
Dokonujemy sprawdzenia warunków wstępnych i jeżeli nie są one spełnione to możemy wyrzucić wyjątek
Nie połykamy wyjątków!
Rzucanie z powrotem tego samego wyjątku, kontynuacja stacktrace
Try, catch spowalnia, czy ma sens łapanie wyjątków w pętli?
Kiedy ?
Przywracanie właściwego stanu systemu
„Czyszczenie” systemu, w szczególności zwalnianie zasobów
Rejestrowanie informacji o zdarzeniach
Generowanie dodatkowych informacji diagnostycznych
Wyjątków należy używać do obsługi nieoczekiwanych zdarzeń, nie do
implementowania logiki aplikacji (Np. niewłaściwy format pola „e-mail” czy nieprawidłowe hasło można obsłużyć w logice kodu aplikacji)
– Np. brak którejś z wymaganych tabel bazy lub awaria sprzętowa wymagają zastosowania odpowiednich wyjątków
GŁÓWNE KOLEKCJE:
- List <T>
Dowolna długość, rozszerzają się w razie potrzeby (< Max(RAM))
Indeksowany dostęp do elementów z użyciem operatora [ ] np. list[5]
Podstawowe operacje: Contains, Insert, Add, AddRange, Remove ….
List<T> - wewnętrzna implementacja oparta na tablicy
Złożoność poszczególnych operacji taka jak na tablicach:
[] - O(1)
Insert - O(n)
Add - O(1)
Remove, RemoveAt - O(n)
- LinkedList < T >
LinkedList<T> składa się z LinkedListNode<T>
Lista łączona, węzły wskazują na siebie nawzajem
Dobra do przechodzenie sekwencyjnego,
Niektóre operacje rzędu O(n)
Nie ma indeksatora,
- Słownik Dictionary < TKey , TValue >
Przechowuje pary wartości, pozwalające na indeksowanie dowolnym typem
Pozwala na definiowanie mapowania klucz->wartość
KeyValuePair<TKey, TValue> - klasa przechowująca parę wartości
Do słownika nie możemy dodać dwóch różnych wartości o tym samym kluczu!
Szybkie sprawdzenie czy coś istnieje, powiązanie dwóch obiektów
- HashSet < T > - zbiór
Klasa implementuje matematyczne pojęcie zbioru
W zbiorze nie mogą znajdować się dwa egzemplarze tego samego obiektu
- Queue<T> - Kolejka FIFO - First In First Out
Służy raczej do organizacji i tymczasowego zapamiętania pojawiających się elementów, aby później przetworzyć je w odpowiedniej kolejności
Dobrze myśleć o kolejkach w kategoriach: worker, producer pattern
Najistotniejsze metody:
Enqueue - dodaje elementy do kolejki (na koniec)
Dequeue - pobiera element z kolejki (z początku)
Peek - pobiera element, ale go nie usuwa
- Stack<T> - stos LIFO - Last In First Out
Podobnie jak kolejka, służy do organizacji pojawiających się elementów do późniejszego wykorzystania
Element ostatnio dodany do stosu znajduje się na jego wierzchołku, możemy ściągną tylko to co na górze
Najważnejsze metody:
Push - wstawia element
Pop - pobiera element z stosu
Peek - zwraca wierzchołek, bez usuwania
STRUMIENIE
Klasa pozwalająca przesyłać ciągi danych z jednego miejsca w drugie
Zazwyczaj z pamięci do jakiegoś urządzenia magazynującego
do pliku, potoku, gniazda sieciowego, pliku zaszyfrowanego
Mają możliwość czytania i pisania asynchronicznego!
Implementują interfejs IDisposable - można użyć ‘using’
Wraz z klasami strumieni mamy klasy pozwalające pisać i czytać do/z strumienia:
StreamWriter i StreamReader
Strumienie – przykładowe klasy
FileStream - do odczytu i zapisu do pliku.
IsolatedStorageFileStream - do odczytu i zapisu do pliku w wydzielonej pamięci
MemoryStream - do odczytu i zapisu w pamięci jako magazyn zapasowy.
BufferedStream - dla poprawy wydajności odczytu i zapisu.
NetworkStream - do odczytu i zapisu za pośrednictwem gniazd sieciowych.
PipeStream - do odczytu i zapisu za pośrednictwem potoków.
CryptoStream - do łączenia strumieni danych z przekształceniami kryptograficznymi.
Przykład pisania do pliku:
FileStream fs = new FileStream("Foo.txt",
FileMode.OpenOrCreate, FileAccess.ReadWrite);
try
{
StreamWriter sw = new StreamWriter(fs);
sw.WriteLine("Hello World!");
sw.WriteLine("Bye!");
sw.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Pola FileMode
`Append` |
Tworzy lub otwiera plik i przechodzi na jego koniec. Wymaga utworzenia obiektu z parametrem FileAccess.Write. |
`Create` |
Tworzy plik, a w razie gdy on już istnieje, zastępuje jego dotychczasową zawartość. |
`CreateNew` |
Tworzy plik, a w razie gdy on już istnieje, generuje odpowiedni `wyjątek`. |
`Open` |
Otwiera nowy plik do odczytu. Jeżeli plik nie istnieje, generowany jest wyjątek. |
OpenOrCreate |
Otwiera plik, a jeżeli ten nie istnieje — tworzy nowy. |
`Truncate` |
Otwiera plik i czyści jego zawartość. |
`Read` |
Dane mogą być jedynie odczytywane. |
`Write` |
Dane mogą być tylko zapisywane. |
`ReadWrite` |
Dane mogą być zarówno zapisywane, jak i odczytywane. |
Obsługa systemu plików – Directory
Zarządzanie katalogami, statyczna klasa Directory
Directory.GetFiles(path);
Directory.GetDirectories(path);
Directory.CreateDirectory(path);
Directory.Delete(path);
Directory.Move(path, path);
Podstawowe informacje o katalogach uzyskamy dzięki DirecotryInfo
Attributes - Atrybuty katalogu
CreationTime - Czas utworzenia katalogu
FullName - Pełna ścieżka do katalogu
LastAccessTime - Czas ostatniego dostępu do katalogu
LastWriteTime - Czas ostatniego zapisu do katalogu
Name Zwraca nazwę katalogu
Parent Zwraca macierzysty katalog
Podobnie do klas do obsługi katalogów mamy klasy do zarządzania plikami:
tworzenie, kopiowanie, przenoszenie, atrybuty plików
klasy File, FileInfo
Komunikacja TCP
Klasy TcpListener, TcpClient
Definiujemy adres IP oraz Port
Tworzymy strumień do komunikacji (NetworkStream)
Tworzymy Reader’a i Writer’a (StreamReader, StreamWriter)
Przykład
TcpClient client = new TcpClient("localhost", port);
NetworkStream stream = client.GetStream();
StreamReader reader = new StreamReader(stream);
StreamWriter writer = new StreamWriter(stream) { AutoFlush = true };
TcpListener listener = new TcpListener(IPAddress.Loopback, port);
listener.Start();
TcpClient client = listener.AcceptTcpClient();
NetworkStream stream = client.GetStream();
StreamWriter writer=new StreamWriter(stream, Encoding.ASCII){AutoFlush = true };
StreamReader reader = new StreamReader(stream, Encoding.ASCII);
WZORCE PROJEKTOWE
Wzorzec projektowy (ang. design pattern) – uniwersalne, sprawdzone w praktyce rozwiązanie często pojawiających się, powtarzalnych problemów projektowych. Pokazuje powiązania i zależności pomiędzy klasami oraz obiektami i ułatwia tworzenie, modyfikację oraz pielęgnację kodu źródłowego.
Zalety wzorców projektowych
nie trzeba rozwiązywać problemów na nowo, budujemy z klocków i wykorzystujemy doświadczenie innych
wspólne słownictwo:
Wykorzystajmy strategię w celu implementacji generowania raportów, wiemy że będzie ich wiele ale klient jeszcze sam nie wie jakie.
często pozwalają lepiej zdefiniować pierwotny problem i rozpoznać rzeczy o których jeszcze nie pomyśleliśmy
myślimy o rozwiązaniu na wyższym poziomie abstrakcji
łatwiejsze utrzymanie kodu, wzorce projektowe tworzone są z myślą nie tylko eleganckiego rozwiązania problemu, ale także rozszerzalności
bazują na głównych założeniach prog. obiek. : abstrakcja, dziedziczenie, hermetyzacja, polimorfizm
Podział wzorców projektowych
kreacyjne - opisują elastyczne sposoby tworzenia obiektów, uniezależniają system od sposobu tworzenia obiektu
strukturalne - opisują sposób konstrukcji struktur obiektowych, korzystają z dziedziczenia i delegacji
czynnościowe- opisują algorytmy i podział odpowiedzialności, charakteryzują sposób interakcji między obiektami
Przykłady
wzorce kreacyjne:
Budowniczy (obiektowy),
Fabryka abstrakcyjna (obiektowy),
wzorce strukturalne:
wzorce czynnościowe/operacyjne:
Łańcuch zobowiązań/odpowiedzialności (obiektowy),
Strategia (obiektowy),
Wzorzec projektowy Strategia
Konkretne algorytmy zaimplementowane są w klasach ConcreteAlgorithm, klasa główna nie zawiera implementacji algorytmu tylko referencję do niej jako Algorithm. Działanie możemy ustawiać w trakcie działania aplikacji setAlgorithm. Klient wykonując metodę performOperation nie wie że jej wykonanie jest delegowane do Algorithm.operation()
Działanie wzorca strategia
Nasza główna klasa Host ma zadanie realizacji jakiegoś algorytmu
Sposobów realizacji tego algorytmu jest wiele
Poszczególne algorytmy wiemy że z czasem będą się zmieniać
efektywniejsze implementacje
zmiana technologii/producenta realizacji zadania
jednocześnie chcemy dostarczyć kilka sposobów realizacji zadania, niech klienci sobie wybiorą
Chcemy dynamicznie zmieniać algorytmy realizujące dane zadanie w zależności od kontekstu
Każdy konkretny algorytm implementuje interfejs Algorithm, przy pomocy którego klasa Host wywołuje uruchomienie danego algorytmu
Klasa Host ma pole będące referencją do Algorytmu
Przykłady wykorzystania:
Generowanie Raportów w różnych formatach: pdf, word, excel, latex, html
Wysyłanie informacji różnymi sposobami: mail, sms, FB messanger, twitter,
Realizacja płatności przez różnych dostawców płatności
Pobranie danych z źródeł które wiemy że mogą się zmieniać: baza, xml, api, api innego dostawcy, nowa wersja api,
Wyodrębnienie algorytmu obliczającego: zaimpelmentowanie go w pierwszej najprostszej wersji gdzie wiemy że inny zespół pracuje nad kolejnymi efektywnymi wersjami
Klient email -> rozpoznawanie spamu, wiemy że z czasem będziemy musieli zmienić algorytm
Aplikacja do edycji zdjęć - dodawanie nowych filtrów
Wzorzec obserwator
Nie przegap gdy coś się ciekawego (w twoim systemie) zdarzy!
Definiuje sposób komunikowania zmian stanu klasy, bądź zdarzeń
Obiekt obserwowany i wiele obiektów obserwujących (oczekujących gdy tylko coś się stanie)
Pozwala w trakcie działania programu na określenie kto ma zostać powiadomiony
Działanie wzorca obserwator
Klasa ConcreteSubject jest naszym podmiotem obserwowanym, tym co dostarcza innym informacji, lub inni czekają na jej zdarzenia
Aby obserwator mógł się zarejestrować u dowolnej klasy podmiotu, tworzymy interfejs Subject z niezbędnymi metodami do subskrybcji i usunięcia powiadamiania.
Metoda notifyObservers() - wywoływana jest przez inną klasę lub sam podmiot w celu wypuszczenia notyfikacji do poszczególnych obserwatorów
Stworzyliśmy interfejs Observer z jedną metodą update, Podmiot na każdym obserwatorze ją wywołuje, wszyscy obserwatorzy mają wspólny interfejs
Podmiot nie zna konkretnych klas Obserwatorów, wystarczy mu tylko jedna jedyna rzecz, wie jak powiadomić obserwatora -> wywołując na nim “update”
Zalety wzorca obserwator
Klasy są luźno powiązane ze sobą
Rozwiązanie jest otwarte na rozbudowę (dodawanie nowych obserwatorów) a zamknięte na modyfikację, raz napisany kod klasy Podmiot nie musi być zmieniany
Obserwatorzy mogą się zarejestrować u wielu Podmiotów
Dane przekazywane są poprzez metodą “update” jednak możliwa jest także sytuacja że przekazujemy poprzez cały obiekt podmiotu i wtedy obserwatorzy biorą sobie co ich interesuje z podmiotu (dobrze by było zdefiniować większy interfejs dla danych Podmiotu)
Przykładowe wykorzystania
Publikacja artykułu w CMS - obserwatorzy: klasy wysyłające powiadomienia do innych serwisów, zaangażowanych osób w pisanie artykułu, aktualizacja RSS’ów,
W systemie zarządzania projektami, podczas akcji wzięcia danego “issue” przez programistę, obserwatorami mogą być klasy: klasa wysyłająca komunikat na slacku/dev chat
W systemie turystycznym, podczas rezerwacji wycieczki, powiadomieni muszą być partnerzy: linia lotnicza, ubezpieczalnia, wynajem samochodów, hotel