5.1: Newtype-Pattern und parse-dont-validate als Entwurfsgrundlage

This commit is contained in:
2026-09-01 11:51:10 +02:00
parent 0b0b89c3cc
commit d09434f72f
3 changed files with 25 additions and 3 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ screenshots habe ich in figures/screenshots gelegt.
-> "Figure 3.2: Zeitplanung des Projekts" aktualisieren -> "Figure 3.2: Zeitplanung des Projekts" aktualisieren
-> "5.10.5 Stand bei Abgabe" aktualisieren -> "5.10.5 Stand bei Abgabe" aktualisieren
-> allgemein ueber alle chapter nochmal drueber gehen und schauen ob das den aktuellen stand reflektiert -> allgemein ueber alle chapter nochmal drueber gehen und schauen ob das den aktuellen stand reflektiert
- [ ] architektur der implementierung: - [x] architektur der implementierung:
- ich habe in meiner freizeit sehr viel rust programmiert und dort auch das NewType pattern kennen gelernt, an dem ich mich hier orientierte (auch wenn es C# ist), da ich die vorteile besonders hier wo man eigentlich mit den nur mit ganz vielen string keys rum hantiert, sie aus einander baut, und wieder zusammenbaut, erkannte. id input string -> s3 gibt keys zurueck -> parsing -> domain model -> verbarbeitung -> wenn weitere requests gebraucht werden, vom domain model wieder einen key zusammenbauen - ich habe in meiner freizeit sehr viel rust programmiert und dort auch das NewType pattern kennen gelernt, an dem ich mich hier orientierte (auch wenn es C# ist), da ich die vorteile besonders hier wo man eigentlich mit den nur mit ganz vielen string keys rum hantiert, sie aus einander baut, und wieder zusammenbaut, erkannte. id input string -> s3 gibt keys zurueck -> parsing -> domain model -> verbarbeitung -> wenn weitere requests gebraucht werden, vom domain model wieder einen key zusammenbauen
- "parse, don't validate" philosophie - "parse, don't validate" philosophie
- [ ] mit den vom pidi 2 noch relevanten abbildungen ergaenzen - [ ] mit den vom pidi 2 noch relevanten abbildungen ergaenzen
+9 -2
View File
@@ -17,12 +17,19 @@ Die oberste Schicht bilden drei Razor Pages: \texttt{Documents} für Liste, Such
Darunter liegt der \texttt{DocumentsService} als fachliche Schicht mit den Regeln des Ablagekonzepts: Auflösung des Kundenordners, Typableitung aus dem Pfad, Ausblendung technischer Einträge. Der \texttt{S3DocumentsClient} kapselt den technischen Speicherzugriff und ist die einzige Stelle, an der \texttt{IAmazonS3} unmittelbar verwendet wird. Darunter liegt der \texttt{DocumentsService} als fachliche Schicht mit den Regeln des Ablagekonzepts: Auflösung des Kundenordners, Typableitung aus dem Pfad, Ausblendung technischer Einträge. Der \texttt{S3DocumentsClient} kapselt den technischen Speicherzugriff und ist die einzige Stelle, an der \texttt{IAmazonS3} unmittelbar verwendet wird.
\subsection{Eigene Typen statt Zeichenketten} \subsection{Eigene Typen statt Zeichenketten}
\label{sec:newtype}
Eine Entwurfsentscheidung, die sich im Verlauf herausbildete, betrifft den Umgang mit Pfaden. Anfangs wurden Dokumentschlüssel als Zeichenketten durch die Schichten gereicht. Im Review des Downloads führte das zu wiederholten Rückfragen zur Pfadvalidierung, weil einer Zeichenkette nicht anzusehen ist, ob sie bereits geprüft wurde. Eine Entwurfsentscheidung, die sich im Verlauf herausbildete, betrifft den Umgang mit Pfaden. Anfangs wurden Dokumentschlüssel als Zeichenketten durch die Schichten gereicht. Im Review des Downloads führte das zu wiederholten Rückfragen zur Pfadvalidierung, weil einer Zeichenkette nicht anzusehen ist, ob sie bereits geprüft wurde.
Daraufhin wurden eigene Typen eingeführt: Ein \emph{Schlüssel} bezeichnet den vollständigen, validierten Pfad; ein \emph{relativer Pfad} den Anteil unterhalb des Kundenordners. Beide können nur über Konstruktionswege entstehen, die die jeweilige Prüfung durchführen. Eine vergessene Prüfung führt zu einem Übersetzungsfehler statt zu einer Sicherheitslücke. Der Ausweg war ein Muster, das ich außerhalb der Arbeit beim Programmieren in Rust kennengelernt habe: das \emph{Newtype-Pattern}. Ein primitiver Wert wird in einen eigenen, sonst inhaltsgleichen Typ verpackt, damit der Übersetzer zwei Werte unterscheiden kann, die als Zeichenkette identisch aussehen \autocite{rust-newtype}. In C\# lässt sich das mit \texttt{readonly record struct} ohne Laufzeitkosten nachbilden. Gerade hier lag der Nutzen auf der Hand, weil das Modul fast ausschließlich mit Schlüsseln arbeitet, die zerlegt, umgeformt und wieder zusammengesetzt werden.
Der zugehörige Pull Request durchlief 23 Iterationen und bestand zu einem erheblichen Teil aus diesem Refactoring — ein Aufwand, der sich auszahlte, weil ZIP-Download, PDF-Vorschau und Freigabelinks dieselben Typen wiederverwenden konnten. Nach demselben Muster entstanden \texttt{DocumentType}, \texttt{DocumentName} und \texttt{OrgSlug}. Ergänzt wird das Muster durch die Haltung „parse, don’t validate“ \autocite{king-parse}: Eine Prüfung soll nicht nur ein Ja oder Nein zurückgeben, sondern das geprüfte Ergebnis in einem Typ festhalten, der die Zusicherung trägt. Eine Funktion, die einen \texttt{DocumentKey} entgegennimmt, muss die Gültigkeit nicht erneut prüfen — sie wäre sonst gar nicht aufrufbar gewesen.
So entstanden \texttt{DocumentKey} für den vollständigen Pfad einschließlich Kundenordner (Listing~\ref{lst:document-key}) und \texttt{DocumentRelativePath} für den Anteil darunter. Beide sind nur über eine Fabrikmethode erzeugbar, die zerlegt und dabei prüft; eine vergessene Prüfung führt zu einem Übersetzungsfehler statt zu einer Sicherheitslücke. Nach demselben Muster entstanden \texttt{DocumentType}, \texttt{DocumentName}, \texttt{DocumentId} und \texttt{OrgSlug}.
Der Datenfluss folgt daraus unmittelbar: Aus der Anfrage kommt eine Zeichenkette, der Speicher liefert Schlüssel als Zeichenketten zurück, beide werden einmal am Rand in das Domänenmodell geparst. Die gesamte weitere Verarbeitung — Typableitung, Filterung, Sortierung, Blätterung — arbeitet nur noch auf Typen. Erst wenn ein weiterer Speicheraufruf nötig ist, wird aus dem Modell wieder ein Schlüssel erzeugt. Zeichenketten existieren damit ausschließlich an den Systemgrenzen.
Der zugehörige Pull Request durchlief 23 Iterationen und bestand zu einem erheblichen Teil aus diesem Refactoring — ein Aufwand, der sich auszahlte, weil ZIP-Download, PDF-Vorschau und Freigabelinks dieselben Typen wiederverwenden konnten.
\subsection{Zentrale Autorisierung} \subsection{Zentrale Autorisierung}
+15
View File
@@ -90,3 +90,18 @@
url = {https://learn.microsoft.com/en-us/aspnet/core/razor-pages/}, url = {https://learn.microsoft.com/en-us/aspnet/core/razor-pages/},
urldate = {2026-08-25} urldate = {2026-08-25}
} }
@online{rust-newtype,
author = {{The Rust Project Developers}},
title = {Using the Newtype Pattern for Type Safety and Abstraction},
url = {https://doc.rust-lang.org/book/ch20-02-advanced-traits.html},
urldate = {2026-08-25}
}
@online{king-parse,
author = {King, Alexis},
title = {Parse, don't validate},
year = {2019},
url = {https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/},
urldate = {2026-08-25}
}