Wzorce projektowe C#

DekoratorWzór.1 Dekorator. Śmiało mogę powiedzieć, że jest to jeden z najważniejszych wzorców projektowych. Można powiedzieć, że jest on prawie częścią każdego systemu, ponieważ nie ma co ukrywać, jest on pożyteczny i użyteczny nawet do dzisiaj.

Dekorator pozwala dodać istniejącej klasie nowe zachowanie.  Nie zmienia on jednak działanie klasy podstawowej. Oznacza to, że spełnia następujące zasady S.O.L.I.D:

 

  • pojedynczej odpowiedzialności
  • zasady otwartej-zamkniętej

Dekorator, jak i jego klasa bazowa mają tylko jedno zadanie. Dekorator pozwala nawet rozbić zachowanie klas, jeśli widzisz, że te rozbicie jest potrzebne. W tym przykładzie to pokażę.

Chciałbyś się zapewne nauczyć tego wzorca. Zamiast tego przyjrzymy się przykładzie Pizzy. Kod jest do pobranie na końcu artykułu.

Wyobraź sobie, że masz następujący zestaw klas.

SingletonWzór.2 Wzorzec projektowy Singleton urósł na podstawie prostego pomysłu : Co jeśli chcesz mieć instancję jednego pewnego komponentu w swojej aplikacji, bez względu na to ile razy go utworzysz  przy pomocy konstruktora. 

Przykładowo masz klasę, która ładuje pewne dane z bazy tylko raz w cyklu życia w aplikacji.

 

Taka klasa jest dobrym kandydatem na  wzorzec projektowy Singleton. Po co obciążać pamięć swojej aplikacji obiektami, które mają dokładnie te same dane.

Wzorzec projektowy Singleton można zaimplementować w C# na kilka sposobów

Chain of ItemsWzór.3 Pomyśl, że pracujesz nad daną aplikacją i okazuje się, że coś zostało źle zaprogramowane. Kto jest za to odpowiedzialny? Ty? Twój szef, który dał Ci zadanie? A może analityk aplikacji? A może osoba biznesową źle zrozumiała proces, który próbujesz zaprogramować

To jest właśnie łańcuch odpowiedzialności. Dodatkowo jest to także wzorzec projektowy. Jego zadaniem jest wywołać elementy systemu jeden po drugim.

Implementacja tej filozofii wydaje się prosta. Potrzebna Ci jest lista jednokierunkowa oraz klasa abstrakcyjna, która posłuży za szablon do działania

Event BrokerWzór.4 Poprzedni przykład wzorca  jest trochę sztuczny. W prawdziwym świecie będziesz chciał, aby stwory, zyskiwały lub traciły bonusy w dowolny sposób. To jest coś, czego nie zrobisz, mając listę jednokierunkową czy łańcuch zobowiązań

Nie chciałbyś permanentnie zmieniać dane swojego stwora. Chcesz, aby ta zmiana działa w tymczasowo i tylko wtedy gdy jest ona potrzebna.

 

Potrzebny nam będzie scentralizowany komponent. Będzie on trzymał listę wszystkich modyfikatorów dostępnych w grze. Będziemy wyszukiwać te modyfikatory i dodawać je do stworów mając pewność, że wszystko odbywa się w dobrej kolejności.

Ten komponent nazywa się Event Broker. Co ciekawe jest on połączeniem kilku wzorców projektów jak Mediator, Observer, Command 

NullWzór.5 W pewnym wpisie omówiłem, że niektórzy architekci nie lubią wartość NULL, która występuję we wszystkich typach referencyjnych. Co, jeśli chciałbyś się tego pozbyć i zapomnieć o tym, jak C# 8 próbuje rozwiązać ten problem

No cóż, wtedy korzystasz ze wzoru projektowego Null Object

Ask meWzór.6 Powiedzmy, że chcesz przypisać wartość do zmiennej. Wystarczy do tego prosty kod jak : a = 4

Zmienna została zmieniona, jednak nie ma żadnej historii, aby zapisać, że takie zdarzenie nastąpiło. Nikt nam przecież nie może dać poprzedniej wartości tej zmiennej. Nie możemy przecież zapisać i serializować faktu zmiany wartości.Czyż nie?

 

Co, jeśli chciałbyś cofnąć swoje działanie przypisania zmiennej. 

No cóż, skoro nie masz historii, nie masz informacji o poprzedniej wartości to zrobienie takiej operacji jest niemożliwe

Wzorzec projektowy "Command" stawia sobie ten właśnie cel. Zamiast działać na obiektach bezpośrednio, co by było, gdybyśmy mogli wysłać, im polecenia i instrukcje co trzeba zrobić.

Klasa reprezentująca tę polecenie będzie opisem tego, co trzeba zrobić i jak.

ConnectingWzór.7 Większa część kodu, który piszemy, ma różne komponenty (klasy), które gadają ze sobą poprzez bezpośrednią referencję. Jednakże zdarzają sytuację, w których chciałbyś ,aby obiekty nie były świadome swojej egzystencji. 

Może chciałbyś, aby obiekty były świadome siebie, ale nie chcesz przekazywać za każdym razem referencyjni do nich

W końcu za każdym razem, gdy wysyłasz referencje obiektu, to przedłużasz jego życie w pamięci.

Mediator jest wzorce projektowym, który ma ułatwić komunikację pomiędzy obiektami, które fachowo nazywam kompomentami 

Mediator ma dostęp do każdego komponenty . Oznacza to, że powinien być on albo publiczny polem statycznym, albo singletonem, który jest wstrzykiwany do każdego komponentu, który go potrzebuje.

SaveStateWzór.8 Używając wzorca projektowego Command, zapisywaliśmy akcje, które wykonywaliśmy w systemie i dzięki temu mogliśmy później te akcje cofać. 

Istnieje wzorzec projektowy, który też spełnia tę funkcję. Wzorzec Memento przechowuje stan systemu w dedykowanym obiekcie tylko do odczytu.

Ten token będzie użyty do przywrócenia systemu

How to?Wzór.9 Mam więc listę napisów (Stefan,Jarek,Bajek) i chciałbyś je wyświetlić w specyficzny sposób. Możesz to zrobić po przecinku : (Stefan,Jarek,Bajek).

A może wyświetlić je po kropce : (Stefan.Jarek.Bajek)

A może wyświetlić je korzystają z elementu <ul><li> w HTML

  • Stefan
  • Jarek
  • Bajek

Każdy z tych formatów wymaga swojej logiki przetłumaczenia listy napisów na odpowiedni wynik.

Można by powiedzieć, że strategii.

Co, jeśli bym chciał wybierać tę strategię w trakcie działania programu? Zobaczmy jak wzorzec projektowy strategi działa.

TemplateWzór.10 Wzorzec projektowy Strategy i Template Method są podobne do siebie.  Jakie jednak są różnice . Wzorzec projektowy Strategy używa interfejsów i kompozycji do określenia zachowania algorytmu. 

Natomiast Template Method używa dziedziczenia . Klasa ustala szkielet algorytmu i jego kolejności natomiast implementacja poszczególnych kroków jest w zupełnie innym miejscu. Tym zajmą się klasy, które będą dziedziczyć po tej klasie szablonu

Istnieje mnóstwo przykładów tego wzorca. Często jest on reprezentowany przez artykuły jedzenia. Przykładowo masz tutaj szablon robienia naleśnika.

EventWzór.11 Wzorzec projektowy Obserwator polega na tym,że jeden komponent informuje drugi, że coś się wydarzyło. Ten wzorzec jest używany wszędzie. W aplikacjach WPF i Windows Forms, jak i ASP.NET Web Forms i wszelkiej formie UI w platformie .NET mamy system zdarzeniowy.

Ten system do program wysyła obiekty określający jakie zmiany zaszły przy interakcji z użytkownikiem.

Wzorzec Obserwator jest popularny i potrzebnym wzorcem w wielu miejscach w aplikacji nawet bez twojego kodu.

Nic dziwnego, że twórcy C# postanowili zaszyć ten wzorzec do języka w postaci słowa kluczowego event 

IObservableWzór.12 Mówiąc o wzorcu projektowym Observer warto także wspomnieć o tym, że w najnowszym .NET-cię mamy dwa ciekawe interfejsy generyczne : IObserver<T> i IObservable<T>. Te dwa interfejsy miały swoją premierę z biblioteką Reactive Extensions (Rx)

Ich celem było obsłużyć strumień reaktywnych informacji. 

 

Zobaczmy co interfejs IObservable<T> w ogóle robi.  Jaki jest on mechanizm? Do podpinania się do zdarzeń używamy operatora += . W tym interfejsie będziemy używać metody Subscribe() . Ta metoda bierze za parametr tylko interfejs IObserver<T>

Wszystko to jest interfejsem więc w przeciwieństwie do delegat i zdarzeń nie mamy tutaj określonego mechanizmu działania. Możemy z tymi interfejsami zrobić co chcemy

A co z odpinanie się od obserwacji, czyli od subskrypcją. Idea ta jest wpierana przez interfejsy Metoda Subscribe() zwraca obiekt interfejsu  IDisposable. Będzie to token, który metodę Dispose() odłączy się obserwacji obiektu observable.

MachnineWzór.13 Nasze zachowanie jest sterowane przez nasz stan. Jeżeli nie wyspałeś się dobrze to twoje zachowanie zapewne się zmieni. Jeżeli wypiłeś dużo alkoholu to nie będziesz prowadził samochodu.  Stany więc decydują o tym co możesz i czego nie możesz zrobić.

Możesz oczywiście przejść z jednego stanu w drugi. Energetyk/Kawa może być takim wyzwalaczem, który zmienia twój stan.  Poranne ćwiczenia mogę Cię też bardziej rozbudzić.

Wzorzec projektowy State jest bardzo prosty. Stan kontroluje zachowania, a sam stan może zostać zmieniony.  Jak jednak ten stan zobrazować. Możemy to zrobić na dwa sposoby:

  • Stany są klasami z zachowaniami i te zachowania powodują zmianę z jednego stanu na drugi
  • Stany i przejścia są typami wyliczeniowymi. Mam specjalny oddzielny komponent, który wykonuje te przemianę stanu.

Oba te rozwiązania są właściwe. Drugi jest najbardziej popularny i kto wie być może już go użyłeś w swojej pracy. Pierwszy wymaga stworzenia wielu klas więc wydaje się on bardziej skomplikowany. Zauważ, że o ile wzorce projektowe mogą wydawać się cool czasami wygrywa rozwiązanie, które jest bardziej przejrzyste niż bardziej złożone. W końcu inni programiści muszą zrozumieć co chciałeś osiągnąć w kodzie

FactoryWzór.15 Cześć witaj ponownie we wpisie na temat wzorów projektowych. Oprócz opisywania znanych wzorców projektowych chce także dać swoje 5 groszy i wyjaśnić, że nawet książkowe wzorce projektowe we współczesnym świecie mają lepsze alternatywy.

Dziś spojrzymy na wzorzec projektowy Fabryki.

Po co na fabryka, która ma wypluwać określone instancje obiektów.

Spójrz na ten przykład.

BuilderWzór.16 Hej dawno nie pisałem coś na temat wzorców projektowych, a jeszcze trochę ich zostało . Wzorzec projektowy Budowniczy zajmuje się utworzeniem złożonego obiektu i jego zadaniem jest ten proces uprościć. 

Jest to jeden ze wzorców, który przetrwał próbę czasu i nadal można zobaczyć jego użycie w różnych językach programowania nie tylko w C#. 

Technicznie mógłbyś uznać, że "StringBuilder" jest przykładem wzorca "builder" jak sama jego nazwa wskazuje.

CloneWzór.17 Prototype jest wzorcem, którego zadaniem jest stworzenie swoich kopii do dalszej modyfikacji. Jest to ostatni wzorzec z kategorii "Creational Patterns", który został opisany na tym blogu. 

Prototype może być użyty w każdej klasie. Wystarczy w niej stworzyć metodę, która umożliwi stworzenie dokładnej kopii tego obiektu.

Musisz przyznać, że pomysł na szybkie tworzenie obiektu jest ciekawy. Zamiast tworzyć jakieś Fabryki, Budowniczych, dlaczego po prostu nie klonować istniejących obiektów i tak przyspieszać ich tworzenie w kodzie.

Sam wzorzec projektowy Prototype. Nie wymaga jakieś diagramu UML. Szczerze to patrzenie na takie coś może bardziej cię skołować.

AdapterWzór.18 Być może zdarzyło Ci się posiadać ładowarkę do telefonu ze złym wejściem.W takiej sytuacji jesteś zmuszony kupić albo nowy kabel z prawidłowym dla Ciebie wejściem, albo adapter.

Być może jako podróżnik nosisz ze sobą adapter, aby podłączyć urządzenie z  Europejskim wejściem do socketów amerykańskich i brytyjskich. 

Wzorzec projektowy adapter polega na tym, że mamy już jakiś interfejs, ale nie podoba nam  się jego wejścia albo wyjścia więc nakładamy na niego swój adapter, który nam da to, co chcemy.

Tak jak każdy wzorzec i ten ma swój diagram UML

BridgeWzór.19 Programowanie obiektowe tworzy wiele problemów, jeśli nie będziemy się pilnować. Jeden z tych problemów nazywa się "state space explosion" . Ten problem polega  na tym, że liczba encji, czyli klas wręcz może eksplodować, gdy próbujemy opisać każdy możliwy stan danego przedmiotu

Załóżmy, że w swoim programie masz kwadraty i prostokąty, które jeszcze mają inne kolory i inne style ich renderowania.

W takim wypadku ile byś klas stworzył, aby to przestawić.

Istnieje parę wzorców projektowych, które starają się rozwiązać ten problem. Na przykład wzorzec "dekorator". Możesz też do tego problemu podejść jeszcze inaczej i określić, że dana właściwość jak "kolor" kwadratu i prostokąta to po prostu typ wyliczeniowy w tej klasie.

Z drugiej strony, jeśli kolor ma coś więcej niż informację o swoim stanie, czyli nasz kolor musi także zawierać informację o swoim zachowaniu wtedy typ wyliczeniowy nam nie wystarczy. 

Natomiast skończymy wtedy z grupą warunków if-else, które będą wykonywał akcję w zależności od wartości tego typu wyliczeniowego. Zapewne ten kod  także wyląduje w jakieś niezależnej klasy od figur.

Czy można to zrobić lepiej?

KompozycjaWzór.20 Czym jest ten kompozyt? Dlaczego zalicza się go do wzorców "Structural Patterns"?

Nasze obiekty składają się z innych obiektów. Najłatwiejszym przykładem kompozytu jest klasa, która implementuje interfejs IEnumerable<T> gdzie T jest tym innym obiektem. Czyli najłatwiejszym kompozytem jest klasa, która zachowuje się jak kolekcja innych obiektów?

Nie, jeszcze czegoś tutaj brakuje.

Nie musi to być koniecznie interfejs  IEnumerable<T>  bo w .NET taki interfejsów do budowania kolekcji jest więcej : Collection<T>, List<T>.

Alternatywnie twój kompozyt może mieć właściwość, która wystawia listę innych obiektów. Zazwyczaj tak jest, ale traktowanie kompozytu jako kolekcję też ma swoje zalety.

O co chodzi więc z tym wzorcem? O ile przykład z tworzeniem swojej kolekcji jest prosty i zrozumiały to zapomniałem zaznaczyć bardzo ważną rzecz.

Façade Wzór.21 Czasami chcemy współpracować z zaawansowanym, skompilowanym systemem w bardzo prosty sposób. Czasami chcemy się zabezpieczyć przed zmianami pochodzących od tego złożonego systemu. W Domain Driven Desing nawet na to się mówi "warstwę anty korupcyjną" (Anti-Corruption Layer). 

My jednak dzisiaj mówimy o wzorcu projektowym "Fasada", który istnieje i będzie istniał zawsze. 

Przykładem fizycznym może być twój dom, który ma skomplikowane połączenia elektryczne, ale ostatecznie Ciebie interesuje fakt, że jak wciśniesz guzik to zapali Ci się światło.

W prawdziwym życiu pisałem wiele fasad do skomplikowanych usług. Czasem rola fasady też polegała na tłumaczeniu jednego API pełnego polskich nazw na fasadę, która zwróci te dane w obiektach i polach po angielsku. Chociaż można się kłócić, że ten ostatni przykład podchodzi pod adapter.

Jaka jest różnica między adapterem a fasadą? Bo na pierwszy rzut oka oba wzorce projektowe wydają się bardzo podobne. Oto moja analogia.

ProxyWzór.23 We wzorcu projektowy "Dekorator" widzieliśmy jak można dodawać kolejne funkcjonalności bez zmiany oryginalnego zachowania. Wzorzec Proxy próbuje osiągnąć to samo tylko gorzej. Warto zaznaczyć, że ten wzorzec nie ma jednej słusznej implementacji. Wiele osób podchodzi do tego wzorca na wiele sposobów. 

Gdyby pada słowo "Proxy" to zazwyczaj mówimy o pośredniku komunikacyjnym między serwerami, które gadają do siebie.

Wzorzec projektowy Proxy też jest takim pośrednikiem między obiektami i jego rola polega na zabezpieczeniu,rozszerzeniu, zmodyfikowaniu jakieś innej funkcji systemu, która jest  pod nim.

W zależności od celu wzorzec ten będzie miał inną implementację. Dlatego nie ma on jednego dobrego podejścia.

Spójrz więc na te różne podejścia do tego wzorca.

InterpreterWzór.24 Celem wzorca "Interpreter" jest zinterpretować dane wyjściowe zazwyczaj w formacie tekstowym, tak abyśmy mogli wykonać specyficzne akcje. Jednakże dane wyjściowymi nie muszą być koniecznie w formacie tekstowym.  

"Interpreter" jest powiązany z kompilatorem. Warto zaznaczyć, że oba pojęcia nie mówią dokładnie o tym samym, chociaż można ich używać zamiennie.  Przypadku języków programowania różnice są takie:

VisitorWzór.25 Drogi czytelniku omówiliśmy prawie wszystkie wzorce projektowe z "Gang of Four". Do skończenia tej kolekcji wpisów pozostało nam omówić ostatni wzorzec projektowy, a jest nim wzorzec projektowy "Visitor".

Jak najlepiej wyjaśnić ten wzorzec?

Najlepiej jest od razu przeskoczyć do przykładu.

Powiedzmy, że mamy następujące wyrażenie matematyczne, które dla ułatwienia składa się tylko z liczb (możliwie ułamkowych) i operatora odejmowania.

Oto przykład takiego wyrażenia : (1.0 - (2.0 - 3.0))

Chcemy teraz zapisać te wyrażenie matematyczne w sposób obiektowy.

Wszystkie Kategorie