Freigeben über



Oktober 2016

Band 31, Nummer 10

Dieser Artikel wurde maschinell übersetzt.

Cutting Edge: Event-Command-Saga-Ansatz für Geschäftslogik

Von Dino Esposito

Dino EspositoWenn die Grundlage unserer Arbeit relativ endgültige Anforderungen wären, würde sich jeder Entwurfsaufwand vor der Entwicklung ganz sicher auszahlen. Wenn Sie den Entwicklungsansatz „Big Design Up Front“ (BDUF) kennen, wissen Sie, wovon ich spreche. Sie können sich über diesen Ansatz unter bit.ly/1jVQr3g informieren. Ein umfassendes Domänenmodell, das zahlreiche und komplexe Workflows sowie reichhaltige und denormalisierte Datenansichten verarbeitet, erfordert solide und stabile Kenntnisse der Domäne bei gleichzeitiger Verlängerung der Entwicklungszeit. Anders ausgedrückt: Unter dem Aspekt der Relevanz, die Software heutzutage für das tägliche Geschäft besitzt, ist ein umfassendes Domänenmodell eine genau so schlechte Idee wie BDUF es war, bevor die Agile-Bewegung auf den Plan trat.

Der Event-Command-Saga-Ansatz (ECS) ermöglicht viel agilere Verfahren zum Implementieren von Geschäftsworkflows. Zwar sind immer noch solide Kenntnisse der Prozesse und Geschäftsregeln erforderlich, es muss aber kein umfassender Entwurf vorhanden sein, bevor mit der Codierung begonnen werden kann. Außerdem ist dieser Ansatz bei möglichen Änderungen und (noch wichtiger) Geschäftsaspekten flexibel, die anfangs vergessen oder übersehen wurden.

Der ECS-Ansatz verwendet eine nachrichtenbasierte Formulierung der Geschäftsprozesse. Dies ist erstaunlicherweise näher an der Abstraktionsebene von Flussdiagrammen. Aus diesem Grund können Projektbeteiligte den Entwurf einfacher kommunizieren und überprüfen. Eine nachrichtenbasierte Formulierung der Geschäftsprozesse ist außerdem für Entwickler viel einfacher zu verstehen. Dies gilt selbst dann, wenn ihr Verständnis der jeweiligen Geschäftsdomäne eingeschränkt ist. Der Begriff „ECS“ hört sich möglicherweise neu an. Die Konzepte, auf denen er gründet, sind jedoch die gleichen, die in anderen Quellen ggf. als CQRS/ES bezeichnet werden.

Meine Kolumne in diesem Monat stellt ein auf .NET basierendes Framework vor, das speziell dafür konzipiert ist, die Geschäftslogik von Anwendungen mithilfe relativ neuer Konzepte zu implementieren, z. B. mit Befehlen und Sagas. Eine Einführung in dieses Thema finden Sie in meiner Kolumne aus September 2016 (msdn.com/magazine/mt767692). Der Begriff „Geschäftslogik“ bezieht sich hier auf die Anwendungs- und auf die Domänenlogik. In der „Anwendungslogik“ werden alle Fälle implementiert, die von einem bestimmten Front-End abhängen. Die „Domänenlogik“ ist hingegen für Anwendungsfälle gleichbleibend und kann in allen vorhandenen Präsentationskonfigurationen und Anwendungsschichten vollständig wiederverwendet werden.

MementoFX in Aktion

Beginnen wir mit einem neuen ASP.NET MVC-Projekt, das bereits für die Verwendung allgemeiner Technologien wie Bootstrap, jQuery, Entity Framework und ASP.NET SignalR konfiguriert ist. Fügen Sie nun eine Controllerklasse mit einer Methode und zugehörigen Ansicht hinzu, die ein HTML-Formular für Benutzer anzeigt. Wenn der Benutzer das Formular sendet, wird die Ausführung des folgenden Codes erwartet:

[HttpPost]
public ActionResult Apply(NewAccountRequestViewModel input)
{
  _service.ApplyRequestForNewBankAccount(input);
  return RedirectToAction("index", "home");
}

Auf den ersten Blick ist dies Standardcode. Der Clou verbirgt sich aber unter der Oberfläche. Öffnen Sie also den Code für die Methode „ApplyRequestForNewBankAccount“ in der Anwendungsschicht.

Als Geschäftsaktion hat der Benutzer der Anwendung (wahrscheinlich ein Bankangestellter) das Formular aufgrund einer Kundenanfrage zum Eröffnen eines neuen Kontos soeben ausgefüllt. Ein bestimmter Prozess muss immer dann gestartet werden, wenn eine solche neue Anforderung eingeht. Sie können alle Schritte des Workflows prozedural direkt in der Anwendungsschicht codieren, oder Sie können den ECS-Ansatz ausprobieren. Im letzteren Fall sieht das Ergebnis folgendermaßen aus:

public void ApplyRequestForNewBankAccount(NewAccountRequestViewModel input)
{
  var command = new RequestNewBankAccountCommand(
    input.FullName, input.Age, input.IsNew);
  Bus.Send(command);
}

Die Klasse „RequestNewBankAccountCommand“ ist etwas mehr als einfach nur eine POCO-Klasse (Plain Old CLR Object). Es ist zwar eine POCO-Klasse, aber sie erbt von „Command“. Die Command-Klasse ist ihrerseits in einem der NuGet-Pakete definiert, aus denen das MementoFX-Framework besteht. Sie fügen die Pakete dann wie in Abbildung 1 gezeigt hinzu.

Installieren der MementoFX NuGet-Pakete
Abbildung 1: Installieren der MementoFX NuGet-Pakete

Das MementoFX-Framework besteht aus drei Hauptteilen: einer Kernbibliothek, einem Ereignisspeicher und einem Bus. In der Beispielkonfiguration habe ich die eingebettete Version von RavenDB zum Speichern von Domänenereignissen und einen In-Memory-Bus (Postie) für die Anwendungsschicht sowie die Sagas zum Veröffentlichen und Abonnieren von Ereignissen verwendet. Wenn Sie die NuGet-Plattform weiter untersuchen, finden Sie außerdem eine auf Rebus basierende Buskomponente und eine auf MongoDB basierende Ereignisspeicherkomponente. Der folgende Code lässt sich nun problemlos kompilieren:

public class RequestNewBankAccountCommand : Command
{
  ...
}

In der aktuellen Version von MementoFX ist die Basisklasse „Command“ nur ein Marker. Sie enthält keinen funktionalen Code. Dies wird sich in zukünftigen Versionen aber wahrscheinlich ändern. Der Befehl dient als Wrapper für die Eingabeparameter für den ersten Schritt des Geschäftsprozesses. Damit der Geschäftsprozess ausgelöst wird, wird der Befehl an den Bus übergeben.

Konfigurieren der MementoFX-Umgebung

Das anfängliche Setup von MementoFX ist einfacher, wenn Sie ein IoC-Framework (Inversion of Control, Steuerungsumkehr) wie z. B. Unity verwenden. Sie müssen die folgenden drei Schritte ausführen, um MementoFX zu konfigurieren: Initialisieren Sie zuerst den Ereignisspeicher Ihrer Wahl. Weisen Sie MementoFX im zweiten Schritt an, wie generische Schnittstellentypen in konkrete Typen aufgelöst werden sollen (dies bedeutet im Wesentlichen, das Framework über den zu verwendenden Bustyp, den Ereignisspeicher und das Ereignisspeicherdokument zu informieren). Im dritten Schritt lösen Sie den Bus in eine konkrete Instanz auf und binden ihn entsprechend an Sagas und Handler. Abbildung 2 fasst diesen Vorgang zusammen.

Abbildung 2: Konfigurieren von MementoFX

// Initialize the event store (RAVENDB)
NonAdminHttp.EnsureCanListenToWhenInNonAdminContext(8080);
var documentStore = new EmbeddableDocumentStore
{
  ConnectionStringName = "EventStore",
  UseEmbeddedHttpServer = true
};
documentStore.Configuration.Port = 8080;
documentStore.Initialize();
// Configure the FX
var container = MementoFxStartup
  UnityConfig<InMemoryBus, EmbeddedRavenDbEventStore,
     EmbeddableDocumentStore>(documentStore);
// Save global references to the FX core elements
Bus = container.Resolve<IBus>();
AggregateRepository = container.Resolve<IRepository>();
// Add sagas and handlers to the bus
Bus.RegisterSaga<AccountRequestSaga>();
Bus.RegisterHandler<AccountRequestDenormalizer>();

Wie Abbildung 2 zeigt, besitzt der Bus zwei Abonnenten: die zwingend erforderliche „AccountRequestSaga“ und den optionalen „AccountRequestDenormalizer“. Die Saga enthält den Code, der die Anforderung verarbeitet. Möglicherweise vorhandene Geschäftslogik wird hier angewendet. Der Denormalisierer empfängt Informationen zum Aggregat und erstellt (wenn erforderlich) eine Projektion der Daten ausschließlich für Abfragezwecke.

Entwerfen der Saga

Eine Saga ist eine Klasse, die eine aktuell ausgeführte Instanz eines Geschäftsprozesses darstellt. Abhängig von den tatsächlichen Funktionen des verwendeten Bus kann die Saga je nach Bedarf persistent gespeichert, angehalten und fortgesetzt werden. Der Standardbus, der in MementoFX vorhanden ist, arbeitet nur im Arbeitsspeicher. Jede Saga ist also ein einmaliger Vorgang, der transaktional vom Beginn bis zum Ende ausgeführt wird.

Eine Saga muss ein Startereignis oder einen Startbefehl besitzen. Sie geben die Startnachricht über die Schnittstelle „IAmStartedBy“ an. Eine weitere Nachricht (Befehl oder Ereignis), die die Saga verarbeiten kann, wird durch die IHandlesMessage-Schnittstelle gebunden:

public class AccountRequestSaga : Saga,
  IAmStartedBy<RequestNewBankAccountCommand>,
  IHandleMessages<BankAccountApprovedEvent>
{
  ...
}

Beide Schnittstellen bestehen aus einer einzelnen Methode „Handle“, wie hier gezeigt:

public void Handle(RequestNewBankAccountCommand message) { ... }
public void Handle(BankAccountApprovedEvent message) { ... }

Beschäftigen wir uns erneut mit dem HTML-Formular, von dem wir angenommen haben, dass es in der Benutzeroberfläche vorhanden ist. Wenn der Bankangestellte mit der Maus klickt, um die Kundenanfrage für ein neues Bankkonto zu senden, wird ein Befehl mithilfe von Push an den Bus übertragen, und der Bus löst automatisch eine neue Saga aus. Abschließend wird die Methode „Handle“ der Saga für den angegebenen Befehl ausgeführt.

Hinzufügen von Verhalten zur Saga

Eine Sagaklasse wird wie hier gezeigt instanziiert:

public AccountRequestSaga(
  IBus bus, IEventStore eventStore, IRepository repository)
  : base(bus, eventStore, repository)
{
}

Es wird ein Verweis auf den Bus bereitgestellt, damit die aktuelle Saga neue Befehle und Ereignisse mithilfe von Push an den Bus übertragen kann, die andere Sagas oder Handler und Denormalisierer verarbeiten. Dies ist in der Tat der Schlüsselfaktor, der einen flexiblen und agilen Entwurf von Geschäftsworkflows ermöglicht. Außerdem ruft eine Saga einen Verweis auf das Repository ab. In MementoFX ist das Repository eine Facade, die auf dem Ereignisspeicher basiert. Das Repository speichert Aggregate und gibt diese zurück. Dabei wird der Zustand des Aggregats jedes Mal erneut erstellt, indem alle Ereignisse wiedergegeben werden, die es durchlaufen hat. Praktischerweise bietet das MementoFX-Repository auch eine Überladung zum Abfragen des Zustands eines bestimmten Aggregats zu einem bestimmten Datum.

Die folgende Saga würde die Anforderung für ein neues Bankkonto persistent speichern:

public void Handle(RequestNewBankAccountCommand message)
{
  var request = AccountRequest.Factory.NewRequestFrom(
    message.FullName, message.Age, message.IsNew);
  Repository.Save(request);
}

In diesem Beispiel ist die AccountRequest-Klasse ein MementoFX-Aggregat. Ein MementoFX-Aggregat ist eine einfache Klasse, die von einem bestimmten übergeordneten Element abgeleitet wird. Das Zuweisen einer übergeordneten Klasse erspart Ihnen zahlreiche Codierungsaufgaben bezüglich der Verwaltung interner Domänenereignisse:

public class AccountRequest : Aggregate,
  IApplyEvent<AccountRequestReceivedEvent> { ... }

Ein weiterer interessanter Aspekt von MementoFX-Aggregaten ist die IApplyEvent-Schnittstelle. Der der IApplyEvent-Schnittstelle zugeordnete Typ definiert ein Domänenereignis, das vom Aggregat nachverfolgt werden muss. Anders ausgedrückt: Dies bedeutet, dass alle der IApplyEvent-Schnittstelle zugeordneten Ereignisse im Ereignisspeicher für die betreffende Instanz der Aggregatklasse gespeichert werden. Für eine Bankkontoanforderung können Sie daher ermitteln, wann sie empfangen, verarbeitet, genehmigt, verzögert, abgelehnt usw. wurde. Außerdem werden alle Ereignisse in ihrer natürlichen Reihenfolge gespeichert. Dies bedeutet, dass das Framework alle Ereignisse bis zu einem bestimmten Datum auf einfache Weise erfassen und dann eine Ansicht des Aggregats zu einem beliebigen Zeitpunkt im Lebenszyklus des Systems zurückgeben kann. Beachten Sie, dass in MementoFX die Verwendung von „IApplyEvent“ optional in dem Sinn ist, dass Sie auch wahlweise relevante Ereignisse im Speicher manuell persistent speichern können, wenn eine andere Methode des Aggregats aufgerufen wird. Die Verwendung der Schnittstelle ist eine empfohlene Vorgehensweise, durch die der Code klarer und genauer bleibt.

Beim Definieren eines Aggregats müssen Sie dessen eindeutige ID angeben. Laut Konvention erkennt MementoFX die ID als eine Eigenschaft mit dem Namen der Aggregatklasse mit dem Zusatz „Id“. In diesem Fall würde es sich um „AccountRequestId“ handeln. Wenn Sie einen anderen Namen verwenden möchten (z. B „RequestId“), verwenden Sie das AggregateId-Attribut wie im folgenden Beispiel gezeigt:

public void ApplyEvent(
  [AggregateId("RequestId")]
  AccountRequestReceivedEvent theEvent)
{ ... }

In C# 6 können Sie auch den nameof-Operator verwenden, um die Verwendung einer einfachen Konstante im kompilierten Code zu vermeiden. Mit MementoFX und dem ECS-Ansatz müssen Sie die Persistenzlogik geringfügig ändern, mit der Sie vertraut sind. Wenn die Saga im Begriff ist, die Anforderung für das Konto zu protokollieren, wird z. B. die Factory von „AccountRequest“ zum Abrufen einer neuen Instanz verwendet. Beachten Sie, dass zum Vermeiden von Fehlern zur Kompilierungszeit die Factoryklasse im Text der AccountRequest-Klasse definiert werden muss:

public static class Factory
{
  public static AccountRequest NewRequestFrom(string name, int age, bool isNew)
  {
    var received = new AccountRequestReceivedEvent(Guid.NewGuid(), name, age, isNew);
    var request = new AccountRequest();
    request.RaiseEvent(received);
    return request;
  }
}

Wie Sie sehen können, füllt die Factory die neu erstellte Instanz des Aggregats nicht aus. Sie bereitet nur ein Ereignis vor und löst es aus. Die RaiseEvent-Methode gehört zur Aggregate-Basisklasse, fügt dieses Ereignis der aktuellen Instanz des Aggregats hinzu und ruft „ApplyEvent“ auf. Auf komplexe Weise haben Sie an diesem Punkt erreicht, dass aus der Factory ein vollständig initialisiertes Aggregat zurückgegeben wird. Der Vorteil besteht darin, dass das Aggregat nicht nur seinen aktuellen Zustand enthält, sondern auch alle relevanten Ereignisse, die im aktuellen Vorgang übertragen wurden.

Was geschieht, wenn die Saga das Aggregat in der Persistenzschicht speichert? Die Methode „Save“ des integrierten Repositorys durchläuft die Liste der ausstehenden Ereignisse im Aggregat und schreibt diese dann in den konfigurierten Ereignisspeicher. Wenn stattdessen die Methode „GetById“ aufgerufen wird, wird die ID verwendet, um alle verwandten Ereignisse abzurufen. Es wird eine Instanz des Aggregats zurückgegeben, die sich aus der Wiedergabe aller protokollierten Ereignisse ergibt. Abbildung 3 zeigt eine Benutzeroberfläche, von der Sie wahrscheinlich erwarten würden, dass sie aus einem Standardansatz stammt. Was im Hintergrund geschieht, ist jedoch etwas vollkommen Anderes. Beachten Sie, dass ich in der Benutzeroberfläche ASP.NET SignalR verwendet habe, damit die Änderungen auf der Hauptseite angezeigt werden.

Die MementoFX-Beispielanwendung in Aktion
Abbildung 3: Die MementoFX-Beispielanwendung in Aktion

Eine Anmerkung zu Denormalisierern

Eine der wichtigsten Änderungen in Software in der letzten Zeit ist die Trennung zwischen dem Modellideal zum Speichern von Daten und dem Modellideal zum Nutzen von Daten gemäß dem CQRS-Muster. Bis jetzt haben Sie nur ein Aggregat mit allen Informationen gespeichert, deren Speicherung relevant ist. Jeder Typ von Benutzer benötigt jedoch ggf. eine andere Sammlung relevanter Informationen für das gleiche Aggregat. Wenn dies der Fall ist, müssen Sie mindestens eine Projektion der gespeicherten Daten erstellen. Eine Projektion ist in diesem Kontext in vieler Hinsicht einer Ansicht in einer SQL Server-Tabelle ähnlich. Sie müssen Denormalisierer zum Erstellen einer Projektion von Aggregaten verwenden. Ein Denormalisierer ist ein Handler, der an ein Ereignis gebunden ist, das mithilfe von Push an den Bus übertragen wird. Angenommen, Sie müssen z. B. ein Dashboard für die Manager erstellen, die für das Genehmigen neuer Kontoanforderungen erforderlich sind. Sie möchten in diesem Fall vielleicht eine etwas andere Aggregation der gleichen Daten mit einigen Indikatoren bereitstellen, die für das Geschäft relevant sind:

public class AccountRequestDenormalizer :
  IHandleMessages<AccountRequestReceivedEvent>
  {
    public void Handle(AccountRequestReceivedEvent message)
    { ... }
}

Denormalisierte Daten müssen nicht im Ereignisspeicher gespeichert werden. Sie können problemlos eine beliebige Datenbank verwenden. In den meisten Fällen ist ein klassisches relationales Modul die effektivste Lösung.

Zusammenfassung

In dieser Kolumne haben Sie einen kleinen Einblick in eine neue Möglichkeit zum Organisieren von Geschäftslogik gewinnen können, bei der CQRS und Event Sourcing kombiniert werden, ohne sich um die Low-Level-Details und Komplexität beider Muster kümmern zu müssen. Außerdem orientiert sich der ECS-Ansatz eng am echten Geschäftsleben, um Kommunikation zu befördern und das Risiko von Missverständnissen zu verringern. MementoFX steht Ihnen auf NuGet zum Testen zur Verfügung. Ich freue mich schon auf Ihr Feedback.


Dino Espositoist Autor von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press, 2014) und „Modern Web Applications with ASP.NET“ (Microsoft Press, 2016). Esposito ist Technical Evangelist für die .NET- und Android-Plattformen bei JetBrains und spricht häufig auf Branchenveranstaltungen weltweit. Auf software2cents.wordpress.com und auf Twitter unter twitter.com/despos lässt er uns wissen, welche Softwarevision er verfolgt.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Andrea Saltarello