Skip to content

4. [TD]: Багаторівневі архітектури

Ключові слова: багаторівнева архітектура, Spring, ін’єкція залежностей.

4.1. Introduction

Нагадаємо, що було зроблено:

  • у частині 1 вправи ELECTIONS не використовувався жоден клас. Ми побудували рішення так, як би ми це зробили на мові C.
  • У частині 2 вправи було введено два класи:
    • [ListeElectorale], який представляє атрибути (id, ім'я, голоси, місця, виключення) списку кандидатів
    • [ElectionsException] — клас неконтрольованих винятків. Цей тип винятку використовується щоразу, коли у виборчій програмі трапляється фатальна помилка. Він є неконтрольованим (c.a.d), тобто розробник не зобов’язаний обробляти його за допомогою блоку try-catch.

Розрахунок результатів виборів до цього часу здійснювався за допомогою методу [main] класу [MainElections]

package istia.st.elections;

import java.io.*;

public class MainElections {

   // деякі дані
  private static final double barre = 0.05;

  // ----------------------------------------------------------------------
   // основна процедура
  public static void main(String[] arguments) throws IOException {

     // підготовка вхідного потоку з клавіатури
    BufferedReader clavier = new BufferedReader(new InputStreamReader(System.in));

     // введення даних, необхідних для підрахунку місць
...
     // розрахунок кількості місць, отриманих різними списками
....
     // виведення результатів
...
  } // головна
} // клас

Попереднє рішення включає три класичні етапи:

  • отримання даних, рядки 17–18
  • обчислення результату, рядки 19–20
  • виведення на екран та/або збереження результатів, рядки 21–22

Лише етап 2 є справді незмінним. Етап 1 може варіюватися: дані можуть надходити з клавіатури, як у розглянутих прикладах, із текстового файлу, з графічного інтерфейсу, з бази даних, з мережі тощо. Так само існує безліч способів представлення результатів на етапі 3: відображення їх на екрані, як це було зроблено в розглянутих прикладах, збереження у файлі, у базі даних, надсилання через мережу тощо.

У більш загальному сенсі додаток часто можна моделювати у вигляді трьох рівнів, кожен з яких має чітко визначену роль:

Цю архітектуру також називають «трирівневою архітектурою» (з англійської «three-tier architecture»). Термін «трирівнева» зазвичай позначає архітектуру, в якій кожен рівень розташований на окремій машині. Коли рівні знаходяться на одній машині, архітектура стає «тришаровою».

  • Рівень [metier] — це той, що містить бізнес-правила додатка. Для нашого додатка для проведення виборів це правила, які дозволяють обчислити кількість місць, отриманих різними списками, після того як відомі голоси, отримані кожним із них. Цей рівень потребує даних для роботи. Наприклад, у додатку для проведення виборів:
  • списки, кожен із яких має свою назву та кількість голосів
  • кількість місць, що підлягають заповненню
  • виборчий поріг, нижче якого список виключається

На наведеному вище схематичному зображенні дані можуть надходити з двох джерел:

  • з рівня доступу до даних або [dao] (DAO = Data Access Object) для даних, що вже записані у файлах або базах даних. У даному випадку це можуть бути назви списків, кількість місць, що підлягають заповненню, та виборчий поріг. Адже ця інформація відома ще до самих виборів.
  • рівень інтерфейсу з користувачем або [ui] (UI = User Interface) для даних, що вводяться користувачем або відображаються для нього. Це може стосуватися голосів за списки, які стають відомими лише в останній момент, а також відображення результатів виборів.
  • Загалом, рівень [dao] відповідає за доступ до постійних даних (файли, бази даних) або тимчасових даних (мережа, датчики тощо).
  • Рівень [ui], у свою чергу, відповідає за взаємодію з користувачем, якщо такий є.
  • Ці три шари є незалежними завдяки використанню інтерфейсів Java.
  • Для інтеграції цих рівнів у додаток існують різні методи. Ми будемо використовувати інструмент під назвою «Spring». На схемі він проходить поперек інших рівнів.

Ми повернемося до раніше розробленого додатка [Elections], щоб надати йому тришарову архітектуру. Для цього ми розглянемо шари [ui, metier, dao] по черзі, починаючи з шару [dao], який відповідає за постійні дані.

Перед цим нам потрібно визначити інтерфейси різних рівнів додатка [Elections].

4.2. Інтерфейси додатка [Elections]

Нагадаємо, що інтерфейс визначає набір сигнатур методів. Класи, що реалізують інтерфейс, наповнюють ці методи конкретним змістом.

Повернемося до трирівневої архітектури нашого додатка:

У такій архітектурі ініціативу часто бере на себе користувач. Він надсилає запит у [1] і отримує відповідь у [8]. Це називається циклом «запит — відповідь». Розглянемо приклад підрахунку місць, отриманих у вечір день виборів. Це потребуватиме кількох етапів:

  1. рівень [ui] повинен буде запитати у користувача кількість голосів, отриманих кожним із списків. Для цього він повинен буде показати користувачеві назви списків, що беруть участь у виборах. Користувачеві залишиться лише вказати кількість голосів навпроти кожного списку, а потім подати запит на підрахунок місць.
  2. Рівень [ui] не має назв списків. Вони зберігаються у джерелі даних праворуч від схеми. Для їх отримання він використовуватиме шлях [2, 3, 4, 5, 6, 7]. Операція [2] — це запит на списки, а операція [7] — відповідь на цей запит. Після цього вона може представити їх користувачеві за допомогою [8].
  3. Користувач передасть на рівень [ui] кількість голосів, отриманих кожним із списків. Це операція [1], зазначена вище. На цьому етапі користувач взаємодіє лише з рівнем [ui]. Саме цей рівень, зокрема, перевіряє правильність введених даних. Після цього користувач запитує список місць, отриманих кожним із списків.
  4. Рівень [ui] звернеться до бізнес-рівня з проханням розрахувати кількість місць. Для цього він передасть йому дані, отримані від користувача. Це операція [2].
  5. Рівень [metier] потребує певної інформації для виконання своєї роботи. Він уже має списки, отримані в результаті операції (b). Йому також потрібна кількість місць, що підлягають заповненню, а також значення виборчого порогу. Вона запитає цю інформацію у шару [dao], використовуючи шлях [3, 4, 5, 6]. [3] — це початковий запит, а [6] — відповідь на цей запит.
  6. Отримавши всі необхідні дані, рівень [metier] обчислює кількість місць, отриманих кожним зі списків.
  7. Рівень [metier] тепер може відповісти на запит рівня [ui], зроблений у пункті (d). Це шлях [7].
  8. шар [ui] оформить ці результати, щоб представити їх користувачеві у відповідному вигляді, а потім відобразить їх. Це шлях [8].
  9. Можна припустити, що ці результати мають бути збережені у файлі або базі даних. Це можна зробити автоматично. У цьому випадку після операції (f) шар [metier] звернеться до шару [dao] із запитом на збереження результатів. Це буде шлях [3, 4, 5, 6]. Це також можна зробити лише на запит користувача. У цьому випадку цикл «запит — відповідь» використовуватиме шлях [1-8].

З цього опису видно, що рівень використовує ресурси рівня, який знаходиться праворуч від нього, але ніколи — того, що ліворуч. Розглянемо два суміжні рівні:

Рівень [A] надсилає запити до рівня [B]. У найпростіших випадках один рівень реалізується одним класом. Додаток з часом розвивається. Таким чином, рівень [B] може мати різні класи реалізації, наприклад [B1, B2, ...]. Якщо рівень [B] є рівнем [dao], то він може мати першу реалізацію [B1], яка отримує дані з файлу. Через кілька років може виникнути потреба розмістити дані в базі даних. Тоді буде створено другий клас реалізації [B2]. Якщо в початковому додатку рівень [A] працював безпосередньо з класом [B1], доводиться частково переписати код рівня [A]. Припустимо, наприклад, що в шарі [A] було написано щось на зразок:

1
2
3
B1 b1=new B1(...);
..
b1.getData(...);
  • рядок 1: створюється екземпляр класу [B1]
  • рядок 3: від цього екземпляра запитуються дані

Якщо припустити, що новий клас реалізації [B2] використовує методи з тією самою сигнатурою, що й клас [B1], то доведеться замінити всі [B1] на [B2]. Це дуже сприятливий і досить малоймовірний випадок, якщо не приділяли уваги цим сигнатурам методів. На практиці часто трапляється, що класи [B1] та [B2] мають різні сигнатури методів, і тому значну частину шару [A] доводиться повністю переписати.

Ситуацію можна покращити, якщо ввести інтерфейс між шарами [A] та [B]. Це означає, що в інтерфейсі фіксуються сигнатури методів, які рівень [B] надає рівню [A]. Тоді попередня схема набуває такого вигляду:

Рівень [A] відтепер звертається не безпосередньо до рівня [B], а до його інтерфейсу [IB]. Таким чином, у коді рівня [A] клас реалізації [Bi] шару [B] з’являється лише один раз — під час реалізації інтерфейсу [IB]. Отже, у коді використовується саме інтерфейс [IB], а не його клас реалізації. Попередній код перетворюється на такий:

1
2
3
IB ib=new B1(...);
..
ib.getData(...);
  • рядок 1: екземпляр [ib], що реалізує інтерфейс [IB], створюється шляхом інстанціювання класу [B1]
  • рядок 3: дані запитуються у екземпляра [ib]

Відтепер, якщо замінити реалізацію [B1] шару [B] на реалізацію [B2], і якщо обидві ці реалізації дотримуються одного й того самого інтерфейсу [IB], то змінити потрібно лише рядок 1 шару [A], і більше ніщо. Це велика перевага, яка сама по собі виправдовує систематичне використання інтерфейсів між двома шарами.

Можна піти ще далі й зробити шар [A] повністю незалежним від шару [B]. У наведеному вище коді рядок 1 створює проблему, оскільки містить жорстке посилання на клас [B1]. Ідеально було б, щоб рівень [A] міг мати реалізацію інтерфейсу [IB] без необхідності вказувати ім’я класу. Це відповідало б нашій схемі, наведеній вище. З неї видно, що рівень [A] звертається до інтерфейсу [IB], і не зрозуміло, навіщо йому знати ім’я класу, що реалізує цей інтерфейс. Ця деталь не є корисною для рівня [A].

Фреймворк Spring (http://www.springframework.org) дозволяє досягти такого результату. Попередня архітектура змінюється наступним чином:

Поперечний шар [Spring] дозволить шару отримати через конфігурацію посилання на шар, розташований праворуч від нього, без необхідності знати ім’я класу реалізації цього шару. Це ім’я буде вказано у файлах конфігурації, а не в коді Java. Java-код шару [A] матиме такий вигляд:

1
2
3
IB ib; // ініціалізовано Spring
..
ib.getData(...);
  • рядок 1: екземпляр [ib], що реалізує інтерфейс [IB] шару [B]. Цей екземпляр створюється Spring на основі інформації, знайденої у файлі конфігурації. Spring подбає про створення:
    • екземпляр [b], що реалізує рівень [B]
    • екземпляр [a], що реалізує шар [A]. Цей екземпляр буде ініціалізовано. Поле [ib], зазначене вище, отримає як значення посилання [b] на об’єкт, що реалізує шар [B]
  • рядок 3: запитуються дані з екземпляра [ib]

Тепер видно, що клас реалізації [B1] шару B ніде не з’являється в коді шару [A]. Коли реалізація [B1] буде замінена на нову реалізацію [B2], у коді класу [A] нічого не зміниться. Просто змінять конфігураційні файли Spring, щоб створити екземпляр [B2] замість [B1].

Поєднання Spring та інтерфейсів Java забезпечує суттєве поліпшення обслуговування додатків, роблячи їхні шари взаємонезалежними. Саме це рішення ми використаємо для додатка [Elections].

Повернемося до трирівневої архітектури нашого додатка:

У простих випадках можна почати з рівня [metier], щоб визначити інтерфейси додатка. Для роботи йому потрібні дані:

  • які вже є у файлах, базах даних або через мережу. Їх надає рівень [dao].
  • ще недоступні. У такому разі їх надає рівень [ui], який отримує їх від користувача додатка.

Який інтерфейс повинен надавати рівень [dao] рівню [metier]? Які можливі взаємодії між цими двома рівнями? Рівень [dao] повинен надавати рівень [metier] такі дані:

  • кількість місць, що підлягають заповненню
  • значення виборчого порогу, нижче якого список виключається
  • назви списків

Ця інформація, насправді, відома ще до виборів і тому може бути збережена. У напрямку [metier] → [dao] шар [metier] може звернутися до шару [dao] із запитом про запис результатів виборів, зокрема про кількість місць, отриманих різними списками.

Маючи цю інформацію, можна спробувати дати перше визначення інтерфейсу рівня [dao]:


public interface IElectionsDao {

  public double getSeuilElectoral();

  public int getNbSiegesAPourvoir();

  public ListeElectorale[] getListesElectorales();

  public void setListesElectorales(ListeElectorale[] listesElectorales);
}
  • рядок 1: інтерфейс називається [IElectionsDao]. Він визначає чотири методи:
    • три методи для зчитування даних із джерела даних: [getSeuilElectoral, getNbSiegesAPourvoir, getListesElectorales]. Ці три методи дозволять шару [metier] отримати дані, що характеризують поточні вибори.
    • один метод для запису даних у джерело даних: [setListesElectorales]. Цей метод дозволить шару [metier] ініціювати запис результатів, які він обчислив.

Повернемося до трирівневої архітектури нашого додатка:

Який інтерфейс повинен надавати рівень [metier] рівню [ui]? Розглянемо можливі взаємодії між цими двома рівнями.

  1. Роль шару [ui] полягатиме в тому, щоб запитувати у користувача голоси за різні списки, що беруть участь у виборах. Для цього їй потрібно знати кількість списків. Вона може запитати цю інформацію у шару [metier], який, у свою чергу, може запитати таблицю списків-учасників у шару [dao]. Якщо рівень [metier] має цей масив, то його можна передати на рівень [ui]. Таким чином, цей рівень матиме назви списків і зможе уточнювати свої повідомлення користувачеві, запитуючи, наприклад, «Кількість голосів за список А».
  2. Коли рівень [ui] отримає голоси за всіма списками, він звернеться до рівня [metier] із запитом про підрахунок місць. Цей рівень зможе виконати цей підрахунок і передати результат рівню [ui].
  3. Після цього рівень [ui] зможе представити ці результати користувачеві. Користувач також зможе подати запит на їх збереження.
  4. Крім того, рівень [ui] може захотіти надати користувачеві додаткову інформацію, таку як виборчий поріг або кількість місць, що підлягають заповненню.

На основі цієї інформації можна спробувати дати перше визначення інтерфейсу шару [metier] :


public interface IElectionsMetier {

    public ListeElectorale[] getListesElectorales();

    public int getNbSiegesAPourvoir();

    public double getSeuilElectoral();

    public void recordResultats(ListeElectorale[] listesElectorales);

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

}
  • рядок 1: інтерфейс називається [IElectionsMetier]. Він визначає такі методи:
    • рядок 3: метод [getListesElectorales], який дозволить шару [ui] отримати масив списків-конкурентів;
    • рядок 5: метод [getNbSiegesAPourvoir] дозволяє отримати кількість місць, що підлягають заповненню;
    • рядок 7: метод [getSeuilElectoral] дозволяє отримати виборчий поріг;
    • рядок 11: метод [calculerSieges] (рядок 36), який дозволить шару [ui] запросити розрахунок місць, щойно стануть відомі кількості голосів за різні списки. Параметром є масив списків-учасників, без їхніх місць та без булевого значення «виключений». Результатом є той самий масив, але цього разу з ініціалізованими полями [sièges, elimine];
    • рядок 9: метод [recordResultats], який дозволить шару [ui] ініціювати реєстрацію результатів.

Примітка: завдяки своєму положенню шар [métier] переймає деякі методи шару [DAO], щоб надати їх шару [UI]. Через цю надмірність може виникнути спокуса об’єднати все в єдиний шар, який би поєднував як бізнес-логіку, так і доступ до даних. Цей єдиний рівень іноді називають моделлю, тобто літерою «М» у абревіатурі MVC (Модель — Вигляд — Контролер). MVC — це шаблон проектування (design pattern), широко поширений у веб-додатках.

Розглянемо сигнатуру методу [calculerSieges]:


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

Вище було зазначено: «Параметром є масив списків-конкурентів без їхніх місць та без виключеного булевого значення. Результатом є той самий масив, але цього разу з полями [sièges, elimine]». Сигнатура методу також може бути такою:


public void calculerSieges(ListeElectorale[] listesElectorales);

Параметр [listesElectorales] є посиланням на об’єкт, у даному випадку — масив. Кожен елемент, у свою чергу, є посиланням на об’єкт, у даному випадку — типу [ListeElectorale]. Метод [calculerSieges] змінить поля [sieges, elimine] кожного з цих об’єктів. Викликаючий метод має покажчик [listesElectorales], який:

  • до виклику — це посилання на масив об’єктів [ListeElectorale], поля якого [sieges, elimine] не ініціалізовані;
  • після виклику — це посилання (те саме) на масив об’єктів [ListeElectorale], поля якого [sieges, elimine] ініціалізовані;

Тоді навіщо використовувати сигнатуру:


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

Під час написання інтерфейсу варто пам’ятати, що його можна використовувати у двох різних контекстах: local та distant. У контексті local викликаючий метод і викликаний метод виконуються в одній і тій самій JVM (Java Virtual Machine):

Якщо рівень [ui] викликає метод calculerSieges з рівня [DAO], він дійсно має посилання на параметр [ListeElectorale[] listesElectorales], який вона передає методу.

У контексті distant викликаючий метод і викликаний метод виконуються в різних JVM:

У наведеному вище прикладі шар [ui] виконується в JVM 1, а шар [métier] — у JVM 2 на двох різних машинах. Ці два рівні не взаємодіють безпосередньо. Між ними розташований проміжний рівень, який ми назвемо рівнем зв’язку [1]. Він складається з рівня передачі [2] та рівня прийому [3]. Зазвичай розробнику не доводиться писати ці комунікаційні шари. Вони генеруються автоматично за допомогою програмних інструментів. Рівень [metier] написано так, ніби він виконується в тому самому JVM, що й рівень [DAO]. Отже, жодних змін у коді не відбувається.

Механізм взаємодії між шаром [ui] та шаром [métier] є таким:

  • шар [ui] викликає метод calculerSieges шару [métier], передаючи йому параметр [ListeElectorale[] listesElectorales1];
  • цей параметр фактично передається шару передачі [2]. Цей шар передасть у мережу значення параметра listesElectorales1, а не його посилання. Точний вигляд цього значення залежить від використовуваного протоколу зв’язку;
  • приймальний рівень [3] отримає це значення і на його основі відтворить об’єкт [ListeElectorale[] listesElectorales2], що є відображенням початкового параметра, надісланого рівнем [metier]. Тепер ми маємо два однакові (за змістом) об’єкти у двох різних JVM: listesElectorales1 та listesElectorales2.
  • Приймаючий рівень передасть об’єкт listesElectorales2 методу calculerSieges рівня [métier], який збереже його в базі даних. Після цієї операції посилання listesElectorales2 вказує на масив об’єктів [ListeElectorale], поля яких [sieges, elimine] ініціалізовано. . Це не стосується об’єкта listesElectorales1, на який посилається шар [ui]. Якщо потрібно, щоб шар [ui] мав посилання на об’єкт listesElectorales2, його потрібно передати цьому шару. Тому для методу [calculerSieges] доводиться використовувати такий підпис:

public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
  • З цією сигнатурою метод calculerSieges поверне як результат посилання listesElectorales2. Цей результат передається приймальному шару [3], який викликав шар [métier]. Останній поверне значення (а не посилання) listesElectorales2 на рівень відправлення [2];
  • шар передачі [2] отримає це значення і на його основі відтворить об’єкт [ListeElectorale[] listesElectorales3] зображення результату, отриманого методом calculerSieges шару [métier].
  • Об’єкт [ListeElectorale[] listesElectorales3] передається методу шару [ui], виклик якого до методу calculerSieges шару [DAO] і започаткував весь цей механізм;

У цьому процесі об’єкти типу [ListeElectorale] будуть передаватися між шарами [2] та [3]:

  • коли рівень [2] передає значення об’єкта [ListeElectorale] на рівень [3], кажуть, що об’єкт серіалізується. Точна форма цієї серіалізації залежить від використовуваного протоколу зв’язку;
  • коли рівень [3] отримує значення об’єкта [ListeElectorale] для того, щоб знову створити об’єкт [ListeElectorale], кажуть, що об’єкт десеріалізується;

Щоб об’єкт міг пройти цю серіалізацію/десеріалізацію, деякі протоколи вимагають, щоб об’єкт реалізовував інтерфейс [Serializable]. Цей інтерфейс є лише маркером. Методів, які потрібно реалізувати, немає. Отже, клас [ListeElectorale] відтепер буде оголошуватися наступним чином:


public abstract class ListeElectorale implements Serializable {
    private static final long serialVersionUID = 1L;
  • поле рядка 2 є обов'язковим. Його можна залишити без змін і використовувати для будь-якого класу типу [Serializable].

4.3. Клас винятків

Повернемося до інтерфейсу шару [DAO]:


public interface IElectionsDao {

  public double getSeuilElectoral();

  public int getNbSiegesAPourvoir();

  public ListeElectorale[] getListesElectorales();

  public void setListesElectorales(ListeElectorale[] listesElectorales);
}

Ці методи працюють із базою даних і можуть стикатися з різними помилками, наприклад, SGBD недоступний. Під час написання методу завжди слід передбачати випадки помилок. Зазвичай про них повідомляється за допомогою винятку. Ми вже зустрічали клас [ElectionsException] у розділі 3.3. Ми продовжимо його використовувати, але розширимо його таким чином:


package ...;

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

// клас винятків для додатка «Вибори»
// виняток є неконтрольованим

public class ElectionsException extends RuntimeException implements Serializable {

    // серіал ID
    private static final long serialVersionUID = 1L;

    // локальні поля
    private int code;
    private List<String> erreurs;

    // конструктори
    public ElectionsException() {
        super();
    }

    public ElectionsException(int code, Throwable e) {
        // батьківський
        super(e);
        // локальний
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message, Throwable e) {
        // батьківський
        super(message,e);
        // локальний
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public ElectionsException(int code, String message) {
        // батьківський
        super(message);
        // локальний
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public ElectionsException(int code, List<String> erreurs) {
        // батьківський
        super();
        // локальний
        this.code = code;
        this.erreurs = erreurs;
    }

    // список повідомлень про помилки винятку
    private List<String> getErreursForException(Throwable th) {
        // отримується список повідомлень про помилки винятку
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // отримуємо повідомлення, лише якщо воно !=null і не порожнє
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // наступна причина
            cause = cause.getCause();
        }
        return erreurs;
    }

    // методи getter та setter
...
}
  • рядки 16–17: тип [ElectionsException] інкапсулює:
    • код помилки, рядок 16;
    • список повідомлень про помилки, рядок 17;

Клас підтримує п’ять конструкторів:

  • рядок 20: ElectionsException()
  • рядок 24: ElectionsException(int code, Throwable e): другий параметр — це тип [Throwable], який є батьківським класом класу [Exception]. Цей конструктор дозволяє інкапсулювати виняток e разом із кодом помилки. Тип [Throwable] (а отже, і тип Exception) дозволяє інкапсулювати один або кілька винятків. Ідея полягає в тому, щоб:
    • перехопити (catch) виняток, що виник;
    • додати до нього повідомлення, інкапсулюючи його в нове виключення;
    • передавати нове виключення далі;
try{
...
}catch (Exception1 e1){
   throw new Exception2(«un message»,e1);
}

Інкапсуляція відбувається в рядку 34 за допомогою команди [super(message,e)]. Цей процес інкапсуляції можна повторювати, а початкове виключення доповнювати різними повідомленнями. У такому випадку кажуть, що маємо стек виключень. Метод [private List<String> getErreursForException(Throwable th)] дозволяє отримати різні повідомлення, пов’язані з інкапсульованими винятками:

  • (продовження)
    • (продовження)
      • інкапсульоване виключення отримується за допомогою методу Throwable [Throwable].getCause();
      • повідомлення, пов’язане з винятком, отримується за допомогою методу String [Throwable].getMessage();
  • рядки 28–29: створюються поля [code, erreurs];
  • рядок 32: public ElectionsException(int code, String message, Throwable e): цей конструктор аналогічний попередньому, за винятком того, що він доповнює виняток, який він буде інкапсулювати, кодом та повідомленням;
  • рядок 40: public ElectionsException(int code, String message): конструктор без інкапсуляції винятку;
  • рядок 50: public ElectionsException(int code, List<String> errors): конструктор без інкапсуляції винятку та повідомлення;

Клас [ElectionsException] можна використовувати наступним чином:

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

де повідомлення може бути присутнім або відсутнім. Після створення виняток [ElectionsException] не призначений для інкапсуляції нових винятків. У наведеному вище прикладі воно інкапсулює виняток e1 та винятки, які інкапсулює e1. Далі нових інкапсуляцій більше немає.

Клас [ElectionsException] також можна використовувати таким чином:

// код, у якому може виникнути помилка (але не у вигляді винятку)
...
if(erreur){
    throw new ElectionsException(un_code,un_message);
}