Februar 2017

Band 32, Nummer 2

Cutting Edge: Interne Business Intelligence mit Ereignissen und CQRS

Von Dino Esposito | Februar 2017

Dino EspositoNeulich habe ich mit einer Kundin einige Wireframes besprochen, um die Effektivität eines Geschäftsprozesses und die Gesamtqualität der UX zu überprüfen, die das Team aktuell erstellt. Im System befanden sich einige gemeinsam genutzte Arbeitselemente, die Benutzer auswählen und verarbeiten konnten. Die Kundin war eine durch und durch technisch orientierte Person. Die einzige Vorstellung, die sie hatte, war jedoch eine Tabelle einer relationalen Datenbank mit einem Datensatz, der angibt, wer welche Arbeiten ausführt und wie der aktuelle Status der Verarbeitung ist.

Natürlich könnte eine ähnliche Architektur diese Aufgabe definitiv erledigen. Mit einer anderen und ereignisbasierten Architektur können Sie jedoch eine viel größere Anzahl von Informationen zu den Vorgängen im System erfassen. Daher stellt sich die folgende Kernfrage: Muss die Kundin (jetzt oder in der nahen Zukunft) möglicherweise auf die zusätzlichen Information, die Ereignisse bereitstellen, zugreifen und diese nutzen?

Mit dieser Kolumne schließe ich dort an, wo ich in der Kolumne des letzten Monats aufgehört habe: Ich füge der Beispielanwendung zum Buchen von Besprechungsräumen ein Business Intelligence-Modul (BI-Modul) hinzu. Der Begriff „BI“ ist heutzutage ein recht überladenes Schlagwort, das häufig mit bestimmten Technologien und Produkten in Verbindung gebracht wird. Im Kern bezieht sich BI jedoch auf eine Sammlung von Techniken zum Abrufen und Transformieren von Rohdaten in sinnvolle Informationen, die für Analysten zum Optimieren von Prozessen oder als reine statistische Evidenz verfügbar sind. 

Durch Hinzufügen von Event Sourcing zu einer Anwendung stehen Rohdaten bereit. Durch Kombinieren von Event Sourcing und CQRS erhalten Sie Rohgeschäftsereignisse, die im Befehlsstapel gespeichert und ordnungsgemäß denormalisiert werden, um Kernfunktionen der Anwendung zu unterstützen. Sie können jederzeit ein zusätzliches Modul hinzufügen, das Rohdaten liest und diese in andere sinnvolle Informationen für beliebige Geschäftszwecke transformiert.

Nichts mehr verpassen

Wenn es darum geht, die Vorteile von Event Sourcing herauszustellen, wird der folgende Punkt normalerweise zuerst genannt: „Durch Ereignisse verpassen Sie nichts mehr, was im System geschieht“. Das ist sicher war, verdient aber möglicherweise eine etwas pragmatischere Erklärung. Schauen Sie sich Abbildung 1 an.

Mehrere Projektionen können auf der Grundlage von Rohereignissen erstellt werden
Abbildung 1: Mehrere Projektionen können auf der Grundlage von Rohereignissen erstellt werden

In einem CRUD-System (Create, Read, Update, Delete) liegt normalerweise eine Darstellung der Daten (größtenteils relational) vor sowie mindestens eine einfache Projektion, die meistens nur Tabellendaten an die Anforderungen der Präsentationsschicht anpasst. Bei der Verwendung von Event Sourcing wird dieses Modell erheblich erweitert. Dabei ist eine tiefere Abstraktionsebene der gespeicherten Daten der Schlüsselfaktor. Je mehr domänengenaue Informationen gespeichert werden, desto reichhaltigere und zahlreichere Projektionen können zu jedem beliebigen späteren Zeitpunkt erstellt werden.

In einem Softwaresystem sind Benutzeraktionen die kleinsten Einheiten beobachtbaren Verhaltens, und durch diese Aktionen bewirkte Geschäftsereignisse sind die grundlegendste Informationseinheiten, die gespeichert werden können. In meiner vorherigen Kolumne (msdn.com/magazine/mt790196) habe ich das MementoFX-Framework über NuGet zum transparenten Verarbeiten und persistenten Speichern relevanter Ereignisse während der Lebensdauer von Domänenaggregatobjekten verwendet. Außerdem habe ich spezielle Synchronisierungstools (als Denormalisierer bezeichnet) zum Erstellen einer anzeigbaren Projektion nach jedem relevanten Geschäftsereignis genutzt. Letztlich hat die Kolumne nur gezeigt, wie ein CRUD-System gemäß dem ECS-Muster (Event-Command-Saga) erneut geschrieben wird. Sehen wir uns nun an, was erforderlich ist, um eine weitere Projektion hinzuzufügen, die auf dem Dashboardbildschirm eines Managers Verwendung findet.

Auf dem Weg zu einer eigenen BI-Schicht

In Abbildung 2 sehen Sie den primären Bildschirm der Beispielanwendung. Wie bereits erwähnt, handelt es sich um ein Buchungssystem für eine gemeinsam verwendete Ressource (z. B. Besprechungsräume).

Die Benutzeroberfläche des Beispielbuchungssystems
Abbildung 2: Die Benutzeroberfläche des Beispielbuchungssystems

Wie Sie der Abbildung entnehmen können, haben alle im System zulässigen Benutzer die Möglichkeit, einen Raum zu buchen und die Reservierung dann zu verschieben oder zu stornieren. In einem klassischen, CRUD-orientierten System ist eine Tabelle „Booking“ vorhanden, in der jeder Datensatz eine Reservierung identifiziert. Wenn eine Reservierung verschoben wird, werden die Startzeit und die Dauer überschrieben. Wenn eine Reservierung storniert wird, wird der Datensatz einfach gelöscht. Eine einfache Abfrage der Datensätze gibt jederzeit den aktuellen Status der Buchungen zurück. Wenn Sie das einfache CRUD-System in ein historisches CRUD-System umwandeln (siehe Ausgaben dieser Kolumne aus Mai und Juni 2016 unter msdn.com/magazine/mt703431 bzw. msdn.com/magazine/mt707524), können Sie eine feste Anzahl von Ereignissen nachverfolgen und mit beliebigen relevanten Informationen in einigen anderen Tabellen speichern. Warum also ist eine Lösung mit Event Sourcing, die auf MementoFX basiert, einem einfachen historischen CRUD-System vorzuziehen? Der Grund liegt in der Codeflexibilität und der Resilienz des endgültigen Softwareartefakts.

Mit MementoFX konzentrieren Sie sich auf das relevante Domänenverhalten und auf Ereignisse. Sie modellieren Domänenobjekte um diese Anforderungen herum. Das war es schon. Auf der anderen Seite verwenden Sie eine API zum Abfragen der Aktionen, des Status des Systems an einem beliebigen Datum und zu einer beliebigen Uhrzeit sowie zum Ermitteln, ob ein Protokoll aller Ereignisse oder nur benutzerdefiniert aggregierte und transformierte Rohdaten vorhanden sind, die sich für einen gelegentlich ergebenden Geschäftszweck eignen. Dies ist die eigentliche Kernbedeutung und das Wesen von BI.

Erstellen eines einfachen Ereignisprotokolls

Bezüglich der Beispielanwendung verfügen Sie über eine RavenDB-Datenbank, die alle Rohereignisse speichert, sowie eine denormalisierte SQL Server-Tabelle, die die Liste der ausstehenden Buchungen enthält. Gespeicherte Ereignisse beinhalten das Ereignis, das eine Buchung erstellt hat, und alle nachfolgenden Ereignisse, die diese Buchung an eine andere Uhrzeit verschoben haben. Selbst Ereignisse, die diese Buchung storniert haben, sind enthalten. Ein storniertes Ereignis wird nicht mehr in der Hauptbenutzeroberfläche angezeigt. Gleiches gilt für Zwischenslots, in die eine Buchung verschoben wurde. Alle diese Ereignisse sind für das Erstellen der primären Benutzeroberfläche nicht relevant. Sie sind jedoch wesentlich für das Erstellen einer Dashboardbenutzeroberfläche für einen Manager oder einen Administrator.

Sehen wir uns als Beispiel an, was erforderlich ist, um alle Buchungen im System (oder in einem bestimmten Zeitintervall erstellte Buchungen) zu gruppieren und den gesamten Verlauf jeder Buchung anzuzeigen. Abbildung 3 zeigt dies.

Das vollständige Ereignisprotokoll im System
Abbildung 3: Das vollständige Ereignisprotokoll im System

Das System zählt 16 ausstehende Buchungen, aber jede Buchung besteht aus mindestens einem Ereignis. Die hervorgehobene Buchung wurde z. B. zuerst erstellt und dann zwei Mal in andere Slots an verschiedenen Tagen verschoben.

Wenn Sie einen ähnlichen Bildschirm generieren möchten, müssen Sie definitiv alle Ereignisse im Ereignisspeicher abfragen und dann ausarbeiten, um eine Form zu erzielen, die für die beabsichtigte Benutzeroberfläche geeignet ist. Der Razor-Code, der die tatsächliche Ansicht generiert hat, hat ein ähnliches Datenmodell wie das folgende empfangen:

public class BookingWithHistory
{
  public BookingWithHistory()
  {
    History = new List<BookingHistory>();
  }
  public BookingSummary Current { get; set; }
  public IList<BookingHistory> History { get; set; }
}

Die Klasse „BookingSummary“ stellt den aktuellen Status einer bestimmten Buchung dar und ist die Klasse im Hintergrund der primären Ansicht von Abbildung 1.

public class BookingSummary : Dto
{
  public Guid BookingId { get; set; }
  public string DisplayName { get; set; }
  public DateTime Day { get; set; }
  public int StartHour { get; set; }
  public int StartMins { get; set; }
  public int NumberOfSlots { get; set; }
  public BookingReason Reason { get; set; }}

Eine Instanz dieser Klasse wird während des Denormalisierungsvorgangs nach jeder Aktion „create” oder „move” erstellt. Diese Klasse wird persistent gespeichert und über Entity Framework in eine klassische und aus einer klassischen SQL Server-Datenbank gelesen. Diese Klasse ist also das Element, das die Standardansicht des Systems um die aktuelle Momentaufnahme des Status formt. Unter technischen Aspekten gehört diese Klasse zum Lesemodell.

Die Ansicht, die in Abbildung 3 gezeigt wird, erfasst stattdessen auch Daten von Rohereignissen, die im Ereignisspeicher protokolliert werden. In diesem Beispiel ist dies ein RavenDB-Speicher. Der folgende Codeausschnitt zeigt die Abfragen, die für den Ereignisspeicher ausgeführt wurden, um alle interessanten Ereignisse abzurufen, die während der Lebensdauer der Buchung mit der angegebenen ID aufgetreten sind:

var createdEvents = EventStore.Find<NewBookingCreatedEvent>(e =>
  e.BookingId == bookingId).ToList();
var movedEvents = EventStore.Find<BookingMovedEvent>(e =>
  e.BookingId == bookingId).ToList();
var deletedEvents = EventStore.Find<BookingCanceledEvent>(e =>
  e.BookingId == bookingId).ToList();

Die Vereinigung dieser Ereignisse stellt den vollständigen Verlauf eines angegebenen Aggregats bereit. Die Wiedergabe aller Ereignisse (d. h. das sequenzielle Anwenden des Status jedes Ereignisses auf eine neue Instanz des Aggregatobjekts) gibt den aktuellen Status des Objekts für Anzeige- oder Verarbeitungszwecke zurück. Selbstverständlich können Sie der Abfrage einen Datumsfilter für Ereignisse hinzufügen und auf diese Weise den Status des Domänenobjekts (der Buchung) bis zu einem angegebenen Datum erneut erstellen. 

Extrapolieren einiger Geschäftsinformationen

Angenommen, Sie sind ein Manager, der für interne Vorgänge verantwortlich ist. In Ihrer Funktion wünschen Sie, dass gemeinsam verwendete Ressourcen wie etwa ein Besprechungsraum effektiv genutzt werden. Wie können Sie das gewährleisten und Buchungsrichtlinien entsprechend aktualisieren? Abbildung 4 stellt nützliche Informationen für die Beantwortung dieser Frage bereit.

Das Kreisdiagramm zeigt, wie viele Buchungen im Zeitintervall erstellt und wie viele davon später verschoben oder storniert wurden. Das Balkendiagramm schlüsselt die gleichen Informationen stattdessen auf einer wöchentlichen Basis auf. Eine erste grobe Analyse scheint zu zeigen, dass fast die Hälfte der Reservierungen irgendwann verschoben wird. Ungefähr eine von vier Reservierungen wird sogar storniert.

Wenn Sie ein Manager sind, der Prozesse optimieren möchte, lassen die Diagramme in Abbildung 4 ggf. die Alarmglocken bei Ihnen schrillen. Wie Sie es auch drehen und wenden: Die Anzahl der Änderungen nach dem Vornehmen von Reservierungen ist erheblich. Verhindert diese Tatsache, dass andere potenzielle Benutzer ihre Räume problemlos buchen können? Zu Ihrer Beruhigung möchten Sie vielleicht die durchschnittliche Belegung jedes Raums im gleichen Zeitintervall überprüfen. Wenn dieses spezielle Abdeckungsdiagramm noch nicht in Ihrem Dashboard vorhanden ist, müssen Entwickler für das Hinzufügen nicht sehr viel Arbeitszeit investieren. Letztlich muss nur ein weiteres Prädikat für eine Abfrage im LINQ-Stil geschrieben werden:

var eventsByWeek = bookingStatuses.GroupBy(b => b.WeekDate).ToList();
foreach (var weekEvents in eventsByWeek)
{
  var wr = new WeekActivityReport
  {
    StartOfWeek = weekEvents.Key,
    TotalBookingCreated = weekEvents.Count(),
    TotalBookingMoved = weekEvents.Count(b => b.Moved),
    TotalBookingDeleted = weekEvents.Count(b => b.Deleted)
  };
  reports.Add(wr);
}

Ein grafischer Bericht der Buchungsaktionen während eines angegebenen Zeitintervalls
Abbildung 4A: Ein grafischer Bericht der Buchungsaktionen während eines angegebenen Zeitintervalls

Die Beispielanwendung ruft alle Ereignisse für alle Buchungen im angegebenen Zeitintervall ab und gruppiert diese nach der Woche. Anschließend erstellt sie ein wöchentliches Aktivitätsberichtobjekt, das dann auf einfache Weise an ChartJS zum Erstellen beeindruckender Grafiken übergeben werden kann.

Der wichtigste Aspekt beim Event Sourcing, den ich in dieser Kolumne herausstellen möchte, ist dieser: Sobald Rohinformationen auf der niedrigsten möglichen Abstraktionsebene gespeichert wurden, können diese auf vielfältige Weise jederzeit genutzt werden. Im Rahmen der Besprechungszimmerdemo können Sie das Managerdashboard in einem Folgerelease bereitstellen oder als weiteres nagelneues Produkt vermarkten. Sie können auch alle Ereignisse während der Lebensdauer einer Anwendung analysieren und darauf aufbauend neue Datenprojektionen für beliebige denkbare Geschäftsziele erstellen. Der wichtigste Aspekt ist jedoch der folgende: Alle nachfolgenden Entwicklungsanstrengungen sind größtenteils unabhängig von den bereits vorhandenen Komponenten, und das Erstellen neuer Teilaspekte wirkt sich nicht auf das aus, was bereits vorhanden ist. Dies ist die ultimative Anlagenrendite für Investitionen in eine Kombination aus CQRS und Event Sourcing.

Zusammenfassung

Das Verarbeiten von Geschäftsereignissen in Software ist keine neue Idee. Sie können möglicherweise ähnliche Ergebnisse mithilfe eines historischen CRUD-Systems (H-CRUD) erzielen: ein ausgefallener Name für eine beliebige handgefertigte Lösung, mit der Sie die verschiedenen Status eines Geschäftsobjekts nachverfolgen können. Event Sourcing übernimmt die gleiche Aufgabe. Die Ausführung erfolgt jedoch auf einer anderen Abstraktionsebene und verwendet leistungsfähigere und maßgeschneiderte Tools (z. B. Ereignisspeicher) und Muster (z. B. Event Sourcing) im Kontext spezialisierter Architekturen (z. B. CQRS).

Ich schreibe in MSDN Magazine bereits seit einiger Zeit über CQRS und Event Sourcing und bin der Meinung, dass bezüglich der Relevanz von CQRS und Ereignissen ein Konsens besteht. Ich finde es aber schwierig, eine Ausgangsposition zu finden. Benutzern, denen es ebenso geht, empfehle ich die erneute Beschäftigung mit H-CRUD. Zu diesem Thema habe ich im Mai (msdn.com/magazine/mt703431) und Juni (msdn.com/magazine/mt707524) einen Artikel verfasst. Außerdem empfehle ich die aktuelleren Artikel zum ECS-Muster (auch als CQRS/ES bezeichnet) und zu MementoFX. Diese Artikel sollten den Start vor einem vertrauten Hintergrund erleichtern. Anschließend können Sie Ihre Kenntnisse so erweitern, dass Sie einen Punkt erreichen, an dem Sie altbekannte Dinge auf neue, effektivere Art angehen können.

In diesem Sinne: Bei Software geht es nicht um Zauberei oder Religion. Software dient zum Erledigen von Aufgaben, und zwar vorzugsweise so, dass das Verfahren für den Kunden und für das Entwicklungsteam funktioniert. In Kombination mit Ereignisspeichern, Bussen, Ereignismustern und Architekturen unterstützt MementoFX Sie beim schnellen und effektiven Erledigen von Aufgaben.


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 @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