Zeiterfassungsanwendung mit Clean Architecture – Schritt-für-Schritt-Anleitung #54
Pinned
hfanieng
started this conversation in
Architektur
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Zeiterfassungsanwendung mit Clean Architecture – Schritt-für-Schritt-Anleitung
Einführung in Clean Architecture
Schichten und ihre Aufgaben
Abb. 1: Schematische Darstellung der Clean Architecture. Die innersten Kreise (gelb und rot) repräsentieren die Enterprise Business Rules (Entitäten der Domäne) und Application Business Rules (Use Cases). Darum liegen die Interface Adapters (grün), z. B. Controller, Presenter und Gateways, welche zwischen der Domäne und äußeren Systemen vermitteln. Die äußerste Schicht bilden die Frameworks & Drivers (blau) – z. B. Datenbank, UI oder externe Devices. Alle Abhängigkeits-Pfeile zeigen von außen nach innen und folgen damit dem Dependency Inversion Principle, sodass die Kernlogik nichts über die äußeren Details wissen muss.
Die Clean Architecture unterteilt eine Anwendung typischerweise in vier Schichten mit klar getrennten Verantwortlichkeiten:
Wichtig: Bei Clean Architecture hängt jede Schicht nur von innen liegenden Schichten ab, nie umgekehrt. Die innere Geschäftslogik kennt also keine Details der äußeren Schichten. Dieses Prinzip der Abhängigkeitsumkehr (Dependency Inversion) stellt sicher, dass die Domäne nicht von Frameworks oder Datenbanken beeinflusst wird.
Praktisches Beispiel: Zeiterfassungsanwendung Schritt für Schritt
Schritt 1: Projektstruktur einrichten
Diese Struktur trennt klar die Domäne (innen) von den technischen Details (außen). Als Nächstes füllen wir diese Schichten mit Inhalt.
Schritt 2: Zentrale Use Cases definieren
Durch das klare Definieren dieser Use Cases wissen wir, welche Funktionen unser System bieten muss. Jeder Use Case wird in der Clean Architecture später durch eine eigene Interaktor-Klasse (Use-Case-Klasse) repräsentiert, die genau diese Aktion durchführt.
Schritt 3: Entities implementieren
Nun erstellen wir die zentralen Entitäten der Domäne. In unserer Zeiterfassung ist die wichtigste Entität ein Zeiteintrag, den wir als Klasse TimeEntry umsetzen. Dieses Domänenobjekt hält die relevanten Daten eines Zeiteintrags und kann ggf. geschäftslogische Regeln kapseln (z. B. Validierung, dass die Endzeit nach der Startzeit liegt).
Die TimeEntry-Klasse hält hier Start- und Endzeit sowie eine Beschreibung des Zeiteintrags. Eine optionale ID kann vom System vergeben werden (z. B. automatisch im Repository). In der Konstruktor-Logik könnten wir bereits Geschäftsregeln überprüfen (im Beispiel: die Endzeit darf nicht vor der Startzeit liegen). Weitere Domänenlogik ließe sich ebenfalls in Entities kapseln (z. B. Dauer eines Eintrags berechnen).
Schritt 4: Use Cases umsetzen
Als Nächstes implementieren wir die Use Cases RecordTime (Zeit erfassen) und ListTimeEntries (Zeiten auflisten). Diese Use-Case-Klassen gehören zur Anwendungsschicht und enthalten die Ablauflogik für die definierten Szenarien. Wichtig: Die Use Cases sollen unabhängig von Details wie der Datenbank sein – sie arbeiten stattdessen mit Abstraktionen (Schnittstellen), um z. B. auf ein Repository zuzugreifen.
Wir definieren zunächst ein Repository-Interface, das von der Domäne aus gesehen die Datenhaltung repräsentiert (z. B. für Zeiteinträge speichern und laden). Dieses Interface wird im nächsten Schritt genauer erläutert. Die Use-Case-Klasse RecordTimeUseCase nutzt das Interface, um den neuen Eintrag zu persistieren:
Und analog dazu der Use Case zum Anzeigen der Zeiten:
In RecordTimeUseCase sehen wir, dass der neue TimeEntry erstellt und dann über repository.save(...) gespeichert wird – ohne zu wissen, wie genau gespeichert wird. ListTimeEntriesUseCase ruft über das gleiche Repository-Interface die Liste aller Einträge ab. Die Geschäftslogik (hier trivial: Erstellen bzw. Laden von Einträgen) ist damit gekapselt. Beide Use Cases hängen nur von der Abstraktion TimeEntryRepository ab, nicht von einer konkreten Datenbank oder Liste.
Hinweis: In einer komplexeren Anwendung könnte RecordTimeUseCase statt eines direkten Rückgabewerts auch einen Output-Presenter aufrufen, um z. B. eine Bestätigung oder aufbereitete Daten an die UI weiterzugeben. Für unser einfaches Beispiel genügt es, den neuen Eintrag zurückzugeben.
Schritt 5: Schnittstellen (Input, Output, Gateways) definieren
Bis jetzt haben wir im Code bereits angedeutet, welche Schnittstellen zwischen den Schichten nötig sind. In Clean Architecture sprechen wir oft von Ports und Gateways:
Wir definieren nun explizit das Repository-Interface für Zeiteinträge. Dieses Interface gehört zur Domäne bzw. Anwendungslogik (inneren Schicht) und stellt die Methoden bereit, die wir zum Speichern und Laden von TimeEntry-Objekten brauchen. Damit wenden wir das Dependency-Inversion-Prinzip an: Die innere Schicht definiert den Vertrag, den die äußere Schicht erfüllen muss.
Dieses Interface wird von den Use Cases verwendet, kennt aber keine Details der Implementierung. Die konkrete Speicherung (etwa in einer Datenbank oder in einer Liste) wird erst in der äußeren Schicht entschieden. Durch diese Schnittstelle bleibt unsere Geschäftslogik (Use Cases + Entities) komplett unabhängig von der Datenbank-Technologie.
Zusammengefasst erstellen wir also Schnittstellen für alle Stellen, an denen die innere Logik mit etwas Äußerem kommuniziert:
All diese Interfaces liegen innerhalb der Kernlogik und definieren Anforderungen, die außerhalb erfüllt werden. Damit „kennt“ die innere Schicht nur Abstraktionen, keine konkreten Klassen der äußeren Welt – ein zentrales Prinzip von Clean Architecture und SOLID (Dependency Inversion).
Schritt 6: Adapter und äußere Schicht umsetzen
Im letzten Schritt kümmern wir uns um die Adapter in der äußeren Schicht, die unsere Use Cases in ein laufendes System einbinden. Wir betrachten zwei mögliche Ansätze:
Zur Veranschaulichung implementieren wir einen einfachen CLI-Adapter. Dabei nutzen wir ein In-Memory Repository (eine schnelle Implementierung von TimeEntryRepository, die die Daten in einer Liste hält) und rufen die Use Cases aus einer main-Methode heraus auf:
Führen wir TimeTrackerCLI.main() aus, würde das Programm einen neuen Zeiteintrag erzeugen und anschließend alle Einträge auflisten. Hier übernimmt TimeTrackerCLI die Rolle eines einfachen Controllers: Es sammelt Eingaben (in unserem Beispiel sind die Daten fest im Code vorgegeben), ruft die Geschäftslogik auf (recordTime.execute(...) und listTimes.execute()) und gibt die Ergebnisse auf der Konsole aus. Die Use Cases selbst sind dabei völlig unverändert, egal ob wir sie über eine Konsole, eine Web-API oder z. B. durch automatisierte Tests aufrufen – wir könnten anstelle der CLI auch einen REST-Controller schreiben, der die gleichen Use-Case-Methoden nutzt. Für eine Web-API würde man z. B. einen HTTP-Controller implementieren, der die eingehenden JSON-Daten in die Parameter von RecordTimeUseCase.execute umwandelt, und das Ergebnis (eine TimeEntry-Liste) wieder als JSON an den Client zurückgibt. Die Logik bleibt jedoch in den Use-Case-Klassen gekapselt, und nur die Adapter-Schicht ändert sich je nach Rahmenwerk.
Damit haben wir eine vollständige vertikale Scheibe unserer Zeiterfassungsanwendung umgesetzt: Von der Domäne (Entity TimeEntry), über die Anwendungsfälle (RecordTimeUseCase, ListTimeEntriesUseCase und Interface TimeEntryRepository), bis zur Infrastruktur (In-Memory-Repo und CLI als einfache UI).
Best Practices und Tipps für Einsteiger
Mit diesen Tipps und dem obigen Leitfaden sollte ein Entwicklungsteam ohne Vorerfahrung in der Lage sein, eine einfache Zeiterfassungsanwendung nach Clean Architecture umzusetzen. Wichtig ist, die Trennung der Schichten konsequent umzusetzen – so werden die Vorteile Wartbarkeit, Testbarkeit und Flexibilität Schritt für Schritt erfahrbar. Viel Erfolg beim Ausprobieren!
Beta Was this translation helpful? Give feedback.
All reactions