Wenn MVVM und MVC nicht mehr retten und Tests zur Hölle werden
Haben Sie jemals eine App in Swift geschrieben, bei der nach einem halben Jahr das Hinzufügen einer Funktion drei andere kaputt gemacht hat und Sie Tests komplett neu schreiben mussten, weil die Datenbank von Core Data auf Realm gewechselt wurde? Falls ja, sind Sie nicht allein. Klassische Schichtenarchitekturen (MVC, MVVM) funktionieren am Anfang gut, werden aber mit zunehmender Projektgröße zu einem „Chaos“.
Ich zeige Ihnen die Hexagonale Architektur (auch Ports & Adapters genannt) – ein Ansatz aus der Java- und DDD-Welt, der sich hervorragend auf Swift übertragen lässt. Wir analysieren sie anhand realer Beispiele, ohne abstrakte „User“ und „Manager“. Sie werden sehen, wie Sie die Geschäftslogik von Datenbank, UI und Netzwerk so trennen, dass diese ausgetauscht werden können, ohne den Kern der App zu berühren.
Dies ist keine weitere Theorie – es ist Praxis, die Sie noch heute in Ihrem Projekt anwenden können.
Was ist die Hexagonale Architektur? Einfach erklärt
Stellen Sie sich ein Sechseck vor (daher der Name). Im Zentrum befindet sich Ihre reine Geschäftslogik (Domäne). An den Rändern befinden sich Ports (Schnittstellen), über die die Außenwelt mit dem Kern kommuniziert. Adapter sind die konkreten Implementierungen: Datenbank, API, UI, Dateisystem.
Die Hauptregel: Der Kern weiß nichts über die Außenwelt. Weder über UIKit, noch über URLSession oder Core Data. Er kennt nur seine Schnittstellen (Ports).
Ein Beispiel aus dem echten Leben: Stellen Sie sich einen Online-Shop vor. Der Kern weiß, dass es einen „Warenkorb“ und „Bestellung aufgeben“ gibt. Es ist ihm egal, woher die Daten kommen – von einem WebSocket, einer lokalen JSON-Datei oder der Tastatur. Die Adapter verbinden sich mit den Ports und übersetzen externe Signale in Domänenbefehle.
Warum sollte man dies in Swift-Projekten verwenden?
- Testbarkeit – Sie testen die Domäne ohne echte Datenbank und Netzwerk. Sie ersetzen einfach die Adapter durch Mocks.
- Flexibilität – Sie möchten von Firebase zu Ihrem eigenen Server wechseln? Sie tauschen einen Adapter aus, nicht die Hälfte der App.
- Lesbarkeit – Die Geschäftslogik ist nicht über ViewController verteilt. Sie liegt an einem Ort.
- Unabhängigkeit von Frameworks – Ihr Kern importiert kein UIKit, Combine oder Alamofire.
Projektstruktur mit Hexagonaler Architektur in Swift
Kommen wir direkt zum Code. Angenommen, wir schreiben eine App zur Aufgabenverwaltung (Todolist). So würde die Ordnerstruktur aussehen:
App/├── Domain/│ ├── Entity/│ │ └── Task.swift│ ├── UseCase/│ │ └── CreateTaskUseCase.swift│ └── Port/│ ├── TaskRepository.swift│ └── TaskOutputPort.swift├── Adapter/│ ├── Persistence/│ │ └── CoreDataTaskRepository.swift│ ├── Network/│ │ └── APITaskRepository.swift│ └── Presentation/│ └── TaskListViewModel.swift└── Main/ └── AppDelegate.swiftBeachten Sie: Domain importiert nichts außer Foundation. Adapter kann CoreData, UIKit, Alamofire importieren – alles, was nötig ist.
Schritt 1: Domänenentitäten und Ports
Beginnen wir mit dem Wichtigsten – dem Domänenmodell. Dies sind structs oder enums, die die Geschäftseinheit beschreiben. Keine NSManagedObject, keine Codable.
// Domain/Entity/Task.swiftstruct Task { let id: UUID var title: String var isCompleted: Bool let createdAt: Date}Jetzt definieren wir den Port für das Repository (Ausgangsport). Dies ist ein Protokoll, das beschreibt, wie wir Aufgaben speichern und laden.
// Domain/Port/TaskRepository.swiftprotocol TaskRepository { func fetchAll() async throws -> [Task] func save(task: Task) async throws func delete(taskID: UUID) async throws}Und den Eingangsport – darüber kommuniziert die Außenwelt (UI, API) mit dem Kern. Normalerweise ist dies ein UseCase.
// Domain/Us