Angular @ WPS Teil 4: Komponenten sind schlechte Erben

Angular @ WPS Teil 4: Komponenten sind schlechte Erben

Das ist Teil 4 unserer Reihe Entwicklung von Reale-Welt-Anwendungen ohne Orientierungsverlust

von Richard Voß @richard_voss

Seit (vor-)letztem Jahr entwicklen wir bei WPS immer öfter Anwendungen für unsere Kunden mit Hilfe von Angular 4, 5, 6 und auch 7. Angular ist ein populäres Framework (es wird auch als Plattform bezeichnet) zur Entwicklung von web-basierten Frond-Ends mit Typescript, HTML und CSS. Es ist Open Source Software und wird hauptsächlich durch Google vorangetrieben.

Das Framework macht es wesentlich einfacher, komplexe GUIs zu strukturieren, fördert die Wiederverwendung von Komponenten und – am wertvollsten – erlaubt es tatsächlich, testgetriebene Front-End-Entwicklung zu betreiben, die Spaß macht.

Trotz all dieser Großartigkeit des Frameworks bleiben genug Fehler übrig, die man machen kann. Wir haben einige erfolgreiche Muster aber auch Fallstricke gefunden und gesammelt.

Dieser Artikel gibt einen weiteren Einblick in unsere „Angular Pattern Library“. Beim letzten Mal ging es um das richtige Unit Testing für Komponenten, diesmal um das erste Anti-Muster rund um Vererbung und Komponenten.

👎 Avoid Component Class Inheritance with Template Duplication

Hier nun die Geschichte einer schmerzhaften Erfahrung, die unser Team gemacht hat.

Es geht weiterhin um die Entwicklung von Komponenten. Hierbei ist es nicht unüblich, eine Reihe von ähnlichen Komponenten zu entwickeln. Betonung auf ähnlich, also in einigen Aspekten gleich, in anderen Unterschiedlich. Komponenten basieren doch auf Klassen: Warum also nicht Vererbung nutzen und die Gemeinsamkeiten in einer Superklasse bündeln?

Leider, wenn man genauer hinsieht, besteht eine Komponente aber aus einer Klasse und einem Template. Und diese beiden sind auch noch eng gekoppelt. Das kann eine Superklassse nur die eine Hälfte der Komponenten (die Klasse) betreffen: Ein Stück Template kann nicht über Vererbung extrahiert werden.

Wie man ähnliche Komponenten besser nicht umsetzt

Also war dies ein typischer Fall von Composition over Inheritance, aber wir haben ihn nicht erkannt. Wir entwickelten damals eine Reihe von “Editoren”. Sie sollten alle ähnlich sein – basierend auf einem Formular (Reactive Forms), mit “Speichern”, “Abbrechen” und anderen Standard-Knöpfen. Aber auch unterschiedlich: Das Formular wäre jeweils anders aufgebaut und die Logik hinter “Speichern” würde jeweils eine andere sein.

Um das Problem vereinfacht deutlich zu machen, hier zwei Editoren für “Äpfel” und “Bananen”.

abstract-editor.ts
abstract class AbstractEditor {
  form: Form;
  readonly: boolean;

  abstract initForm();
  abstract submitForm();

  utilities();
}
apple-editor.component.ts
@Component()
class AppleEditorComponent extends AbstractEditor {
  initForm() { … }
  submitForm() { … }
}
apple-editor.component.html
<form (ngSubmit)="submitForm()" [formGroup]="form">
  (apple stuff)
</form>
banana-editor.component.ts
@Component()
class BananaEditorComponent extends AbstractEditor {
  initForm() { … }
  submitForm() { … }
}
banana-editor.component.html
<form (ngSubmit)="submitForm()" [formGroup]="form">
  (banana stuff)
</form>

Das sieht zunächst nur nach etwas Code-Diplizierung im Template aus. (<form ...) ) In der Realität war das natürlich sehr schnell mehr als die eine Zeile, viel eher ein ganzer Block und eine ganze Reihe von gebunden Variablen und Methoden.

Template Duplication with Shared Superclass

Die roten Pfeile zeigen die (problematische) Kopplung zwischen den Templates der Subklassen-Komponenten und den Attributen und Methoden der Superklasse.

Eine Komponente extrahieren löst das Problem nicht

Leider ist es ab hier nicht damit getan, einfach die Template-Duplizierungen in einer dritte Komponenten zu verlagern. Wir haben es ungefähr so versucht:

editor-shell.component.ts
@Component()
class EditorShellComponent {
  @Input()
  form: Form;
  @Output()
  submitForm: EventEmitter<void>()
}
editor-shell.component.html
<form (ngSubmit)="submitForm()" [formGroup]="form">
  <ng-content></ng-content>
</form>
apple-editor.component.ts
@Component()
class AppleEditorComponent extends AbstractEditor {
  initForm() { … }
  submitForm() { … }
}
apple-editor.component.html
<editor-shell (submitForm)="submitForm()" [form]="form">
  (apple stuff)
</editor-shell>
banana-editor.component.ts
@Component()
class BananaEditorComponent extends AbstractEditor {
  initForm() { … }
  submitForm() { … }
}
banana-editor.component.html
<editor-shell (submitForm)="submitForm()" [form]=“form”>
  (banana stuff)
</editor-shell>

Gut, also etwas Template-Redundant ist jetzt weg, toll. Aber ehrlich gesagt, die Verwendung von <editor-shell> ist immernoch sehr redundant und verletzt DRY: Alle Parameter (ein- wie ausgehend) müssen immer gleich sein, weil sie aus der Superklasse kommen. Im Ergebnis war diese Konstruktion für uns im Projekt eher schmerzhaft, denn es ging nicht um zwei, sondern um 6-7 Attribute und Methoden. Es war schmerzhaft eine weiteren Editor zu bauen, noch schmerzhafter regelmäßig alle Editoren anpassen zu müssen. (Ich habe das Wort schmerzhaft drei mal hintereinander verwendet, ich denke das ist deutlich genug.)

Konzeptionell hat nämlich der letzte Schritt das Problem sogar noch vergrößert:

Coupling Hell

 

Die gestrichelten roten Pfeile zeigen jetzt nämlich die noch viel problematischere Kopplung zwischen AbstractEditor und EditorShellComponent, denn es ist nicht-offensichtliche Kopplung. Sieht man sich den Code von EditorShellComponent an, sieht man diesen Zusammenhang nicht. Die Interaktion zwischen beiden funktioniert nur, wenn man sie in jedem Editor immer und immer wieder auf die gleiche Weise miteinander verbindet.

Das Konstrukt produziert also alle Defekte, die man sich von nicht-offensichtlicher Kopplung erhoffen darf: Änderungen haben große Reichweite, man bricht scheinbar “unbeteiligten” Code, man muss häufig Code wiederholen, was mühsam und fehleranfällig ist.

Aber was ist denn nun die Lösung?

Tja, leider ist die Antwort hier erstmal: Es gibt keinen leichten Weg da raus. Deswegen ist dies hier ein sog. “DON’T” (Daumen runter Emoji 👎). Das Beste ist, diesen Weg niemals zu gehen. Allerdings werden wir in weiteren Artikeln Lösungsansätze diskutieren und zu diesem Beispiel zurückkommen.

Eine gute Strategie ist, wie immer, den eigenen Werkzeuggürtel zu erweitern: Da wäre zu Beispiel “Advanced Transclusion” (nächster Artikel), aber auch dynamische Erzeugung von Komponenten sowie nicht-globale Komponenten-gebunden Services…

2019-04-17T14:56:30+02:0017. April 2019|