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