CodeGym /Kurse /Java Server /Externes JSON und das Anwendungsmodell

Externes JSON und das Anwendungsmodell

Java Server
Level 11, Lektion 1
Verfügbar

1. Zwei verschiedene Welten: Vertrag und internes Objekt

Stellt man sich die Anwendung als ein Geschäft vor, dann ist der JSON-Vertrag das Schaufenster und die Kasse: das, was der Kunde sieht und woran er beurteilt, „wie alles hier eingerichtet ist“. Das interne Modell ist das Lager und die Küche: dort ist es wichtig, dass alles für die Mitarbeiter bequem ist, nicht für die Kunden. Und wenn man das Lager ins Schaufenster stellt, wird es … ungewöhnlich.

Wenn wir von „externem JSON-Vertrag“ sprechen, meinen wir das Versprechen, das die API dem Kunden gibt: welche Felder er senden kann, welche er als Antwort erhält, wie diese Felder heißen, welche Typen und Werte erwartet werden. In der Backend-Welt ist das nicht „ungefähr so“, sondern „entweder so oder Schmerz“. Der Schmerz kommt schnell: Clients brechen, Postman-Sammlungen veralten, Integrationen werden zu Ratespielen.

Das „interne Anwendungsmodell“ hingegen lebt nach anderen Gesetzen. Es ist wichtig, dass es für Ihre Algorithmen, Überprüfungen und zukünftige Änderungen bequem ist. Es ist völlig normal, zusätzliche Felder, Methoden, berechnete Werte, interne Vermerke zu haben. Das interne Modell kann „reichhaltig“ sein – aber dieser Reichtum muss nicht öffentlich werden.

Um nicht durcheinander zu kommen, ist es nützlich, ein einfaches Schema im Kopf zu behalten:

flowchart TD
    C[Client / Postman] -->|JSON request| R[Request DTO]
    R -->|Übersetzung / Mapping| M[Internes Modell]
    M -->|Übersetzung / Mapping| S[Response DTO]
    S -->|JSON response| C

Hier gibt es keine Magie: DTOs an der Grenze, das interne Modell in der Mitte. Und unsere Aufgabe ist es, sicherzustellen, dass diese Welten unabhängig voneinander wachsen können, ohne sich gegenseitig ein Bein zu stellen.

2. Internes Modell: Was ist das und warum?

Nach dem Wort „Modell“ schalten viele sofort in den Modus „das ist etwas Schreckliches aus der Enterprise-Welt“. Beruhigen wir das Gehirn: Das interne Modell sind einfach Ihre Arbeitsobjekte, mit denen es angenehm ist, Code zu schreiben. Nicht für den Client, nicht für JSON, nicht „damit es schön aussieht“, sondern damit die Anwendung nicht bei der ersten Änderung auseinanderfällt.

Stellen wir uns die zukünftige Server-Phase von ReadLater Starter vor (wir schreiben noch keinen Server, wir lernen zu denken). Wir werden eine Entität „LeseListenelement“ haben. Innerhalb der Anwendung kann es praktisch sein, z.B. die Erstellungszeit oder eine interne Notiz zu speichern, die wir niemals nach außen geben wollen. Oder eine Methode zu haben, die den Zustand prüft (isFinished()), um Vergleiche nicht über den gesamten Code zu verstreuen.

Beispiel eines internen Objekts (kein DTO), das „für die Anwendung bequem“ ist:

import java.time.Instant;

public class ReadingListItem {
    private final long id;
    private final String title;

    // Innen ist es bequemer, mit dem Status als Enum zu arbeiten, statt mit einem String aus JSON.
    private ReadingStatus status;

    // Internes Feld: wichtig für interne Logik/Audit, muss aber nicht Teil der API sein.
    private final Instant createdAt = Instant.now();

    // Verhalten lebt im Modell: das reduziert doppelte Prüfungen im gesamten Code.
    public boolean isFinished() {
        return status == ReadingStatus.FINISHED;
    }
}

Beachten Sie zwei Dinge. Erstens gibt es hier Verhalten (isFinished()), nicht nur Daten. Zweitens gibt es ein internes Feld createdAt, das für uns nützlich ist, aber für den Client möglicherweise überhaupt nicht benötigt wird.

Und nun ein Beispiel eines DTOs, das die externe Antwort beschreibt (sein Zweck ist der Vertrag):

// DTO für die Antwort: Minimum an Logik, Maximum an Vorhersagbarkeit für den Client.
public record ReadingItemResponse(
        long id,
        String title,
        String status // Im externen Vertrag kann der Status ein String sein (z.B. "FINISHED").
) { }

DTOs sind in der Regel einfacher: Sie müssen keine Methoden oder „internen Geheimnisse“ enthalten. Sie müssen für den Client vorhersagbar sein. Das interne Modell muss für Ihren Code vorhersagbar sein.

Manchmal möchte man sagen: „Warum zwei Klassen, wenn eine reicht?“ Die Antwort ist ähnlich wie auf die Frage „Warum zwei Paar Schuhe, wenn man ein Paar für alle Gelegenheiten haben kann?“ Man kann, natürlich. Besonders wenn man nie nach draußen geht und keine Menschen trifft. Aber sobald das wirkliche Leben beginnt, wird die Ersparnis zu ständig schmutzigen Turnschuhen und merkwürdigen Blicken der Umgebung.

3. Wenn das interne Modell in die API gelangt

Die Versuchung, „nach außen zu geben, was ohnehin intern vorhanden ist“, ist sehr verständlich: weniger Code, weniger Klassen, weniger Gründe zum Nachdenken. Aber das ist genau der Fall, in dem „weniger jetzt“ zu „mehr Schmerz später“ wird. Und der Schmerz kommt normalerweise nicht beim Schreiben des Codes, sondern wenn Sie eine Woche später etwas ändern.

Stellen Sie sich vor, Sie haben beschlossen, innerhalb von ReadingListItem ein Feld internalNote zu speichern. Eine Kleinigkeit: „warum ich dieses Buch hinzugefügt habe“ oder „die gedruckte Version kaufen“. Intern ist das bequem. Aber wenn Sie das interne Objekt als Antwortmodell verwenden, könnte dieses Feld plötzlich Teil des externen Vertrags werden. Auch wenn Sie es persönlich nicht geplant haben.

Problem Nr. 1 – der Vertrag wird zerbrechlich. Sie haben das interne Objekt geändert (Feld hinzugefügt, umbenannt, Typ geändert), und der Client bekommt plötzlich einen anderen JSON-Body oder kann die Anfrage nicht mehr „wie früher“ senden. Der Client denkt in diesem Moment nicht: „Oh, der Entwickler macht einen schönen Refactoring!“ Er denkt: „Warum ist Ihre API kaputt?“

Problem Nr. 2 – Datenleck. Auch wenn es keine Geheimnisse sind (unser Kurs hat keine Sicherheit), könnte es Müll für den Client sein: interne Notizen, interne Flags, technische Werte. Der Client muss das ignorieren, und dann tauchen Fragen auf wie „ist dieses Feld Pflicht?“, „kann ich es ändern?“, „was passiert, wenn ich es sende?“

Problem Nr. 3 – Sie verlieren die Freiheit der internen Evolution. Das interne Modell möchte man verbessern: mal einen enum machen, mal ein berechnetes Feld hinzufügen, mal die Struktur ändern. Aber wenn das interne Modell = der externe Vertrag ist, wird jede interne Änderung zu einer öffentlichen API-Änderung. Das ist ein sehr schlechter Deal.

Problem Nr. 4 – Sie beginnen zu verwechseln: „bequem für den Code“ und „bequem für den Client“. Zum Beispiel ist es Ihnen bequem, den Autor als einen String „Martin, Robert C.“ zu speichern oder sogar als Objekt mit Feldern. Der Client findet das vielleicht anders bequem. Aber wenn Sie ein Modell haben, quälen Sie entweder den Client oder sich selbst. Sie wählen, wer leidet. Meistens leiden beide.

Nützlich ist ein Vergleichstabelle – ohne Tragödie, einfach als technische Tatsache:

Frage DTO (externe Form) Internes Modell
Für wen gemacht für den Client und den Vertrag für Ihren Code
Was ist wichtig stabile Namen/Typen der Felder Bequemlichkeit der Logik und Invarianten
Kann man „interne Felder“ hinzufügen meist nicht nötig oft ja
Kann man die Struktur ohne Folgen ändern nur wenn man bereit ist, den Vertrag zu ändern ja, wenn die Grenze erhalten bleibt
Gibt es Verhalten (Methoden) fast nie oft ja

Kurz gesagt, das „Durchsickern“ eines internen Objekts nach außen macht die API zu einem Raum, in dem Sie alles auf dem Boden lagern. Es scheint alles sichtbar und „einfache Logik“, aber versuchen Sie etwas zu ändern – und es beginnt eine Quest „nicht auf Lego treten“.

4. Beispiel mit ReadLater: internes Feld außerhalb der API

Damit die Grenze keine abstrakte Philosophie ist, machen wir ein sehr bodenständiges Beispiel. Stellen Sie sich vor: Sie haben ein Buch zur Leseliste hinzugefügt und möchten intern eine Notiz für sich als Entwickler speichern – z.B. „automatisch aus dem Katalog hinzugefügt“ oder „aus Datei importiert“. Der Client braucht das nicht, und manchmal stört es sogar.

public class ReadingListItem {
    private final long id;
    private final String title;
    private final String author;

    // Dieses Feld gibt es für interne Bedürfnisse der Anwendung, nicht für den externen Vertrag.
    private String internalNote;

    public ReadingListItem(long id, String title, String author) {
        this.id = id;

        // Innen kann man Daten normalisieren, wie es bequem ist (z.B. Leerzeichen am Rand entfernen).
        this.title = title.strip();
        this.author = author.strip();
    }
}

Hier ist internalNote ein rein internes Detail. Wir sind nicht einmal verpflichtet, es jetzt zu füllen. Aber wichtig ist: wir wollen das Recht haben, es hinzuzufügen, ohne der Welt erklären zu müssen, warum plötzlich ein Feld internalNote in der Antwort erscheint.

Und die externe Antwort, die der Client wirklich braucht, könnte so aussehen:

// Externe Form: nur das, was der Client im Szenario wirklich braucht.
public record ReadingItemResponse(
        long id,
        String title,
        String author
) { }

Die Frage „warum geben wir internalNote nicht weiter“ hat eine sehr langweilige und richtige Antwort: weil der Client nicht darum gebeten hat, weil es nicht Teil des Szenarios ist, weil der Vertrag minimal ausreichend sein sollte.

Manchmal fragen Studenten: „Was ist schlimm daran, es zu geben? Soll er es ignorieren.“ Das Problem ist, dass Ignorieren schnell zur Abhängigkeit wird. Heute ignoriert der Client es, morgen entscheidet ein anderer Client „oh, nützliches Feld“, beginnt sich darauf zu stützen – und Sie sind plötzlich Geisel Ihres zufälligen Details.

Es gibt eine gute Regel: die externe API sollte wie eine Speisekarte in einem Café sein. Auf der Karte steht nicht „wir vergessen manchmal das Salz“ oder „der Koch ist heute traurig“. Das sind Lebenswahrheiten, aber sie sollten nicht Teil des Vertrags sein.

5. Ein Objekt – mehrere externe Formen

Wenn Sie zum ersten Mal hören „internes Objekt und DTO sind verschiedene Dinge“, scheint es, dass es „einfach zwei Klassen“ sind. Aber der wirkliche Vorteil zeigt sich, wenn Sie verstehen: Ein und dasselbe interne Objekt kann mehrere externe Formen für verschiedene Szenarien haben. Und das ist keine Verkomplizierung um der Verkomplizierung willen, sondern im Gegenteil – eine Möglichkeit, den Vertrag klar zu halten.

Zum Beispiel werden Sie in der lokalen API der Leseliste (die später kommt) logischerweise verschiedene Antworten haben: eine für die Liste, eine für die Detailkarte. Intern kann es dasselbe ReadingListItem sein. Außen geben Sie in einem Szenario „kurz“, im anderen „etwas detaillierter“.

Kurzes Listenelement (beispielhafter, nicht endgültiger Entwurf):

// DTO für die Liste: kompakt, ohne überflüssige Felder.
public record ReadingListItemShortResponse(
        long id,
        String title,
        String status
) { }

Detailierte Antwort mit Kommentar:

// DTO für Details: erweiterte Form, weil das Szenario mehr Daten erfordert.
public record ReadingItemDetailsResponse(
        long id,
        String title,
        String author,
        String status,
        String comment
) { }

Aus Sicht des Clients ist das superlogisch. Aus Sicht des internen Modells müssen es nicht zwei verschiedene Objekte sein. Wenn Sie versuchen, eine „universelle Antwort für alles“ zu machen, wird sie mit Feldern anschwellen, die in der Hälfte der Fälle null sind. Am Ende sieht der Vertrag aus wie ein Koffer, in den Sie Sommerkleidung, Winterkleidung und einen Topfsatz gestopft haben, „für den Fall, dass es nützlich ist“. Man kann es benutzen, aber es ist schwer.

Und hier ist es wichtig, nicht mit dem Thema der Vorlesung 1 zu verwechseln. In Vorlesung 1 haben wir gesagt, dass Request und Response sich im Sinn unterscheiden. Hier sagen wir, dass selbst zwei Response-Szenarien unterschiedlich sein können. Es ist immer noch dieselbe Idee: Die Form der Daten hängt davon ab, wofür sie benötigt wird.

Am Ende ergibt sich ein sehr gesundes Bild: Das interne Objekt „lebt sein eigenes Leben“, und die externen DTOs sind „verschiedene Wege, es der Welt zu zeigen“, ohne Kontrollverlust.

6. Mapping DTO ↔ internes Modell

Die Grenze zwischen externem Vertrag und internem Modell scheint offensichtlich, bis man anfängt, Code zu schreiben. Dann taucht die Frage auf: „Okay, ich habe CreateReadingItemRequest und ich habe ReadingListItem. Wer erzeugt wen? Wo findet diese Umwandlung statt? Wer ist für die Übersetzung verantwortlich?“ Heute brauchen wir keine ideale architektonische Antwort, aber ein verständliches Prinzip.

Eine nützliche Metapher ist der Übersetzer. DTO spricht die Sprache der „JSON-Grenze“, das interne Modell spricht die Sprache „bequem für die Anwendung“. Der Übersetzer sollte genau dort leben, wo diese Sprachen aufeinandertreffen. Nicht überall, nicht in jeder Klasse „ein bisschen“.

Als Beispiel machen wir einen minimalen „Mapper“ – einfach als Idee:

public class ReadingItemMapper {

    public static ReadingItemResponse toResponse(ReadingListItem item) {
        // Hier kontrollieren wir bewusst, welche Felder in die Antwort (den Vertrag) gelangen.
        return new ReadingItemResponse(
                item.getId(),
                item.getTitle(),
                item.getAuthor()
        );
    }
}

Hier gibt es eine Feinheit: ReadingListItem sollte Getter haben. Das ist normal, wir schreiben sie einfach nicht aus, um nicht in Boilerplate-Code zu versinken.

In die andere Richtung (vom Request zum internen Objekt) kann man auch eine Übersetzung haben:

public class ReadingItemFactory {

    public static ReadingListItem fromRequest(long id, CreateReadingItemRequest req) {
        // id wird normalerweise vom Server generiert, kommt also nicht aus der Anfrage, sondern als separater Parameter.
        return new ReadingListItem(id, req.title(), req.author());
    }
}

Und das Request-DTO selbst könnte so aussehen:

// Request-DTO: das, was der Client senden darf.
public record CreateReadingItemRequest(
        String title,
        String author
) { }

Der Sinn ist nicht, sofort ein ideales Mapping-System zu bauen. Der Sinn ist, den Ort festzuhalten, an dem die Umwandlung stattfindet. Sobald Sie diesen Ort nicht haben, beginnen die Transformationen, sich im Code „auszubreiten“, und Sie erhalten wieder die Situation „es funktioniert irgendwie, aber man hat Angst, es anzufassen“.

Und noch ein wichtiger Punkt. Solange wir nicht den Code für JSON-Parsing und Serialisierung schreiben, sieht alles harmlos aus. Aber sobald die automatische Konvertierung von Objekten in JSON auftaucht (und sie wird später kommen), beginnt das Durchsickern des internen Modells nach außen sehr leicht. Daher ist die Gewohnheit, die Grenze zu halten, schon im Voraus nötig, solange das Projekt klein und freundlich ist.

7. Feld innerhalb und Feld des Vertrags

An dieser Stelle möchte man normalerweise eine Liste „mach eins, mach zwei“ bekommen. Aber wir haben vereinbart, Listen um der Listen willen zu vermeiden, also machen wir es so: Wir geben ein paar Fragen, die Sie sich stellen können, wenn Sie über das Schicksal eines Feldes entscheiden. Diese Fragen sind nicht formal, aber sie funktionieren hervorragend als Filter.

Die erste Frage lautet: „Braucht der Client dieses Feld, um das Szenario auszuführen?“ Wenn nein, ist es fast sicher intern. Zum Beispiel kann createdAt intern sein, wenn Sie es nicht in der Antwort zeigen und keine Szenarien darum herum aufbauen.

Die zweite Frage: „Sollte der Client die Möglichkeit haben, dieses Feld zu setzen?“ Wenn ja, ist es ein Kandidat für ein Request-DTO. Wenn nein, ist es entweder berechnet, systembedingt oder servergeneriert. Ein typisches Beispiel ist id: wird meist servergeneriert und erscheint im Response, muss aber nicht im Create-Request enthalten sein.

Die dritte Frage: „Wenn ich dieses Feld morgen intern ändere, möchte ich der Welt verkünden, dass sich der Vertrag geändert hat?“ Wenn die Antwort „nein, will ich nicht“ ist, sollte das Feld durch eine DTO-Schicht vom Vertrag getrennt sein.

Die vierte Frage: „Spiegelt dieses Feld einen Geschäftssinn für den Benutzer wider oder ist es ein technisches Implementierungsdetail?“ Zum Beispiel ist „Lesestatus“ ein Geschäftssinn. „internalNote“ oder „cacheKey“ sind technische Details. Technische Details sind nach außen fast nie nötigt.

Das lässt sich in einer kleinen Hilfstabelle zusammenfassen:

Merkmal des Feldes Eher DTO Eher internes Modell
Vom Client für das Szenario benötigt ja nicht unbedingt
Client darf es setzen ja (Request) meistens nein
Ändert sich beim Refactoring unerwünscht oft ja
Es ist „Sinn“ und nicht „Technik“ ja manchmal nein
Wollen vor der Außenwelt verbergen nein ja

Diese Regeln garantieren kein ideales Design, schützen aber vor dem häufigsten Anfängerfehler: „Ich habe ein Feld hinzugefügt, weil es mir intern bequem ist, also soll es in JSON sein.“ Nein. Lass es intern bleiben. Es gelangt nur in JSON, wenn es Teil des Vertrags wird.

8. Spring: Was automatisiert wird und was nicht

Jetzt machen wir alles „manuell im Kopf“: wir trennen DTOs und interne Objekte als Konzept. Später in der Welt von Spring und anderen Werkzeugen wird es viel Automatisierung geben: bequemes Binden von Request-JSON an Objekte, bequeme Serialisierung von Responses, bequeme Annotationen usw. Aber das ändert nichts an der Hauptsache: Sie müssen immer noch entscheiden, wo die Grenze verläuft.

Spring (und nicht nur Spring) nimmt die Routine, aber nicht die Verantwortung für den Vertrag. Es kann nicht für Sie verstehen, dass internalNote nicht in der API sein sollte. Es kann nicht erraten, dass der Client in der Liste eine kurze Karte und in den Details eine erweiterte braucht. Es serialisiert ehrlich, was Sie ihm geben. Wenn Sie ein internes Objekt geben, macht es das interne Objekt zu einem Teil der Außenwelt. In diesem Sinne ist Spring sehr wohlerzogen: Es diskutiert nicht, es führt einfach aus.

Daher ist die Gewohnheit, externen Vertrag und internes Modell zu trennen, nicht „vor Spring, dann vergessen wir es“. Es ist „vor Spring, um in Spring nicht dieselben Fehler zu machen, nur schneller“.

9. Typische Fehler beim Vermischen von Modellen

Fehler Nr. 1: „Geben wir einfach das interne Objekt aus, später regeln wir das.“
Das scheint Zeit zu sparen, aber in der Praxis schieben Sie nur eine Zahlung für einen Kredit mit hohem Zinssatz auf. Sobald sich das Objekt intern ändert, brechen Sie entweder den Vertrag oder Sie haben Angst, den internen Code zu ändern. Beides ist ein schlechtes Szenario.

Fehler Nr. 2: Hinzufügen von Feldern „für alle Fälle“ zum DTO.
Wenn Sie unsicher sind, ob der Client das Feld braucht, schlägt das Gehirn vor: „lass es drin“. Am Ende wird das DTO zu einem „Müllhaufen der Möglichkeiten“, und der Vertrag wird unklar. Wenn das Feld nicht wirklich am Client-Szenario teilnimmt, ist es besser, es nicht hinzuzufügen. Es ist einfacher, später hinzuzufügen, als zu erklären, warum ein „überflüssiges Feld“ plötzlich nicht mehr funktioniert oder seinen Sinn geändert hat.

Fehler Nr. 3: Verwechseln von „einfach zu serialisieren“ mit „bequem zu nutzen“.
Manchmal macht man das interne Modell bequem für den Code und versucht dann, den Client in dieser Logik leben zu lassen: seltsame Namen, seltsame Strukturen, seltsame Typen. Denken Sie daran: DTO existiert für den Client. Auch wenn Sie jetzt selbst der „Client“ in Postman sind, kann in einer Woche ein anderer Service oder ein anderer Entwickler der Client sein.

Fehler Nr. 4: Den externen JSON von der zufälligen Klassenstruktur abhängig machen.
Wenn die Antwortform einfach „welche Felder zufällig in der Klasse sind“ ist, befinden Sie sich im Modus eines zufälligen Vertrags. Der Vertrag sollte als Vereinbarung entworfen werden, nicht als Nebeneffekt entstehen. Heute haben Sie ein Feld der Bequemlichkeit halber hinzugefügt – und es wurde versehentlich öffentlich. Morgen löschen Sie ein Feld – und brechen versehentlich die Integration.

Fehler Nr. 5: Versuchen, alles mit einer Klasse abzudecken: Request, Response, internes Modell und „noch ein bisschen“.
Ein „universelles DTO“ sieht zunächst fast immer wie ein Sieg aus, dann wie eine Bestrafung. Es vermischt die Kriterien der Anfrage, die Antwortdaten, interne Dienstfelder und beginnt ein Eigenleben zu führen. Das Ergebnis ist der unangenehmste Effekt: Code ist vorhanden, aber lesen kann man ihn nicht.

1
Aufgabe
Java Server, Level 11, Lektion 1
Gesperrt
Internes Objekt und externe Antwort
Internes Objekt und externe Antwort
1
Aufgabe
Java Server, Level 11, Lektion 1
Gesperrt
Zwei externe Formen eines internen Objekts
Zwei externe Formen eines internen Objekts
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION