Aus der Reihe “Blogposts, die man kennen sollte”: Parse, don’t validate.
Wenn Ihr eine Anwendung baut, macht ungültige Zustände in Eurer Typdomäne nicht-repräsentierbar. Ihr müsst dann anschließend nichts mehr validieren, sondern nur noch parsen. Letzteres macht ggf. sogar ein Framework für Euch. Und Ihr zwingt Euch dazu, Fehlerfälle nicht zu übersehen.
Ein einfaches Beispiel in TypeScript:
Szenario: Ein Server kann zwei Antworten geben:
{ "ok": true, "data": "Bitteschön" }
{ "ok": false, "message": "Ich bin ein Kaffeepott" }
Was man nicht machen sollte, wäre folgendes DTO als Modellierung für die Antwort zu nehmen:
interface Response {
ok: boolean;
message?: string;
data?: string;
}
const r = await fetch("...").then(r => r.json()) as Response;
Warum nicht?
- Weil man andauernd prüfen muss, ob
message
vorhanden ist. - Weil es bei diesem DTO gültig ist, dass das Objekt weder
message
nochdata
hat. Dieser ungültige Zustand wäre in dieser Modellierung möglich! - Weil man vergessen könnte, auf
ok
zu überprüfen. - Kann bei Refactorings kaputt gehen.
Was könnte man stattdessen machen? TypeScript hat (wie andere Sprachen auch) discriminated/tagged Unions. Rust-Menschen kennen das als ihr Enum, nur dass das in TS auf JS-Objekten funktioniert. Dabei fungiert ein- oder mehrere gemeinsame Propertys der Typen als Discriminator (also “Unterscheider”).
Wir definieren genau die zwei Möglichkeiten, die uns der Server geben kann und sagen “das oder das”:
interface SuccessResponse {
ok: true;
data: string;
}
interface ErrorResponse {
ok: false;
message: string;
}
type Response = SuccessResponse | ErrorResponse;
const r = await fetch("...").then(r => r.json()) as Response;
ok
ist hier der Discriminator, der zwischen den beiden Typen unterscheidet.
Auffällig ist:
- Weder
message
nochdata
sind jetzt optional. - Man wird vom Typsystem gezwungen, auf
ok
zu prüfen, bevor man.data
verwendet. Man kann es nicht vergessen. - Eine Funktion, die nur mit einer erfolgreichen Serverantwort etwas anfangen kann, kann dies in ihrer Parametersignatur sagen. Man spart sich das entpacken der Antwort sowie sonstige Checks innerhalb der Funktion.
- Wenn Refactorings etwas daran ändern, merkt man das.
- Es ist nicht möglich,
ok: true
undmesage: "test"
zu haben - ungültige Zustände können hier nicht repräsentiert werden.
Eine Überprüfung, ob die Antwort jetzt erfolgreich war oder nicht, muss man natürlich so früh wie möglich machen, dann spart man sich das Überprüfen an späteren Stellen.
Das oben gezeigte Pattern lässt sich gut verwenden, um State-Machines typsicher zu implementieren.
Noch ein paar Pointer für andere Sprachen:
- std::variant für >= C++17
- mypy kann tagged Unions. Ob das mypy-spezifisch ist, weiß ich leider nicht.
- Interesting find: Seit Python 3.10 kann man
A | B
für Unions nehmen (stattUnion[A, B]
); das macht es weniger verbose in der Nutzung
- Interesting find: Seit Python 3.10 kann man
- Java 17 macht sowas mit Sealed Classes und später noch etwas angenehmer mit Records.
- Wikipedia zu Tagged Unions / Sum Types, mit Code-Beispielen
Das ist nur eine Methode, ungültige Zustände im Typsystem festzuhalten. Aber eine, die (meiner Meinung nach) zu wenig verwendet wird.