O roli języka Java i J2EE w Oracle: część 5 – J2EE a projektowanie

Sebastian Wyrwał

Poprzednie odcinki ukazały w teoretyczny i praktyczny sposób pewne (bardzo zresztą podstawowe) właściwości omawianej technologii. Przed przejściem do dalszych, bardziej zaawansowanych zagadnień, należy zwrócić uwagę na problemy znajdujące się na styku projektu realizowanego w oparciu o UML i implementacji opartej o J2EE. Jest to o tyle istotne, że z jednej strony projekt powinien być niezależny od implementacji, z drugiej jednak specyfika J2EE nakłada określoną, bardzo konkretną architekturę, co zaburza ową niezależność lub wręcz sprawia, iż projekt może być niezrozumiały dla osób nie znających technologii implementacji.

W typowych językach programowania opartych o paradygmat obiektowy istnieje relacja dziedziczenia, której w języku UML odpowiada relacja generalizacji. Oprócz powyższych relacji ważna jest możliwość wyrażenia implementowania interfejsu (na rysunku 1 relacja ze stereotypem realize) oraz relacja zależności. Ta ostatnia bywa mocno niedoceniana.

Klasa Uczen dziedziczy z klasy Osoba i implementuje interfejs ObowiazekSzkolny.
public class Osoba {
private String imie;
private String nazwisko;
public void create(String nazwisko, String imie){
}
}
public interface ObowiazekSzkolny {
public void zapiszDoSzkoly(String Szkola);
}
public class Uczen extends Osoba implements ObowiazekSzkolny {
public void create(String nazwisko, String imie){
}
public void zapiszDoSzkoly(String Szkola){
}
}

 


Ilustracja 1:
Generalizacja i implementacja interfejsu wyrażona w UML

Kod źródłowy (wygenerowany automatycznie narzędziem Enterprise Architect firmy Sparx Systems) jest bardzo prosty. Relacje generalizacji i implementowania są wyrażone na poziomie kodu źródłowego słowami „extends” i „implements” oraz istnieniem metody zapiszDoSzkoly. W zaprezentowanym przykładzie w klasie Uczen istnieje również metoda create, choć nie jest to konieczne. W praktyce metoda ta zostanie zaimplementowana w metodzie klasy potomnej w wypadku, gdy chcemy ją przedefiniować1. Istotne jest to, iż odpowiednia metoda z klasy potomnej ma dokładnie taką samą nazwę i sygnaturę jak metoda z klasy bazowej, czyli create(String nazwisko, String imie) w powyższym przypadku. Analogicznie operacja z interfejsu i implementująca ją metoda mają takie same nazwy i sygnatury – zapiszDoSzkoly(String szkola).

Można zapytać, czy możliwa jest sytuacja, w której nazwy metod nie będą zgodne tzn. „praca” metoda create ma być wykonana np. przez metodę createMe klasy potomnej. Odpowiedź jest twierdząca, z tym, że rozwiązanie wykracza poza dziedziczenie (i generalizację). Wiąże się to z zastosowaniem delegacji, która na poziomie projektu powinna być wyrażona explicite na diagramie sekwencji lub kolaboracji.

 


Ilustracja 2:
Klasy realizujące delegację


Ilustracja 3:
Przykład delegacji

public class Car {
public Engine m_Engine = new Engine();
public void start(){
m_Engine.ignition();
}
}

Metoda start() klasy Car realizuje delegację do metody ignition (ang. zapłon) obiektu klasy Engine.

Jest to wyrażone wprost zarówno na diagramie sekwencji, jak i w kodzie źródłowym klasy Car.

Powyższe rozważania można podsumować w ten sposób: zależność pomiędzy metodami (metodami i operacjami) może być wyrażona albo poprzez relację dziedziczenia, względnie poprzez implementację interfejsu, albo poprzez zastosowanie i jawne ukazanie na poziomie projektu delegacji.

Niestety nie wszystko jest takie proste…

Architektura oparta o J2EE zaburza te proste i oczywiste zależności; po pierwsze dlatego, iż „odpowiadające sobie” metody mają różne nazwy, a po wtóre dlatego, że nie każda „implementacja” może być wyrażona słowem kluczowym implements. Oczywistym następstwem tego stanu rzeczy są zależności, których nie można łatwo zapisać w projekcie. Pozostaje więc albo użycie relacji zależności pomiędzy klasami i interfejsami, co czynią niektóre narzędzia lub rozszerzenie języka UML tak, aby bardziej pasował do różnych technologii implementacji.

Przykładowy diagram klas dla klasy UslugiDlaUcznia, z zaprezentowanego w poprzednim odcinku przykładu, został przedstawiony na ilustracji 4. Wynika z niego jasno, iż klasa UslugiDlaUcznia nie jest w jakiejkolwiek relacji z żadnym ze swoich interfejsów. Interfejsy te, znajdujące w się w pakiecie interfaces, to: UslugiDla-Ucznia2 (zdalny), UslugiDlaUczniaLocal (lokalny), UslugiDlaUczniaHome (domowy) i UslugiDlaUczniaLocalHome (domowy lokalny).

Przedstawiony powyżej przykład nie jest wynikiem żadnej pomyłki, a wynika jedynie z technologii J2EE. Co więcej, technologia ta, pomimo formalnych (tj. wyrażonych np. na poziomie projektu w UML) związków narzuca ściśle określone zależności pomiędzy klasą komponentu a jej interfejsami. Trzeba podkreślić iż, aby posługiwać się technologią EJB, należy znać te zależności, pomimo faktu, iż narzędzia programistyczne automatycznie generują te interfejsy.

 


Ilustracja 4:
Klasa UslugiDlaUcznia i jej relacje

 


Ilustracja 5:
Interfejsy klasy UslugiDlaUcznia

Interfejsy „domowe” czyli UslugiDlaUczniaHome i UslugiDlaUczniaLocalHome pełnią m.in. rolę fabryk, które zwracają odpowiednio interfejs zdalny i lokalny. Listy argumentów wywołania metody create z obydwu interfejsów domowych muszą być zgodne z argumentami metody ejbCreate klasy UslugiDlaUcznia. Należy zwrócić uwagę, iż ta ostatnia nic nie zwraca (void). Pomimo, że na diagramie metod tych nic nie łączy, to znajdują się one w (nie bezpośredniej) delegacji.3

Jeszcze bardziej ścisłe powiązanie istnieje pomiędzy klasą UslugiDlaUcznia a jej interfejsami UslugiDlaUcznia (interfejs zdalny) i UslugiDlaUczniaLocal (interfejs lokalny). Metody pobierzPytania i zapiszHistorie są zgodne zarówno jeśli chodzi o nazwy jaki i sygnatury. Pomimo tego, żaden formalny związek nie zachodzi. Jedyną relacją jaką można w tym wypadku narysować jest relacja zależności.

Jak projektować?

Podręczniki związane z technologią EJB odnotowują powyższe fakty, nie analizując jednak jego implikacji dla sposobu zapisu projektu np. w języku UML. Warto więc zastanowić się, co można uczynić aby poprawnie projektować rozwiązania, które będą implementowane z użyciem EJB. To, że samo projektowanie powinno być niezależne od implementacji jest oczywiste, jednak spotkanie się projektu i implementacji na pewnym poziomie jest raczej nieuniknione. Wpływ J2EE na projekt jest bowiem bardzo silny i nie wydaje się, aby całkowita niezależność była możliwa. Zwłaszcza, że wykorzystanie omawianej technologii to przykład zastosowania komponentów. Można wskazać kilka podejść:

  1. Uwzględnianie wyłącznie metod biznesowych i ukrywanie metod interfejsów i klas związanych z zastosowaniem J2EE (EJB);
  2. Projektowanie nie uwzględniające związków wynikających z technologii, których nie da się zapisać formalnie;
  3. Stosowanie relacji zależności do wyrażenia architektonicznych związków wynikających z EJB;
  4. Modyfikacja języka UML np. poprzez mechanizm rozszerzeń tak, aby możliwe było uwzględnienie związków wynikających z technologii w bardziej naturalny sposób.

Pierwsze podejście pokrywa się w zasadzie z modelowaniem. Jest ono również zgodne z ogólnymi zasadami projektowania, gdyż zakłada niezależność od technologii implementacji. Rozwiązanie to sprawdzi się wtedy, gdy na pewnym poziomie projektowania projekt przekształci się (co można częściowo zautomatyzować) do postaci zgodnej z EJB.

Drugie i trzecie podejście, to uzależnienie projektu od implementacji, z uwzględnieniem w mniejszym lub większym stopniu specyfiki architektury. Projekt może być nieczytelny dla osób nieznających technologii implementacji. Inną rzeczą jest to, że aby czytać projekt wystarczy znajomość pewnych idei architektonicznych; znajomość wszystkich szczegółów nie jest potrzebna.

Czwarte rozwiązanie zakłada przystosowanie języka UML np. poprzez mechanizm rozszerzania (ang. extension mechanism) do specyficznych potrzeb technologii implementacji tak, aby w sposób sformalizowany można było wyrazić związki ważne z punktu widzenia technologii, niemożliwe do wyrażenia w dotychczasowej formie na poziomie wyższym, niż relacja zależności z odpowiednim stereotypem.

Analizując powyższe podejścia można dojść do wniosku, iż odpowiedź na postawione w śródtytule pytanie sprowadza się w zasadzie do odpowiedzi na cztery częściowo zależne pytania:

  1. Czy i w jakim stopniu wyrażać charakterystyczne cechy technologii implementacji? Jeśli tak, na jakim etapie (ew. w której iteracji)?
  2. W jaki sposób wyrażać pewne charakterystyczne relacje, które wynikają z technologii, a nie mogą być wyrażone w sposób formalny? Czy relacje zależności ze stereotypami są wystarczające?
  3. Czy projektanci muszą znać technologię implementacji?
  4. Jakie są konsekwencje przekształcenia modelu niezależnego od technologii implementacji na projekt ją uwzględniający? Jak zmienią się diagramy sekwencji?

Problem jest bardzo poważny – zwłaszcza dlatego, że na etapie projektowania technologia implementacji może być jeszcze nieustalona. Ważne jest również to, iż projektanci nie muszą (a być może nawet nie powinni) znać szczegółów technologii implementacji.

O ile odpowiedź na postawione pytania nie jest niezbędna na etapie modelowania (zwłaszcza gdy pogodzimy się z tym, że projekt nie będzie zwykłym uszczegółowieniem artefaktów powstałych na etapie modelowania) o tyle na etapie projektowania, wraz ze zbliżaniem się do implementacji, staje się ona niezbędna. W tym miejscu można oczywiście dyskutować o celowości budowy modelu, który będzie zupełnie inny od projektu. Oczywiście warto budować taki „niemożliwy do uszczegółowienia” model, ponieważ umożliwia on głębsze zrozumienie systemu. Poza tym, w wypadku, gdy technologia implementacji nie jest jeszcze wybrana lub projektanci jej nie znają, budowa takiego modelu jest koniecznością. Pytanie o to jak projektować, jest również bardzo istotne w świetle możliwości generowania kodu źródłowego, która jest oferowana przez wiele narzędzi do projektowania. Na szczęście sam widok statyczny na poziomie modelowania może być przekształcony (zależnie od technologii implementacji) automatycznie, czemu może towarzyszyć wygenerowanie kodu.

W toku dalszych rozważań przeanalizujmy bardzo proste PU:

Przygotowanie testu dla ucznia aktor: Nauczyciel

  1. Operator wprowadza identyfikator4 ucznia,
  2. Jeżeli identyfikator jest poprawny i uczeń o takim identyfikatorze istnieje, system wyszukuje ucznia następnie 4. Wpw 3.
  3. Komunikat o błędzie (format identyfikatora niepoprawny lub informacja, iż nie ma takiego ucznia, nie można utworzyć testu, błąd podczas losowania pytań itp). Koniec.
  4. System tworzy nowy test dla ucznia i losuje pytania testowe, 5. W przypadku błędu 3.
  5. System umożliwia zalogowanie się ucznia do systemu. Koniec.

Dodanie pytań: aktor: Nauczyciel

  1. Operator wprowadza do systemu zestaw definicji pytań.
  2. Dla każdej definicji system tworzy pytanie.

Prosta analiza wykazuje, że w systemie istnieją klasy: Uczen, Test, Pytanie5. Dobrze jest, aby oprócz nich istniała klasa sterująca zawierająca logikę biznesową i izolująca interfejs użytkownika od reszty systemu. Załóżmy, iż uczeń ma imię, nazwisko i identyfikator. Test zawiera informacje na temat wykonanego (wykonywanego) testu takie, jak np. data i godzina wykonania. Pytanie, to pytanie testowe wraz z możliwymi odpowiedziami i atrybutami, które z nich są poprawne. W tym momencie należy podjąć decyzję: w jakiej formie ma być przechowywana informacja o przebiegu testu dla ucznia. Być może celowe będzie wprowadzenie klasy Odpowiedz reprezentującej odpowiedź udzieloną przez ucznia. Od razu nasuwa się pytanie o relację łączącą klasy Pytanie i Odpowiedź oraz konsekwencje jakie rodzi zmodyfikowanie pytania po przeprowadzeniu testu, a co za tym idzie po udzieleniu odpowiedzi przez ucznia. Pierwszym pomysłem jest zastosowanie relacji asocjacji. Rozwiązanie takie nie obsługuje jednak w sposób właściwy modyfikacji pytania po przeprowadzeniu jakiegoś testu. Lepszym pomysłem jest generalizacja (być może równolegle z asocjacją) i przepisywanie „zawartości” pytania do odpowiedzi. Kwestią otwartą jest jednak sposób tworzenia i wyszukiwania nowych obiektów klas reprezentujących encje.

Rozwiązania abstrahujące od użytej technologii

Przyjmijmy, iż realizowane prace są na etapie modelowania lub projektu, ale jeszcze dość dalekiego od implementacji. Tworzenie obiektu może być modelowane w standardowy dla UML sposób, a wyszukiwanie może być metodą biznesową klasy, której obiekt ma być wyszukany. Tak więc operacja wyszukania odbywa się 2-etapowo: w pierwszym etapie tworzymy „pustego ucznia”, a w drugim „wypełniamy” go danymi ucznia o zadanym identyfikatorze lub np. zgłaszamy wyjątek.

 


Ilustracja 6:
Uproszczona realizacja omawianego PU nieuwzględniająca architektury charakterystycznej dla J2EE

Oczywiście, przedstawione powyżej rozwiązanie nie uwzględnia tego, że tworzenie lub wyszukiwanie obiektów klasy Uczen odbywa się przy pomocy innej klasy. Można oczywiście założyć, że operacje takie, jak wyszukiwanie, czy tworzenie nowych obiektów będą realizowane przy pomocy pewnego rodzaju fabryki. Nie jest to rozwiązanie charakterystyczne dla J2EE, ale jest to jedynie przykład dobrej praktyki projektowej opartej o wzorce projektowe.

 


Ilustracja 7:
Rozwiązanie abstrahujące od J2EE ale oparte o wzorce projektowe

Rozwiązania uwzględniające użycie technologii implementacji

Zaprojektowanie rozwiązania, które uwzględnia architekturę wybranej technologii implementacji, wymaga spełnienia kilku warunków:

  1. Wybór technologii implementacji musi być znany przed mającymi nastąpić pracami projektowymi.
  2. Zespół projektantów musi znać pewne kluczowe właściwości używanej technologii.
  3. Osoby, które finansują przedsięwzięcie i w nim uczestniczą, muszą pogodzić się z utratą niezależności projektu od implementacji.

Istotne jest również to, w jakim stopniu projektanci muszą znać technologię implementacji, która zostanie zastosowana.

Projektant powinien:

  1. wiedzieć, że dostęp do obiektów klas komponentów odbywa się poprzez interfejsy zwykłe (lokalny i zdalny)6;
  2. wiedzieć, że tworzenie, niszczenie i wyszukiwanie obiektów odbywa się przy użyciu interfejsów domowych, które pełnią rolę podobną do fabryk;
  3. znać konwencje nazewnicze i niemożliwe do wyrażenia w sposób formalny powiązania;
  4. znać różnice pomiędzy metodami find (widoczna w interfejsie „domowym”) i select (widoczna w obrębie samego obiektu do użycia w metodzie biznesowej);
  5. wiedzieć, gdzie umieszczać metody biznesowe dotyczące konkretnego obiektu, a gdzie ich zbiorowości (metody biznesowe interfejsu domowego);
  6. znać metody związane z dostępem do atrybutów i obsługą relacji pomiędzy encjami EJB.

Oczywiście, narzucane przez technologię rozwiązania architektoniczne dotyczą w większym stopniu metod związanych w jakiś sposób z cyklem życia obiektu i wyszukiwaniem, a w niewielkim metod biznesowych.

 


Ilustracja 8:
Diagram uwzględniający architekturę EJB

Diagram na ilustracji 8 uwzględnia architekturę J2EE i został wykonany na podstawie kodu przedstawionego w poprzednim odcinku. Powstaje pytanie, czy byłby on do narysowania w przebiegu projektu? Teoretycznie tak, jednak jego stworzenie nie ma większego sensu, gdyż uwzględnia głównie klasy wygenerowane automatycznie przez narzędzie programistyczne.

Oczywiście, mając pewne doświadczenie z J2EE można przyjąć pewne założenia, co pozwoli z kolei na uproszczenie samego projektu. Dostęp do EJB encji może być realizowany wyłącznie lokalnie, a do EJB sesyjnych zarówno zdalnie i lokalnie.

Oczywiście, największym problem formalnym jest to, o czym już wspominano: że klasa komponent EJB nie implementuje swoich zwykłych interfejsów. Ponadto trzeba pamiętać, że klient nigdy nie komunikuje się bezpośrednio z komponentem. Odbywa się to za pośrednictwem obiektu EJBObject. Próba poszerzania języka UML być może powinna dążyć do umożliwienia modelowania luźniejszego implementowania interfejsów i zdefiniowania pewnego kontraktu definiującego reguły nazewnictwa odpowiadających sobie metod.

Znając J2EE można przyjąć inne rozwiązanie. Jest ono jednak, zdaniem autora, krańcowo nieeleganckie. Polega ono na rysowaniu wywołań do metod zdefiniowanych w komponencie tj. zamiast do metody create interfejsu domowego, do metody ejbCreate samego komponentu. Zamiast do metody biznesowej interfejsu domowego, rysujemy wywołanie metody ejbHomeMetodaBiznesowa(). Oczywiście rozwiązanie takie uzależnia projekt całkowicie od J2EE i może rodzić poważne błędy. Rozwiązanie takie jest również niezgodne z architekturą J2EE.

Jak radzi sobie narzędzie

Używane w tym odcinku narzędzie do projektowania – Enterprise Architect – potrafi dokonać transformacji „zwykłej” klasy do zestawu klas i interfejsów zgodnych ze specyfikacją EJB.

Na podstawie klasy przedstawionej na ilustracji 9 narzędzie dokonało transformacji do zestawu klas i interfejsów zgodnych ze specyfikacją EJB. Widać, iż relacje pomiędzy klasą komponentu EJB są modelowane jako relacje zależności ze stereotypami: EJBRealizeRemote, instantiate, EJBPrimaryKey, EJBRealizeHome. Dla osoby znającej EJB jest to rozwiązanie czytelne i chyba jedyne możliwe do zrealizowania bez modyfikacji języka UML. Warto jednak podkreślić, iż zastosowanie relacji zależności podyktowane jest pewnego rodzaju koniecznością. Rysowanie diagramów interakcji wymaga pewnej (choć niezbyt dogłębnej) znajomości omawianej technologii. Jak już powiedziano, transformacja widoku statycznego modelu na bliski implementacji widok statyczny na poziomie projektowania, może odbyć się automatycznie. Problem pojawia się jednak przy rysowaniu diagramów interakcji.

 


Ilustracja 9:
Klasa jednostka testu

Podsumowując rozważania z tego odcinka, można dojść do wniosku, iż niezależność na poziomie projektowania od zaawansowanej technologii implementacji jest nie do uzyskania w praktyce. Projektowanie musi być w zasadzie poprzedzone wyborem technologii. Bardzo groźna dla projektu jest jej zmiana. Jest to duża cena, jednak w zamian można otrzymać możliwość automatycznego generowania kodu.

 


Ilustracja 10:
Wygenerowany automatycznie zestaw klas i interfejsów

 

1 Formalnie biorąc zmieniamy metodę implementującą operację z klasy bazowej.

2 Ma tę samą nazwę co klasa, ta ostatnia znajduje się jednak w innym pakiecie.

3 W wywołaniach pośredniczy kontener. Jednak metody są ewidentnie powiązane.

4 Dla uproszczenia przyjmijmy, że jest to id. W funkcjonującym systemie byłby to numer ucznia lub nr albumu w przypadku studenta.

5 Celowo przyjęto nazwy inne niż w poprzednim odcinku.

6 Przyjęto pewne uproszczenie; w rzeczywistości nigdy nikt poza kontenerem nie ma dostępu do komponentu.