Zum Inhalt springen

Application – die Domain-API für die View

Der +State-Artikel hat der Fachlichkeit eine klare Heimat gegeben: fachlich relevanter Zustand, Business Rules, Ableitungen, Selektionen, Zustandsübergänge und fachliche Intents liegen dort.

Eine konkrete View benötigt davon meistens nur einen kleinen Ausschnitt:

customers
loading
selectCustomer()
createCustomer()

Hier beginnt Application.

Wenn dieser Layer kompliziert wird, ist meist Verantwortung hineingerutscht, die eigentlich darunter oder darüber gehört.

In klassischen DDD-orientierten Layering-Modellen wird die Application-Schicht häufig mit Application Services, Use Cases und deren Koordination verbunden. Sie steuert einen Anwendungsfall, ohne die eigentlichen Business Rules selbst zu besitzen.

Dieses Modell schneidet die Verantwortung noch enger. Die fachlichen Entscheidungen, Zustandsübergänge und Projektionen existieren bereits in +State. Oberhalb davon wird keine zweite Use-Case-Orchestrierung aufgebaut.

Application bleibt deshalb bewusst dünner als in vielen klassischen DDD-Architekturen. Im Idealfall besteht dieser Layer fast ausschließlich aus Facades.

Das ist keine Abkürzung, sondern eine bewusste Frontend-Interpretation: Eine View braucht einen stabilen Zugang zur Domain, aber keinen weiteren Ort, an dem Fachlichkeit neu entsteht.

Die Presentation sollte nicht wissen müssen, dass die Domain intern vielleicht aus CustomerState, OrderState, PermissionState und SelectionState besteht.

Ohne Facade könnte eine View direkt auf mehrere interne Bereiche zugreifen:

customerState.customers;
customerState.selectedCustomer;
orderState.canOrder;
permissionState.canEdit;

Damit wird die interne Zerlegung der Domain Teil des Wissens der Presentation.

Mit Facade sieht die View stattdessen nur:

facade.customers;
facade.selectedCustomer;
facade.canOrder;
facade.canEdit;

Die Facade kennt die darunterliegende Struktur. Die View kennt nur ihren Contract.

Eine Customer List View greift ausschließlich über ihre CustomerListFacade auf mehrere interne State-Bereiche der Domain zu.

Die Grundidee folgt dem klassischen Facade Pattern: Eine Facade stellt einem komplexeren Subsystem eine vereinfachte Oberfläche gegenüber (Design Patterns: Elements of Reusable Object-Oriented Software, Gamma/Helm/Johnson/Vlissides, 1994). Genau dieses Prinzip wird hier auf die Grenze zwischen Presentation und Domain angewendet.

Wer aus klassischen MVC-Strukturen kommt, kann sich die Position grob wie einen viewnahen Controller vorstellen: Zwischen Oberfläche und Modell liegt ein definierter Zugang. Die Verantwortung der Facade ist hier allerdings deutlich enger – sie entwickelt keine eigene fachliche oder Anwendungslogik.

Eine Facade darf vorhandene Werte und Datenflüsse aus +State einsammeln und unter einem passenden Contract bereitstellen:

export class CustomerDetailFacade {
readonly customer = this.customerState.selectedCustomer;
readonly canDelete = this.customerState.canDelete;
readonly loading = this.customerState.loading;
deleteCustomer(): void {
this.customerState.deleteSelected();
}
}

Das ist eine gute Facade. Sie exponiert, delegiert und versteckt interne Struktur. Sie erzeugt keine neue fachliche Bedeutung.

Auch mehrere bestehende Streams oder reaktive Werte dürfen nebeneinander Teil desselben Contracts sein. Was die Facade nicht tun soll, ist daraus erst eine neue fachliche Aussage zu berechnen:

readonly canOrder = combineLatest([
this.customerState.customer$,
this.orderState.orders$,
this.permissionState.permissions$,
]).pipe(
map(([customer, orders, permissions]) => {
// fachliche Entscheidung entsteht hier
}),
);

Wenn mehrere Datenquellen gemeinsam eine fachliche Aussage ergeben, gehört diese Aussage bereits in +State.

Application exponiert anschließend nur noch das Ergebnis:

readonly canOrder = this.orderState.canOrder;

Eine Facade kann mehrere Datenflüsse einsammeln. Sie führt sie nicht fachlich zusammen.

Die Facade trifft keine Entscheidungen.

Eine Business Rule gehört nicht hierher:

save(): void {
if (this.customerState.customer().status === CustomerStatus.Active) {
// ...
}
}

Auch keine fachliche Ableitung:

readonly visibleCustomers = computed(() =>
this.customerState.customers()
.filter(...)
.sort(...),
);

wenn Filterung und Sortierung Teil des State oder einer fachlich relevanten Projektion sind.

Und auch keine UI-Verantwortung:

delete(): void {
if (confirm('Really delete?')) {
// ...
}
}

Eine Facade darf dagegen schlicht delegieren:

deleteCustomer(): void {
this.customerState.deleteSelected();
}

oder vorhandenen State exponieren:

readonly canDelete = this.customerState.canDelete;

Die Trivialität dieser Methoden ist beabsichtigt. Wenn eine Facade interessant wird, lohnt sich der Blick darauf, ob gerade Verantwortung aus einem anderen Layer dort landet.

Wenn die Facade nur exponiert und delegiert, könnte die Presentation auch direkt auf +State zugreifen.

Damit würde sie allerdings die interne Struktur der Domain kennenlernen.

Heute vielleicht nur:

CustomerListComponent
CustomerState

Mit einer neuen Anforderung kommen weitere Abhängigkeiten hinzu:

CustomerListComponent
├── CustomerState
├── PermissionState
├── SelectionState
└── OrderState

Die interne Aufteilung der Domain wird damit Teil des Contracts der View.

Mit Facade bleibt es bei einem Eingangspunkt:

CustomerListView
CustomerListFacade
interne Domain-Struktur

Die Domain kann darunter anders geschnitten werden, ohne dass die Presentation diese Änderung nachvollziehen muss.

Genau darin liegt der Wert dieser scheinbar trivialen Schicht: Sie reduziert strukturelle Kopplung.

Die Regel in diesem Modell ist denkbar einfach:

Eine View besitzt genau eine zugehörige Facade.

CustomerListView ↔ CustomerListFacade
CustomerDetailView ↔ CustomerDetailFacade
CustomerEditView ↔ CustomerEditFacade

Eine allgemeine CustomerFacade für sämtliche Customer-Views wirkt zunächst attraktiv:

CustomerList
CustomerDetail
CustomerEditor
CustomerFacade

Mit jeder weiteren View wächst jedoch auch ihr Contract:

CustomerList
CustomerDetail
CustomerEditor
CustomerHistory
CustomerSearch
CustomerAdmin
CustomerExport
CustomerFacade

Aus customers und selectedCustomer werden irgendwann history, orders, permissions, search, filter, sort, create, update, delete, archive, export und weitere Funktionen, die jeweils nur ein Teil der Consumer benötigt.

Die Facade entwickelt sich zum Götter-Service.

Das eigentliche Problem ist die fehlende natürliche Grenze. Drei, fünf oder zehn Views sind keine architektonische Schwelle. Auch ein bestimmter Prozentsatz gemeinsam verwendeter Properties liefert keine stabile Regel.

Die 1:1-Beziehung löst diese Frage bewusst trivial:

Die Verantwortung einer Facade endet dort, wo die Verantwortung ihrer View endet.

Keine Größenheuristik. Keine Diskussion über eine maximal erlaubte Zahl von Properties. Die View selbst setzt die Grenze.

Eine große gemeinsame CustomerFacade für viele Views wird einer Struktur gegenübergestellt, in der jede View ihre eigene kleine Facade besitzt.

Die 1:1-Regel erzeugt zwangsläufig Stellen, an denen zwei Facades ähnlich oder sogar identisch aussehen:

export class CustomerListFacade {
readonly customers = this.customerState.customers;
}
export class CustomerSearchFacade {
readonly customers = this.customerState.customers;
}

Das ist Absicht.

Beide Contracts können sich unabhängig voneinander entwickeln. Dass sie heute dieselbe Projection exponieren, ist kein Grund, ihre Verantwortungen miteinander zu koppeln.

Duplikation von Delegation ist billig. Kopplung von Verantwortlichkeiten ist teuer.

Das ist kein Freibrief gegen DRY. Entscheidend ist der Änderungsgrund. CustomerListFacade ändert sich, wenn sich der Contract der Customer List ändert. CustomerSearchFacade ändert sich, wenn sich der Contract der Search View ändert.

Die Facade beantwortet deshalb nicht die Frage:

Was kann unsere Anwendung alles mit Customers?

Sondern:

Was benötigt genau diese View von unserer Domain?

Namen wie CustomerListFacade, CustomerDetailFacade und CustomerEditFacade machen diese Verantwortung sichtbar. Die Facade bildet eine view-spezifische Domain-API und exponiert nur das, was diese View konsumieren darf.

Application beziehungsweise Facades dürfen:

  • +State konsumieren
  • vorhandene State- und Projection-Werte exponieren
  • bereits vorhandene Streams einsammeln
  • Intents und Commands an +State delegieren
  • einen view-spezifischen Contract bereitstellen
  • interne Domain-Strukturen vor der Presentation verstecken
  • mehrere interne Domain-Sources hinter einem einzigen View-Contract kapseln
  • bereits erzeugte ViewModels oder Projektionen exponieren

Application beziehungsweise Facades sollten nicht:

  • Business Rules implementieren
  • fachliche Entscheidungen treffen
  • fachlichen Derived State erzeugen
  • Streams fachlich zusammenführen
  • eigene Zustandsübergänge modellieren
  • DTOs parsen
  • HTTP oder externe SDKs bedienen
  • technische Contracts interpretieren
  • den Router steuern
  • Dialoge, Toasts oder Snackbars auslösen
  • DOM- oder UI-Framework-Details kennen
  • Material-, Ionic- oder Bootstrap-spezifische Entscheidungen treffen
  • mehrere Views aus Wiederverwendungsgründen in einer gemeinsamen Götter-Facade bündeln

Eine View bekommt genau einen Eingangspunkt zur Domain. Sie muss weder wissen, woher einzelne Werte kommen, noch wie viele Stores oder Projektionen hinter ihrem Contract stehen. Dadurch kann die Domain intern anders geschnitten werden, ohne dass jede Presentation diese Änderung nachvollziehen muss.

Auch die Änderungsgründe bleiben lokal. Eine neue Funktion im Customer Editor vergrößert nicht automatisch den Contract der Customer List. Jede Facade bleibt so klein wie die View, für die sie existiert. Dass dabei an einigen Stellen Delegation doppelt auftaucht, ist der Preis, den unabhängige Contracts kosten – und er ist niedrig.

Der eigentliche Nutzen entsteht deshalb nicht durch besonderen Code in der Application-Schicht. Er entsteht durch die Grenze, die dieser Code sichtbar macht.

Ein realistischer Application Layer kann fast nur aus Klassen wie dieser bestehen:

export class CustomerListFacade {
readonly customers = this.customerState.customers;
readonly loading = this.customerState.loading;
selectCustomer(id: CustomerId): void {
this.customerState.select(id);
}
createCustomer(): void {
this.customerState.create();
}
}

Das ist kein Zeichen dafür, dass die Schicht überflüssig ist. Der Wert liegt im Contract und in der entkoppelten Struktur, nicht in der Menge des Codes.

Eine gute Facade ist langweilig. Sie macht nichts Fachliches neu, sondern macht vorhandene Fachlichkeit für genau eine View zugänglich.

+State besitzt die Fachlichkeit. Application stellt einer konkreten View einen passenden Contract darauf bereit. Die Domain kennt die View nicht. Die Facade kennt beide Seiten, erzeugt dabei aber keine neue Fachlichkeit.

Auf der anderen Seite dieses Contracts beginnt die Presentation.