Zwei Figuren schieben Würfel- und Zylinderformen und symbolisieren so den Prozessvergleich.

Frage nicht, was Gewerkschaftstypen für dich tun können; frage, was du für Gewerkschaftstypen tun kannst.

3 Leseminute

Union-Typen sind nützlich, wenn man eine Sammlung von Klassen hat, die nicht interagieren, aber alle auf die gleiche Weise behandelt werden müssen.

Als Softwareentwickler bei Swimlane habe ich immer wieder die Möglichkeit, Neues zu lernen, von dem nicht nur ich und das Team profitieren, sondern auch unsere Produkte effektiver machen. In einem kürzlich abgeschlossenen Projekt war ich mit der Implementierung eines Audit-Logs beauftragt. Die Anforderung bestand darin, jede Änderung (z. B. Erstellen, Aktualisieren, Löschen) an unseren Datenbank-Sammlungen in einer neuen Sammlung namens „AuditLog“ zu protokollieren und jeden Eintrag mit der jeweiligen Aktion zu kennzeichnen. Ganz einfach.

Die Aktualisierungsaktionen stellten jedoch eine Komplikation dar: Verschiedene Objekte in der Workflow-Engine können auf unterschiedliche Weise aktualisiert werden (z. B. Benutzer aktiviert, deaktiviert oder angemeldet; Plugins aktualisiert), und ich wollte die Aktualisierungsfunktionen selbst nicht mit Protokollierungslogik überladen. Ich musste eine Funktion schreiben, die den Zustand des zu ändernden Dokuments vor und nach der Änderung als Argumente entgegennimmt. Anschließend sollte sie anhand der Änderungen im Dokument die Aktion ermitteln, die diese Änderungen vorgenommen hat. Doch welchen Datentyp sollten die Argumente für „vorher“ und „nachher“ haben?

class AuditLogsService { async log(before: ???, after: ???): Promise { //... } }

Eine Lösung wäre die Verwendung eines Interfaces gewesen. Da unsere Dokumente weder eine Basisklasse noch ein Interface implementieren, erschien dies einfach. Man erstellt ein BaseDocument-Interface und lässt alle acht Dokumentklassen dieses implementieren. Anschließend können die Elemente „before“ und „after“ dem Interface zugeordnet werden.

interface AuditLogable {}

Das Problem bei dieser Lösung ist, dass die Schnittstelle sinnlos wäre, da keine der Dokumentklassen Eigenschaften oder Methoden gemeinsam nutzt. Nun ja, das stimmt nicht ganz – sie teilen sich ein “id”-Feld. Ich müsste also acht verschiedene Klassen ändern, nur um eine Schnittstelle mit einer einzigen Eigenschaft und ohne Methoden zu implementieren.

interface AuditLogable { id: string; }

Wie wäre es, die `log`-Methode zum Interface hinzuzufügen und jedem Dokumenttyp die Möglichkeit zu geben, sein eigenes Logging-Verhalten zu definieren? Das wäre doch echte Objektorientierung, oder? Nur müsste ich jetzt acht Logging-Methoden implementieren, die sich alle weitgehend ähnlich verhalten. Warum verwende ich nicht eine Basisklasse anstelle eines Interfaces, um Wiederholungen zu vermeiden? Denn das Verhalten jeder Logging-Methode ist nur meistens Ähnlich verhält es sich mit Plugins. Diese sind beispielsweise der einzige Dokumenttyp, der keine “Erstellen”-Aktion besitzt. Stattdessen nennen wir sie “Installieren”.

Mir wurde klar, dass ich das Problem falsch angegangen war. Dokumente sollten nicht die Protokollierung durchführen, sondern die Protokollierung sollte für sie erfolgen. So konnte ich alles in einer einzigen Funktion implementieren, ohne unnötige Schnittstellen oder verwirrende Abstraktionen.

Glücklicherweise bietet TypeScript eine einfache Möglichkeit, dieses Muster zu erstellen: Union-Typen. Mit einem Union-Typ muss ich keine Eigenschaften oder Verhaltensweisen abstrahieren. Ich kann effektiv sagen:,

“Das Argument 'before' ist einer der Typen, die protokolliert werden können.‘ type AuditLogable = User | Plugin | Playbook; // … class AuditLogsService { async log(before: AuditLogable, after: AuditLogable): Promise { //... } }

Da die Typinformationen zur Laufzeit verloren gehen, muss ich außerdem einen zusätzlichen Parameter übergeben, der angibt, welchen Typ aus der Union ich als Vorher- und Nachher-Argumente übergebe.

enum SwimType = { user, plugin, playbook, // ... } class AuditLogsService { async log(before: AuditLogable, after: AuditLogable, type: SwimType): Promise { //... } }

Ich habe nun AuditLogging mit einer einzigen Funktion und einem neuen Union-Typ implementiert. Da keines der protokollierten Dokumente verändert werden muss, ist das Hinzufügen einer neuen Klasse zum Logger so einfach wie das Hinzufügen zu `AuditLogable` und das Erweitern des benötigten Verhaltens zur Protokollierungsfunktion. Alles ist leicht zu testen und zu verstehen.

Fordern Sie eine Live-Demo an