This is an old revision of the document!
Strategien sind mächtige und sinnvolle Werkzeuge für Verhalten, das austauschbar ist. Zudem kann das Verhalten von allen abgeleiteten Klassen geerbt werden1). Ein tolles Beispiel hierfür ist die Authentifizierung eines Kontos. Es gibt diverse Möglichkeiten2) wie ein Konto authentifiziert werden kann. Beispiele sind:
Wenn die Authentifizierung als Strategie umgesetzt wird, kann man sich erst mal für eine entscheiden. Die anderen können dann bei Bedarf dazu gebaut werden, und in einigen Fällen werden neue Strategien dazu kommen, die zum jetzigen Zeitpunkt noch gar nicht bekannt waren.
Eine Strategie wird vorbereitend in ein Aggregate eingebaut3). Sie wird abstrakt definiert. Sie kann beim Erzeugen des Aggregates gesetzt werden und kann Implementationsabhängig danach verändert werden oder nicht. Wie das Ganze in CQRS/ES funktioniert, kann am besten an einem Beispiel erläutert werden.
Da Strategien austauschbar und erweiterbar sind, muss das Aggregate wissen, wie es aus den gespeicherten Daten eine Strategie laden kann. Dafür gibt es eine ISelectAuthenticationStrategy Schnittstelle
public interface ISelectAuthenticationStrategy { AuthenticationStrategy Load(AuthenticationStrategyDTO); }
Im Konstruktor des Aggregates wird dieses Interface übergeben
public abstract Account: Aggregate { private ISelectAuthenticationStrategy AuthStrategySelector; public Account(ISelectAuthenticationStrategy authStrategySelector) { AuthStrategySelector = authStrategySelector; } public Account(Guid id, ISelectAuthenticationStrategy authStrategySelector) { AuthStrategySelector = authStrategySelector; ... } }
Das Aggregate4) wird entweder vom Command Handler neu erzeugt, oder vom Repository wenn es erneut geladen wird. Der Command Handler wird ein neuen UserAccount z.B. wie folgt erzeugen5)
public void Execute(AddUserAccount message, SecurityPrincipal principal) { var authSelector = ContextRegistry.GetContext().GetObject("AccountAuthenticationStrategySelector") as ISelectAuthenticationStrategy; var authStrategyConverter = ContextRegistry.GetContext().GetObject("AccountAuthenticationStrategyConverter") as IConvertAuthenticationStrategy; var UserAccount = new UserAccount(message.AggregateId, message.PersonId, authStrategyConverter.convert(message.AuthenticationStrategyDTO, authSelector)); _repository.Save(UserAccount);
IConvertAuthenticationStrategy kann dieselbe Implementierung wie ISelectAuthenticationStrategy verwenden, falls das AuthenticationStrategyDTO dieselbe Klasse ist wie das AuthenticationStrategyDTO das im Domain Event zum Speichern des Ereignisses verwendet wird6).
Wenn das Konto nicht vom Command Handler neu erstellt wird, sondern vom Repository erneut geladen wird, muss die Schnittstelle ISelectAuthenticationStrategy ebenfalls an das Aggregate übergeben werden. Das Repository kann beim Erzeugen eines Accounts nun ebenfalls über Service Locator das korrekte ISelectAuthenticationStrategy Interface instanziieren und damit den UserAccount leer erzeugen, bevor die Ereignisse angewendet werden. Die Implementierung von ISelectAuthenticationStrategy wird in aller Regel einen ServiceLocator verwenden, dazu gleich mehr.
Es wird die abstrakte Klasse AuthenticationStrategyDTO geben7), mit bestimmten Implementierungen8)
[DataContract] public abstract AuthenticationStrategyDTO { } [DataContract] public UsernameAuthenticationStrategyDTO: AuthenticationStrategyDTO { [DataMember] public string UserName { get; private set; } [DataMember] public string Password { get; private set; } public UsernameAuthenticationStrategyDTO(string userName, string password) { UserName = userName; Password = password; } }
Ebenso wird es die tatsächlichen Strategien geben9)
public abstract AuthenticationStrategy { } public UsernameAuthenticationStrategy: AuthenticationStrategy { public Account AggregateRoot { get; set; } public string UserName { get; private set; } public string Password { get; private set; } public UsernameAuthenticationStrategy(string userName, string password) { UserName = userName; Password = password; } public UsernameAuthenticationStrategyDTO Serialize() { return new UsernameAuthenticationStrategyDTO(UserName, Password); } // hier die tatsächliche Funktionalität }
Wenn der Konstruktor10) von UserAccount über den CommandHandler aufgerufen wird, wird nach erfolgreichen Prüfungen u.a. das Setzen der Strategie vorgenommen
public abstract UserAccount: Account { public UserAccount(ISelectAuthenticationStrategy authStrategySelector) : base(authStrategySelector) { } public UserAccount(Guid id, AuthenticationStrategy authStrategy, ISelectAuthenticationStrategy authStrategySelector) : base(authStrategySelector) { // Prüfungen ApplyChange(new AuthenticationStrategySet(authStrategy.Serialize()); // es wird also das DTO in das Ereignis gepackt } }
Das Ereignis AuthenticationStrategySet muss nun angewendet werden. Das passiert in der Basisklasse Account
public class Account: AggregateRoot { private AuthenticationStrategy authenticationStrategy; // man braucht in der Domäne ja nur Eigenschaften die die Business Logik betreffen. In diesem Fall muss das Konto Befehle an die Strategie weiterleiten, d.h. sie muss zwingend gehalten werden .... private void Apply(AuthenticationStrategySet e) { authenticationStrategy = ISelectAuthenticationStrategy.Load(e.AuthenticationStrategyDTO); authenticationStrategy.AggregateRoot = this; // wichtig für das Auslösen der Ereignisse aus der Strategie heraus } }
Da Apply auch vom Repository aufgerufen wird wenn das Konto erneut geladen wird, haben wir jetzt einen relativ einfachen Weg gefunden, um Strategien zu setzen und das Ergebnis zu speichern. Zudem kann der Strategie in ISelectAuthenticationStrategy.Load() alles Mögliche übergeben werden, was dem Konto unbekannt ist. Im Falle einer Kerberos Strategie z.B. eine Verschlüsselungsschnittstelle für die Tickets. Die Veschlüsselung kann zwar Teil der Kerberos Strategie sein, da das Konto aber nach erfolgreicher Ticket Erstellung nicht erneut verwendet wird solange das Ticket gültig ist, müssen sich die Strategie und der Ticket Authentifizierungsdienst eine Verschlüsselung teilen. Diese soll also nicht fest in der Domäne verankert sein, sondern austauschbar an die Domäne und an den Dienst übergeben werden.
Strategien werden in der Literatur immer sehr einfach Beispielhaft dargestellt. Sie haben alle dieselben Methoden mit denselben11) Parametern, dafür aber unterschiedliche Ergebnisse. Zudem haben sie keinen eigenen Zustand. Dazu einige Punkte, die im einzelnen besprochen werden
Eine Strategie kann zur Durchführung ihrer Aufgabe weitere Informationen benötigen. Eine Strategie zur Authentifizierung per Benutzername und Kennwort muss wissen, gegen welches Kennwort authentifiziert werden muss. Sie muss also u.a. eine Methode haben, um das Kennwort zu ändern. Eine andere Strategie, z.B. für die Windows Authentifizierung, hält kein Kennwort für das Konto.
Um das umsetzen zu können, muss eine Strategie für die eigenen Methoden eigene Command Handler haben. Diese sind genauso umgesetzt wie die Command Handler des Aggregates, nur dass sie Methoden in der Strategie aufrufen und nicht im Aggregate. Dafür muss das Konto die Strategie freigeben12). Es heißt zwar, dass jede Änderung durch das Aggregate Root laufen müssen, wenn ich aber eine Methode auf einer Entität aufrufe, die mir vom Aggregate Root gegeben wurde, dann geschieht die Änderung rein logisch auch durch das Aggregate Root.
Der Command Handler für ChangeUserAccountPassword würde dann wie folgt aussehen
public class AccountUsernameAuthorizationHandler: RequestHandler, Executes<ChangeUserAccountPassword> { private readonly Repository<Account> _repository; public AccountUsernameAuthorizationHandler(Repository<Account> repository) { _repository = repository; } public void Execute(ChangeUserAccountPasswordmessage, SecurityPrincipal principal) { var accnt = _repository.GetById(message.AggregateId, message.RequiredVersion); (accnt.AuthenticationStrategy as UsernameAuthenticationStrategy).ChangePassword(message.Password); _repository.Save(accnt); } }
Das fängt alles danach auszusehen, als ob UsernameAuthenticationStrategy vielleicht doch eher ein eigenes Aggregate wäre. Man gibt dem Account ein ISelectAuthenticationStrategy mit, was im Grunde ein Repository für eine Strategie ist; die Strategie hat eigene Befehle und Handler für diese; und wie wir sehen werden, auch eigene Ereignisse. Könnte man nicht einfach eine Strategie als eigenes Aggregate definieren und dann im Konto eine Strategie setzen indem die ID der Strategie dort gesetzt wird?
Die Antwort ist ja, könnte man. Die Id der Strategie verhält sich dann wie ein Funktionszeiger, was eine Strategie ja im Grunde ist. Es wäre sicher gestellt, dass das Konto immer genau eine Strategie hat. Man müsste noch sicherstellen, dass keine zwei Konten ein und dieselbe Strategie verwenden13).
Getrennte Aggregate wären in diesem Fall aber irgendwie merkwürdig, unnatürlich. Bei einer Rolle ist das nicht so sehr der Fall. Die Rolle Mitarbeiter würde auch ohne eine Person Sinn haben, und alles was man mit dem Mitarbeiter macht passiert unabhängig von der Person14). Man kann den Mitarbeiter eine Personalnummer geben, einem Projekt zuordnen, ein Arbeitszeitmodell geben, usw.
Eine UsernameAuthenticationStrategy ohne dazugehöriges Konto kann es aber nicht geben. Wenn ich mich gegen die Strategie mit meinem Benutzernamen und Kennwort authentifiziere, und es kein Konto dazu gibt, oder das Konto die Strategie gar nicht (mehr) verwendet, habe ich ins Leere authentifiziert. Zudem kann das Konto inaktiv sein, was Teil der Authentifizierung ist. D.h. dass das Verhalten der Strategie das Konto benötigt, bzw. verwendet.
Dann fällt mir der Fall ein, in dem man die Strategie für ein Konto ändert, z.B. von Username zu Windows. Bei einem anderen Konto macht man das anders herum. Die Ereignisse müssten so behandelt werden, dass die ersetzten Strategien abgeschaltet werden, ansonsten blockiert die Username Strategie des ersten Kontos eventuell einen Benutzernamen, der nun vom zweiten Konto verwendet werden soll.
Zusammengefasst, ist das alles machbar. Man kann die Strategie als eigenes Aggregate umsetzen, was vielleicht einiges vereinfacht. Andererseits ist es ein interessantes Gedankenspiel, das Strategie Muster in CQRS mit ES einzubauen, weil es vielleicht irgendwann Situationen gibt, in denen es nicht anders geht. Also weiter mit dem Beispiel.
Wenn die Methode ChangePassword in der UsernameAuthenticationStrategy eines Kontos aufgerufen wird, wird die Methode nach erfolgreichen Prüfungen ein UsernameAuthenticationStrategyPasswordChanged Ereignis erstellen. Das Ereignis gehört zu dem Konto, es muss also die Id des Kontos beinhalten, und der AggregateRoot15) bekannt gemacht werden, damit es in die Liste der Ereignisse mit aufgenommen werden kann. Das passiert wie oben aufgezeigt in Apply(AuthenticationStrategySet e) von Account. Nun muss die Strategie die Ereignisse nur noch über die gesetzte AggregateRoot veröffentlichen. Dazu ist die Methode ApplyChange(DomainEvent @event) des AggregateRoots öffentlich16).
public class UsernameAuthenticationStrategy: AuthenticationStrategy { .... public void ChangePassword(string password) { AggregateRoot.ApplyChange(new UsernamePasswordChanged(password)); } }
Damit ist es fast getan. ApplyChange im Konto wird per Reflektion über die Parametertypen gefunden und aufgerufen. Was soll das Konto aber mit UsernamePasswordChanged anfangen? Es wird dafür keine entsprechende Apply Methode geben, weil das Konto ja gar nichts von allen möglichen Strategie Ereignissen weiß. Die Lösung ist die, dass alle Strategie Ereignisse von dem abstrakten AccountAuthenticationStrategyEvent abgeleitet werden.
[DataContract] public abstract class AccountAuthenticationStrategyEvent: DomainEvent { } [DataContract] public class UsernamePasswordChanged : AccountAuthenticationStrategyEvent { public UsernamePasswordChanged (string password) { Password= password; } [DataMember] public string Password{ get; private set; } }
In Account werden dann alle AccountAuthenticationStrategyEvents reflektiv an die vorhandene Strategie angewandt.
public class Account: AggregateRoot { .... private void Apply(AccountAuthenticationStrategyEvent e) { Apply(authenticationStrategy, e); // die authenticationStrategy muss es geben, und sie sollte das Ereignis auch bearbeiten können, weil sie es ja ausgelöst hat! } ... }
Die Apply-Methode, die ein Objekt und ein Ereignis als Parameter hat, wendet das Ereignis reflektiv im Objekt an, genau wie das im AggregateRoot gemacht wird. So können Ereignisse mehrere Ebenen durchlaufen, sollte es mehrere Ebenen von erweiterbaren Entitäten im Aggregate geben.
Das war's! Zugegeben, dadurch, dass man das Aggregate auf diese Art und Weise öffnet, öffnet man die Möglichkeit, dass die Strategien Ereignisse des Aggregates anwenden, die verboten sind. Das kann dadurch verhindert werden, dass IApplyChanges.ApplyChange im AggregateRoot, bzw. in der Entität17) verhindert, dass Ereignisse angewandt werden, die das AggregateRoot selbst auslöst. In dem Fall müssten die Entitäten durch Methoden des Aggregates gehen. Also alles in allem sehr sicher, stärker gekoppelt als getrennte Aggregates, und dennoch einfach anzuwenden, sobald das Framework dafür gebaut ist.
new AddUserAccount(new Guid("3F2504E0-4F89-11D3-9A0C-0305E82C3301"), PersonId, new UserNameStrategyDTO("abc", "Kennwort789"));
ApplyEvent(new UserAccountAdded(Id, PersonId, strategy));
new AuthenticationStrategy(@event.UserName, Decrypt(@event.Password));
also kein DTO das22) in Befehle und Ereignisse geschrieben wird, sondern ein Strategie Objekt mit einem entsprechenden Verhalten.
Was die Projektions-Seite angeht, läuft das ähnlich wie bei der Vererbung. Wenn man für die AuthenticationStrategy gemeinsame Eigenschaften identifizieren kann, kann man diese über einen weiteren Info23)-Kontext implementieren, und für jede Implementierung der Authentifizierungs Strategie müsste dieser Kontext befüllt werden.
Wie kann eine Authentifizierungs Strategie verwendet werden? Authentifiziert werden immer die credentials24). Es muss also die abstrakte Klasse Credentials geben. Das UI erzeugt die entsprechenden Credentials, und sendet dann einen AuthenticateAccountCredentials Befehl an die Domäne25), z.B.
authenticationProxy.Apply(new AuthenticateAccountCredentials(accountId, textBoxPassword.Text));
Der Command Handler macht dann folgendes
public void Handle(AuthenticateAccountCredentials command) { var repo = new Repository<Account>(); var agg = repo.Get(command.Id); agg.Authenticator.Authenticate(command.Credentials); repo.Save(agg); }
In diesem Fall ist somit nicht nur die Strategie austauschbar, sondern auch die Information, die jede Strategie zur Prüfung benötigt. Möglicherweise sind die Anmeldeprozesse so unterschiedlich26), dass der Command Handler abhängig des Typs der Credentials noch über Service Locator einen weiteren Dienst aufruft, der weiß, wie die Authentifizierung im Einzelnen ablaufen muss.
Die Kerberos Authentifizierung läuft in mehreren Schritten ab. Das sollte aber wie folgt möglich sein: