Puff-Architektur ist die Rettung in der Welt der Unternehmensentwicklung. Mit seiner Hilfe können Sie Eisen entladen, Prozesse parallelisieren und die Reihenfolge im Code wiederherstellen. Wir haben versucht, das CQRS-Muster bei der Entwicklung eines Unternehmensprojekts zu verwenden. Alles ist logischer und ... komplizierter geworden. Kürzlich habe ich darüber gesprochen, was ich beim
Panda-Meetup C # .Net- Meeting zu
tun hatte , und jetzt
teile ich es mit Ihnen.

Haben Sie jemals bemerkt, wie Ihre Unternehmensanwendung aussieht? Warum kann es nicht wie Apple und Google sein? Ja, weil wir ständig Zeitmangel haben. Anforderungen ändern sich häufig, der Begriff für ihre Änderungen ist normalerweise "gestern". Und was am unangenehmsten ist, das Geschäft mag wirklich keine Fehler.

Um irgendwie damit zu leben, begannen die Entwickler, ihre Anwendungen in Teile zu unterteilen. Alles begann einfach - mit Daten. Viele kennen das Schema, wenn die Daten getrennt sind, der Client getrennt ist, während die Logik am selben Ort wie die Daten gespeichert ist.

Guter Umriss. Die größten DBMS verfügen über voll funktionsfähige prozedurale Erweiterungen für SQL. Es gibt ein Sprichwort über Oracle: "Wo es Oracle gibt, gibt es Logik." Es ist schwer, über den Komfort und die Geschwindigkeit einer solchen Konfiguration zu streiten.
Wir haben jedoch eine Unternehmensanwendung, und es gibt ein Problem: Die Logik ist schwer zu skalieren. Und es ist nicht zumutbar, die DBMS-Kapazität zu laden, die bereits unter Problemen beim Extrahieren und Aktualisieren von Daten sowie unter trivialen Geschäftsaufgaben leidet.
Um ehrlich zu sein, sind die im DBMS integrierten Programmierwerkzeuge für die Geschäftslogik für die Erstellung normaler Unternehmensanwendungen schwach. Die Aufrechterhaltung der Geschäftslogik in T-SQL / PL-SQL ist ein Problem. Nicht umsonst sind OOP-Sprachen in Unternehmensanwendungen so verbreitet: C #, Java, Sie müssen für ein Beispiel nicht weit gehen.

Es scheint eine logische Lösung zu sein: Wir heben die Geschäftslogik hervor. Sie wird auf ihrem Server leben, die Basis alleine, der Client alleine.
Was kann an dieser dreistufigen Architektur verbessert werden? Architektur ist in die Ebene der Geschäftslogik eingebunden, das möchte ich vermeiden. Die Geschäftslogik möchte nichts über Datenspeicherung wissen. Die Benutzeroberfläche ist auch eine separate Welt, in der es Entitäten gibt, die nicht spezifisch für die Geschäftslogik sind.
Erhöhen Sie Ebenen helfen. Diese Lösung sieht fast perfekt aus, sie hat eine Art innere Schönheit.

Wir haben eine DAL (Data Access Layer) - die Daten sind von der Logik getrennt, normalerweise ein CRUD-Repository mit ORM sowie gespeicherte Prozeduren für komplexe Abfragen. Mit dieser Option können Sie sich schnell genug entwickeln und eine akzeptable Leistung erzielen.
Geschäftslogik kann Teil von Services sein oder eine separate Schicht sein. Die Interaktion zwischen Schichten kann über Transportobjekte (DTO) erfolgen.
Die Anfrage von der Benutzeroberfläche geht an den Dienst, er kommuniziert mit der Geschäftslogik, klettert in die DAL, um auf die Daten zuzugreifen. Dieser Ansatz wird als N-Tier bezeichnet und hat klare Vorteile.
Jede Ebene hat ihre eigenen offensichtlichen Ziele, die wir als Programmierer so sehr mögen. Jede Betonschicht ist nur in ihrem eigenen Geschäft tätig. Services können horizontal skaliert werden. Der Ansatz ist selbst für unerfahrene Entwickler klar. Eine Person versteht schnell, wie das System funktioniert. Es ist sehr einfach, alle Interaktionen zu verfolgen, da die Anforderung von Anfang bis Ende reicht.
Eine weitere Konsistenz: Alle Projekt-Subsysteme arbeiten mit denselben Daten. Sie müssen sich keine Sorgen machen, dass wir Daten an einem Ort aufgezeichnet haben und der Benutzer sie an einem anderen Teil nicht sieht.
Schichtkuchen 1. N-Tier
Im Folgenden finden Sie ein Beispiel für ein typisches Fragment einer Anwendung, die auf diesen Prinzipien basiert. Wir haben eine finanzielle Anforderung, hier habe ich das anämische Modell untersucht. Und es gibt ein klassisches Repository, mit dem über ORM gearbeitet wird.

Dies ist ein typischer Service, sie werden auch Manager genannt. Er arbeitet mit dem Repository, empfängt Anfragen und gibt Antworten an Kunden. In diesem Service sehen wir einige Verwirrung: Wir haben einen Verarbeitungsprozess, einen Prozess für die Arbeit mit der Benutzeroberfläche und einen Prozess für einige interne Steuereinheiten, sie sind schwach miteinander verbunden.
So sieht eine typische Methode dieses Dienstes aus. Zum Beispiel die Registrierung einer Geldforderung.

Wir erhalten Daten, führen einige Geschäftsprüfungen durch. Dann gibt es ein Update und danach einige Nachaktionen, zum Beispiel das Senden einer Benachrichtigung oder das Schreiben in das Benutzerprotokoll.
Bei diesem Ansatz gibt es trotz seiner Schönheit Probleme. In Unternehmensanwendungen ist die Last sehr häufig asymmetrisch: Lesevorgänge sind ein oder zwei Aufträge mehr als Schreibvorgänge. Es gibt bereits ein Problem mit der Skalierung der Datenbank. Dies geschieht natürlich, und selbst mittels eines DBMS im Datenbankmaßstab wird die Partitionierung aufgerufen. Aber es ist schwierig. Wenn dies mit den falschen Qualifikationen oder früher als nötig erfolgt, schlägt die Partitionierung fehl.
In einem unserer Systeme erreichte das Datenvolumen beispielsweise 25 TB, und es traten Probleme auf. Wir selbst haben versucht zu skalieren, haben harte Jungs von einem bekannten Unternehmen eingeladen. Sie schauten und sagten: Wir brauchen 14 Stunden vollständige Ausfallzeit der Basis. Wir dachten und sagten: Leute, es wird nicht funktionieren, das Geschäft wird es nicht akzeptieren.
Zusätzlich zum Volumen der Datenbank wächst die Anzahl der Methoden in Diensten und Repositorys. Zum Beispiel gibt es in einem Dienst für Geldforderungen mehr als hundert Methoden. Es ist schwierig zu warten, es gibt ständige Konflikte bei Zusammenführungsanfragen, die Codeüberprüfung ist schwieriger durchzuführen. Und wenn Sie berücksichtigen, dass die Prozesse unterschiedlich sind und verschiedene Entwicklergruppen daran arbeiten, wird die Aufgabe, alle mit einem Problem verbundenen Änderungen zu verfolgen, zu echten Kopfschmerzen.
Blätterteig 2. CQRS
Was tun? Es gibt eine Lösung, die im alten Rom erfunden wurde: Teilen und Herrschen.

Wie sie sagen, ist alles Neue gut vergessen, alt. Bereits 1988 formulierte Bertrand Meyer das Prinzip der imperativen CQS-Programmierung - Befehl-Abfrage-Trennung - für die Arbeit mit Objekten. Alle Methoden sind klar in zwei Typen unterteilt. Die erste Abfrage, die das Ergebnis zurückgibt, ohne den Status des Objekts zu ändern. Das heißt, wenn Sie sich die monetären Anforderungen des Kunden ansehen, sollte niemand in die Datenbank schreiben, dass der Kunde so und so ausgesehen hat, dass die Anfrage keine Nebenwirkungen haben sollte.
Der zweite Befehl - Befehle - ändert den Status eines Objekts, ohne Daten zurückzugeben. Das heißt, Sie haben etwas zum Ändern bestellt und warten im Gegenzug nicht auf einen Bericht mit 10.000 Zeilen.

Hier ist das Lesedatenmodell klar vom Schreibmodell getrennt. Der größte Teil der Geschäftslogik arbeitet mit Schreibvorgängen. Das Lesen kann an materialisierten Darstellungen oder allgemein auf einer anderen Basis arbeiten. Sie können durch Ereignisse oder einige interne Dienste aufgeteilt und synchronisiert werden. Es gibt viele Möglichkeiten.
CQRS ist nicht kompliziert. Wir müssen Teams klar unterscheiden, die den Status des Systems ändern, aber nichts zurückgeben. Hier kann der Ansatz ausgewogener sein. Es ist nicht besonders beängstigend, wenn der Befehl das Ergebnis der Ausführung zurückgibt: Wenn ein Fehler oder beispielsweise die Kennung der erstellten Entität vorliegt, liegt darin kein Verbrechen vor. Es ist wichtig, dass das Team nicht mit der Anforderung arbeitet. Es sollte nicht nach Daten suchen und Geschäftseinheiten zurückgeben.
Anfragen - dort ist alles einfach. Es ändert den Zustand nicht, so dass es keine Nebenwirkungen gibt. Dies bedeutet, dass der Status des Objekts in beiden Fällen identisch bleiben sollte, wenn wir die Anforderung zweimal hintereinander aufgerufen haben und keine anderen Befehle vorhanden waren. Auf diese Weise können Sie Abfragen parallelisieren. Interessanterweise wird für die Arbeit kein separates Modell für Abfragen benötigt, weil Es macht keinen Sinn, dafür Geschäftslogik aus einem Domänenmodell zu ziehen.
Unser CQRS-Projekt
Folgendes wollten wir in unserem Projekt tun:

Unsere bestehende Anwendung ist seit 2006 in Betrieb und verfügt über eine klassische Schichtarchitektur. Altmodisch, aber immer noch funktionsfähig. Niemand will es ändern und weiß nicht einmal, durch was er es ersetzen soll. Der Moment kam, als es notwendig war, etwas Neues zu entwickeln, praktisch von Grund auf neu. In den Jahren 2011-2012 waren Event Sourcing und CQRS ein sehr modisches Thema. Wir fanden es cool, um den ursprünglichen Zustand des Objekts und die Ereignisse, die dazu führten, zu speichern.
Das heißt, wir aktualisieren das Objekt nicht sozusagen. Es gibt einen ursprünglichen Zustand und daneben wurde das angewendet, was darauf angewendet wurde. In diesem Fall gibt es ein großes Plus: Wir können den Zustand eines Objekts jederzeit in der Geschichte wiederherstellen. Tatsächlich wird das Magazin nicht mehr benötigt. Während wir Ereignisse speichern, verstehen wir, was genau passiert ist. Das heißt, es wurde nicht nur der Wert des Kunden in der Zelle "Adresse" aktualisiert, sondern es wird auch das genaue Ereignis aufgezeichnet, z. B. der Umzug des Kunden.
Es ist klar, dass ein solches Schema beim Empfang von Daten langsam funktioniert, daher haben wir eine separate Datenbank mit Materialdarstellungen zur Auswahl. Nun, und Ereignissynchronisation: Mit jedem Eintreffen von Ereignissen bei einer Statusänderung erfolgt eine Veröffentlichung. Theoretisch scheint alles in Ordnung zu sein, aber ... Ich habe noch nie Leute getroffen, die dies in der Produktion bei hohen Belastungen und akzeptabler Geschäftskonsistenz vollständig erkannt haben.

Das Schema kann weiterentwickelt werden, wenn die Handler und Befehle / Anforderungen getrennt sind. Hier haben wir als Beispiel ein Team - eine registrierte Geldforderung: Es gibt ein Datum, einen Betrag, einen Kunden und andere Felder.
Wir beschränken den Registrierungsprozessor der Geldforderung darauf, dass er nur unser Team akzeptieren kann (wobei TCommand: ICommand). Wir können Handler schreiben, ohne die alten zu ändern, indem wir einfach komplexe Anforderungen hinzufügen. Aktualisieren Sie beispielsweise zuerst das Datum, notieren Sie sich dann den Wert und senden Sie hier eine Benachrichtigung an den Client - all dies wird in verschiedenen Handlern pro Befehl geschrieben.
Wie verursachen wir das alles? Es gibt einen Disponenten, der weiß, wo er all diese Handler aufbewahrt.

Der Dispatcher wird (z. B. über einen DI-Container) an die API übergeben. Und wenn der Befehl eintrifft, wird er nur ausgeführt. Er weiß, wo sich der Container befindet, wo sich die Teams befinden und führt sie aus. Bei Anfragen - ähnlich.
Was ist das Problem bei einem solchen Schema: Alle Wechselwirkungen werden weniger offensichtlich. Wir bauen eine Hierarchie auf Typen auf, die in Containern registriert sind, und antworten dann auf unsere Befehle / Anforderungen. Es erfordert ein sehr klares Design der Architektur. Jede Aktion mit einer Methode mit einem Parameter ist nicht mehr beschränkt. Sie schreiben einen Befehl, schreiben einen Handler, registrieren sich in einem Container. Der Overhead steigt. In einem großen Projekt treten Probleme mit der elementaren Navigation auf. Wir haben uns für einen klassischeren Weg entschieden.
Für die asynchrone Kommunikation wurde der Rebus-Servicebus verwendet.

Für einfache Aufgaben ist es mehr als genug.
Mit CQRS können Sie den Code etwas anders angehen und sich auf den Prozess konzentrieren, da alle Aktionen als Teil des Prozesses geboren werden. Wir haben ein Repository für Anforderungen zugewiesen, Befehle für die Verarbeitung und für Anforderungen separat verarbeitet. Zum Lesen haben wir kein separates Repository verwendet, sondern arbeiten nur mit ORM in Teams.

Hier ist zum Beispiel eine Methode, mit der alles Überflüssige weggeworfen wird. Im Registrierungsteam für Geldforderungen registrieren wir die Nachfrage und veröffentlichen das Ereignis im Bus, dass die Geldforderung registriert ist.

Jeder, der daran interessiert ist, wird darauf reagieren. Beispielsweise funktionieren dort die Benutzerauthentifizierung und -protokollierung.
Hier ist eine Beispielanfrage. Auch alles wurde einfach: Wir lesen und geben an das Repository.

Ich möchte separat bei Rebus.Saga bleiben. Dies ist ein Muster, mit dem Sie einen Geschäftsvorgang in atomare Aktionen aufteilen können. Auf diese Weise können Sie nicht alle gleichzeitig, sondern schrittweise und nacheinander blockieren.

Das erste Element ergreift eine Aktion und sendet eine Nachricht, der zweite Teilnehmer antwortet darauf, erfüllt sie, sendet seine Nachricht, auf die der dritte Teil des Systems bereits reagiert. Wenn alles gut ausgegangen ist, generiert Saga eine eigene Nachricht des angegebenen Typs, auf die andere Abonnenten bereits antworten.
Mal sehen, wie die Klasse für die Bearbeitung einer Geldforderung in diesem Fall aussieht. Alles ist klar: Es gibt Befehle, es gibt Anfragen, die sich auf den Registrierungsprozess beziehen, also einen Bus mit Protokollen.

In diesem Fall gibt es einen Handler. Wenn ein Ereignis eintritt und ein Team ankommt, um Geldforderungen zu registrieren, reagiert er darauf. Im Inneren ist alles wie zuvor, aber die Besonderheit ist, dass es eine Gruppierung nach Prozessen gibt.

Aus diesem Grund wurde es etwas einfacher, weniger Änderungen in jeder Datei.
Schlussfolgerungen
Woran müssen Sie bei der Arbeit mit CQRS denken? Sie benötigen einen besseren Entwurfsansatz, da das Umschreiben des Prozesses etwas komplizierter ist. Es gibt einen kleinen Overhead, ein bisschen mehr Klassen sind geworden, aber das ist nicht kritisch. Der Code ist weniger verbunden, jedoch nicht so sehr wegen CQRS, sondern wegen des Übergangs zum Bus. Es war jedoch CQRS, das uns dazu veranlasste, diese Ereignisinteraktion zu verwenden. Code wurde häufiger hinzugefügt als geändert. Es gibt mehr Klassen, aber sie sind jetzt spezialisierter.
Muss jeder alles fallen lassen und massiv zu CQRS wechseln? Nein, Sie müssen sich ansehen, welches Szenario für ein bestimmtes Projekt am besten geeignet ist. Wenn Ihr Subsystem beispielsweise mit Verzeichnissen arbeitet und CQRS nicht benötigt wird, liefert der klassische mehrschichtige Ansatz ein einfacheres und bequemeres Ergebnis.
Die Vollversion der Aufführung bei Panda Meetup ist unten verfügbar.
Wenn Sie sich eingehender mit dem Thema befassen möchten, ist es sinnvoll, diese Ressourcen zu studieren:
CQRS-Architekturstil - von MicrosoftAlexander Bendyus BlogBeispiele für die Contoso-Universität mit CQRS, MediatR, AutoMapper und mehr - von Jimmy BogardCQRS - von Martin FowlerRebus