User Tools

Site Tools


patterns:inheritancerolestragegyoverview:inheritance

This is an old revision of the document!


Vererbung - Implementierung und Konsequenzen

Implementierung

Authentication BC

  • UserAccount und SystemAccount werden beide von Account abgeleitet,
  • In Account gibt es einen abstrakten Konstruktor der in den Kindklassen implementiert werden muss. Das Systemkonto wird z.B. eine Beschreibung erhalten1), und das Personenkonto eine PersonID,
  • Es gibt das abstrakte Ereignis AccountCreated, welches nur die ID des Kontos beinhaltet. UserAccount wird im Konstruktor das UserAccountCreated:AccountCreated Ereignis melden, welches neben der ID des Kontos die PersonID beinhalten wird. Entsprechendes gilt für SystemAccount oder neue Kontenarten,
  • Auf der Befehlsseite wird es den abstrakten Befehl CreateAccount geben. Für UserAccount wird es CreateUserAccount:CreateAccount geben, entsprechendes gilt für SystemAccount,
  • Im Command Handler wird Service Locator verwendet, um die korrekte Factory für die entsprechenden Implementierungen von CreateAccount zu ermitteln

Authentication Info BC

Der Authentication BC ist dafür da, die Domäne der Authentifizierung abzubilden. Es gibt dazu fast immer noch den Aspekt der Darstellung. Gemeinsame Eigenschaften des Kontos, die mit der Geschäftslogik nichts zu tun haben, und nicht vom Anwender eingegeben werden, sondern ermittelt werden müssen2) erhalten am besten einen eigenen Kontext. Es ist sinnvoll die Eigenschaften explizit zu machen, denn wenn sie implizit bleiben, sind wird schnell wieder bei der dritten Normalform3), etwas wovon wir mit DDD/CQRS/ES weg wollten4).

Es wurde also identifiziert, dass die Bezeichnung des Subjektes des Kontos im Authentication Info BC wichtig ist. Für ein Systemkonto möchte man gerne sehen, was das Konto repräsentiert, z.B. “Konto für die Outlook-Schnittstelle”. Für ein Benutzerkonto möchte man gerne sehen, wen das Konto repräsentiert, z.B. “Max Mustermann”.

Umsetzung

Es gibt das Aggregate AccountInfo im Authentication Info BC. Es gibt den Befehl SetAccountInfo { ID, SubjectDescription }. Es gibt das Ereignis AccountInfoSet { ID, SubjectDescription }.

Für jede Kontoart, die von Account abgeleitet wird, sollte ein Handler gebaut werden, der die AccountInfo aktuell hält. Beim SystemAccount ist das einfach. Ein handler lauscht auf SystemAccountAdded und SystemAccountRenamed und veröffentlicht einfach SetAccountInfo mit der ID des Kontos und der Beschreibung des Systemkontos.


Hinweis: Denormalisierer, die Informationen aus Account und AccountInfo kombinieren, müssen darauf achten, dass sie SystemAccountAdded und AccountInfoSet nicht zwingend in der erwarteten Reihenfolge erhalten. AccountInfoSet kann durchaus vor SystemAccountAdded behandelt werden. In dem Fall kann man entweder warten, bis SystemAccountAdded erhalten wurde, und erst dann das Konto denormalisieren, oder man schreibt schon mal einen Datensatz mit der Konto-ID und der Bezeichnung, alles andere bleibt leer. Die DTOs müssen nur darauf vorbereitet sein, leere Felder zu haben, oder aber die Abfragen liefern nur Konten mit allen Feldern, so dass die DTOs sicher sein können, dass sie immer vollständig gefüllt sind.


Wie fülle ich die Personeninformation beim Benutzerkonto? Das ist etwas schwieriger. Es muss einen Handler für Ereignisse aus dem Personenkontext und dem Authentifizierungskontext geben, und dieser Handler muss Daten cachen5), weil er nicht weiß, in welcher Reihenfolge die Ereignisse kommen. Der Handler muss folgende beiden Tabellen vorhalten

PersonDescription
PersonID Description
Row 1 Col 2 Row 1 Col 3
Row 2 Col 1 some colspan (note the double pipe)
Row 3 Col 1 Row 3 Col 2 Row 3 Col 3
  • Es werden alle gemeinsamen Punkte identifiziert, die relevant sein können, und vorbereitend *explizit* in die Domäne, die Ereignisse, die Befehle und die DTOs eingebaut! Dieser Punkt ist immens wichtig, vor allem die Konsequenzen daraus. Wenn das nicht so gemacht würde, wären Projektionen auf den BC sehr schwierig und wir wären wieder in der dritten Normalform6) und im relationalen Datenbanken Paradigma von dem wir uns ja eigentlich lösen wollten.

Wie wäre es ohne explizites Vorhandensein der gemeinsamen Aspekte

Wen oder was ein Konto repräsentiert ist ein wichtiger Aspekt eines Kontos. Ein Systemkonto repräsentiert ein System, bzw. Dinge, die das Konto tun können soll, z.B. ein “Konto für die Outlook Schnittstelle”. Ein Benutzerkonto repräsentiert eine Person, z.B. “Max Mustermann”. Die Bezeichnung für das Systemkonto steht aber in SystemAccount, die der Person in der Person in einem anderen BC, repräsentiert lediglich durch die PersonID in UserAccount. Somit ist die Bezeichnung des Subjektes des Kontos nur implizit ermittelbar über das Systemkonto oder über das Benutzerkonto, das dann noch die Person dafür benötigt.

Was, wenn ein neuer Kontotyp dazu kommt? Man kann im Vorfeld nicht wissen, wie die Bezeichnung des Subjekts zu ermitteln ist. Man könnte sowas wie Projektionsereignisse definieren, wie z.B. AccountSubjectRenamed(Guid AccountId, string SubjectName), und von den unterschiedlichen Konto Implementierungen verlangen, diese Ereignisse auszulösen. Was aber ist ein Projektionsereignis? Die Domänenereignisse sollen doch alles, was die Domäne betrifft, erfassen. Und was, wenn die Projektionen falsch sind und korrigiert werden müssen? Die Projektionsereignisse einfach neu erzeugen7)? Das wäre nicht richtig, weil dann historische *Ereignisse* verändert würden.

Gemeinsamkeiten explizit darstellen

Gemeinsame Aspekte müssen explizit gemacht werden. Das ist Speicherintensiver, Daten werden in der ersten Normalform8) doppelt gehalten, können zeitweise abweichen, werden aber eventuell schlüssig sein9), usw. Der Vorteil ist aber der, dass das, was danach kommt10) wesentlich einfacher sein wird. Wenn ich weiß, dass es einen Bezeichner des Subjektes im Konto gibt, egal ob es ein Systemkonto, ein Benutzerkonto oder sonst eine Kontoart ist, dann ist das Arbeiten mit dieser Bezeichnung direkt und einfach. Es vollendet auch den BC als solches, denn was ist ein Benutzerkonto mit einer PersonenId von 3F2504E0-4F89-11D3-9A0C-0305E82C3301, wem gehört es? Wenn im Konto steht, dass es “Max Mustermann gehört, der eine Person mit der Identität 3F2504E0-4F89-11D3-9A0C-0305E82C3301 ist”, hat der Kontext alles, was er benötigt.

  • Die Projektionsseite ist nun tatsächlich ein wenig komplexer. Projektionen werden dadurch erzeugt, dass Ereignisse behandelt werden, und aus diesen Ereignissen Sichten erstellt werden, die abgefragt werden. Die Sichten werden dann in Data Transfer Objects11) gewandelt, die dann an andere Systeme12) gesendet werden können. Die anderen Systeme müssen nun mit den DTOs arbeiten können. Die Komplikation liegt darin, dass jede neue Kontoart neue Ereignisse, DTOs und Anforderungen and die Sichten einführt. Das wird am besten hier besprochen:

Konsequenzen

Ich13) bin an der Erstellung von Konten interessiert, will wissen, wann ein Konto erstellt wird. Ich abonniere somit das AccountCreated Ereignis. Da UserAccountCreated und SystemAccountCreated von AccountCreated abgeleitet sind, würden beide von mir behandelt werden. Wenn eine neue Kontoart mit einem neuen Erzeugungsereignis dazu kommt, wird mir das auch automatisch zum Behandeln übergeben.

In diesem Fall sollte ich nicht fest auf die vorhandenen [User/Sytem]AccountCreated reagieren, sondern auf die Basisklasse AccountCreated. Ansonsten würde ich Ereignisse eines neu dazu gekommenen Kontos nicht behandeln, und die Systemerweiterbarkeit ist nicht gegeben. Angenommen, ich möchte eine Liste aller Konten darstellen, mit der Bezeichnung der Kontoart und der Kontobezeichnung. Für Systemkonten ist das “Systemkonto”14) plus die Bezeichnung des Kontos, z.B. “Outlook-Schnittstellenkonto”, für Personenkonten “Benutzerkonto” samt der Personenbezeichnung. Das ist schon nicht ganz so einfach, weil die Kontoart in die Sprache des Anwenders übersetzt werden sollte, und weil UserAccount nur die PersonID hält, nicht aber die Personenbezeichnung kennt. Und was, wenn neue Kontenarten dazu kommen?

Wir haben somit identifiziert, welche Gemeinsamkeiten die unterschiedlichen Kontoarten besitzen sollen. Die Bezeichnung der Kontoart plus die Bezeichnung des Kontoinhabers. Welche Ereignisse führen nun dazu, dass sich die Bezeichnung des Kontoinhabers ändert? SystemAccountDescriptionChanged wäre eins. Was ist mit der Personenbezeichnung? Die Person eines UserAccounts ändert sich nie, aber die Bezeichnung kann sich ändern, entweder weil sie korrigiert wurde, oder weil die Person ihren Namen geändert hat15). Muss ich also wissen, dass ich auch auf PersonNameChanged Ereignisse lauschen muss? Das ginge ja noch, aber bei neuen Konten wäre das nicht mehr möglich.

Push

Zusammenfassende Ereignisse für Projektionen von abstrakten Klassen. Als erstes muss identifiziert werden, welche Gemeinsamkeiten für abstrakte Klassen existieren. Dann werden Ereignisse eingeführt, die Änderungen an diesen Gemeinsamkeiten melden. Diese Ereignisse werden ausgelöst durch

  • andere Ereignisse. Z.B. wird ein handler von PersonNameChanged eingebaut, der den neuen Namen meldet. Dieser handler behandelt ebenfalls UserAccountCreated, um zu wissen, zu welchem Benutzerkonto die Person gehört. Beim behandeln von PersonNameChanged wird also der neue Personenname gespeichert, das Konto16) der Person werden ermittelt, und für jedes wird ein AccountDescriptionChanged(Guid AccountID, string Description) Ereignis ausgelöst. Keine Ahnung wie dieses Muster heißt, es ist aber einfach zu verstehen und genauso einfach zu implementieren,
  • regelmäßige Abfragen. Wenn Namensänderungen z.B. nicht über Ereignisse gemeldet werden, würde man die Personenbezeichnungen regelmäßig abfragen. Wenn sich eine ändert, wird wie im ersten Fall dafür ein AccountDescriptionChanged Ereignis ausgelöst.

Es können beliebig tiefe Ereignisketten gebildet werden. AccountDescriptionChanged könnte z.B. von einem weitern handler in ein weiteres Ereignis gewandelt werden, usw.

Das Push-Problem

Das Problem bei solche einer Push Lösung ist die, dass man es nur per Konvention von der Implementierung einer weiteren Account Klasse verlangen kann. Das ist aber immer noch besser als die Notwendigkeit, vorhandene Klassen für einen neue Account Klasse ändern zu müssen. Es geht ja darum, ein System erweitern zu können ohne vorhandene Systeme zu (zer)stören. Und genau das wäre damit erreicht. Zudem kann man die Konvention per Pull ein wenig formalisieren.

Pull

Account hat eine abstrakte Methode GetSubjectDescription(). Die muss von der geerbten Kindklasse implementiert werden; wie, ist der Kindklasse überlassen. SystemAccount würde einfach den Wert des Feldes Description raus geben. UserAccount würde entweder eine Projektion von der Person fragen, oder die Person direkt befragen, wenn sie im selben Prozess laufen17):

public class UserAccount: Account
{
    ...
    private IPersonQuery PersonQuery { get; set; }
    public Guid PersonId { get; private set; }
    ...
    public UserAccount(IPersonQuery personQuery)
    {
        PersonQuery = personQuery;
    }
    ...
    public string GetDescription()
    {
        return personQuery.Get(PersonId).GetDescription();
    }
    ...   
}

IPersonQuery kann ein Repository<Person> dahinter liegen haben, eine Abfrage, etc.

Das Pull-Problem

Immer wenn die Kontobezeichnung benötigt wird, kann das Konto direkt befragt werden. Das Problem besteht darin, wann danach gefragt werden soll. Vor jeder Abfrage auf Daten, die die Kontobezeichnung benötigen? Wenn es 1.000 Konten gibt, und man eine Abfrage auf sie startet, müßten dann erst 1.000 Pullaktionen durchgeführt werden? Sinnvoller wäre ein regelmäßiges Abfragen, oder eine Push-Pull Kombilösung.

Push-Pull Kombilösung mit Event Enrichment - Beispiel UserAccount

  1. Es gibt einen Denormalisierer im Personen Kontext, der die Personenbezeichnung aktualisiert und dann eine PersonNameChanged(Guid id, string newName) Nachricht18). Darüber wird dann ein AccountSubjectDescriptionChanged Ereignis Es gibt im UserAccount Kontext einen Handler, der eine SAGA startet sobald ein UserAccount erzeugt wurde. Die Saga lauscht auf AccountSubjectDescriptionChanged und auf PersonNameChanged. Derselbe Handler lädt dann aus dem Abfragedienst aus Punkt 2. die Personenbezeichung und meldet UserAccount Es gibt im UserAccount Kontext einen Denormalisierer, der auf UserAccountCreated Ereignisse lauscht, sowie auf die PersonNameChanged Ereignisse aus 1. Es muss einen Denormalisierer geben, der auf Ereignisse die Personenbezeichnung betreffend wartet, sowie auf Ereignisse, die ein UserAccount anlegen. Wird ein UserAccount angelegt, wird die PersonID gespeichert, damit der Denormalisierer weiß, dass die Bezeichnung dieser Person benötigt wird. Dann wird geguckt, ob die Personenbezeichnung bereits vorhanden ist. Wenn ja, wird gleich hinterher ein AccountSubjectRenamed mit der Bezeichnung hinterher geschickt.

    Der Handler für UserAccountCreated kann jetzt ==== Service Locator ==== Die Kontoartbezeichnung sollte einen Default haben, z.B. in Deutsch. Jedes DTO für Account wird zudem den Typ beinhalten, also z.B. PROM.Authentication.UserAccount. Anhand dieses Typs kann nun überall über Service Locator entsprechend mit dem Konto umgegangen werden. Das UI kann zum Bearbeiten den entsprechenden Controller laden, ebenso kann ein Übersetzer gefunden werden, der aus der Typenbezeichnung eine Bezeichnung in der Sprache des angemeldeten Users macht.
1) z.B. “Outlook-Schnittstellenkonto”
2) eventuell aus anderen BCs
3) , 6) 3NF
4) Man müsste Eigenschaften referenzieren, eventuell sogar aus anderen BCs
5) in einer Datenbank
7) damit ist gemeint, dass ein Adapter aus einem PersonRenamed Ereignis AccountSubjectRenamed Ereignisse für alle Konten erzeugt und veröffentlicht, die zu dieser Person gehören
8) 1NF
9) Eventual Consistency
10) Zusammenarbeit mit anderen Objekten und Kontexten, und vor allem die Systemwartbarkeit
11) DTO
12) vorwiegend an das UI
13) ein Event Handler
14) wenn die Liste in Deutsch dargestellt werden soll
15) z.B. durch Heirat
16) bzw. die Konten
17) In dem Beisiel ist UserAccount das Query-Objekt, was entweder gemeinsam mit dem Command-Objekt existiert, oder getrennt, wenn CQRS umgesetzt wird
18) kein Ereignis, und auch kein Befehl. Es ist lediglich eine ….AHHHHHHHHHHHHHHHHHHHH das sind ja gar keine Ereignisse. Und was wenn doch? AccountSubjectDescriptionChanged oder PersonNameChanged als Ereignisse einführen? Übel!!! sendet. Die Reihenfolge ist hierbei äußerst wichtig,
  1. Es gibt einen Dienst im Personen Kontext, der nach der Personenbezeichnung für eine PersonID gefragt werden kann. Dieser Dienst bezieht sich auf die Denormalisierung aus 1.
  2. Es gibt im UserAccount Kontext einen Handler, der auf PersonNameChanged Ereignisse lauscht, sowie auf UserAccountCreated. Wenn UserAccountCreated behandelt wird, zieht sich der Handler die Personenbezeichnung aus dem Dienst aus Punkt 2((es kann sein, dass die Person in der Projektion noch gar nicht existiert, also muss auch ein null-Bezeichner möglihch sein
patterns/inheritancerolestragegyoverview/inheritance.1357654459.txt.gz · Last modified: 2013/01/08 15:14 by rtavassoli