Gateway [Brama]

poniedziałek, 26 maja 2008 0 komentarze

Wzorce dotyczące architektury aplikacji biznesowych
Wzorce podstawowe - Gateway [
Brama]

Obiekt, który hermetyzuje dostęp do zewnętrznych systemów lub zasobów.

Rzeczywiste oprogramowanie rzadko "żyje" w izolacji. Nawet w przypadku, gdy oprogramowanie zostało przygotowane w postaci czystego zorientowanego obiektowo systemu, często zachodzi konieczność jego współdziałania z elementami, które nie są obiektami np. tabele relacyjnej bazy danych, struktury danych XML itp.

W celu dostępu do zasobów zewnętrznych, zwykle używane jest specjalnie przygotowane w tym celu API dla tych zasobów. Jednakże API tego typu, biorąc pod uwagę charakter tych zasobów, w naturalny sposób jest czymś skomplikowanym. Każdy kto chciałby zrozumieć zasób musi zrozumieć jego API: czy jest to JDBC i SQL dla relacyjnych baz danych, czy API W3C lub JDOM dla XML-a. Powoduje to nie tylko trudności w zrozumieniu oprogramowania, ale również powoduje, że jest ono o wiele trudniejsze do zmiany, gdy w przyszłości wymagane będzie przesłanie danych pochodzących z relacyjnej bazy danych do XML-a.

Rozwiązaniem jest opakowanie całości specjalizowanego kodu API klasą, której interfejs wygląda jak regularny obiekt. Inne obiekty dostają się do zasobu poprzez tak stworzoną bramę [GATEWAY] a obiekt bramy transformuje proste wywołania metod na stosowne wywołania specjalizowanego API.

JAK TO DZIAŁA ?
W rzeczywistości Gateway jest prostym wzorcem typu wrapper. Wykorzystanie wzorca polega na przeglądnięciu API dla określonego zasobu / systemu zewnętrznego, stwierdzeniu co jest wymagane z punktu widzenia projektowanego systemu oraz utworzeniu jak najprostszego API spełniającego rolę bramy dla skomplikowanego niejednokrotnie API systemu zewnętrznego.

Należy pamiętać, aby starać się utrzymać bramę tak prostą jak to tylko możliwe. Należy skupić się na zasadniczych zadaniach adaptacji serwisu zewnętrznego oraz dostarczenia dobrego punktu do stubbingu (przygotowanie stub-ów czyli elementów po stronie klienta zajmujących się tworzeniem i wysyłaniem zleceń). Należy starać się minimalizować zakres odpowiedzialności bramy tak by wykonywała jedynie wymienione zadania. Całość bardziej skomplikowanej logiki powinna być umieszczana w elementach stanowiących warstwę kliencką dla bramy.

Często dobrym pomysłem jest użycie mechanizmu generacji kodu w celu utworzenia klasy bramy na podstawie definicji struktury zasobu zewnętrznego. W tym celu można użyć zarówno meta danych dla tabel bazodanowych jak również schematu DTD w przypadku XML-a. Będąca rezultatem brama jest "głupia" jednak spełnia swoją rolę, inne obiekty przeprowadzają bardziej skomplikowane manipulacje.

Czasem dobrą strategią jest zbudowanie bramy składającej się z więcej niż jednego obiektu. Oczywistą formą jest użycie dwóch obiektów: back-end i front-end. Back-end działa jak minimalna nakładka dla zasobu zewnętrznego i nie upraszcza w ogóle API zasobu zewnętrznego. Front-end następnie transformuje niewygodne API na takie które jest dogodne do użycia przez projektowaną aplikację. To podejście stosuje się w przypadku, gdy zarówno opakowanie zewnętrznego serwisu jak również adaptacja do określonych wymagań są widocznie skomplikowane. W tym podejściu każda odpowiedzialność jest obsługiwana przez pojedynczą klasę.

Przygotowano na podstawie:
Martin Fowler: Patterns Of Enterprise Application Architecture

2 komentarze

Wzorce projektowe

Wzorce projektowe to udokumentowane reużywalne rozwiązania dla problemów projektowych. Cechy wzorców projektowych:

  • posiadają nazwy: Composite, Singleton ....
  • nie są same w sobie komponentami
  • pociągają za sobą wymienność
  • mogą być implementowane w różny sposób oraz w różnych kontekstach
Istotną kwestią jest by używać wzorców projektowych tylko tam, gdzie jest to naprawdę potrzebne. Dlaczego ? Ponieważ:
  • większość wzorców sprawia, że projekt staje się bardziej złożony
  • każdy wzorzec posiada swoje zamienniki
  • wzorce projektowe nie są "najlepsze" tylko dlatego, że są poprostu jedynie wzorcami

AdapterKonwersja interfejsu określonej klasy na inny interfejs oczekiwany przez klienta.
ProxyDostarczenie środka zastępczego dla obiektu w celu kontroli dostępu do niego.
CompositeKompozycja obiektów w hierarchii typu część-całość tak, że klient może traktować zarówno pojedyncze obiekty jak kompozycję jednakowo.
Template MethodDefinicja szkieletu algorytmu określonej operacji oraz umożliwienie przedefiniowania części kroków tego algorytmu w klasach pochodnych.
SingletonZapewnienie, że określona klasa posiada wyłącznie jedną instancję oraz dostarcza punkt dostępu do niej.

0 komentarze

Zasady projektowania OOD (Object-Oriented Design)

Programowanie obiektowe (OOP) jest metodyką modelowania i projektowania oprogramowania, które obejmuje podstawowe pojęcia hermetyzacji, abstrakcji, dziedziczenia i polimorfizmu. Ta metodyka kieruje się zestawem zasad zwanych zasadami projektowania. Zasady te pokazują właściwy kierunek projektowania i pozwalają uniknąć kosztownych pomyłek na etapie projektowania.

Stosowanie zasad projektowania jest ważne nie tylko z punktu widzenia projektowania oprogramowania, ale również z punktu widzenia biznesowego, gdyż umożliwiają one tworzenie elastycznych projektów, które mogą ewaluować wraz z wymaganiami biznesowymi przy minimalnym wysiłku, czasie i kosztach.

Poniżej zamieszczony został opis dotyczący niektórych zasad.

Zasady dotyczące projektu klas - --- SOLID ---

Open Close Principle (OCP)
Zasada ta stanowi najbardziej podstawową regułę uelastycznienia oprogramowania. Wszystkie inne zasady, metodyki i konwencje OOP koncentrują się wokół tej zasady. OCP określa co następuje:

Elementy oprogramowania powinny być otwarte na rozszerzania (extensions) natomiast zamknięte na modyfikacje (modifications).

Na pierwszy rzut oka stwierdzenie powyższe wydaje się to wewnętrznie sprzeczne. W celu wyjaśnienia zdefiniujmy następujące terminy:
  • Encje oprogramowania w projekcie to funkcje, metody, moduły, klasy.
  • Otwarty na rozszerzanie oznacza, że jego zachowanie może zostać rozszerzone lub zmienione w zależności od potrzeb
  • Zamknięte na modyfikacje oznacza ograniczenie zmian kodu źródłowego modułu do rozszerzenia lub zmiany jego zachowania.
Otwartość na rozszerzanie pozwala na zaspokojenie rzeczywistej potrzeby zmian w oprogramowaniu spowodowanej zmianą wymagań biznesowych bez konieczności modyfikacji kodu źródłowego. Dlaczego nie powinno być zmian w kodzie źródłowym ? Aby zdać sobie sprawę z korzyści otwartości rozważymy kod składający się z logiki if-then-else w przykładzie dotyczącym logowania. Załóżmy, że mamy przygotowaną i działającą następującą funkcję logującą:
void log() {
prepareMessage();
writeMessageToFile();
}
Biznes teraz wymaga od nas, by logowanie było możliwe nie tylko do pliku ale również do bazy danych. Tak więc robimy modyfikację w kodzie źródłowym:

void log() {
prepareMessage();
if (target is file) {
writeMessageToFile();
} else {
writeMessageToDatabase();
}
}
Wszystko działa, natomiast problem polega na tym, że w przyszłości, gdy wymagania się znowu zmienią np. logowanie do pliku XML, będzie musiał być również znowu zmieniony kod źródłowy. Natomiast każda modyfikacja w kodzie źródłowym wprowadza potencjalne błędy, jak również prowadzi do zwiększenia ilości testów w celu zapewnienia poprawności działania i sprawdzenia czy przy okazji modyfikacji coś co działało poprawnie nie zostało zepsute. W szczególności, gdy zmiana została wykonana w części core-owej projektu, będzie musiał być wykonany cały zestaw testów.

Open-Closed Principle zaleca realizację elastycznego oprogramowania poprzez podejście polegające na inkrementacji kodu. Innymi słowy raczej dodawanie nowego kodu niż zmiana w kodzie istniejącym i działającym.

Powracając do przykładu z logowaniem, dobry projekt z punktu widzenia OCP powinien być zrealizowany poprzez przygotowanie interfejsu oraz klas specyficznych w odniesieniu do specyficznej realizacji logowania. Patrz diagram poniżej:

Główna klasa logująca nie musi być zmieniana w celu dodania nowego punktu przeznaczenia. Jest to przykład otwartości na rozszerzenie jednak zamknięcia na modyfikacje.


Liskov Substitution Principle (LSP)
Zasada OCP wymieniona wcześniej jest główną zasadą OOD i można powiedzieć o niej, że jest sercem OOD. Tak jak zostało wcześniej opisane, zaleca ona stosowanie abstrakcji w celu umożliwienia rozszerzalności obiektów, która typowo implementowana jest wykorzystując mechanizm dziedziczenia. Dziedziczenie samo w sobie wprowadza wiele możliwości, z których część może prowadzić do błędnego projektu. Zasada Liskov Substitution Principle (LSP) wprowadza wskazówki dotyczące projektowania używając dziedziczenia. Została sformułaowana przez Barbarę Liskov i w wolnym tłumaczeniu brzmi następująco:

Jeżeli dla każdego obiektu o1 typu S istnieje obiekt o2 typu T, taki że dla wszystkich programów P zdefiniowanych używając T, zachowanie P pozostaje niezmienne po zamianie o1 na o2 to typ S jest podtypem typu T.

Powyższe stwierdzenie dostarcza definicji podtypu lub klasy pochodnej. Z punktu widzenia programowania, można to zinterpretować tak, że zachowanie funkcji lub metody używającej obiektu klasy bazowej powinno pozostać niezmienne po zmianie na obiekt klasy pochodnej wywodzącej się z tej klasy bazowej.

Co z tego wynika możemy prześledzić na przykładzie. Załóżmy, że mamy zdefiniowaną klasę Rectangle która definiuje prostokąt. Następnie chcielibyśmy przygotować klasę Square definiującą typ kwadrat. Wiemy, że kwadrat to prostokąt o równych bokach, tak więc naturalnym podejściem jest zaimplementowanie dziedziczenia klasy Square z klasy Rectangle. Podejście jest jak najbardziej słuszne, jednak jak zostanie to pokazane dalej może prowadzić do niespełnienia zasady LSP. Poniżej został zaprezentowany diagram klas dla opisywanego przypadku:
Jak wynika z diagramu prostokąt ma 4 metody 2 do pobierania długości boków oraz 2 do ustawiania długości boków. Ponieważ kwadrat dziedziczy z prostokąta, dziedziczy również te 4 metody. Teraz, aby zadbać o wewnętrzną spójność kwadratu możemy dodać kod ustawiający długość dla obydwu boków przy ustawieniu długości któregokolwiek z nich, tak jak poniżej:
public void setLengthA(int lengthA) {
this.lengthA = lengthA;
this.lengthB = lengthA;
}

public void setLengthB(int lengthB) {
this.lengthB = lengthB;
this.lengthA = lengthB;
}
Wszystko jest niby OK, ale zasada LSP nie jest spełniona. Można by zapytać w czym to przeszkadza. Otóż, załóżmy że w celu testu zostanie przygotowany następujący kod:

public void test(Rectangle rect) {
rect.setLengthA(2);
rect.setLengthB(3);

asert(6 == rect.getLengthA() * rect.getLengthB());
}
Test w przypadku wykonania na obiekcie klasy Rectangle działa OK, natomiast gdy to metody test() przekażemy obiekt klasy Square niestety test nie powiedzie się. W tym przypadku oczekiwanie jak najbardziej słuszne osoby piszącej test, nie jest spełnione. W tym przypadku można powiedzieć, że klasa Square nie spełnia zasady LSP.

Design by Contract (DBC)
Zasada DBC jest skojarzona z zasadą LSP. DBC określa, że każda encja oprogramowania jest zobowiązana do ciągłego dostarczania usługi innym encjom. Umowa (contract) jest definiowana przez warunki początkowe (preconditions) oraz warunki końcowe (postconditions), które w programowaniu przenoszą się na sygnaturę funkcji czy metody. Element wywołujący zapewnia spełnienie określonych warunków początkowych na podstawie, których element wywoływany zapewnia spełnienie warunków końcowych. DBC określa:

W przypadku przedefiniowania procedury [w elemencie pochodnym] można jedynie zmienić jej warunki początkowe na słabsze oraz jej warunki końcowe na mocniejsze.


W przykładzie opisywanym w LSP dotyczącym prostokątów i kwadratów, załóżmy, że klasa Rectangle ma metodę setDimension(lenghtA, lengthB), która w klasie Square może być przetransformowana do setDimension(length). W tym przypadku klasa Square zamienia warunki początkowe zdefiniowane w klasie Rectangle na mocniejsze powodując złamanie zasady DBC.

Dependency Inversion Principle (DIP)
Projekt oprogramowania definiuje obiekty oraz komunikację pomiędzy nimi. Ostatecznie dostajemy sieć wewnętrznych połączeń pomiędzy tymi obiektami, która w połączeniu z ograniczeniami biznesowymi takimi jak czas czy budżet, prowadzi do złego projektu oprogramowania. Robert Martin zdefiniował zły projekt oprogramowania trzema podstawowymi atrybutami przedstawionymi poniżej.

Część oprogramowania spełniająca swoje wymagania i wykazująca jeszcze którąkolwiek lub wszystkie z następujących trzech cech posiada zły projekt (design):
  1. Jest bardzo trudna do zmiany, ponieważ każda zmiana ma wpływ na wiele części systemu - rigidity.
  2. W przypadku wprowadzenia zmiany, nieoczekiwane części systemu ulegają zepsuciu - fragality.
  3. Jest bardzo ciężko ją użyć ponownie w innej aplikacji, ponieważ nie może być wyciągnięta z aktualnej aplikacji - immobility.
Gdziekolwiek funkcjonalność jest zaimplementowana, może ona być podzielona na moduły wysokopoziomowe i niskopoziomowe. Moduły wysokopoziomowe są zwykle reprezentatywne dla koncepcji aktywności, podczas gdy niskopoziomowe aktywności są szczegółami modułu wysokopoziomowego. Przykładowo aktywność logowania (moduł wysokopoziomowy) może być podzielona na:
  • sformułowanie komunikatu do zalogowania
  • zapis komunikatu do miejsca przeznaczenia np. pliku, bazy danych

W implementacji, moduły niskopoziomowe mogą mieć specyficzny kod np. moduł do zapisu komunikatu może zapisywać go do pliku:

prepareMessage
writeMessageToFile
Później, gdy będzie wymagane logowanie do bazy danych, algorytm ulegnie zmianie do następującej wersji:
prepareMessage
writeMessageToDatabase
W opisywanym przypadku zmiana w module niskopoziomowym powoduje zmianę w module wysokopoziomowym. Oznacza to, że nie można użyć ponownie modułu logującego nie używając implementacji modułu niskopoziomowego do zapisu komunikatu.

Na poziomie koncepcyjnym wszystko jest w porządku, problemy pojawiają się podczas implementacji. Dlaczego nie włączyć koncepcji w samym projekcie ? Może to być wykonane poprzez wprowadzenie abstrakcji, jak w algorytmie poniżej:

logging:
prepareMessage
writeMessage

writeMessage:
writeMessageToFile
Został tutaj wprowadzony poziom pośredni, moduł logowania nie komunikuje się bezpośrednio z modułem zapisu komunikatu na dysk. Komunikuje się on z modułem zapisu komunikatu, który ukrywa całość zmian swojej implementacji wewnętrznie. W tym przypadku moduł logowania jest niezależny od implementacji zapisu komunikatu i może być użyty ponownie.

To co zostało opisane powyżej jest ogólnym sformułowaniem zasady DIP, która stanowi:

Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Obydwa moduły natomiast powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny natomiast zależeć od abstrakcji.

W rzeczywistości oznacza to, że odseparowana została koncepcja od implementacji poprzez wprowadzenie abstrakcji. Jest to definicją reguły projektowania i programowania, która mówi, by projektować interfejsy a nie implementacje. Klasa potrzebująca funkcjonalności logowania powinna być projektowana z wykorzystaniem modułu wysokopoziomowego zamiast modułu niskopoziomowego.

Law of Demeter (LoD)
LoD nazywane jest zasadą minimalnej wiedzy o innych obiektach. Głównym celem LoD jest kontrola informacji przeładowanej (overload) poprzez definicję reguł dla interakcji pomiędzy obiektami - "Rozmawiaj tylko z najbliższymi przyjaciółmi". Bardziej powszechną formą zasady jest:

Każda jednostka powinna mieć jedynie ograniczoną wiedzę o innych jednostkach. Powinna mieć wiedzę dotycząca jedynie jednostki "blisko" z nią skojarzonej.

Każda jednostka powinna jedynie rozmawiać z przyjaciółmi, a nie rozmawiać z nieznajomymi.

W przypadku OOP, jednostką jest metoda a blisko powiązanymi jednostkami metody są:

  • argumenty przekazane do metody
  • pola klasy, do której należy, ale nie ich pola
  • obiekty, które sama tworzy
  • elementy klasy do której należy
Kiedykolwiek metoda wywołuje metody należące do innych obiektów, zakłada znajomość struktury ich klas. Idąc dalej, taka sama sytuacja występuje, gdy używa obiektów zwracanych z tych metod i wywołuje na nich metody. Zmiana w strukturze wyżej wymienionych obiektów może powodować zmiany w opisywanej metodzie. LoD określa zakres obiektów, których struktura może być znana i do których mogą odwoływać się metody. Przedstawiony zostanie teraz przykład łamiący LoD:
obj1 = obj2.getObj1();
obj3 = obj1.getObj3();
Zakładając, że obj2 został przekazany jako argument metody, dopuszczalna jest widza o strukturze klasy dla obiektu obj2. Została jednak również wywołana metoda na obiekcie obj1 odkrywając w ten sposób jego strukturę. Jeżeli struktura klasy dla obj1 zmieni się, istnieje możliwość, że przedstawiony kod także będzie musiał ulec zmianie. Przedstawioną powyżej sekwencję można zastąpić przez:
obj3 = obj2.getObj3()
Z tego, że obj2 zwraca obiekt obj1 wynika, że obj2 zna strukturę obj1 i może wywoływać jego metody. W wymienionym przypadku obj2 jest jednostką ściśle powiązaną, podczas gdy obj1 jest elementem obcym.

Rumbaugh podsumował zasadę Law of Demeter w następujący sposób:

Metoda powinna mieć ograniczoną wiedzę o modelu obiektów.

Interface Segrgation Principle (ISP)
Wiele małych interfejsów jest znacznie lepsze od jednego wielkiego.



Zasady dotyczące projektu pakietów.

Reuse/Release Equivalency Principle (REP)

The Common Reuse Principle (CRP)

Common Closure Principle (CCP)
Klasy w pakietach powinny być wspólnie zamknięte względem tego samego rodzaju zmian. Zmiana, która wpływa na pakiet dotyczy wszystkich klas w tym pakiecie.

Zasada określa, że klasy, które współistnieją razem powinny być grupowane razem. Co to oznacza ? Oznacza to tyle, że klasy, które zmieniają się wspólnie po zmianie wymagań, powinny być umieszczane w tym samym pakiecie. Zasada minimalizuje liczbę pakietów do zmiany w przypadku zmiany wymagań.


Acyclic Dependencies Principles (ADP)
Zależności pomiędzy pakietami nie mogą pod żadnym pozorem być cykliczne.

Stable Dependencies Principle (SDP)

Stable Abstractions Principle (SAP)

GlossyBlue Blogger by Black Quanta. Theme & Icons by N.Design Studio
Entries RSS Comments RSS