Читать книгу Angular - Ferdinand Malcher - Страница 161
Klasse oder Interface für die Datenmodelle?
ОглавлениеTypeScript kennt sowohl Klassen als auch Interfaces, um Daten abzubilden. Beide Sprachbestandteile haben wir im TypeScript-Kapitel bereits kennengelernt.
Klasse
Eine Klasse können wir als Vorlage verstehen, mit deren Hilfe wir konkrete Objekte instanziieren können. Diese instanziierten Objekte haben klar definierte Methoden und Eigenschaften. Für unser Buch könnte eine Klasse zur Abbildung unserer Fachlichkeit (Domäne) wie folgt aussehen:
export class Book {
constructor(
public isbn: string,
public title: string,
public rating?: number,
// weitere Eigenschaften
) { }
// Methode
isVeryPopular() {
return this.rating > 4;
}
}
// Verwendung:
const book = new Book('ISBN', 'TITLE', 5, /* weitere */);
const isVeryPopular = book.isVeryPopular();
Interface
Ganz ähnlich zu Klassen verhält es sich mit Interfaces. Interfaces beschreiben die Methoden und Eigenschaften von Objekten. Allerdings können Interfaces weder eine konkrete Implementierung noch eine Initialisierung bereitstellen. Man kann sie als Vertrag verstehen. Wir versprechen per Vertrag, dass ein bestimmtes Objekt die benötigte Struktur besitzt. Das zuvor gezeigte Beispiel könnte bei der Verwendung eines Interface so programmiert werden:
export interface Book {
isbn: string;
title: string;
rating?: number;
// weitere Eigenschaften
}
export class BookHelper {
static isVeryPopular(b: Book) {
return b.rating > 4;
}
}
// Verwendung:
const book = {
isbn: 'ISBN',
title: 'TITLE',
rating: 5
};
const isVeryPopular = BookHelper.isVeryPopular(book);
Beide Konstrukte ähneln sich, und so können wir sowohl mit Klassen als auch mit Interfaces unsere Daten beschreiben. Wer von einer objektorientierten Programmiersprache kommt, wird wahrscheinlich das Beispiel mit der Klasse bevorzugen.
Es sprechen einige gute Gründe für die Wahl einer Klasse:
Wir bilden ein sauberes Domain Model5 ab, die Daten und dazu passende Methoden sind möglichst nahe beieinander. Als Beispiel hierfür haben wir die frei erfundene Methode isVeryPopular() angedeutet. Beim Einsatz von Interfaces müsste man sich hingegen auf irgendeine Art und Weise einer Hilfsklasse bedienen. Zur Veranschaulichung haben wir den BookHelper eingeführt.
Wir erzwingen durch den Konstruktor eine korrekte Verwendung. TypeScript kann bereits während des Kompilierens sicherstellen, dass kein Objekt mit falschen Werten entstehen wird.
Klassen existieren in JavaScript und sind Teil der Programmlogik (Interfaces hingegen nicht).
Werfen wir jedoch einen Blick in den offiziellen Styleguide von Angular, so konnten wir dort bis zum Frühjahr 2019 noch die folgende Empfehlung finden:
Angular Style Guide: 03-03
Consider using an interface for data models.
Aha? Über diese Empfehlung kann man durchaus erstaunt sein! Auch wir haben uns für den BookMonkey für Interfaces entschieden, und wir möchten diese Entscheidung detailliert begründen.
Als Erstes müssen wir einmal die Wortwahl betrachten: Domänenmodell und Datenmodell. Ein Domänenmodell bildet die Geschäftslogik ab. Handelt es sich bei dem Buch in unserer Angular-Anwendung um ein Domänenmodell? Wir sagen: Nein! Unser BookMonkey soll eine klassische Client-Server-Architektur implementieren. Bei einer Systemarchitektur mit einem Angular-Frontend und einem HTTP-Backend wollen wir die kritische Geschäftslogik auf das Backend verlagern. Dort befindet sich die Domäne. Zum Frontend wird lediglich ein vereinfachtes Datenmodell übertragen. Das bedeutet also, dass wir eine Software planen, bei der die kritische Geschäftslogik nicht im Frontend stattfindet, sondern auf dem Server. Es ist gar nicht notwendig, dass wir die üblichen Ansprüche an unser Modell legen. Im Endeffekt soll unsere gesamte Angular-Anwendung vor allem die Daten vom Server empfangen, darstellen, verarbeiten und wieder zurücksenden. Unter diesen Gesichtspunkten bieten uns Interfaces folgende Vorteile:
Man muss nicht den Konstruktor einer Klasse aufrufen und dafür eine Methode zum »Mappen« der Daten programmieren. Die Daten vom Server im JSON-Format können dementsprechend sofort als passendes Objekt weiterverwendet werden, sofern die Daten vom Server und das Interface übereinstimmen (siehe Kapitel zu HTTP ab Seite 189).
Die Daten können jederzeit kopiert werden (siehe Abschnitt zum Spread-Operator ab Seite 42), ohne dass wir eine falsche Verwendung befürchten müssen.
Das Datenmodell ist kompatibel bzw. »bereit« für den Einsatz von NgRx/Redux, denn wir werden dort die Objekte mehrfach mit dem Spread-Operator kopieren (siehe Kapitel zu Redux ab Seite 607).
Führende Codegeneratoren wie swagger-codegen, appollo-codegen usw. (siehe Kasten im Kapitel zu RxJS und HTTP auf Seite 239) generieren standardmäßig Interfaces und keine Klassen.
Zwei Interfaces mit der gleichen Signatur sind miteinander kompatibel und untereinander austauschbar. Zwei unterschiedliche Klassen mit der gleichen Signatur kann man hingegen nicht untereinander austauschen, dies verhindert bereits der Compiler. In großen Projekten kann diese Tatsache relevant werden, vor allem dann, wenn man Module getrennt voneinander entwickelt.
Diese Punkte, vor allem die Kompatibilität mit der Redux-Architektur, haben uns zu unserer Entscheidung bewegt. Wenn Sie Ihr eigenes Projekt planen, so möchten wir Ihnen dazu raten, ebenso die Entscheidung zwischen Klassen oder Interfaces rechtzeitig zu fällen. Eine späte Änderung kann sonst viel Aufwand bedeuten.