Ein kurzer Hinweis vorab: Den Inhalt des Blogeintrags habe ich auch in einem Webcast zusammengefasst. Das Video gibt es auf YouTube. Den Code findet ihr hier.
In meinem letzten Blogeintrag habe ich das Strategy Pattern in der Praxis gezeigt. Primär gab es noch einen Smell, welchen ich erwähnt hatte: Das Open Closed Principle ist damit noch nicht 100%ig eingehalten und an einer Stelle heble ich mein IoC Konzept aus. Das passiert genau hier:
1: public StateHandler(IClock clock, IBuildDefaultProposalsValues defaultValues)
2: {
3: Handler = new Dictionary<SelloutStates, ICreateOrderProposal>
4: {
5: {SelloutStates.Oversupply, null},
6: {SelloutStates.Predicted, new PredictedGap(defaultValues)},
7: {SelloutStates.Urgent, new UrgentGap(defaultValues, clock)},
8: {SelloutStates.AppointmentWarnings, new AppointmentWarning(defaultValues, clock)},
9: {SelloutStates.Unknown, null},
10: {SelloutStates.Verify, new NoForecastPossible(defaultValues)},
11: {SelloutStates.Ok, new UncomplicatedAppointment(defaultValues, clock)}
12: };
13: }
Zum einen müsste ich die Klasse immer anpassen, wenn ich einen weiteren Status aufnehmen möchte. Zum anderen erzeuge ich mir Instanzen von Klassen, welche ich nicht über den Container erhalten habe, sprich ich hole mir die Abhängigkeiten selbst, statt mir diese gemäß Inversion of Control Principle zuweisen zu lassen. Das wäre zwar möglich gewesen, allerdings würde der Konstruktor dadurch erheblich aufblähen, weil ich pro Status eine Implementierung reinreichen müssten. Dieses Problem könnte ich über Service Location lösen, d.h. ich lasse mir lediglich den Container reinreichen und mache die Auflösung intern. Für mich persönlich auch keine schöne Lösung (Hinweis: Dafür müsste man den Container in sich selbst registrieren…).
Ob nun saubere Dependency Injection mit aufgeblähtem Konstruktor oder Service Location, in jedem Fall bliebe das Problem, dass ich jedes Mal im Container eine weitere Registrierung für einen neuen Status durchführen müsste.
Daraufhin kamen Rückfragen, wie dies am besten zu lösen sei. Das soll Thema dieses Blogeintrags und des dazu passenden Webcasts sein, v.a. weil im Netz auch nur selten saubere Implementierungen anzutreffen sind, die alle Prinzipien einhalten.
Als erstes wird der obere Code gänzlich entfernt, da dieser nicht mehr benötigt wird. Hinter dem Code steckte eigentlich das Factory Pattern. Ich lasse mir durch die Factory (abhängig vom Status) das entsprechende Objekt erzeugen und arbeite dann mit diesem weiter. Castle.Windsor als IoC Container bietet uns hierfür sogenannte interface-based factories an.
Anmerkung: Der folgende Code basiert nicht mehr auf dem bisherigen, welchen ich im vorherigen Blogeintrag verwendet habe. Im Webcast ist dazu alles erläutert und ich habe das Projekt unter Github zur Verfügung gestellt.
1: yield return Component
2: .For<IchVermittleVorschlagsrechner>()
3: .AsFactory(c => c.SelectedWith(new VorschlagsrechnerVermittler()));
Wie man sieht, muss man lediglich das Factory-Interface, welches man zuvor selbst ausimplementiert hatte, angeben und im Anschluss ‘AsFactory’ anhängen. Als Parameter übergebe ich den Verweis auf eine Logik, welche die Factory später intern verwendet, um mir das korrekte Objekt (den korrekten Service) zurückzuliefern. Das Bedeutet, dass “new VorschlagsrechnerVermittler” der Factory, wenn auf ihr ein Aufruf stattfindet, sagen muss, was sie zurückgeben soll. Es folgt die Implementierung:
1: public class VorschlagsrechnerVermittler : DefaultTypedFactoryComponentSelector
2: {
3: protected override string GetComponentName(MethodInfo method, object[] arguments)
4: {
5: if (method.Name == "Für" && arguments.Length == 1 && arguments[0] is Bestandsstatus)
6: {
7: var status = arguments[0].ToString();
8: //von mir frei gewählte Konvention wie die Komponente im Container heißt
9: return string.Format("BerechnerFürArtikelstatus{0}", status);
10: }
11: return base.GetComponentName(method, arguments);
12: }
13: }
Dieser Code macht nun genau das, was ich vorher explizit im Dictionary definiert habe, allerdings anhand einer Konvention. Die Methode in Zeile 3 liefert als String den Namen der Komponente zurück basieren auf dem Namen des Status, also z.B. ergibt sich für den Status “Ok” der Komponentenname “BerechnerFürArtikelstatusOk”.
Nun fehlt lediglich noch eine kleine Anpassung für den IoC Container. Dieser weiß bisher nichts von einer Komponente “BerechnerFürArtikelstatusOk”. Eine Möglichkeit wäre, dass ich – wie bereits eingangs erwähnt – für jeden Status die Ausprägung im Container registriere, also in diesem Fall beispielsweise so:
1: yield return Component.For<IchErrechneArtikelZukaufsvorschlag>()
2: .ImplementedBy<Ok>()
3: .Named("BerechnerFürArtikelstatusOk")
4: .LifestyleTransient();
Das widerspricht immer noch dem OCP, sodass ich es wie folgt löse:
1: yield return AllTypes.FromThisAssembly()
2: .BasedOn<IchErrechneArtikelZukaufsvorschlag>()
3: .Configure(a => a.Named(string.Format("BerechnerFürArtikelstatus{0}",
4: a.Implementation.UnderlyingSystemType.Name)))
5: .Configure(a => a.LifestyleTransient());
Zunächst hole ich mir alle Klassen aus der aktuellen Assembly, welche auf “IchErrechneArtikelZukaufsvorschlag” basieren, also alle meinen konkreten Status-Implementierungen. Im Anschluss sage ich noch, welchen Name diese Komponenten im Container bekommen sollen. Hier steht die selbe Konvention wie in dem Code Sample zuvor. Der Vollständigkeit halber sei erwähnt, dass ich hier gegen das Don’t Repeat Yourself Principle verstoße. Dies soll lediglich der Vereinfachung im Beispiel dienen.
Ab jetzt muss ich, wenn ein weiterer Status hinzu kommt, lediglich eine Implementierung für diesen machen. Bestehender Code muss nicht mehr angefasst werden.
Gefällt mir:
Like Wird geladen …