Wzorce projektowe w .NET

Transkrypt

Wzorce projektowe w .NET
Daniel Dusiński
Na CD:
Wzorce projektowe
w .NET
W
zorce ułatwiają użycie platformy NET. Są
użyteczne podczas rozstrzygania problemów pojawiających się w okresie tworzenia aplikacji. Pomagają w zaprojektowaniu i wdrożeniu skutecznego rozwiązania. Dokumentują prosty
dobrze funkcjonujący mechanizm oraz wprowadzają wspólne nazewnictwo pozwalające zwięźlej opisywać pojawiające się rozwiązania.
Geneza
Złożony system, który działa, niezaprzeczalnie ewoluował z prostego systemu, który działał... Złożony
system sklecony ze skrawków nigdy nie działa oraz
nie można poprawić go tak, aby działał [ John Gall,
Systemantics:How Systems Really Work and How
They Fail.]
Różne wzorce mogą powstawać w oparciu o odmienne filozofie oraz dążenia projektantów. Przykładowo podstawowym celem wzorców opisanych
w książce Design Patterns (wzorce te będę nazywał
dalej klasycznymi) było uczynienie kodu jak najbardziej elastycznym, a co za tym idzie – dającym się
wielokrotnie wykorzystać.
Inna filozofia może opierać się na spostrzeżeniu,
że w nieustannie ewoluującym środowisku, w którym istnieje aplikacja, każda złożoność powoduje dodatkowe koszty. Potrzebny jest większy nakład pracy oraz lepsi specjaliści, aby stworzyć aplikację. Ponadto wiadomo, że każda dobrze działająca aplikacja jest bardzo prosta, gdy powstaje, a następnie
rozrasta się z biegiem czasu. Tańsze może się więc
okazać stworzenie maksymalnie prostej aplikacji,
spełniającej dzisiejsze potrzeby (a gdyby potrzeby
wzrosły, rozbudowanie jej lub nawet zastąpienie inną aplikacją) niż tej od początku starannie zaprojektowanej, spełniającej wszystkie możliwe przyszłe potrzeby. Tym bardziej, że i tak zawsze będziemy ponosić koszty dopasowywania rozwiązania do zmieniających się warunków pracy.
Taka filozofia będzie sugerować stosowanie początkowo bardzo prostych wzorców (pozwalających
szybko i z dużym prawdopodobieństwem napisać
działającą aplikację), a dopiero z biegiem czasu (w
ciągu życia aplikacji) przekształcać te wzorce w bardziej zaawansowane.
Kolejne podejście uwzględnia fakt, że różni ludzie
pracujący nad aplikacją są zainteresowani różnymi
Autor jest studentem Politechniki Łódzkiej, a także
Członkiem Grupy .NET PL.
Kontakt: [email protected]
44
Rysunek 1. Wzorzec Struktury Warstwowej
jej aspektami i czego innego wymagają od wzorców.
Przykładowo czego innego wymagają administratorzy, którzy będą zarządzać aplikacją, niż piszący ją
programiści. Takie podejście może także uwzględniać fakt wyszkolenia osób biorących udział w projekcie – od słabo wyszkolonego zespołu nie można wymagać stosowania skomplikowanych wzorców,
które bez problemu są wykorzystywane przez doświadczonych programistów.
Poziomy abstrakcji
Każdy wzorzec projektowy zależy zarówno od mniejszych wzorców zawartych wewnątrz niego jak i od
większych wzorców, w których jest zawarty. [Christopher Alexander, The Timeless Way of Building]
Różne wzorce mogą istnieć na różnych poziomach abstrakcji. Możliwe jest także występowanie wzajemnych relacji pomiędzy nimi. Przykładowo
wzorzec Obserwatora jest często używany do zaimplementowania części wzorca Model–Widok–Kontroler.
Jednym z podstawowych wzorców istniejących
na bardzo wysokim poziomie abstrakcji jest Struktura Warstwowa (ang. Layers pattern). Wzorzec ten
opisuje, jak należy podzielić aplikację na warstwy.
Wskazuje, że komponenty składające się na jedną
warstwę powinny być na porównywalnym poziomie
abstrakcji. Tłumaczy, dlaczego komponenty znajdujące się w jednej warstwie powinny komunikować się
tylko z komponentami istniejącymi w tej samej warstwie lub warstwie niższej (pozwala to na zredukowanie zależności i ułatwia dokonywanie ewentualnych późniejszych modyfikacji)
Chociaż wzorzec struktury warstwowej jest, generalnie rzecz biorąc, wystarczający do wielu zastosowań, nie rozstrzyga on wielu kwestii pojawiających
www.software20.org
Software 2.0 11/2004
Czym są wzorce projektowe?
Enterprise Solution Patterns Using
Microsoft .NET
Wzorzec projektowy (ang. design pattern) to zwyczajowo przyjęte
rozwiązanie typowego problemu. Opisuje pewne często spotykane zagadnienie projektowe systemu obiektowego oraz określa jego ogólne rozwiązanie. Zazwyczaj opisane we wzorcu rozwiązanie
może być zastosowane w wielu różnych kontekstach. Wiele wzorców jest także niezależnych od używanego języka programowania.
się podczas projektowania aplikacji. Programując z użyciem
platformy .NET często używa się bardziej wyspecjalizowanego wzorca Aplikacji Trójwarstwowej (ang. Three–Layered Services Application). Wzorzec ten jest po prostu uszczegółowieniem poprzedniego; wszystkie spostrzeżenia dotyczące
Struktury Warstwowej odnoszą się także do wzorca Aplikacji
Trójwarstwowej – ale oczywiście nie na odwrót.
Oczywiście podczas tworzenia poszczególnych warstw
(niezależnie od tego, którego wzorca użyjemy) wewnątrz nich
możemy zawrzeć kolejne wzorce. Będą to już wzorce znajdujące się na jeszcze niższym poziomie abstrakcji. Niejednokrotnie będą występować na poziomie kodu, które – w odróżnieniu od wzorców warstwowych – dotykają architektury, ale są
praktycznie niezależne od kodu.
Przykładowymi sposobami podziału wzorców ze względu
na poziom abstrakcji są:
•
Wzorce Architektoniczne – wyrażają podstawowe struktury organizacyjne systemu. Dostarczają zestawu predefiniowanych podsystemów, określają ich zobowiązania oraz
zawierają wskazówki dotyczące organizacji powiązań między nimi
Na
stronach
Microsoftu
(http://
msdn.microsoft.com/architecture/patterns/
) jest dostępna za darmo bardzo dobra
książka dotycząca wzorców projektowych
używanych na platformie .NET – Enterprise Solution Patterns Using Microsoft .NET.
Zawarte w niej są opisy zarówno wzorców
klasycznych (Obserwator czy Singleton) jak
i wzorców używanych konkretnie na platformie .NET. Praktycznie każdy opisany wzorzec jest poparty rozbudowanym przykładem. Na stronie dostępne są także inne publikacje dotyczące wzorców oraz spraw związanych z architekturą oprogramowania.
•
•
Wzorce Projektowe – dostarczają schematów ulepszających podsystemy lub komponenty składające się na system. Opisują często powtarzającą się strukturę komunikacji, która rozwiązuje problem projektowy w pewnym kontekście
Wzorce Implementacyjne – wzorce niskiego poziomu dostosowane do specyficznej platformy. Opisują jak implementować szczególne aspekty komponentu lub relacji pomiędzy komponentami, używając cech platformy. Wzorce
takie mogą odnosić się nie tylko do czystego oprogramowania, ale np. ułatwiać ostateczną instalację rozwiązania
lub wybór platformy sprzętowej.
Konkretne wzorce
W tym miejscu chciałbym przedstawić zbiór kilku wzorców,
które są w pewien sposób powiązane z platformą .NET. Stosowanie ich jest w znacznym stopniu ułatwione na platformie
.NET lub też są one po prostu w nią wbudowane.
Buforowanie Strony
Wzorzec Buforowania Strony (ang. Page Cache) pozwala na
zwiększenie szybkości działania dynamicznie generowanej
Rysunek 2. Wzorzec Aplikacji Trójwarstwowej
Software 2.0 11/2004
strony web. Skraca czas potrzebny na wysłanie odpowiedzi
do użytkownika bądź też umożliwia obsłużenie większej liczbę użytkowników w tym samym czasie.
Zbudowanie dynamicznej strony WWW (z czym mamy do
czynienia w ASP.NET) może wymagać wielu zasobów systemowych. W rezultacie, podczas gdy wielu użytkowników przegląda daną stronę, może ona działać zbyt wolno. Pomysł rozwiązania tego problemu opiera się na tym, by zapamiętać (zapisać gdzieś w pamięci) wygenerowaną stronę WWW – czyli to, co zostaje zwrócone do użytkownika. W momencie gdy
kolejny użytkownik wysyła żądanie otrzymania strony, nie musimy jej ponownie konstruować; wystarczy zwrócić zapamiętany wcześniej rezultat.
Oczywiście zastosowanie wzorca Buforowania Strony zmusza nas do zastanowienia się, kiedy możemy zwrócić
wcześniej zapamiętaną stronę, a kiedy dla danego żądania
musimy wygenerować ją od nowa. Powinniśmy wiedzieć, po
www.software20.org
45
jakim czasie dane na zapamiętanej stronie będą już przestarzałe (w takiej sytuacji dla identycznego żądania i tak musimy
ponownie generować stronę) oraz czy zawartość danej strony
jest zależna od parametrów przekazanych przez użytkownika wraz z żądaniem. Ponadto zastosowanie tego wzorca daje
oczekiwany efekt tylko w przypadku, gdy wielu użytkowników
wymaga wyświetlenia tej samej zawartości w krótkim odstępie
czasu (zanim zawartość strony ulegnie przedawnieniu).
Generalnie jednak użycie wzorca Buforowania Strony pozwala na znaczne zwiększenie wydajności. Wyobraźmy sobie
stronę wyświetlającą mapę z prognozą pogody, która używa
zewnętrznej usługi webowej. Gdyby w ciągu minuty 100 użytkowników weszło na stronę, to bez zastosowania buforowania nastąpiło by stukrotne wywołanie usługi oraz wygenerowanie mapy pogody na podstawie zwróconych przez nią informacji. Oczywistym jest jednak, że prognoza pogody nie zmieni się istotnie w przeciągu minuty. Zastosowanie buforowania
(powiedzmy z czasem wygasania ustawionym na jedną minutę) pozwoliłoby na jednokrotne wywołanie zewnętrznej usługi
webowej oraz wygenerowanie mapy pogody – w odpowiedzi
na wszystkie pozostałe 99 pytań zwracana byłaby już wcześniej wygenerowana i zapamiętana strona.
Zastosowanie opisywanego wzorca w ASP.NET jest bardzo łatwe. A to wszystko dzięki dyrektywie @OutputCache.
Przykładowo:
<%@ OutputCache Duration="10" VaryByParam="none" %>
<html> ... </html>
Najważniejsze parametry dyrektywy @OutputCache to Duration
– określający w sekundach czas przetrzymywania danej strony w pamięci – oraz VaryByParam określający, czy jeśli zmieniła się wartość parametrów, to należy ponownie generować
stronę, czy też można zwrócić jedną z już przechowywanych.
Jeśli nie chcemy, aby zwracana strona była różna w zależności parametrów żądania, przypisujemy wartość none. Możemy także określić listę parametrów lub wpisać *, aby rozróżnić
zwracaną stronę ze względu na wszelkie wartości parametrów. Możliwe jest także stosowanie innych parametrów (np.
VaryByControl, VaryByHeader, ...)
Dobrym zwyczajem jest stworzenie na wstępie, a następnie przetestowanie strony bez używania buforu (które mogło
by powodować komplikacje w fazie testów). Włączamy go dopiero pod koniec, gdy staramy się skonfigurować stronę dla
uzyskania największej możliwej wydajności.
Kontroler Strony
Wzorzec Kontrolera Strony (ang. Page Controller) pokazuje jedną z możliwych dróg implementacji bardzo popularnego wzorca Model–Widok–Kontroler (ang. MVC, Model–View–
Controller). Pozwala on na odizolowanie interfejsu użytkownika od logiki aplikacji (jest to bardzo pożyteczne, zważywszy
na fakt, że interfejs aplikacji internetowej zmienia się znacznie
częściej niż logika. Dzięki temu, że logika jest niezależna od
interfejsu, łatwiej jest zmodyfikować wygląd strony). Wzorzec
ten przydaje się, gdy kolejne strony WWW są tworzone dynamicznie, ale nawigacja pomiędzy nimi jest generalnie statyczna.
Pomysł wzorca Kontrolera Strony polega na tym, że istnieje centralna klasa zwana Bazowym Kontrolerem, która imple-
46
mentuje podstawowe zachowanie obsługujące żądania HTTP
oraz dokonujące uaktualnienia modelu. Bazowy kontroler zawiera ponadto wszystkie powszechnie wymagane funkcje,
które służą zarządzaniu sesją czy utrzymywaniu bezpieczeństwa.
Podczas stosowania tego wzorca dla każdej osobnej strony WWW (formularza web) jest tworzony indywidualny Kontroler Strony, dziedziczący z Kontrolera Bazowego. Ten indywidualny Kontroler Strony implementuje każde zachowanie
specyficzne dla danej strony.
Wzorzec Kontrolera Strony jest o tyle ważny dla .NET, że
jest domyślnie zaimplementowany w ASP.NET. Nawet najprostsza aplikacja pisana przy użyciu Visual Studio .NET jest
oparta o wzorzec Kontrolera Strony.
Przekazywanie Obiektu
Celem wzorca Przekazywania Obiektu (ang. DTO, Data
Transfer Object) jest przyspieszenie komunikacji poprzez
sieć. Zanim jednak opiszę samą ideę wzorca, zastanówmy
się chwilę nad charakterem przesyłania danych. Do dyspozycji mamy dwa wskaźniki opisujące przepustowość (ilość danych, które można przesłać w jednostce czasu) oraz opóźnienie (czas pomiędzy wysłaniem bloku danych a otrzymaniem
go przez odbiorcę). O ile przepustowość możemy w dużym
stopniu regulować (np. uzyskując dostęp do szybszego połączenia z internetem), o tyle na redukcję opóźnienia często
nie jesteśmy w stanie wpłynąć (szczególnie jeśli dane mają
być transmitowane przez internet). W dzisiejszych warunkach
opóźnienie jest o wiele większym ograniczeniem niż przepustowość; często przesłanie kilku bitów zajmuje tyle samo czasu co kilku tysięcy.
Jak więc poradzić sobie w sytuacji, gdy projektując rozproszoną aplikację w celu obsługi pewnej usługi musimy wykonać wiele wywołań zewnętrznych funkcji(muszących przejść
przez sieć)? Oczywiście takie wielokrotne wywołanie zajmie
sporo czasu, właśnie ze względu na opóźnienie. Dobrym rozwiązaniem tej sytuacji jest zastosowanie wzorca Data Transfer Object.
Rozwiązanie problemu polega na utworzeniu obiektu
DTO, który będzie przechowywał wszystkie dane niezbędne do przeprowadzenia wywołania zewnętrznych funkcji. Tak
modyfikujemy sygnaturę zewnętrznej metody, aby przyjmowała jako parametr obiekt DTO oraz wypełniała go wszystkimi niezbędnymi danymi. Po wykonaniu jednego zewnętrznego wywołania tej metody, aplikacja zachowa lokalną kopię obiektu DTO. Wszystkie kolejne wywołania będą się od-
Rysunek 3. Ogólna struktura wzorca Kontrolera Strony
(bardzo podobna do MVC)
www.software20.org
Software 2.0 11/2004
Bibliografia
•
•
•
Rysunek 4. Struktura dziedziczenia podczas wykorzystania
Kontrolera Strony w aplikacji zawierającej wiele stron. Często
wprowadza się także więcej poziomów dziedziczenia.
W prostych aplikacjach rolę Kontrolera Bazowego może pełnić
już klasa System.Web.UI.Page wbudowana w platformę .NET
woływały do wyników zapamiętanych w DTO bez kłopotliwego opóźnienia związanego z przesyłaniem danych przez sieć.
Jest oczywiste, że zredukowanie ilości zewnętrznych wywołań podnosi wydajność aplikacji.
Na platformie .NET wykorzystanie wzorca Przekazywania Obiektu jest szczególnie proste. A wszystko to za sprawą
udostępnionego obiektu typu DataSet. Obiekty tego typu pozwalają w łatwy sposób przechowywać różne dane (w strukturze przypominającej relacyjną bazę danych), a ponadto wyjątkowo łatwo jest jw transmitować przez sieć (wbudowana
opcja konwersji do XML oraz wczytywania danych ze strumienia lub dokumentu XML do obiektu DataSet). Użycie typowanych DataSetów (klas dziedziczących z DataSet, ale używających dodatkowo zdefiniowanych przez użytkownika schematów XML Schema) pozwala w większym stopniu zachowywać
tożsamość transmitowanych danych; zostają zachowane typy,
a do konkretnych danych możemy odwoływać się po nazwie –
a nie poprzez wyciąganie ich z odpowiednich kolumn tabel.
Interfejs Usługi
Podczas projektowania aplikacji chcemy jej część udostępnić
innym użytkownikom poprzez sieć. Celem, jaki sobie postawiliśmy, jest umożliwienie łatwej współpracy udostępnionej usługi z jak największą liczbą różnorodnych systemów (jest to zagadnienie kluczowe dla aplikacji), a ponadto chcielibyśmy oddzielić elementy odpowiedzialne za komunikację z zewnętrznymi systemami od elementów logiki (dzięki takiemu rozwiązaniu rezerwujemy sobie możliwość zmiany logiki usługi bez
ingerencji w sposób, w jaki jest ona używana przez klientów).
Wzorzec Interfejsu Usługi (ang. Service Interface) ułatwia
nam to zadanie. Rozwiązanie polega na zaprojektowaniu udostępnianej części aplikacji jako zbioru usług, z których każda
posiada dobrze określony interfejs, poprzez który klienci mogą kontaktować się z daną usługą. Interfejs stanowi swego rodzaju kontrakt, który (jeśli jest spełniony przez obie strony)
pozwala na łatwą komunikację pomiędzy klientem a dostawcą usługi.
Software 2.0 11/2004
Gamma, Helm, Johnson, Vlissides, Design Patterns: Elements
of Reusable Object─Oriented Software, Addison--Wesley, 1995
Towbridge, Mancini, Quick, Hohpe, Newkirk, Lavigne, Enterprise Solution Patterns Using Microsoft .NET, Microsoft, 2003
Buschmann, Frank, Patterns─Oriented Software Architecture,
John Wiley & Sons Ltd, 1996
Należy także zwrócić uwagę na fakt, że różni użytkownicy
mogą chcieć używać różnych sposobów komunikacji z usługą (stosując różne technologie czy protokoły). Ponadto część
użytkowników może chcieć wykorzystywać usługę do specyficznych celów, które wymagają optymalizacji usługi pod kątem danego zastosowania. Aby spełnić tak różnorodne wymagania, możemy zaprojektować większą liczbę interfejsów, poprzez które będą dostępne usługi naszej aplikacji (możemy
dać różne interfejsy użytkownikom oczekującym różnych sposobów komunikacji z usługami lub wymagającym specyficznych optymalizacji). Generalnie należy jednak minimalizować
ilość interfejsów, przez które udostępniamy daną usługę. Każdy kolejny interfejs powoduje dodatkowy nakład pracy podczas zmieniania logiki usługi.
Do zaimplementowania interfejsu usługi na platformie
.NET możemy użyć XML Web Services. Taka implementacja
zapewnia dostęp do usługi różnorodnym systemom, ponieważ opiera się na dobrze rozpowszechnionych standardach
internetowych XML, HTTP czy SOAP.
Brama Usługi
Wzorzec Bramy Usługi (ang. Service Gateway) jest przydatny
podczas pisania aplikacji wykorzystujących zewnętrzne usługi WWW. Jest to wersja wzorca Bramy (ang. Gateway) zorientowana na komunikację pomiędzy różnymi systemami z wykorzystaniem usług.
Każda usługa definiuje warunki, jakie muszą spełnić aplikacje korzystające z niej (określa pewien interfejs). Aby aplikacja mogła komunikować się z daną usługą, musimy dostosować ją do tego interfejsu (np. używać odpowiednich protokołów komunikacyjnych). Nawet jeżeli interfejs usługi jest
stworzony w oparciu o tą samą technologię co tworzona
przez nas aplikacja, prawdopodobnie używa innych typów lub
formatów danych w stosunku do logiki naszej aplikacji (np. my
określamy prędkość w km/h, podczas gdy zewnętrzna usługa
wymaga podawania parametrów w m/s). Zatem przed wysłaniem danych do usługi, jesteśmy zmuszeni dokonać pewnych
przekształceń.
Większym problemem jest jednak to, że prawdopodobnie nie mamy żadnej kontroli nad zewnętrzną usługą. Nie mo-
Rysunek 5. Kooperacja dostawcy oraz klienta usługi
z wykorzystaniem wzorców Interfejs Usługi oraz Brama Usługi
www.software20.org
47
żemy zagwarantować, że jej interfejs nie zostanie zmodyfikowany w pewnym momencie lub też cała usługa stanie się niedostępna (co zmusiło by nas do znalezienia innej usługi o podobnych właściwościach).
Rozwiązanie powyższych problemów polega na umieszczeniu całego kodu zależnego od zewnętrznej usługi w osobnym komponencie. Komponent ten sprostałby wymaganiom
stawianym przez interfejs zewnętrznej usługi oraz dokonywałby wszelkich translacji pomiędzy reprezentacją danych stosowaną przez naszą aplikację, a tą stosowaną przez wybraną
usługę. Ponadto przydatne jest, aby tworzona brama zawierała mechanizmy służące wyszukaniu odpowiedniej usługi czy
też zabezpieczenia przed zmianą adresu usługi. Może to być
zrealizowane na wiele różnych sposobów: pobieranie wartości z pliku konfiguracyjnego bądź też wyszukiwanie w katalogu Universal Description, Discovery and Integration.
Dzięki zastosowaniu Bramy Usługi jesteśmy w stanie dość
łatwo dostosować aplikację do zmian w usłudze (dotykają one
tylko i wyłącznie jednego komponentu ─ Bramy). Aczkolwiek
zawsze należy rozważyć opłacalność stosowania tego typu
wzorca ─ w przypadku wielu prostych aplikacji, które używają
raczej niezmieniających się usług, nakład pracy poniesiony na
stworzenie bramy może nie być uzasadniony.
istnieje w kilku wersjach, np. przytoczony w artykule wzorzec
Struktury Warstwowej istnieje w wersji luźnej (ang. Relaxed)
oraz przedstawionej tu wersji ścisłej (ang. Strict) ─ należy wybrać tą, która najlepiej pasuje do tworzonej aplikacji.
Podsumowanie
Niniejszy artykuł oczywiście nie wyczerpuje zagadnienia
wzorców projektowych na platformie .NET. Jest co najwyżej
wierzchołkiem góry lodowej, jaką odkryje osoba chcąca dokładniej poznać to zagadnienie. Dobrym punktem startowym
dla dalszych poszukiwań są podane w ramkach odnośniki do
stron internetowych oraz bibliografia.
Po dokładniejszym poznaniu, wzorce okazują się bardzo
przydatne dla programisty, i są wręcz niezbędne dla projektanta. Pozwalają na łatwiejszą współpracę pomiędzy ludźmi
(ujednolicają nazewnictwo) oraz zwiększają jakość tworzonego oprogramowania.
Od siebie chciał bym jeszcze dodać, że wzorce nie powinny w żaden sposób ograniczać kreatywności. Każdy wzorzec
jest rozwiązaniem ogólnym, które czasem można lepiej dopasować do konkretnego rozwiązania. Często ten sam wzorzec
W Sieci
•
•
•
•
•
•
•
•
48
Zwięzłe opisy najbardziej klasycznych wzorców, pochodzących
z wydanej w 1995 roku książki Design Patterns: Elements of
Reusable Object – Oriented Software autorów popularnie nazywanych The Gang of Four (Gamma, Helm, Johnson, Vlissides). Strona pozwala na bardzo szybkie i łatwe zapoznanie się
z podstawowymi wzorcami projektowymi. Wszystkie opisy są
poparte przykładami w C#.
http://www.dofactory.com/Patterns/Patterns.aspx
Zbiór linków odnoszących się do artykułów traktujących
o wzorcach projektowych z zastosowaniem C#
http://c2.com/cgi-bin/wiki?CsharpPatterns
Sekcja poświęcona wzorcom projektowym jednego z popularniejszych portali dla programistów C#
http://www.c-sharpcorner.com/Design.asp
Protokół UDDI (Universal Description, Discovery and Integration)
http://www.uddi.org/
www.software20.org
Software 2.0 11/2004

Podobne dokumenty