Warum traditionelle Architektur mit modernen Aufgaben nicht zurechtkommt?
\n\nHaben Sie schon einmal die Situation erlebt, dass das einfache Hinzufügen einer neuen Funktion in einer Swift-App zu einem Albtraum wird? Bugs häufen sich, Daten gehen verloren und die Logik wächst auf unglaubliche Ausmaße an? Kommt Ihnen das bekannt vor? Willkommen in der Welt, in der CQRS und Event Sourcing Ihre besten Freunde werden.
\n\nDiese Muster sind nicht nur modische Wörter. Sie sind eine Denkweise, die den Ansatz zur Erstellung von Anwendungen in Swift grundlegend verändert. Und heute werden wir sie anhand realer Beispiele durchgehen. Ohne Umschweife, ohne komplizierte Theorien – nur Praxis.
\n\nWas ist CQRS? Teile und herrsche
\n\nCQRS (Command Query Responsibility Segregation) ist ein Prinzip, das besagt: "Hör auf, Lesen und Schreiben in einer Klasse zu vermischen". Klingt einfach, aber die Konsequenzen sind enorm.
\n\nIn der klassischen CRUD-Architektur (Create, Read, Update, Delete) verwenden Sie ein Modell für alles. Zum Beispiel User. Es wird gespeichert, gelesen und aktualisiert. Was aber, wenn für das Lesen andere Felder benötigt werden als für das Schreiben? Dann beginnen die Krücken: optionale Felder, Flags, Mappings...
\n\nCQRS schlägt vor, die Verantwortung zu trennen:
\n- \n
- Commands (Befehle) – ändern den Zustand. Sie mutieren Daten, geben aber nichts zurück. \n
- Queries (Abfragen) – lesen Daten. Sie ändern den Zustand nicht, sondern geben nur ein Ergebnis zurück. \n
In Swift könnte das so aussehen:
\n\n// Command – nur Mutation\nstruct CreateUserCommand {\n let id: UUID\n let name: String\n let email: String\n}\n\n// Query – nur Lesen\nstruct GetUserQuery {\n let userId: UUID\n}\n\n// Command-Handler\nclass CreateUserCommandHandler {\n func handle(_ command: CreateUserCommand) {\n // Logik zum Erstellen des Benutzers\n print("Benutzer erstellt: \\(command.name)")\n }\n}\n\n// Query-Handler\nclass GetUserQueryHandler {\n func handle(_ query: GetUserQuery) -> User? {\n // Logik zum Abrufen des Benutzers\n return User(id: query.userId, name: "Alexej", email: "alex@example.com")\n }\n}\n\nSehen Sie? Keine Vermischung. Jede Klasse macht genau eine Sache. Das vereinfacht das Testen, die Wartung und die Skalierung.
\n\nEvent Sourcing: Geschichte speichern, nicht Zustand
\n\nKommen wir nun zu Event Sourcing – einem Muster, das nicht den aktuellen Zustand eines Objekts speichert, sondern die gesamte Abfolge von Ereignissen, die zu diesem Zustand geführt haben.
\n\nStellen Sie sich ein Bankkonto vor. Anstatt den Kontostand (1000 Euro) zu speichern, speichern Sie eine Liste von Ereignissen: "Kontoeröffnung", "Einzahlung von 500", "Auszahlung von 200", "Einzahlung von 700". Um den aktuellen Kontostand zu ermitteln, spielen Sie einfach alle Ereignisse ab. Dies bietet unglaubliche Möglichkeiten: Prüfung, Rückgängigmachen von Änderungen, Analysen.
\n\nImplementieren wir das in Swift:
\n\n// Ereignis\nprotocol Event {}\n\nstruct AccountCreated: Event {\n let accountId: UUID\n let initialBalance: Double\n}\n\nstruct MoneyDeposited: Event {\n let accountId: UUID\n let amount: Double\n}\n\nstruct MoneyWithdrawn: Event {\n let accountId: UUID\n let amount: Double\n}\n\n// Aggregat – stellt den Zustand aus Ereignissen wieder her\nclass BankAccount {\n private(set) var balance: Double = 0\n private(set) var isActive: Bool = false\n private(set) var accountId: UUID?\n\n func apply(_ event: Event) {\n switch event {\n case let created as AccountCreated:\n accountId = created.accountId\n balance = created.initialBalance\n isActive = true\n case let deposited as MoneyDeposited:\n balance += deposited.amount\n case let withdrawn as MoneyWithdrawn:\n balance -= withdrawn.amount\n default:\n break\n }\n }\n\n // Wiederherstellung aus der Ereignishistorie\n static func load(from history: [Event]) -> BankAccount {\n l