4. [TD]: Schichtenarchitekturen
Schlüsselwörter: mehrschichtige Architektur, Spring, Dependency Injection.
4.1. Einleitung
Lassen Sie uns noch einmal zusammenfassen, was wir bisher gemacht haben:
- In Teil 1 der ELECTIONS-Übung wurden keine Klassen verwendet. Wir haben eine Lösung erstellt, wie wir sie in der Programmiersprache C erstellt hätten.
- In Teil 2 der Übung wurden zwei Klassen eingeführt:
- [VoterList], die die Attribute (id, name, votes, seats, eliminated) einer Kandidatenliste repräsentiert
- [ElectionsException], eine Klasse für unbehandelte Ausnahmen. Diese Art von Ausnahme wird immer dann verwendet, wenn in der Wahlanwendung ein schwerwiegender Fehler auftritt. Sie ist unbehandelt, was bedeutet, dass der Entwickler sie nicht mit einem try-catch-Block behandeln muss.
Bisher wurde die Berechnung der Wahlergebnisse von einer [main]-Methode der Klasse [MainElections] übernommen
Die vorherige Lösung umfasst drei Standardphasen:
- Datenerfassung, Zeilen 17–18
- Berechnung der Lösung, Zeilen 19–20
- Anzeige und/oder Speicherung der Ergebnisse, Zeilen 21–22
Nur Phase 2 ist wirklich konstant. Phase 1 kann variieren: Die Daten können wie in den untersuchten Beispielen von der Tastatur stammen, aus einer Textdatei, aus einer grafischen Benutzeroberfläche, aus einer Datenbank, aus dem Netzwerk usw. Ebenso gibt es in Phase 3 mehrere Möglichkeiten, die Ergebnisse auszugeben: Anzeige auf dem Bildschirm wie in den untersuchten Beispielen, Speicherung in einer Datei oder einer Datenbank, Übertragung über das Netzwerk usw.
Allgemeiner gesagt lässt sich eine Anwendung oft als drei Schichten modellieren, von denen jede eine klar definierte Rolle hat:
![]() |
Diese Architektur wird auch als „dreistufige Architektur“ bezeichnet. Der Begriff „dreistufig“ bezieht sich normalerweise auf eine Architektur, bei der sich jede Stufe auf einem anderen Rechner befindet. Befinden sich die Stufen auf demselben Rechner, spricht man von einer „dreischichtigen“ Architektur.
- Die [Geschäfts-]Schicht enthält die Geschäftsregeln der Anwendung. Bei unserer Wahlanwendung sind dies die Regeln, die die von den verschiedenen Listen gewonnenen Sitze berechnen, sobald die von jeder Liste erzielten Stimmen bekannt sind. Diese Schicht benötigt Daten, um zu funktionieren. Zum Beispiel in der Wahlanwendung:
- die Listen mit jeweils ihrem Namen und der Anzahl der Stimmen
- die Anzahl der zu besetzenden Sitze
- die Wahlhürde, unterhalb derer eine Liste ausscheidet
In der obigen Abbildung können die Daten aus zwei Quellen stammen:
- die Datenzugriffsebene oder [DAO] (DAO = Data Access Object) für Daten, die bereits in Dateien oder Datenbanken gespeichert sind. Dies könnte hier für die Namen der Listen, die Anzahl der zu besetzenden Sitze und die Wahlhürde der Fall sein. Diese Informationen sind nämlich bereits vor der Wahl bekannt.
- die Benutzeroberflächenschicht oder [ui] (UI = User Interface) für Daten, die vom Benutzer eingegeben oder ihm angezeigt werden. Dies könnte hier für die Stimmen für die Listen gelten, die erst im letzten Moment bekannt sind, sowie für die Anzeige der Wahlergebnisse.
- Allgemein gesagt kümmert sich die [DAO]-Schicht um den Zugriff auf persistente Daten (Dateien, Datenbanken) oder nicht-persistente Daten (Netzwerk, Sensoren usw.).
- Die [UI]-Schicht hingegen kümmert sich um die Interaktion mit dem Benutzer, sofern vorhanden.
- Die drei Schichten werden durch die Verwendung von Java-Schnittstellen voneinander unabhängig gemacht.
- Es gibt verschiedene Methoden, diese Schichten in die Anwendung zu integrieren. Wir werden ein Tool namens „Spring“ verwenden. In der Abbildung durchzieht es die anderen Schichten.
Wir werden die zuvor entwickelte [Elections]-Anwendung noch einmal betrachten, um ihr eine dreischichtige Architektur zu geben. Dazu werden wir die [UI-, Business- und DAO-]Schichten nacheinander untersuchen, beginnend mit der [DAO]-Schicht, die persistente Daten verwaltet.
Zunächst müssen wir die Schnittstellen für die verschiedenen Schichten der [Elections]-Anwendung definieren.
4.2. Die Schnittstellen der [Elections]-Anwendung
Denken Sie daran, dass eine Schnittstelle eine Reihe von Methodensignaturen definiert. Die Klassen, die die Schnittstelle implementieren, liefern die Implementierung für diese Methoden.
Kehren wir zur 3-Schichten-Architektur unserer Anwendung zurück:
![]() |
Bei dieser Art von Architektur ergreift oft der Benutzer die Initiative. Er stellt eine Anfrage bei [1] und erhält eine Antwort bei [8]. Dies wird als Anfrage-Antwort-Zyklus bezeichnet. Nehmen wir das Beispiel der Berechnung der gewonnenen Sitze am Wahlabend. Dazu sind mehrere Schritte erforderlich:
- Die [ui]-Schicht muss den Benutzer nach der Anzahl der Stimmen fragen, die jede der Listen erhalten hat. Dazu muss sie dem Benutzer die Namen der konkurrierenden Listen anzeigen. Der Benutzer gibt dann einfach die Anzahl der Stimmen neben jeder Liste ein und fordert eine Sitzberechnung an.
- Die [ui]-Schicht verfügt nicht über die Namen der Listen. Diese sind in der Datenquelle rechts im Diagramm gespeichert. Sie nutzt den Pfad [2, 3, 4, 5, 6, 7], um sie abzurufen. Operation [2] ist die Anfrage nach den Listen, und Operation [7] ist die Antwort auf diese Anfrage. Sobald dies geschehen ist, kann sie sie dem Benutzer über [8] präsentieren.
- Der Benutzer übermittelt an die [ui]-Ebene die Anzahl der Stimmen, die jede Liste erhalten hat. Dies ist der oben genannte Vorgang [1]. Während dieses Schritts interagiert der Benutzer nur mit der [ui]-Ebene. Diese Ebene überprüft die Gültigkeit der eingegebenen Daten. Sobald dies geschehen ist, fordert der Benutzer die Liste der Sitze an, die jede Liste erhalten hat.
- Die [ui]-Schicht fordert die Business-Schicht auf, die Sitze zu berechnen. Dazu sendet sie die vom Benutzer erhaltenen Daten an die Business-Schicht. Dies ist Vorgang [2].
- Die [business]-Schicht benötigt bestimmte Informationen, um ihre Aufgabe auszuführen. Sie verfügt bereits über die Listen aus Vorgang (b). Außerdem benötigt sie die Anzahl der zu besetzenden Sitze und den Wahlhürdenwert. Sie fordert diese Informationen über den Pfad [3, 4, 5, 6] von der [DAO]-Schicht an. [3] ist die ursprüngliche Anfrage und [6] ist die Antwort auf diese Anfrage.
- Mit allen benötigten Daten berechnet die [Business]-Schicht die von jeder Liste gewonnenen Sitze.
- Die [Business]-Schicht kann nun auf die in (d) gestellte Anfrage der [UI]-Schicht reagieren. Dies ist der Pfad [7].
- Die [UI]-Schicht formatiert diese Ergebnisse, um sie dem Benutzer in geeigneter Form darzustellen, und zeigt sie dann an. Dies ist der Pfad [8].
- Man kann sich vorstellen, dass diese Ergebnisse in einer Datei oder einer Datenbank gespeichert werden müssen. Dies kann automatisch erfolgen. In diesem Fall fordert die [Business]-Schicht nach dem Vorgang (f) die [DAO]-Schicht auf, die Ergebnisse zu speichern. Dies ist der Pfad [3, 4, 5, 6]. Dies kann auch nur auf Benutzeranforderung erfolgen. Der Pfad [1–8] wird vom Anfrage-Antwort-Zyklus verwendet.
Aus dieser Beschreibung geht hervor, dass eine Schicht die Ressourcen der rechts von ihr liegenden Schicht nutzt, niemals jedoch die der links von ihr liegenden Schicht. Betrachten wir zwei benachbarte Schichten:
![]() |
Schicht [A] sendet Anfragen an Schicht [B]. Im einfachsten Fall wird eine Schicht durch eine einzige Klasse implementiert. Eine Anwendung entwickelt sich im Laufe der Zeit weiter. Daher kann Schicht [B] verschiedene Implementierungsklassen [B1, B2, ...] haben. Wenn Schicht [B] die [DAO]-Schicht ist, kann sie eine anfängliche Implementierung [B1] haben, die Daten aus einer Datei abruft. Einige Jahre später möchten wir die Daten vielleicht in einer Datenbank speichern. Wir werden dann eine zweite Implementierungsklasse [B2] erstellen. Wenn in der ursprünglichen Anwendung die Schicht [A] direkt mit der Klasse [B1] zusammengearbeitet hat, sind wir gezwungen, den Code für die Schicht [A] teilweise neu zu schreiben. Nehmen wir zum Beispiel an, wir hätten in der Schicht [A] etwa Folgendes geschrieben:
- Zeile 1: Eine Instanz der Klasse [B1] wird erstellt
- Zeile 3: Daten werden von dieser Instanz angefordert
Wenn wir davon ausgehen, dass die neue Implementierungsklasse [B2] Methoden mit derselben Signatur wie die der Klasse [B1] verwendet, müssen wir alle Verweise auf [B1] in [B2] ändern. Dies ist ein sehr günstiges Szenario und ziemlich unwahrscheinlich, wenn wir diesen Methodensignaturen keine Beachtung geschenkt haben. In der Praxis ist es üblich, dass die Klassen [B1] und [B2] unterschiedliche Methodensignaturen haben, was bedeutet, dass ein erheblicher Teil der Schicht [A] komplett neu geschrieben werden muss.
Wir können die Situation verbessern, indem wir eine Schnittstelle zwischen den Schichten [A] und [B] einführen. Das bedeutet, dass wir die von der Schicht [B] gegenüber der Schicht [A] präsentierten Methodensignaturen in einer Schnittstelle definieren. Das vorherige Diagramm sieht dann wie folgt aus:
![]() |
Schicht [A] kommuniziert nicht mehr direkt mit Schicht [B], sondern mit deren Schnittstelle [IB]. Daher taucht im Code für Schicht [A] die Implementierungsklasse [Bi] von Schicht [B] nur einmal auf, nämlich bei der Implementierung der Schnittstelle [IB]. Sobald dies geschehen ist, wird im Code die Schnittstelle [IB] und nicht deren Implementierungsklasse verwendet. Der vorherige Code sieht nun wie folgt aus:
- Zeile 1: Eine Instanz [ib], die die Schnittstelle [IB] implementiert, wird durch Instanziierung der Klasse [B1] erstellt
- Zeile 3: Daten werden von der Instanz [ib] angefordert
Wenn wir nun die Implementierung [B1] der Schicht [B] durch eine Implementierung [B2] ersetzen und beide Implementierungen dieselbe Schnittstelle [IB] einhalten, muss nur Zeile 1 der Schicht [A] geändert werden, keine anderen Zeilen. Dies ist ein wesentlicher Vorteil, der allein schon den systematischen Einsatz von Schnittstellen zwischen zwei Schichten rechtfertigt.
Wir können sogar noch einen Schritt weiter gehen und die Schicht [A] vollständig unabhängig von der Schicht [B] machen. Im obigen Code stellt Zeile 1 ein Problem dar, da sie eine Referenz auf die Klasse [B1] fest codiert. Im Idealfall sollte die Schicht [A] in der Lage sein, eine Implementierung der Schnittstelle [IB] zu verwenden, ohne einen Klassennamen angeben zu müssen. Dies würde mit unserem obigen Diagramm übereinstimmen. Wir sehen, dass die Schicht [A] mit der Schnittstelle [IB] interagiert, und es gibt keinen Grund, warum sie den Namen der Klasse kennen müsste, die diese Schnittstelle implementiert. Dieses Detail ist für die Schicht [A] nicht von Nutzen.
Das Spring-Framework (http://www.springframework.org) ermöglicht es uns, dieses Ergebnis zu erzielen. Die vorherige Architektur entwickelt sich wie folgt weiter:
![]() |
Die querschnittliche Schicht [Spring] ermöglicht es einer Schicht, über die Konfiguration eine Referenz auf die rechts davon liegende Schicht zu erhalten, ohne den Namen der Klasse kennen zu müssen, die diese Schicht implementiert. Dieser Name befindet sich in den Konfigurationsdateien und nicht im Java-Code. Der Java-Code für die Schicht [A] hat dann folgende Form:
- Zeile 1: eine Instanz [ib], die die Schnittstelle [IB] der Schicht [B] implementiert. Diese Instanz wird von Spring auf der Grundlage von Informationen aus einer Konfigurationsdatei erstellt. Spring übernimmt die Erstellung:
- die Instanz [b], die die Schicht [B] implementiert
- die Instanz [a], die die Schicht [A] implementiert. Diese Instanz wird initialisiert. Dem Feld [ib] oben wird die Referenz [b] des Objekts zugewiesen, das die Schicht [B] implementiert
- Zeile 3: Daten werden von der Instanz [ib] angefordert
Wir sehen nun, dass die Implementierungsklasse [B1] der Schicht B nirgendwo im Code der Schicht [A] vorkommt. Wenn die Implementierung [B1] durch eine neue Implementierung [B2] ersetzt wird, ändert sich nichts am Code der Klasse [A]. Wir ändern lediglich die Spring-Konfigurationsdateien, um [B2] anstelle von [B1] zu instanziieren.
Die Kombination aus Spring und Java-Schnittstellen bringt eine entscheidende Verbesserung für die Wartung der Anwendung mit sich, indem die Schichten der Anwendung eng miteinander gekoppelt werden. Dies ist die Lösung, die wir für die Anwendung [Elections] verwenden werden.
Kehren wir zur dreischichtigen Architektur unserer Anwendung zurück:
![]() |
In einfachen Fällen können wir bei der [Business-]Schicht ansetzen, um die Schnittstellen der Anwendung zu ermitteln. Damit sie funktioniert, benötigt sie Daten:
- die bereits in Dateien, Datenbanken oder über das Netzwerk verfügbar sind. Diese Daten werden von der [DAO]-Schicht bereitgestellt.
- noch nicht verfügbar. Sie werden dann von der [UI]-Schicht bereitgestellt, die sie vom Anwendungsbenutzer erhält.
Welche Schnittstelle sollte die [DAO]-Schicht der [Business]-Schicht bereitstellen? Welche Interaktionen sind zwischen diesen beiden Schichten möglich? Die [DAO]-Schicht muss der [Business]-Schicht die folgenden Daten bereitstellen:
- die Anzahl der zu besetzenden Sitze
- die Wahlhürde, unterhalb derer eine Liste ausscheidet
- die Namen der Listen
Diese Informationen sind vor der Wahl bekannt und können daher gespeichert werden. In der Richtung [Business] -> [DAO] kann die [Business]-Schicht die [DAO]-Schicht auffordern, die Wahlergebnisse zu erfassen, einschließlich der Anzahl der von den verschiedenen Listen gewonnenen Sitze.
Mit diesen Informationen könnten wir eine erste Definition der Schnittstelle der [DAO]-Schicht versuchen:
public interface IElectionsDao {
public double getSeuilElectoral();
public int getNbSiegesAPourvoir();
public ListeElectorale[] getListesElectorales();
public void setListesElectorales(ListeElectorale[] listesElectorales);
}
- Zeile 1: Die Schnittstelle heißt [IElectionsDao]. Sie definiert vier Methoden:
- drei Methoden zum Lesen von Daten aus der Datenquelle: [getVotingThreshold, getNumberOfSeatsToBeFilled, getVoterLists]. Diese drei Methoden ermöglichen es der [Business]-Schicht, die Daten zu erhalten, die die aktuelle Wahl charakterisieren.
- eine Methode zum Schreiben von Daten in die Datenquelle: [setVoterLists]. Diese Methode ermöglicht es der [Business]-Schicht, die Speicherung der von ihr berechneten Ergebnisse anzufordern.
Kehren wir zur dreischichtigen Architektur unserer Anwendung zurück:
![]() |
Welche Schnittstelle sollte die [Business]-Schicht der [UI]-Schicht zur Verfügung stellen? Betrachten wir die möglichen Interaktionen zwischen diesen beiden Schichten.
- Die [UI]-Schicht ist dafür zuständig, den Benutzer um Stimmen für die verschiedenen konkurrierenden Listen zu bitten. Dazu muss sie die Anzahl der Listen kennen. Sie kann diese Information von der [Business]-Schicht anfordern, die wiederum die Tabelle der konkurrierenden Listen von der [DAO]-Schicht anfordern kann. Wenn die [Business]-Schicht über diese Tabelle verfügt, kann sie diese ebenso gut an die [UI]-Schicht weiterleiten. Die [UI]-Schicht verfügt dann über die Namen der Listen und kann ihre Meldungen an den Benutzer verfeinern, indem sie beispielsweise fragt: „Anzahl der Stimmen für Liste A.“
- Sobald die [UI]-Schicht die Stimmen für alle Listen erhalten hat, fordert sie die Sitzberechnung von der [Business]-Schicht an. Die [Business]-Schicht kann diese Berechnung durchführen und das Ergebnis an die [UI]-Schicht zurückgeben.
- Die [UI]-Schicht kann diese Ergebnisse dann dem Benutzer präsentieren. Der Benutzer kann auch verlangen, dass sie gespeichert werden.
- Die [UI]-Schicht möchte dem Benutzer möglicherweise auch zusätzliche Informationen präsentieren, wie beispielsweise die Wahlhürde oder die Anzahl der zu besetzenden Sitze.
Mit diesen Informationen könnten wir eine erste Definition der Schnittstelle für die [ -Metier]-Schicht versuchen:
public interface IElectionsMetier {
public ListeElectorale[] getListesElectorales();
public int getNbSiegesAPourvoir();
public double getSeuilElectoral();
public void recordResultats(ListeElectorale[] listesElectorales);
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
- Zeile 1: Die Schnittstelle heißt [IElectionsMetier]. Sie definiert die folgenden Methoden:
- Zeile 3: eine Methode [getVoterLists], die es der [ui]-Schicht ermöglicht, das Array der konkurrierenden Listen abzurufen;
- Zeile 5: Die Methode [getNbSiegesAPourvoir] ruft die Anzahl der zu besetzenden Sitze ab;
- Zeile 7: Die Methode [getElectoralThreshold] ruft die Wahlhürde ab;
- Zeile 11: eine Methode [calculateSeats] (Zeile 36), die es der [ui]-Schicht ermöglicht, die Berechnung der Sitze anzufordern, sobald die Stimmenzahlen für die verschiedenen Listen bekannt sind. Der Parameter ist das Array der konkurrierenden Listen, ohne deren Sitze und ohne den Booleschen Wert „eliminated“. Das zurückgegebene Ergebnis ist dasselbe Array, diesmal jedoch mit den Feldern [seats, eliminated] initialisiert;
- Zeile 9: eine Methode [recordResults], die es der [ui]-Ebene ermöglicht, die Aufzeichnung der Ergebnisse anzufordern.
Hinweis: Aufgrund ihrer Position verwendet die [business]-Schicht einige der Methoden aus der [DAO]-Schicht wieder, um sie der [UI]-Schicht zur Verfügung zu stellen. Aufgrund dieser Redundanz könnte man versucht sein, alles in einer einzigen Schicht zusammenzufassen, die sowohl die Geschäftslogik als auch den Datenzugriff vereint. Diese einzelne Schicht wird manchmal als Modell bezeichnet, das M im Akronym MVC (Model-View-Controller). MVC ist ein in Webanwendungen weit verbreitetes Entwurfsmuster.
Betrachten wir die Signatur der Methode [calculateSeats]:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
Zuvor hieß es: „Der Parameter ist das Array der konkurrierenden Listen, ohne deren Sitze und ohne den booleschen Wert ‚eliminated‘. Das Ergebnis ist dasselbe Array, diesmal jedoch mit den Feldern [seats, eliminated].“ Die Methodensignatur könnte auch wie folgt lauten:
public void calculerSieges(ListeElectorale[] listesElectorales);
Der Parameter [voterLists] ist eine Objektreferenz, in diesem Fall ein Array. Jedes Element ist wiederum eine Objektreferenz, in diesem Fall vom Typ [VoterList]. Die Methode [calculateSeats] ändert die Felder [seats, eliminated] jedes dieser Objekte. Die aufrufende Methode enthält einen Zeiger [voterLists], der:
- vor dem Aufruf eine Referenz auf ein Array von [VoterList]-Objekten ist, deren Felder [seats, eliminated] nicht initialisiert sind;
- nach dem Aufruf die Referenz (dieselbe) auf ein Array von [VoterList]-Objekten ist, deren Felder [seats, eliminated] initialisiert sind;
Warum also die Signatur verwenden:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
Beim Schreiben einer Schnittstelle ist es wichtig zu beachten, dass sie in zwei verschiedenen Kontexten verwendet werden kann: im lokalen und im Remote- . Im lokalen Kontext werden die aufrufende und die aufgerufene Methode in derselben JVM (Java Virtual Machine) ausgeführt:
![]() |
Wenn die [ui]-Schicht die Methode calculateSeats der [DAO]-Schicht aufruft, verfügt sie tatsächlich über eine Referenz auf den Parameter [VoterList[] voterLists], den sie an die Methode übergibt.
Im Remote-Kontext werden die aufrufende und die aufgerufene Methode in unterschiedlichen JVMs ausgeführt:
![]() |
Im obigen Beispiel läuft die [ui]-Schicht in JVM 1 und die [business]-Schicht in JVM 2 auf zwei verschiedenen Rechnern. Die beiden Schichten kommunizieren nicht direkt miteinander. Zwischen ihnen liegt eine Schicht, die wir als Kommunikationsschicht [1] bezeichnen. Diese besteht aus einer Sendeschicht [2] und einer Empfangsschicht [3]. Der Entwickler muss diese Kommunikationsschichten in der Regel nicht selbst schreiben. Sie werden automatisch von Software-Tools generiert. Die [business]-Schicht wird so geschrieben, als würde sie in derselben JVM wie die [DAO]-Schicht laufen. Daher sind keine Codeänderungen erforderlich.
Der Kommunikationsmechanismus zwischen der [ui]-Schicht und der [business]-Schicht ist wie folgt:
- Die [ui]-Schicht ruft die Methode calculateSeats der [business]-Schicht auf und übergibt ihr den Parameter [VoterList[] voterLists1];
- Dieser Parameter wird tatsächlich an die Übertragungsschicht [2] übergeben. Diese Schicht überträgt den Wert des Parameters `listesElectorales1` über das Netzwerk, nicht dessen Referenz. Die genaue Form dieses Werts hängt vom verwendeten Kommunikationsprotokoll ab;
- Die empfangende Schicht [3] ruft diesen Wert ab und verwendet ihn, um ein Objekt [VoterList[] voterLists2] zu rekonstruieren, das den ursprünglich von der [Business]-Schicht gesendeten Parameter widerspiegelt. Wir haben nun zwei (inhaltlich) identische Objekte in zwei verschiedenen JVMs: voterLists1 und voterLists2.
- Die empfangende Schicht übergibt das Objekt `listesElectorales2` an die Methode `calculerSieges` der [business]-Schicht, die es in der Datenbank speichert. Nach diesem Vorgang verweist die Referenz `listesElectorales2` auf ein Array von [VoterList]-Objekten, deren Felder [seats, eliminated] initialisiert sind. Dies ist nicht der Fall für das Objekt `listesElectorales1`, auf das die [ui]-Schicht eine Referenz hat. Wenn wir möchten, dass die [ui]-Schicht eine Referenz auf das Objekt listesElectorales2 hat, müssen wir es an die [ui]-Schicht übergeben. Daher verwenden wir die folgende Signatur für die Methode [calculerSieges]:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
- Mit dieser Signatur gibt die Methode `calculateSeats` die Referenz `electoralLists2` als Ergebnis zurück. Dieses Ergebnis wird an die empfangende Schicht [3] zurückgegeben, die die [Business]-Schicht aufgerufen hatte. Die [Business]-Schicht gibt den Wert (nicht die Referenz) von `electoralLists2` an die sendende Schicht [2] zurück;
- Die sendende Schicht [2] ruft diesen Wert ab und verwendet ihn, um ein Objekt [VoterList[] voterLists3] zu rekonstruieren, das das von der Methode `calculateSeats` der [Business]-Schicht zurückgegebene Ergebnis widerspiegelt.
- Das Objekt [VoterList[] voterLists3] wird an die Methode in der [UI]-Schicht zurückgegeben, deren Aufruf der Methode calculateSeats der [DAO]-Schicht diesen gesamten Mechanismus ausgelöst hatte;
In diesem Prozess werden Objekte vom Typ [VoterList] zwischen den Schichten [2] und [3] ausgetauscht:
- Wenn die Schicht [2] den Wert eines [VoterList]-Objekts an die Schicht [3] überträgt, spricht man davon, dass das Objekt serialisiert wird. Die genaue Form dieser Serialisierung hängt vom verwendeten Kommunikationsprotokoll ab;
- Wenn die Schicht [3] den Wert eines [VoterList]-Objekts abruft, um ein neues [VoterList]-Objekt zu erstellen, wird das Objekt deserialisiert;
Damit ein Objekt diese Serialisierung/Deserialisierung durchlaufen kann, verlangen bestimmte Protokolle, dass das Objekt die Schnittstelle [Serializable] implementiert. Diese Schnittstelle ist lediglich ein Marker; es gibt keine Methoden, die implementiert werden müssen. Daher wird die Klasse [VoterList] nun wie folgt deklariert:
public abstract class ListeElectorale implements Serializable {
private static final long serialVersionUID = 1L;
- Das Feld in Zeile 2 ist obligatorisch. Es kann unverändert beibehalten und für jede Klasse vom Typ [Serializable] verwendet werden.
4.3. Die Ausnahmeklasse
Kehren wir zur Schnittstelle der [DAO]-Schicht zurück:
![]() |
public interface IElectionsDao {
public double getSeuilElectoral();
public int getNbSiegesAPourvoir();
public ListeElectorale[] getListesElectorales();
public void setListesElectorales(ListeElectorale[] listesElectorales);
}
Diese Methoden arbeiten mit einer Datenbank und können auf verschiedene Fehler stoßen, beispielsweise wenn die Datenbank nicht verfügbar ist. Beim Schreiben einer Methode müssen Sie immer Fehlerfälle behandeln. Diese werden in der Regel durch eine Ausnahme signalisiert. Wir sind der Klasse [ElectionsException] bereits in Abschnitt 3.3 begegnet. Wir werden sie weiterhin verwenden, aber wie folgt erweitern:
package ...;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
// exception class for the Elections application
// the exception is uncontrolled
public class ElectionsException extends RuntimeException implements Serializable {
// serial ID
private static final long serialVersionUID = 1L;
// local fields
private int code;
private List<String> erreurs;
// manufacturers
public ElectionsException() {
super();
}
public ElectionsException(int code, Throwable e) {
// parent
super(e);
// local
this.code = code;
this.erreurs = getErreursForException(e);
}
public ElectionsException(int code, String message, Throwable e) {
// parent
super(message,e);
// local
this.code = code;
this.erreurs = getErreursForException(e);
}
public ElectionsException(int code, String message) {
// parent
super(message);
// local
this.code = code;
List<String> erreurs = new ArrayList<>();
erreurs.add(message);
this.erreurs = erreurs;
}
public ElectionsException(int code, List<String> erreurs) {
// parent
super();
// local
this.code = code;
this.erreurs = erreurs;
}
// list of exception error messages
private List<String> getErreursForException(Throwable th) {
// retrieve the list of exception error messages
Throwable cause = th;
List<String> erreurs = new ArrayList<>();
while (cause != null) {
// the message is retrieved only if it is !=null and not blank
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// next cause
cause = cause.getCause();
}
return erreurs;
}
// getters and setters
...
}
- Zeilen 16–17: Der Typ [ElectionsException] kapselt:
- einen Fehlercode, Zeile 16;
- eine Liste von Fehlermeldungen, Zeile 17;
Die Klasse unterstützt fünf Konstruktoren:
- Zeile 20: ElectionsException()
- Zeile 24: ElectionsException(int code, Throwable e): Der zweite Parameter ist vom Typ [Throwable], der die Oberklasse der Klasse [Exception] darstellt. Dieser Konstruktor ermöglicht es, die Ausnahme e zusammen mit einem Fehlercode zu kapseln. Der Typ [Throwable] (und damit auch der Typ Exception) ermöglicht es Ihnen, eine oder mehrere Ausnahmen zu kapseln. Die Idee dahinter ist:
- eine auftretende Ausnahme abzufangen;
- sie durch Kapselung in eine neue Ausnahme mit einer Meldung anzureichern;
- die neue Ausnahme auszulösen;
Die Kapselung erfolgt in Zeile 34 über die Anweisung [super(message, e)]. Dieser Kapselungsprozess kann wiederholt werden, und die ursprüngliche Ausnahme kann mit verschiedenen Meldungen angereichert werden. Dies wird als Ausnahmestapel bezeichnet. Mit der Methode [private List<String> getErrorsForException(Throwable th)] können Sie die verschiedenen Meldungen abrufen, die mit den gekapselten Ausnahmen verbunden sind:
- (Fortsetzung)
- (Fortsetzung)
- Die gekapselte Ausnahme wird mithilfe der Throwable-Methode [Throwable].getCause() abgerufen;
- die mit einer Ausnahme verbundene Meldung wird über die Methode String [Throwable].getMessage() abgerufen;
- (Fortsetzung)
- Zeilen 28–29: Die Felder [code, errors] werden angelegt;
- Zeile 32: public ElectionsException(int code, String message, Throwable e): Dieser Konstruktor ähnelt dem vorherigen, außer dass er die Ausnahme, die er kapseln wird, um einen Code und eine Meldung erweitert;
- Zeile 40: public ElectionsException(int code, String message): Konstruktor ohne Kapselung einer Ausnahme;
- Zeile 50: public ElectionsException(int code, List<String> errors): Konstruktor ohne Ausnahmekapselung oder Meldung;
Die Klasse [ElectionsException] kann wie folgt verwendet werden:
wobei die Meldung vorhanden sein kann oder auch nicht. Nach ihrer Erstellung ist die Ausnahme [ElectionsException] nicht dazu gedacht, neue Ausnahmen zu kapseln. Im obigen Beispiel kapselt sie die Ausnahme e1 und die Ausnahmen, die e1 kapselt. Darüber hinaus gibt es keine weiteren Kapselungen.
Die Klasse [ElectionsException] kann auch wie folgt verwendet werden:






