-
Notifications
You must be signed in to change notification settings - Fork 1
Implementierung
In diesem Abschnitt wird die Implementierung der einzelnen Komponenten von Prox beschrieben.
Die Repositories zu den einzelnen Komponenten befinden sich unter den folgenden Links:
Das Backend für Prox besteht, wie auf der Seite Architektur beschrieben, unter anderem aus zwei Microservices: Modul-Microservice und Project-Microservice. Beide Microservices verwenden Spring Boot Version 2.1.2.RELEASE und wurden mithilfe des Spring Initializrs erzeugt. Bei der Modellierung der Software wurde Domain-Driven Design (DDD) angewandt. Dabei handelt es sich um einen Design-Ansatz, der ursprünglich von Eric Evans entwickelt wurde und den Fokus sehr stark auf die Fachlichkeit bzw. die Anwendungsdomäne der Software legt und somit eine starke Kollaboration zwischen technischen und fachlichen Experten erfordert. Grundsätzlich werden dabei strategisches und taktisches DDD unterschieden, wobei wir uns größtenteils auf die Implementierung der taktischen DDD-Pattern mithilfe von Spring Boot beschränkt haben.
Um die Building Blocks von DDD nach Eric Evans zu implementieren, haben wir kontinuierliche und umfangreiche Recherche zum Thema durchgeführt und aus den gewonnen Erkenntnissen Blueprints entwickelt, um die zukünftige Entwicklung für uns und nachfolgende Studenten zu erleichtern und eine einheitliche Basis für qualitativ hochwertigen Code zu schaffen.
Die Blueprints sollten dabei die folgenden Eigenschaften erfüllen:
- Möglichst generisch und somit vielfältig einsetzbar
- Einfach zu verstehen
- Wenig Coding-Aufwand
Für den Einsatz der Blueprints sind die folgenden Frameworks und Libraries erforderlich:
Ausschnitt einer möglichen pom.xml:
...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency><dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
...
Entities sind Objekte innerhalb der Domäne, die eine eindeutige Identität haben, die nicht nur über ihre Attribute bestimmt ist. Beispielsweise kann ein Objekt durch unterschiedliche Repräsentationen dargestellt werden oder über die Zeit seine Attribute ändern, ohne dass sich seine Identität ändert. Dass bedeutet, dass zwei Entities immer dann gleich sind, wenn ihre Identitäten gleich sind, und immer dann unterschiedlich, wenn ihre Identitäten unterschiedlich sind, unabhängig von ihren Attributen.
Spring Data JPA unterstützt die Annotationen der JPA Specification, für deren Verwendung wir uns entschieden haben, um möglichst unabhängig von der Implementierung des Persistenz-Providers (in unserem Fall Hibernate) zu sein. Laut dieser Spezifikation müssen alle Entities die folgenden für uns relevanten Kriterien erfüllen:
- The entity class must be annotated with the Entity annotation or denoted in the XML descriptor as an entity.
- The entity class must have a no-arg constructor. The entity class may have other constructors as well.
- The no-arg constructor must be public or protected.
- The entity class must be a top-level class. An enum or interface must not be designated as an entity.
- The entity class must not be final. No methods or persistent instance variables of the entity class may be final.
Dies hat für uns in Bezug auf DDD zwei Probleme zur Folge:
- Da zwangsläufig ein No-Arg-Konstruktor vorhanden sein muss, wäre es möglich über diesen Entities zu erzeugen, die aus Sicht der Domäne einen invaliden (uninitialisierten) Zustand erzeugen. Daher haben wir uns für "protected" Konstruktoren entschieden, die niemals im Code verwendet werden dürfen, da dies der höchsten erlaubten Kapselung entspricht. Persistenz-Provider verwenden Reflection, um über den No-Arg-Konstruktor Entities zu erzeugen und im Anschluss Attribute zu setzen.
- Da der "final" Modifier für persistierbare Instanz-Variablen nicht erlaubt ist, muss die laut DDD geforderte Immutability bspw. für IDs oder Value Objects anderweitig gewährleistet werden. Dies haben wir unter anderem dadurch gelöst, dass wir auf Setter prinzipiell verzichten und diese nur dann implementieren, wenn sie aus Domänensicht benötigt werden.
Ein weiteres Problem, was bei der Implementierung von Entities aufgekommen ist, liegt in der Implementierung der Java Collections:
- Gibt man die Referenz einer Collection bspw. über einen Getter nach außen, so ist es möglich, die Collection zu verändern, indem man darauf Methoden ausführt (add, remove, clear, ...). Um dies zu verhindern, haben wir uns entschieden, für Collections einen Getter zu implementieren, der eine unveränderbare Sicht auf die eigentliche Collection zurückliefert (siehe Collections) und Business-Methoden für die Operationen zu schreiben, die aus Domänensicht gewünscht sind.
Um unnötigen Boilerplate-Code zu vermeiden wurden Lombok-Annotationen verwendet um bspw. Getter-/Setter-, ToString-, Equals- und HashCode-Methoden, etc. zu generieren. Außerdem wurde eine abstrakte Basisklasse "AbstractEntity" implementiert, die jede Entity erweitern sollte:
@MappedSuperclass
@Data
@Setter(AccessLevel.NONE)
public abstract class AbstractEntity {
@Id
@JsonIgnore
private UUID id;
protected AbstractEntity() {
this.id = UUID.randomUUID();
}
}
Wie man sieht enthält die Klasse ein ID-Attribut vom Typ UUID (Universally Unique Identifier). Dieser Datentyp hat den Vorteil, dass er falls er mit Standard-Methoden erzeugt wird (vgl. RFC 4122) immer einzigartig ist, ohne dass zusätzliche Informationen benötigt werden, wie bspw. der Zähler einer Datenbanksequenz. Somit könnte sie auch beispielsweise trotz einer fehlerhaften Netzwerkverbindung zum Backend bereits im Frontend erzeugt werden. Diese ID ist außerdem mit der JPA-Annotation @Id als Primary Key gekennzeichnet und mit der Jackson-Annotation @JsonIgnore von der JSON-Serialisierung ausgeschlossen, damit sie im Body der JSON-Objekte innerhalb der REST-API nicht als Attribut auftaucht. Die JPA-Annotation @MappedSuperClass sorgt dafür, dass die Superklasse einer Entity im zugrundeliegenden Datenbankschema überhaupt nicht auftaucht. Für jede Subklasse existiert eine eigene Tabelle mit allen Attributen der Super- und Subklasse. Damit sind polymorphische Datenbankanfragen auf Basis der Superklasse nicht möglich, aber in unserem Fall auch nicht gewünscht, da die Klasse lediglich der Reduzierung von Code dient. Die Lombok-Annotation @Data (Kurzform für @ToString, @EqualsAndHashCode, @Getter und @Setter) sorgt dafür, dass die Klasse zur Kompilierzeit um eine ToString-, eine Equals-, eine HashCode- und Getter-/Setter-Methoden basierend auf all ihren Attributen (in diesem Fall nur der ID) ergänzt wird. @Setter(AccessLevel.NONE) verhindert die Generierung der Setter, um die ID immutable zu machen. Eine Veränderung der ID hätte aus konzeptueller Sicht zur Folge, dass das Java-Objekt nicht mehr dieselbe Entity verkörpert, was für die Entwickler zu verwirrenden Seiteneffekten führen könnte, daher wurde dies unterbunden. Weiterhin wurde entschieden, dass die ID bereits bei der Erzeugung (im No-Arg-Konstruktor) generiert wird. Die sehr gebrächliche Alternative der @GeneratedValue ID hat nämlich den großen Nachteil, dass sie erst bei der ersten Persistierung generiert wird. Somit ist die Entity vor der ersten Persistierung in einem konzeptuell invaliden Zustand, da sie keine ID besitzt. Das kann unter Umständen zu unvorhersehbarem Verhalten führen, dessen Ursache ohne entsprechendes Hintergrundwissen schwer auszumachen ist. Ein Beispiel für ein solches Verhalten ist das Hinzufügen von zwei oder mehr unpersistierten Entities zu einem Set. Da ihre IDs vor der Persistierung gleich sind (null) und somit der Vergleich mit der Equals-Methode "true" zurückliefert, würde nur die erste Entity zum Set hinzugefügt werden, da ein Set jedes Element nur ein einziges Mal enthalten darf. Ein weiterer Vorteil der ID-Erzeugung im No-Arg-Konstruktor der abstrakten Superklasse ist, dass in Java jeder Konstruktor implizit als erstes den No-Arg-Konstruktor der Superklasse aufruft, falls nicht explizit ein anderer Super-Konstruktor aufgerufen wird (ohne explizite Klassenerweiterung wird der No-Arg-Konstruktor der Object-Klasse aufgerufen). Damit muss sich in den Konstruktoren der Entity-Subklassen nicht ausdrücklich um die ID-Generierung gekümmert werden.
Hier ist ein generisches Beispiel, wie die Implementierung einer Entity nach unserer Empfehlung aussehen sollte:
@Entity
@Getter
@ToString(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SomeEntity extends AbstractEntity {
private SomeValueObject someValueObject;
private List<SomeOtherValueObject> someOtherValueObjects = new ArrayList<>();
public SomeEntity(SomeValueObject someValueObject) {
this.someValueObject = someValueObject;
}
public getSomeOtherValueObjects() {
return Collections.unmodifiableList(this.someOtherValueObjects);
}
public addSomeOtherValueObject(SomeOtherValueObject someOtherValueObject) {
this.someOtherValueObjects.add(someOtherValueObject);
}
public removeSomeOtherValueObject(SomeOtherValueObject someOtherValueObject) {
this.someOtherValueObjects.remove(someOtherValueObject);
}
}
Die Entity erweitert die AbstractEntity-Klasse und wird über die @Entity-Annotation als JPA-Entity gekennzeichnet. Da Lombok-Annotationen nicht vererbt werden, sondern nur der durch die Annotationen generierte Code, müssen @ToString- und @Getter-Annotationen erneut verwendet werden. Die ToString-Methode soll gleichzeitig die Methode der Superklasse berücksichtigen (callSuper = true), damit auch die ID enthalten ist. Konstruktoren werden in Java ebenfalls nicht vererbt, daher wird der benötigte protected No-Arg-Konstruktor über die Lombok-Annotation @NoArgsConstructor(access = AccessLevel.PROTECTED) erzeugt.
Value Objects sind Objekte der Domäne, die lediglich der Beschreibung von Entities dienen. Sie haben keine eigene Identität und sind allein über ihre Attribute bestimmt. Da es laut Eric Evans theoretisch möglich wäre, dass sich zwei unterschiedliche Entities dasselbe Value Object teilen (z. B. zwei Personen mit der selben Adresse), müssen Value Objects zwingend immutable sein, da eine Änderung des Value Objects sonst Einfluss auf alle referenzierenden Entities hätte (z. B. Adressänderung einer der beiden Personen).
Das Äquivalent zu Value Objects innerhalb der JPA-Spezifikation lautet Embeddable. Für Embeddables gelten die selben Kriterien wie für Entities, mit dem Unterschied, dass die @Entity-Annotation durch die @Embeddable-Annotation ersetzt wird.
Da Value Objects rein über die Werte ihrer Attribute bestimmt sind, ist es erforderlich, die Equals- und HashCode-Methoden zu überschreiben, da die standardmäßig von jedem Java-Objekt geerbten Implementierungen auf der Objekt-Identität beruhen. Die @Data-Annotation von Lombok generiert wie bereits erwähnt unter anderem diese beiden Methoden auf Basis der Attribute einer Klasse. Dies entspricht genau dem gewünschten Verhalten. Da diese Annotation jedoch standarmäßig auch Setter generiert und Value Objects zwingend immutable sein müssen, wird dies wieder zusätzlich über die Setter-Annotation mit "AccessLevel.NONE" verhindert.
Hier ist ein generisches Beispiel, wie die Implementierung eines Value Objects nach unserer Empfehlung aussehen sollte:
@Embeddable
@Data
@Setter(AccessLevel.NONE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SomeValueObject {
private String value;
public SomeValueObject(String value) {
this.value = value;
}
}
Die verwendeten Annotationen sollten bereits aus dem Abschnitt über Entities bekannt sein.
Aggregates sind Cluster von Entities und Value Objects die zur Einhaltung von Invarianzen vom Rest der Applikation abgegrenzt werden sollen. Dabei wird eine Entity als sogenannte Aggregate Root ausgewählt. Andere Entities und Value Objects innerhalb des Aggregates dürfen niemals direkt von außen referenziert werden, sondern nur über die Aggregate Root.
In der Java-Enterprise-Welt existiert kein direktes Äquivalent zu den Aggregates. Zur Umsetzung ist es lediglich erforderlich die Prüfung der Invarianzen des Aggregates an geeigneter Stelle zu implementieren, also entweder in der Aggregate Root selbst oder in einem Spring Validator.
Hier ist ein generisches Beispiel für ein Value Object inkl. Validator zur Überprüfung der maximalen Namenslänge von 255 Zeichen:
@Embeddable
@Data
@Setter(AccessLevel.NONE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SomeName {
private String name;
public SomeName(String name) {
this.name = name;
}
}
public class SomeNameValidator implements Validator {
private static final int MAX_LENGTH = 255;
@Override
public boolean supports(Class clazz) {
return SomeName.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "name.empty");
SomeName someName = (someName) target;
if (someName.getName().length() > SomeNameValidator.MAX_LENGTH) {
errors.rejectValue("name", "name.too.long");
}
}
}
Aus Zeitgründen wurde noch nicht abschließend geklärt, wie die Validierung in Spring letztendlich am sinnvollsten konfiguriert wird. An dieser Stelle sei auf Spring Boot Documentation - Validation und Spring Framework Documentation - Validation, Data Binding, and Type Conversion verwiesen.
Repositories bilden die Schnittstelle für CRUD-Operationen auf Aggregates. Sie erzeugen aus Sicht eines Software-Entwicklers die Illusion einer In-Memory-Collection. Somit kann man ein Repository eines bestimmten Typs im Code sehr ähnlich verwenden wie eine Liste dieses Typs, ohne sich über die darunterliegende Speichertechnologie (bspw. eine SQL-Datenbank) Gedanken zu machen. Die Komplexität wird an dieser Stelle drastisch reduziert, da der Infrastruktur-Code komplett von der eigentlichen Anwendung (bzw. dem Domain Layer) entkoppelt ist.
Spring Data JPA bietet extrem komfortable Repository-Interfaces, die den Entwicklern fast die gesamte Implementierung von Repositories abnehmen. Dazu reicht es bereits aus ein Interface zu definieren, welches eines der Repository-Interfaces (bspw. das CrudRepository) erweitert und die generischen Typen durch die Entity-Klasse und den ID-Typen zu ersetzen:
public interface SomeEntityRepository extends CrudRepository<SomeEntity, UUID> {}
Das führt dazu, dass Spring Boot automatisch eine Bean des jeweiligen Repositories erzeugt, für die dann wie bei jeder anderen Bean auch über Dependency Injection eine Referenz injiziert werden kann, um das Repository an beliebiger Stelle zu verwenden.
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
Das Repository für den Modul-Service befindet sich unter: prox-module-service. Es handelt sich um einen Microservice zum Import und zur Bereitstellung von Informationen über Studiengänge des Campus Gummersbach der TH Köln, sowie deren Modulen. Eine Besonderheit des Modul-Service liegt in der starken Kopplung mit dem HoPS der TH Köln, wo die zu importierenden Informationen zentral verwaltet werden. Da sich die Kommunikation mit den zuständigen Ansprechpartnern äußerst ineffizient gestaltet hat, haben wir uns dazu entschieden, einen Anticorruption Layer zu implementieren, der als isolierende Schicht zwischen unserem Modul-Service und der REST-API des HoPS liegt, um unser Domänenmodell möglichst stark von Änderungen oder Problemen der Schnittstelle zu entkoppeln. Jeglicher HoPS-relevanter Quellcode befindet sich in einem eigenen Java-Package. Dort befindet sich unter anderem eine Klasse "ImportRunner", die dafür sorgt, dass beim Starten des Service automatisch ein einmaliger Import der Studiengang- und Modul-Daten des HoPS durchgeführt wird. Die Import-Methoden könnten aber jederzeit auch anderweitig ausgeführt werden, bspw. als regelmäßiges Event (siehe @Scheduled).
Da sich im Laufe der Zeit unterschiedliche Fehler bei der Verwendung der HoPS-API ergeben haben, die weder dokumentiert, noch sinnvoll interpretierbar waren, haben wir zusätzlich einen Fallback-Mechanismus in die Import-Logik eingebaut, der im Fehlerfall die Daten aus manuell erzeugten JSON-Dateien importiert, die mit im Source-Code-Repository untergebracht wurden. Dies war nötig, da wir noch keine richtige Datenbank-Anbindung implementiert haben, sondern bis dato lediglich eine In-Memory-Datenbank verwendet haben, dessen Daten somit nicht persistent waren.
Das Repository für den Project-Service befindet sich unter: prox-project-service. Es handelt sich um einen simplen Microservice zur Verwaltung von Projekten, der keine großen Besonderheiten aufweist.
Das Repository für die Service-Discovery befindet sich unter: prox-service-dicovery. Es handelt sich dabei um einen Netflix Eureka Server, der mit Standard-Parametern nach der Spring Cloud Netflix Dokumentation konfiguriert wurde und keine Besonderheiten aufweist.
Das Repository für das API-Gateway befindet sich unter: prox-api-gateway. Es handelt sich dabei um einen Netflix Zuul Server, der mit Standard-Parametern nach der Spring Cloud Netflix Dokumentation konfiguriert wurde. Hier wurden Routen für die einzelnen REST-Pfade der Backend-Microservices konfiguriert. Wichtig hierbei waren die Einstellungen für den Umgang mit HTTP-Headern: "zuul.addProxyHeaders=false" und "zuul.add-host-header=true". Die erste Einstellung sorgt dafür, dass das API-Gateway unsichtbar agiert, da es damit keinen X-Forwarded-Host-Header verwendet um die Nachricht als weitergeleitet zu kennzeichnen. Die zweite Option sorgt dafür, dass der Host-Header an der weitergeleiteten Nachricht verbleibt und nicht entfernt wird.
Das Repository für das Frontend befindet sich unter: prox-web-client. Es wurde in der Programmiersprache TypeScript mithilfe des Frameworks Angular entwickelt. Zunächst werden in diesem Abschnitt die wesentlichen Frameworks und ihre Rolle im Frontend beschrieben. Anschließend wird die Projektstruktur vorgestellt. Dabei werden die einzelnen Elemente der Komponenten, Ressourcen und Services im Hinblick auf ihre Kommunikation näher erläutert. Nach Erläuterung der Projektstruktur folgt ein Designabschnitt, der beschreibt, welches Designsystem für das Frontend verwendet wird. Am Ende wird das Frontend bzw. die Webplattform präsentiert und ein Ausblick für die Zukunft gegeben. Zusätzlich wird auf offene Probleme eingegangen.
Im Frontend werden hauptsächlich die beiden Frameworks Angular und Angular 4 HAL verwendet. Der Grund für die Auswahl ist im Entscheidungsprotokoll festgehalten.
Das Frontend besteht hauptsächlich aus den drei Elementen: Komponenten, Ressourcen und Services. Komponenten repräsentieren und kontrollieren einen festgelegten Bereich der Nutzersicht. Services werden hauptsächlich dazu verwendet, um externe Daten für Ressourcen zu erhalten, damit diese durch die Komponenten in der Nutzersicht angezeigt werden können. Die Komponenten, Ressourcen und Services, die wir bei uns im Frontend verwenden, und deren Kommunikation zueinander werden in den nächsten drei Abschnitten näher behandelt.
Die Ordner- bzw. Projektstruktur des Frontends (Verzeichnis /app) ist wie folgt aufgebaut:
-
components (Enthält alle Komponenten)
- Komponente (Eine bestimmte Komponente, die aus einem TypeScript-, SCSS-, HTML- und Test-File besteht)
- ...
-
core (Enthält alle Komponenten, Services und andere Funktionalitäten, von
denen in der gesamten App nur eine einzige Instanz (Singleton) existiert)
- Kern Komponente
- ...
- services (Enthält alle Services)
-
shared (Enthält alle Komponenten, Direktiven, Pipes und Modelle, die über
die gesamte Anwendung hinweg geteilt werden (außer Singletons))
- hal-resources (Enthält alle Resourcen)
- ...
Im Frontend verwenden wir die folgenden neun Komponenten:
- home (Startseite)
- project-details (Detail-Ansicht der Projekte)
- project-dialog (Pop-Up Fenster bei der Projekterstellung)
- project-list (Projektseite)
- study-course-details (Detail-Ansicht der Studiengänge)
- study-course-list (Studiengangseite)
- footer
- header
- app (Vom Angular Framework festgelegte Hauptkomponente)
Der Zugriff auf die einzelnen Komponenten geschieht übers Routing. Damit kann über die Webplattform dynamisch navigiert werden. Da project-dialog ein Pop-Up Fenster ist, wird dieser mit Hilfe MatDialog in project-list aufgerufen. Mit dem MatDialog-Service können Pop-Up Fenster mit Material Design-Stilen und -Animationen geöffnet werden. Der Footer wurde erzeugt, besitzt jedoch noch keinen Inhalt. Der Header wird durchgängig auf jeder Seite verwendet. Der Zweck, Nutzen und die Aufgabe der einzelnen Komponenten werden im unteren Abschnitt Ergebnis beschrieben.
Die Ressourcen im Frontend spiegeln die Entities im Backend wieder. Das heißt, dass wir im Frontend die Folgenden Ressourcen besitzen:
- Projects (Projekte)
- Study-Course (Studiengänge)
- Module (Module)
Projekte und Module bestehen beide aus den Standard-Attributen (Name und Beschreibung inkl. Status bei Projekte). Studiengänge besitzen zusätzlich neben den Standard-Attributen (Name und Akademischer Grad) noch eine Beziehung zu den Modulen. Um die Relation zwischen den zwei Entitäten im Frontend abzubilden, wird das Angular 4 HAL Framework verwendet. Das Framework wird grundsätzlich auch für die Spiegelung der Entities auf Ressourcenebene im Frontend verwendet. Das heißt, dass jede erstellte Ressource zunächst einmal vom Angular 4 HAL bereitgestellten Resource-Klasse erbt, da diese Superklasse Methoden zu den Relationen einer Ressource bzw. Entity zur Verfügung stellt. Somit kann beispielsweise über die Methode getRelationArray() die Beziehung zwischen Studiengänge und Module im Frontend-Ebene ausgedrückt werden. Das heißt, dass beim Aufruf der Methode die Module eines bestimmten Studiengangs zurückgegeben werden können. Im Folgenden ist zusehen, wie das Entity Studiengang im Frontend mit Hilfe des Angular 4 HAL Frameworks implementiert ist.
Studiengang-Ressource Implementierung im Frontend:
JSON-Darstellung der Studiengang Entity:
Anhand der beiden oberen Abbildungen kann festgehalten werden, wie eine Entität und dessen Relationen im Frontend mit Hilfe des Angular 4 HAL Frameworks implementiert werden. Die beiden zusätzlichen Attribute studyDirection und parentStudyCourse werden momentan weder im Frontend noch im Backend sinnvoll verwendet.
Da im Backend Daten bezüglich Projekte, Studiengänge und Module in drei unterschiedlichen Schnittstellen zur Verfügung stehen, implementierten wir drei Services im Frontend, um darauf zuzugreifen, und zwar:
- Project-Service
- Module-Service
- Study-Course-Service
Für die Implementierung der einzelnen Services verwendeten wir das Angular 4 HAL Framework. Jeder implementierte Service erbt dabei von der Superklasse RestService mit der dazugehörigen Ressource. Im Konstruktor des Services wird lediglich der Konstruktor vom RestService aufgerufen unter Angabe der Schnittstelle und Ressource (siehe untere Abbildung). Die RestService-Klasse stellt dabei eine vielzahl von Methoden bereit, wie beispielsweise CRUD- und Filter-Methoden, auf die mittels des erstellten Services zugegriffen werden kann. Die Implementierung des Study-Course-Service mittels Angular 4 HAL ist in der unteren Abbildung dargestellt.
Implementierung des Study-Course-Service mittels Angular 4 HAL:
Eigene Filter-Methoden im Backend werden ebenfalls mit Hilfe Angular 4 HAL im Frontend aufgerufen, wie es bei der obigen dargestellten Methode findByAcademicDegree zusehen ist. Insgesamt besitzen wir im Backend zwei Filter-Methoden, und demnach auch zwei Methoden für den Zugriff im Frontend, und zwar, findByAcademicDegree (Filterung der Studiengänge nach dem akademischen Grad) und findByStatus (Filterung der Projekte nach deren Status).
Um beispielsweise alle verfügbaren Studiengänge vom Backend in unserem Frontend dynamisch anzuzeigen, müssen die Daten vorher mittels des Study-Course-Service in der entsprechenden study-course-list Komponente geladen, dem StudyCourse-Ressource Array zugeordnet und im HTML hinzugefügt werden. Die Kommunikation zwischen den drei Elementen wird im Folgenden in drei Schritten näher erläutert:
-
Studiengänge aus dem Backend herbeiholen: Hierfür bietet Angular 4 HAL in seiner RestService-Klasse die Methode getAll() an, die mittels des entsprechenden Services aufgerufen werden kann. In unserem Fall wäre es die StudyCourse-Service, die wir vorher im Konstruktur der study-course-list Komponente laden.
-
Herbeigeholte Studiengänge in StudyCourse-Ressource Array speichern: Um die herbeigeholten Studiengänge im Frontend zu persistieren, müssen diese mittels der subscribe-Methode an dem entsprechenden Array zugeordnet werden. Mehr dazu: Observables & Subscribing
Die beiden oberen Schritte sind in der folgenden Abbildung dargestellt:
- Die Inhalte aus dem StudyCourse-Ressource Array in der Nutzersicht anzeigen: Dafür iterieren wir in der entsprechenden HTML-Datei über die StudyCourse-Ressource Array mittels *ngFor und greifen somit auf die Attribute der einzelnen Studiengänge zu, um sie anzuzeigen. Der ganze Prozess ist in der folgenden Abbildung dargestellt:
Die drei genannten Schritte erläutern die Hauptkommunikationsart in unserem Frontend zwischen den Komponenten, Ressourcen und Services.
In diesem Abschnitt wird auf Material- und Responsive Design eingegangen, da wir beide Methodiken in unserem Frontend verwendet haben. Dabei wird nicht jede Einzelheit beschrieben, sondern nur die nennenswerten die zu berücksichtigen sind.
Das Design des Frontends basiert fast vollständig auf das Material Design für Angular von Google. Der Grund für die Verwendung von Material Design ist im Entscheidungsprotokoll festgehalten. UI-Elemente, die nicht auf das Material Design basieren, sind die Titel, Status-Tags (Laufend, Abgeschlossen, Geplant) eines Projekts und der Header. Diese wurden selbstständig mittels CSS designed. Zu den besonderen Material Design Komponenten, die wir im Frontend verwendet haben, zählen:
-
Material Card: Für die Darstellung der Informationen auf jeder Seite (bspw. die Liste aller Projekte oder die Detailansicht eines Studiengangs) des Frontends wurden Material Cards verwendet. Diese boten sich aufgrund derer Zusatzelemente (Title, Subtitle, Content etc.) idealerweise an.
-
Material Form Field mittels Reactive Forms: Die Dateneingabe bei der Projekterstellung geschieht mittels eines Material Form Fields. Dabei wird der Reactive Form Ansatz von Angular verwendet. Anhand des Ansatzes führen wir die Validierung der Nutzereingaben durch und greifen auf die eingegebenen Daten zu, um daraus ein Projekt zu erstellen. Die Validierung geschieht dabei dynamisch. Das heißt, dass die Form-Felder auf Echtzeit überprüft werden, bevor ein Projekt erstellt werden kann.
-
Material Dialog: Um die Seite bei der Projekterstellung nicht zu wechseln, wird ein Material Dialog für die Darstellung der Projekterstellungs-Komponente (project-dialog) verwendet. Diesem können Argumente, wie Höhe und Breite des Fensters, übergeben werden. In unserem Fall haben wir die Autofokussierung ausgestellt, damit der Abbrechen-Button nicht bei der Öffnung des Fensters automatisch fokussiert wird.
-
Material Sidenav: Der Sidebar aus dem Frontend basiert auf die Material Sidenav Komponente.
Weiterhin ist anzumerken, dass einige Material Design Komponenten, wie beispielsweise Material Buttons und Material Cards mittels CSS aufgrund des Aussehens designtechnisch angepasst worden sind.
Bei der Entwicklung des Frontends wurde auf ein Responsive Design geachtet. Der Grund für die Entscheidung eines responsiven Webdesigns ist im Entscheidungsprotokoll festgehalten. Dabei werden innerhalb einiger Komponenten, wie beispielsweise project-list (Liste aller Projekte) und project-dialog (Projekterstellung) sogenannte Media Queries in den CSS-Dateien der Komponenten verwendet. Mithilfe Media Queries gestalten wir einige UI-Elemente (bspw. Fontgröße, Material Cards und -Buttons) auf Basis der Fenstergröße (Höhe und Breite) des Endgeräts um, damit diese auch bei unterschiedlichen Fenstergrößen optisch richtig angezeigt werden. Bei der Entwicklung des Responsive Designs wurde darauf geachtet, dass es insbesondere bei den beiden Webbrowsern Firefox und Chrome zu keinen Schwierigkeiten und Unterscheidungen kommt. Momentan bleibt das Frontend responsive bis zu einer minimalen Fensterbreite von 300px. Für eine verbesserte optische Darstellung ändert sich ebenfalls das Logo zum Icon bei einer kleinen Fenstergröße.
In diesem Abschnitt wird das Frontend anhand von Bildern vorgestellt. Dabei werden die Funktionalitäten je Komponentenbereich beschrieben.
Die Startseite unseres Frontends zeigt momentan nur eine statische Projektbeschreibung an.
Startseite:
Da der Header auf jeder Komponente bzw. Seite im Frontend zusehen ist, kann der Sidebar mittels des Navigationsicons im Header geöffnet werden.
Sidebar:
Auf der Projektseite werden alle Projekte vom Backend angezeigt. Jedes Projekt erhält dabei eine eigene Material Card. Alle Informationen (Bezeichnung, Status) außer die Projektbeschreibung werden in dem entsprechenden Material Card angezeigt. Über den Detail-Button wird auf die Projektdetailansicht navigiert. Zusästzlich können Projekte mit Hilfe des Add-Buttons hinzugefügt werden. Alle Projekte lassen sich durch deren Status und Bezeichnung kombiniert dynamisch filtern.
Liste aller Projekte:
Auf der Detailansicht eines Projekts werden alle Informationen aus dem Backend bezüglich eines ausgewählten Projekts angezeigt.
Detailansicht eines Projekts:
Bei der Erstellung eines Projekts werden alle benötigten Informationen für ein Projekt vom Nutzer abgefragt, damit diese im Backend angelegt werden können. Nach der Erstellung erhält der Nutzer eine Bestätigung und die Projektliste wird neu geladen. Zusätzlich ist anzumerken, dass die Nutzereingaben bei der Projekterstellung dynamisch validiert werden.
Projekterstellung:
Auf der Studiengangsliste werden alle Studiengänge vom Backend angezeigt. Jeder Studiengang erhält dabei eine eigene Material Card. Alle Informationen (Bezeichnung, Akademischer Grad) außer der zugehörigen Modulliste werden in dem entsprechenden Material Card angezeigt. Über den Module-Button wird auf die Studiengang-Detailansicht navigiert. Alle Studiengänge lassen sich durch deren akademischen Grad und Bezeichnung kombiniert dynamisch filtern.
Liste aller Studiengänge:
Auf der Detailansicht eines Studiengangs werden alle Informationen aus dem Backend bezüglich eines ausgewählten Studiengangs angezeigt.
Detailansicht eines Studiengangs:
Unter dem Kapitel Bewertung und Reflexion wird ein Ausblick über das Frontend gegeben. Ebenfalls werden in dem genannten Kapitel offene Probleme beschrieben, die bei einer weiterführenden Entwicklung beachtet werden sollten.