Skip to content

4. [TD]: Architektury warstwowe

Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności.

4.1. Introduction

Przypomnijmy, co zostało zrobione:

  • w części 1 ćwiczenia ELECTIONS nie wykorzystano żadnej klasy. Rozwiązanie zbudowano tak, jak zbudowalibyśmy je w języku C.
  • W części 2 ćwiczenia wprowadzono dwie klasy:
    • [ListeElectorale], która reprezentuje atrybuty (id, nazwa, głosy, mandaty, wyeliminowani) listy kandydatów
    • [ElectionsException] – klasa wyjątków niekontrolowanych. Ten typ wyjątku jest używany za każdym razem, gdy w aplikacji wyborczej wystąpi błąd krytyczny. Jest to wyjątek niekontrolowany, c.a.d, co oznacza, że programista nie ma obowiązku obsługiwać go za pomocą bloku try-catch.

Obliczanie wyników wyborów było dotychczas powierzone metodzie [main] klasy [MainElections]

package istia.st.elections;

import java.io.*;

public class MainElections {

   // niektóre dane
  private static final double barre = 0.05;

  // ----------------------------------------------------------------------
   // główna procedura
  public static void main(String[] arguments) throws IOException {

     // przygotowanie strumienia danych wprowadzanych z klawiatury
    BufferedReader clavier = new BufferedReader(new InputStreamReader(System.in));

     // wprowadzanie danych niezbędnych do obliczenia liczby mandatów
...
     // obliczanie liczby mandatów uzyskanych przez poszczególne listy
....
     // wyświetlanie wyników
...
  } // główna
} // klasa

Poprzednie rozwiązanie obejmuje trzy klasyczne etapy:

  • pozyskiwanie danych, wiersze 17–18
  • obliczenie wyniku, wiersze 19–20
  • wyświetlanie i/lub zapisywanie wyników, wiersze 21–22

Jedynie faza 2 jest naprawdę stała. Faza 1 może się różnić: dane mogą pochodzić z klawiatury, jak w omawianych przykładach, z pliku tekstowego, z interfejsu graficznego, z bazy danych, z sieci itp. Podobnie istnieje wiele sposobów przedstawienia wyników w fazie 3: wyświetlenie ich na ekranie, jak to miało miejsce w analizowanych przykładach, zapisanie ich w pliku, w bazie danych, wysłanie ich do sieci itp.

Ogólniej rzecz biorąc, aplikację często można przedstawić jako trójwarstwową strukturę, w której każda warstwa pełni ściśle określoną rolę:

Architekturę tę nazywa się również „architekturą trójwarstwową”, co jest tłumaczeniem angielskiego terminu „three-tier architecture”. Termin „trójwarstwowa” zazwyczaj odnosi się do architektury, w której każda warstwa znajduje się na innym komputerze. Gdy warstwy znajdują się na tym samym komputerze, architektura staje się architekturą „trójwarstwową”.

  • Warstwa [metier] zawiera reguły biznesowe aplikacji. W przypadku naszej aplikacji wyborczej są to reguły umożliwiające obliczenie liczby mandatów uzyskanych przez poszczególne listy wyborcze po ustaleniu liczby głosów zdobytych przez każdą z nich. Warstwa ta potrzebuje danych do działania. Na przykład w aplikacji wyborczej:
  • listy wyborcze wraz z nazwą i liczbą głosów dla każdej z nich
  • liczba mandatów do obsadzenia
  • próg wyborczy, poniżej którego lista zostaje wyeliminowana

Na powyższym schemacie dane mogą pochodzić z dwóch źródeł:

  • warstwy dostępu do danych lub [dao] (DAO = Data Access Object) w przypadku danych już zapisanych w plikach lub bazach danych. W tym przypadku mogłyby to być nazwy list, liczba mandatów do obsadzenia oraz próg wyborczy. Informacje te są bowiem znane jeszcze przed samymi wyborami.
  • warstwa interfejsu użytkownika lub [ui] (UI = interfejs użytkownika) dla danych wprowadzanych przez użytkownika lub wyświetlanych użytkownikowi. W tym przypadku mogłoby to dotyczyć głosów oddanych na listy, które są znane dopiero w ostatniej chwili, a także wyświetlania wyników wyborów.
  • Ogólnie rzecz biorąc, warstwa [dao] zajmuje się dostępem do danych trwałych (pliki, bazy danych) lub nietrwałych (sieć, czujniki itp.).
  • Warstwa [ui] zajmuje się natomiast interakcjami z użytkownikiem, o ile taki istnieje.
  • Wszystkie trzy warstwy są od siebie niezależne dzięki zastosowaniu interfejsów Java.
  • Istnieją różne metody integracji tych warstw w aplikacji. Będziemy korzystać z narzędzia o nazwie „Spring”. Na schemacie przebiega ono poprzecznie przez pozostałe warstwy.

Wrócimy do wcześniej opracowanej aplikacji [Elections], aby nadać jej architekturę trójwarstwową. W tym celu przeanalizujemy kolejno warstwy [ui, metier, dao], zaczynając od warstwy [dao], która zajmuje się danymi trwałymi.

Wcześniej musimy zdefiniować interfejsy poszczególnych warstw aplikacji [Elections].

4.2. Interfejsy aplikacji [Elections]

Przypomnijmy, że interfejs definiuje zbiór sygnatur metod. Klasy implementujące interfejs nadają tym metodom konkretną treść.

Wróćmy do trójwarstwowej architektury naszej aplikacji:

W tego typu architekturze to często użytkownik podejmuje inicjatywę. Wysyła żądanie w [1] i otrzymuje odpowiedź w [8]. Nazywa się to cyklem żądanie–odpowiedź. Weźmy na przykład obliczanie liczby mandatów uzyskanych wieczorem w dniu wyborów. Będzie to wymagało kilku etapów:

  1. warstwa [ui] będzie musiała zapytać użytkownika o liczbę głosów uzyskanych przez każdą z list. W tym celu będzie musiała przedstawić mu nazwy list biorących udział w wyborach. Użytkownik będzie musiał jedynie wpisać liczbę głosów obok każdej listy, a następnie poprosić o obliczenie liczby mandatów.
  2. Warstwa [ui] nie dysponuje nazwami list. Są one zapisane w źródle danych po prawej stronie schematu. Warstwa ta wykorzysta ścieżkę [2, 3, 4, 5, 6, 7], aby je uzyskać. Operacja [2] stanowi żądanie list, a operacja [7] – odpowiedź na to żądanie. Po wykonaniu tych czynności warstwa może przedstawić je użytkownikowi za pomocą operacji [8].
  3. Użytkownik przekaże warstwie [ui] liczbę głosów uzyskanych przez każdą z list. Jest to wspomniana powyżej operacja [1]. Na tym etapie użytkownik wchodzi w interakcję wyłącznie z warstwą [ui]. To właśnie ta warstwa sprawdzi w szczególności poprawność wprowadzonych danych. Po wykonaniu tej czynności użytkownik zażąda listy mandatów uzyskanych przez każdą z list.
  4. Warstwa [ui] zwróci się do warstwy biznesowej o obliczenie liczby mandatów. W tym celu przekaże jej dane otrzymane od użytkownika. Jest to operacja [2].
  5. Warstwa [metier] potrzebuje pewnych informacji, aby pomyślnie wykonać swoje zadanie. Posiada już listy wyborcze z operacji (b). Potrzebuje również liczby mandatów do obsadzenia oraz wartości progu wyborczego. Warstwa ta zwróci się o te informacje do warstwy [dao], korzystając ze ścieżki [3, 4, 5, 6]. [3] to żądanie początkowe, a [6] to odpowiedź na to żądanie.
  6. Mając wszystkie potrzebne dane, warstwa [metier] oblicza liczbę mandatów uzyskanych przez każdą z list.
  7. Warstwa [metier] może teraz odpowiedzieć na żądanie warstwy [ui] wysłane w punkcie (d). Jest to ścieżka [7].
  8. Warstwa [ui] sformatuje te wyniki, aby przedstawić je użytkownikowi w odpowiedniej formie, a następnie je wyświetli. Jest to ścieżka [8].
  9. Można sobie wyobrazić, że wyniki te muszą zostać zapisane w pliku lub bazie danych. Można to zrobić automatycznie. W takim przypadku, po operacji (f), warstwa [metier] zwróci się do warstwy [dao] o zapisanie wyników. Będzie to ścieżka [3, 4, 5, 6]. Można to również wykonać wyłącznie na żądanie użytkownika. Wówczas w cyklu żądanie–odpowiedź zostanie wykorzystana ścieżka [1-8].

Z opisu tego wynika, że warstwa korzysta z zasobów warstwy znajdującej się po jej prawej stronie, nigdy z zasobów warstwy po lewej stronie. Rozważmy dwie sąsiadujące warstwy:

Warstwa [A] wysyła żądania do warstwy [B]. W najprostszych przypadkach warstwa jest implementowana przez jedną klasę. Aplikacja ewoluuje z biegiem czasu. W związku z tym warstwa [B] może mieć różne klasy implementacyjne, np. [B1, B2, ...]. Jeśli warstwa [B] jest warstwą [dao], ta ostatnia może mieć pierwszą implementację [B1], która pobiera dane z pliku. Kilka lat później może zaistnieć potrzeba umieszczenia danych w bazie danych. Wówczas stworzymy drugą klasę implementacyjną o nazwie [B2]. Jeśli w pierwotnej aplikacji warstwa [A] współpracowała bezpośrednio z klasą [B1], konieczne jest częściowe przepisanie kodu warstwy [A]. Załóżmy na przykład, że w warstwie [A] napisaliśmy coś takiego:

1
2
3
B1 b1=new B1(...);
..
b1.getData(...);
  • wiersz 1: tworzona jest instancja klasy [B1]
  • wiersz 3: od tej instancji żądane są dane

Jeśli założymy, że nowa klasa implementacyjna [B2] wykorzystuje metody o tej samej sygnaturze co klasa [B1], konieczne będzie zastąpienie wszystkich wystąpień [B1] na [B2]. Jest to bardzo korzystny scenariusz, który jednak jest raczej mało prawdopodobny, jeśli nie zwrócono uwagi na te sygnatury metod. W praktyce często zdarza się, że klasy [B1] i [B2] nie mają takich samych sygnatur metod, w związku z czym znaczna część warstwy [A] musi zostać całkowicie przepisana.

Sytuację można poprawić, wprowadzając interfejs pomiędzy warstwami [A] i [B]. Oznacza to, że w interfejsie ustalamy sygnatury metod przedstawianych przez warstwę [B] warstwie [A]. Poprzedni schemat wygląda wówczas następująco:

Warstwa [A] nie zwraca się już bezpośrednio do warstwy [B], lecz do jej interfejsu [IB]. W ten sposób w kodzie warstwy [A] klasa implementacyjna [Bi] warstwy [B] pojawia się tylko raz, podczas implementacji interfejsu [IB]. W związku z tym w kodzie wykorzystywany jest interfejs [IB], a nie jego klasa implementacyjna. Poprzedni kod zmienia się na następujący:

1
2
3
IB ib=new B1(...);
..
ib.getData(...);
  • wiersz 1: tworzona jest instancja [ib] implementująca interfejs [IB] poprzez instancjonowanie klasy [B1]
  • wiersz 3: dane są pobierane z instancji [ib]

Jeśli teraz zastąpimy implementację [B1] warstwy [B] implementacją [B2], a obie te implementacje będą realizować ten sam interfejs [IB], wówczas należy zmodyfikować jedynie wiersz 1 warstwy [A], a żadnego innego. Jest to ogromna zaleta, która sama w sobie uzasadnia systematyczne stosowanie interfejsów między dwiema warstwami.

Można pójść jeszcze dalej i sprawić, by warstwa [A] była całkowicie niezależna od warstwy [B]. W powyższym kodzie wiersz 1 stanowi problem, ponieważ zawiera stałe odwołanie do klasy [B1]. Idealnym rozwiązaniem byłoby, gdyby warstwa [A] mogła korzystać z implementacji interfejsu [IB] bez konieczności podawania nazwy klasy. Byłoby to zgodne z naszym schematem przedstawionym powyżej. Widać, że warstwa [A] odwołuje się do interfejsu [IB] i nie widać powodu, dla którego musiałaby znać nazwę klasy implementującej ten interfejs. Ten szczegół nie jest przydatny dla warstwy [A].

Framework Spring (http://www.springframework.org) pozwala osiągnąć ten rezultat. Poprzednia architektura zmienia się w następujący sposób:

Warstwa poprzeczna [Spring] umożliwi warstwie uzyskanie poprzez konfigurację odniesienia do warstwy znajdującej się po jej prawej stronie bez konieczności znajomości nazwy klasy implementacyjnej tej warstwy. Nazwa ta będzie zawarta w plikach konfiguracyjnych, a nie w kodzie Java. Kod Java warstwy [A] przyjmuje zatem następującą postać:

1
2
3
IB ib; // inicjowany przez Spring
..
ib.getData(...);
  • wiersz 1: instancja [ib] implementująca interfejs [IB] warstwy [B]. Instancja ta jest tworzona przez Spring na podstawie informacji znalezionych w pliku konfiguracyjnym. Spring zajmie się utworzeniem:
    • instancję [b] implementującą warstwę [B]
    • instancję [a] implementującą warstwę [A]. Instancja ta zostanie zainicjowana. Powyższe pole [ib] otrzyma jako wartość odwołanie [b] do obiektu implementującego warstwę [B]
  • wiersz 3: żądane są dane z instancji [ib]

Widać teraz, że klasa implementacyjna [B1] warstwy B nie pojawia się nigdzie w kodzie warstwy [A]. Gdy implementacja [B1] zostanie zastąpiona nową implementacją [B2], w kodzie klasy [A] nic się nie zmieni. Wystarczy po prostu zmienić pliki konfiguracyjne Springa, aby instancjonować [B2] zamiast [B1].

Połączenie Springa z interfejsami Java stanowi decydujące ulepszenie w zakresie konserwacji aplikacji, zapewniając wzajemną izolację ich warstw. Właśnie to rozwiązanie zastosujemy w aplikacji [Elections].

Wróćmy do trójwarstwowej architektury naszej aplikacji:

W prostych przypadkach można zacząć od warstwy [metier], aby poznać interfejsy aplikacji. Do działania aplikacja potrzebuje danych:

  • już dostępne w plikach, bazach danych lub w sieci. Dane te są dostarczane przez warstwę [dao].
  • które nie są jeszcze dostępne. W takim przypadku są one dostarczane przez warstwę [ui], która pozyskuje je od użytkownika aplikacji.

Jakie interfejsy powinna udostępniać warstwa [dao] warstwie [metier]? Jakie są możliwe interakcje między tymi dwiema warstwami? Warstwa [dao] musi dostarczać warstwie [metier] następujące dane:

  • liczba mandatów do obsadzenia
  • wartość progu wyborczego, poniżej którego lista zostaje wyeliminowana
  • nazwy list

Informacje te są bowiem znane przed wyborami i mogą zatem zostać zapisane. W kierunku [metier] -> [dao] warstwa [metier] może zwrócić się do warstwy [dao] o zapisanie wyników wyborów, w szczególności liczby mandatów zdobytych przez poszczególne listy.

Na podstawie tych informacji można by podjąć próbę wstępnego zdefiniowania interfejsu warstwy [dao]:


public interface IElectionsDao {

  public double getSeuilElectoral();

  public int getNbSiegesAPourvoir();

  public ListeElectorale[] getListesElectorales();

  public void setListesElectorales(ListeElectorale[] listesElectorales);
}
  • wiersz 1: interfejs nosi nazwę [IElectionsDao]. Definiuje on cztery metody:
    • trzy metody odczytu danych ze źródła danych: [getSeuilElectoral, getNbSiegesAPourvoir, getListesElectorales]. Te trzy metody umożliwią warstwie [metier] uzyskanie danych charakteryzujących bieżące wybory.
    • jedną metodę do zapisywania danych w źródle danych: [setListesElectorales]. Metoda ta umożliwi warstwie [metier] zlecenie zapisania obliczonych przez nią wyników.

Wróćmy do trójwarstwowej architektury naszej aplikacji:

Jaki interfejs powinna warstwa [metier] udostępniać warstwie [ui]? Przyjrzyjmy się możliwym interakcjom między tymi dwiema warstwami.

  1. Warstwa [ui] będzie miała za zadanie poprosić użytkownika o oddanie głosów na różne listy biorące udział w wyborach. W tym celu musi znać liczbę list. Może zwrócić się o tę informację do warstwy [metier], która z kolei może zażądać tabeli list biorących udział w wyborach od warstwy [dao]. Jeśli warstwa [metier] posiada tę tabelę, warto ją przesłać do warstwy [ui]. Dzięki temu warstwa ta będzie dysponować nazwami list i będzie mogła doprecyzować komunikaty kierowane do użytkownika, pytając na przykład o „liczbę głosów oddanych na listę A”.
  2. Gdy warstwa [ui] uzyska wyniki głosowania ze wszystkich list, zwróci się do warstwy [metier] z prośbą o obliczenie liczby mandatów. Ta ostatnia będzie mogła wykonać to obliczenie i przekazać wynik do warstwy [ui].
  3. Warstwa [ui] będzie mogła następnie przedstawić te wyniki użytkownikowi. Użytkownik będzie mógł również poprosić o ich zapisanie.
  4. Warstwa [ui] może ponadto chcieć przedstawić użytkownikowi dodatkowe informacje, takie jak próg wyborczy lub liczba mandatów do obsadzenia.

Na podstawie tych informacji można by podjąć próbę wstępnego zdefiniowania interfejsu warstwy [metier] :


public interface IElectionsMetier {

    public ListeElectorale[] getListesElectorales();

    public int getNbSiegesAPourvoir();

    public double getSeuilElectoral();

    public void recordResultats(ListeElectorale[] listesElectorales);

    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}
  • wiersz 1: interfejs nosi nazwę [IElectionsMetier]. Definiuje on następujące metody:
    • wiersz 3: metoda [getListesElectorales], która umożliwi warstwie [ui] uzyskanie tablicy list biorących udział w wyborach;
    • wiersz 5: metoda [getNbSiegesAPourvoir] pozwala uzyskać liczbę mandatów do obsadzenia;
    • wiersz 7: metoda [getSeuilElectoral] pozwala uzyskać próg wyborczy;
    • wiersz 11: metoda [calculerSieges] (wiersz 36), która umożliwi warstwie [ui] zlecenie obliczenia liczby mandatów, gdy tylko znana będzie liczba głosów uzyskanych przez poszczególne listy. Parametrem jest tablica list biorących udział w wyborach, bez przypisanych im mandatów i bez wartości logicznej „wyeliminowany”. Zwracanym wynikiem jest ta sama tablica, tym razem z zainicjowanymi polami [sièges, elimine];
    • wiersz 9: metoda [recordResultats], która umożliwi warstwie [ui] zlecenie rejestracji wyników.

Uwaga: ze względu na swoje położenie warstwa [métier] przejmuje niektóre metody z warstwy [DAO], aby udostępnić je warstwie [UI]. Z powodu tej redundancji można ulec pokusie, by wszystko zgrupować w jednej warstwie, która łączyłaby zarówno logikę biznesową, jak i dostęp do danych. Ta pojedyncza warstwa jest czasami nazywana modelem, czyli literą „M” w skrócie MVC (Model – Widok – Kontroler). MVC to wzorzec projektowy (design pattern) powszechnie stosowany w aplikacjach internetowych.

Przyjrzyjmy się sygnaturze metody [calculerSieges]:


public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

Wcześniej napisano: „Parametr to tablica konkurujących list, bez ich miejsc i bez wyeliminowanej wartości logicznej. Wynikiem jest ta sama tablica, tym razem z polami [sièges, elimine]”. Sygnatura tej metody mogłaby również wyglądać następująco:


public void calculerSieges(ListeElectorale[] listesElectorales);

Parametr [listesElectorales] jest odwołaniem do obiektu, w tym przypadku tablicy. Każdy element jest z kolei odwołaniem do obiektu, w tym przypadku typu [ListeElectorale]. Metoda [calculerSieges] zmieni pola [sieges, elimine] każdego z tych obiektów. Metoda wywołująca posiada wskaźnik [listesElectorales], który:

  • przed wywołaniem jest odwołaniem do tablicy obiektów [ListeElectorale], której pola [sieges, elimine] nie są zainicjowane;
  • po wywołaniu jest odwołaniem (tym samym) do tablicy obiektów [ListeElectorale], której pola [sieges, elimine] są zainicjowane;

Dlaczego więc używać sygnatury:


public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

Podczas pisania interfejsu warto pamiętać, że może on być używany w dwóch różnych kontekstach: local oraz distant. W kontekście local metoda wywołująca i metoda wywoływana są wykonywane w tej samej maszynie wirtualnej Java (JVM):

Jeśli warstwa [ui] wywołuje metodę calculerSieges z warstwy [DAO], to posiada ona odwołanie do parametru [ListeElectorale[] listesElectorales], którą przekazuje do tej metody.

W kontekście distant metoda wywołująca i metoda wywoływana są wykonywane w różnych kontekstach JVM:

W powyższym przykładzie warstwa [ui] jest wykonywana w warstwie JVM 1, a warstwa [métier] w warstwie JVM 2 na dwóch różnych maszynach. Obie warstwy nie komunikują się bezpośrednio. Pomiędzy nimi znajduje się warstwa, którą nazwiemy warstwą komunikacyjną [1]. Składa się ona z warstwy nadawczej [2] oraz warstwy odbiorczej [3]. Programista zazwyczaj nie musi samodzielnie pisać tych warstw komunikacyjnych. Są one generowane automatycznie przez narzędzia programistyczne. Warstwa [metier] jest napisana tak, jakby była wykonywana w tej samej warstwie JVM co warstwa [DAO]. Nie ma zatem żadnych zmian w kodzie.

Mechanizm komunikacji między warstwą [ui] a warstwą [métier] wygląda następująco:

  • warstwa [ui] wywołuje metodę calculerSieges warstwy [métier], przekazując jej parametr [ListeElectorale[] listesElectorales1];
  • parametr ten jest w rzeczywistości przekazywany do warstwy wysyłającej [2]. Warstwa ta przekaże do sieci wartość parametru listesElectorales1, a nie jego odwołanie. Dokładna postać tej wartości zależy od zastosowanego protokołu komunikacyjnego;
  • warstwa odbiorcza [3] pobierze tę wartość i na jej podstawie odtworzy obiekt [ListeElectorale[] listesElectorales2], będący odzwierciedleniem pierwotnego parametru wysłanego przez warstwę [metier]. Mamy teraz dwa identyczne obiekty (pod względem treści) w dwóch różnych warstwach JVM: listesElectorales1 i listesElectorales2.
  • Warstwa odbiorcza przekaże obiekt listesElectorales2 do metody calculerSieges warstwy [métier], która zapisze go w bazie danych. Po tej operacji odwołanie listesElectorales2 wskazuje na tablicę obiektów [ListeElectorale], których pola [sieges, elimine] zostały zainicjowane. . Nie dotyczy to obiektu listesElectorales1, do którego warstwa [ui] posiada odwołanie. Jeśli chcemy, aby warstwa [ui] posiadała odwołanie do obiektu listesElectorales2, należy jej ten obiekt przesłać. W związku z tym należy zastosować następującą sygnaturę dla metody [calculerSieges]:

public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
  • Dzięki tej sygnaturze metoda calculerSieges zwróci jako wynik odwołanie listesElectorales2. Wynik ten jest przekazywany do warstwy odbiorczej [3], która wywołała warstwę [métier]. Ta z kolei zwróci wartość (a nie odwołanie) listesElectorales2 do warstwy wysyłającej [2];
  • warstwa wysyłająca [2] pobierze tę wartość i na jej podstawie odtworzy obiekt [ListeElectorale[] listesElectorales3] obrazu wyniku zwróconego przez metodę calculerSieges warstwy [métier].
  • Obiekt [ListeElectorale[] listesElectorales3] jest przekazywany do metody warstwy [ui], której wywołanie metody calculerSieges z warstwy [DAO] zainicjowało cały ten mechanizm;

W tym procesie obiekty typu [ListeElectorale] będą przechodzić między warstwami [2] i [3]:

  • gdy warstwa [2] przekazuje wartość obiektu [ListeElectorale] do warstwy [3], mówi się, że obiekt jest serializowany. Dokładna forma tej serializacji zależy od zastosowanego protokołu komunikacyjnego;
  • gdy warstwa [3] pobiera wartość obiektu [ListeElectorale] w celu ponownego utworzenia obiektu [ListeElectorale], mówi się, że obiekt jest deserializowany;

Aby obiekt mógł zostać poddany tej serializacji/deserializacji, niektóre protokoły wymagają, aby obiekt implementował interfejs [Serializable]. Interfejs ten jest jedynie znacznikiem. Nie ma żadnych metod do zaimplementowania. W związku z tym klasa [ListeElectorale] będzie odtąd zadeklarowana w następujący sposób:


public abstract class ListeElectorale implements Serializable {
    private static final long serialVersionUID = 1L;
  • Pole w wierszu 2 jest narzucone. Można je pozostawić bez zmian i wykorzystać dla każdej klasy typu [Serializable].

4.3. Klasa wyjątków

Wróćmy do interfejsu warstwy [DAO]:


public interface IElectionsDao {

  public double getSeuilElectoral();

  public int getNbSiegesAPourvoir();

  public ListeElectorale[] getListesElectorales();

  public void setListesElectorales(ListeElectorale[] listesElectorales);
}

Metody te współpracują z bazą danych i mogą napotkać różne błędy, na przykład niedostępny obiekt SGBD. Podczas pisania metody należy zawsze przewidzieć przypadki wystąpienia błędów. Są one zazwyczaj sygnalizowane za pomocą wyjątku. Klasę [ElectionsException] omówiliśmy już w punkcie 3.3. Będziemy z niej nadal korzystać, ale wzbogacimy ją w następujący sposób:


package ...;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// klasa wyjątków dla aplikacji „Wybory”
// wyjątek jest niekontrolowany

public class ElectionsException extends RuntimeException implements Serializable {

    // serial ID
    private static final long serialVersionUID = 1L;

    // pola lokalne
    private int code;
    private List<String> erreurs;

    // konstruktory
    public ElectionsException() {
        super();
    }

    public ElectionsException(int code, Throwable e) {
        // element nadrzędny
        super(e);
        // lokalny
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message, Throwable e) {
        // nadrzędny
        super(message,e);
        // lokalny
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message) {
        // nadrzędny
        super(message);
        // lokalny
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public ElectionsException(int code, List<String> erreurs) {
        // nadrzędny
        super();
        // lokalny
        this.code = code;
        this.erreurs = erreurs;
    }

    // lista komunikatów o błędach związanych z wyjątkiem
    private List<String> getErreursForException(Throwable th) {
        // pobierana jest lista komunikatów o błędach związanych z wyjątkiem
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // pobieramy komunikat tylko wtedy, gdy !=null i nie jest pusty
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // następna przyczyna
            cause = cause.getCause();
        }
        return erreurs;
    }

    // metody pobierające i ustawiające
...
}
  • wiersze 16–17: typ [ElectionsException] zawiera:
    • kod błędu, wiersz 16;
    • listę komunikatów o błędach, wiersz 17;

Klasa obsługuje pięć konstruktorów:

  • wiersz 20: ElectionsException()
  • wiersz 24: ElectionsException(int code, Throwable e): drugi parametr to typ [Throwable], który jest klasą nadrzędną klasy [Exception]. Ten konstruktor pozwala na enkapsulację wyjątku e wraz z kodem błędu. Typ [Throwable] (a zatem typ Exception) pozwala na enkapsulację jednego lub kilku wyjątków. Chodzi o to, aby:
    • przechwycić (catch) wystąpiony wyjątek;
    • uzupełnić ją o komunikat, enkapsulując ją w nowym wyjątku;
    • wywołać ponownie nowy wyjątek;
try{
...
}catch (Exception1 e1){
   throw new Exception2(«un message»,e1);
}

Kapsułkowanie odbywa się w wierszu 34 za pomocą instrukcji [super(message,e)]. Proces ten można powtarzać, a początkowy wyjątek wzbogacać o różne komunikaty. Mówi się wówczas o stosie wyjątków. Metoda [private List<String> getErreursForException(Throwable th)] pozwala uzyskać różne komunikaty powiązane z enkapsulowanymi wyjątkami:

  • (ciąg dalszy)
    • (ciąg dalszy)
      • enkapsulowany wyjątek uzyskuje się za pomocą metody Throwable [Throwable].getCause();
      • komunikat powiązany z wyjątkiem uzyskuje się za pomocą metody String [Throwable].getMessage();
  • wiersze 28–29: tworzone są pola [code, erreurs];
  • wiersz 32: public ElectionsException(int code, String message, Throwable e): ten konstruktor jest analogiczny do poprzedniego, z tą różnicą, że wzbogaca wyjątek, który ma enkapsulować, o kod i komunikat;
  • wiersz 40: public ElectionsException(int code, String message): konstruktor bez enkapsulacji wyjątku;
  • wiersz 50: public ElectionsException(int code, List<String> błędy): konstruktor bez enkapsulacji wyjątku ani komunikatu;

Klasę [ElectionsException] można wykorzystać w następujący sposób:

try{
...
}catch (Exception1 e1){
   throw new ElectionsException(un_code,un_message,e1);
}

gdzie komunikat może, ale nie musi występować. Po utworzeniu wyjątek [ElectionsException] nie ma na celu enkapsulacji nowych wyjątków. W powyższym przykładzie enkapsuluje ona wyjątek e1 oraz wyjątki, które enkapsuluje wyjątek e1. Nie ma już żadnych dalszych enkapsulacji.

Klasę [ElectionsException] można również wykorzystać w następujący sposób:

// kod, w którym może wystąpić błąd (ale nie w postaci wyjątku)
...
if(erreur){
    throw new ElectionsException(un_code,un_message);
}