Strategy Pattern in der Praxis

Heute gibt es von mir einen kleinen Bericht aus der Praxis. Ich habe zunächst folgenden Code geschrieben, welchen ich nun, nachdem die Logik fertig ist, überarbeite.

Ausgangssituation: Anhand diverser Eigenschaften eines Artikels wird dessen Status ermittelt. Abhängig vom Status soll eine Aktion ausgelöst werden. Im vorliegenden Fall ist dies so gelöst, dass eine Switch-Anweisung anhand des Status in den entsprechenden Zweig springt (vgl. ab Zeile 3). In dem jeweiligen Zweig wird eine dedizierte Methode innerhalb der Klasse aufgerufen.

   1: var state = Decide(entry, folder);

   2:  

   3: switch (state)

   4: {

   5:     case SelloutStates.Urgent:

   6:         CreateUrgentGap(result, entry, folder);

   7:         break;

   8:  

   9:     case SelloutStates.Predicted:

  10:         CreatePredictedGap(result,entry,folder);

  11:         break;

  12:  

  13:     case SelloutStates.Verify:

  14:         ForecastNotPossible(result, entry, folder);

  15:         break;

  16:     

  17:     case SelloutStates.AppointmentWarnings:

  18:         CreateAppointmentWarning(result, entry, folder);

  19:         break;

  20:  

  21:     case SelloutStates.Ok:

  22:         CreateUncomplicatedAppointment(result, entry, folder);

  23:         break;

  24:  

  25:     case SelloutStates.Unknown:

  26:         throw new ApplicationException(string.Format("Unbekannter Status für Artikel '{0}'", entry.Article.NumberFormatted));

  27:  

  28:     default:

  29:         throw new ApplicationException(string.Format("Keine Logik zur Verarbeitung des Status für Artikel '{0}'", entry.Article.NumberFormatted));

  30: }

  31:  

  32: SelloutStates Decide(OrderProposalArticleDataEntry entry, OrderProposalFolder folder)

  33: {

  34:     if (folder == null)

  35:         throw new ApplicationException(String.Format("Der Artikel '{0}' war keiner Mappe zugeordnet", entry.Article.NumberFormatted));

  36:  

  37:     if (!entry.AverageUsage.HasValue || !entry.SelloutDate.HasValue)

  38:         return SelloutStates.Verify;

  39:  

  40:     if (!entry.NextIntakeDateAfterSellout.HasValue)

  41:         throw new ApplicationException(String.Format("Bei Artikel '{0}' war kein nächster Wareineingang gesetzt, obwohl ein Leerverkaufsdatum existiert",

  42:             entry.Article.NumberFormatted));

  43:  

  44:     if (entry.SelloutDate < _clock.Now)

  45:         return SelloutStates.Urgent;

  46:  

  47:  

  48:  

  49:     if (entry.SelloutDate < entry.NextIntakeDateAfterSellout)

  50:     {

  51:         var nextPossibleIntake = _clock.Now.AddDays(folder.HandlingTimeInDays).AddDays(folder.DeliveryPeriodInDays);

  52:         var orderInTimeIsPossible = entry.SelloutDate.Value.TimeSpanIgnoringTimeOfDay(nextPossibleIntake).Days >= 0;

  53:  

  54:         return orderInTimeIsPossible ? SelloutStates.AppointmentWarnings : SelloutStates.Predicted;

  55:     }

  56:  

  57:     return SelloutStates.Ok;

  58: }

Ganz offensichtlich wird hier gegen das Open-Closed-Principle und Single-Responsibility-Principle verstoßen, denn wenn ich nun einen weiteren Status aufnehmen möchte, müsste ich das Switch-Statement erweitern und gleichzeitig noch eine weitere Methode einfügen. Übersichtlich sieht das Ganze auch nicht aus.

Deshalb habe ich ein Interface erstellt:

   1: public interface ICreateOrderProposal

   2: {

   3:     void CreateWith(OrderProposals result, OrderProposalArticleDataEntry entry, OrderProposalFolder folder);

   4: }

Für jeden Status habe ich sodann eine Klasse erzeugt, welche das Interface implementiert. Hier ein Beispiel:

   1: public class NoForecastPossible : ICreateOrderProposal

   2: {

   3:     readonly IBuildDefaultProposalsValues _defaultValues;

   4:  

   5:     public NoForecastPossible(IBuildDefaultProposalsValues defaultValues)

   6:     {

   7:         _defaultValues = defaultValues;

   8:     }

   9:  

  10:     public void CreateWith(OrderProposals result, OrderProposalArticleDataEntry entry, OrderProposalFolder folder)

  11:     {

  12:         var appointment = _defaultValues.ForAppointment(entry, folder);

  13:  

  14:         appointment.CalculatedOrderInDays = null;

  15:         appointment.Status = SelloutStates.Verify;

  16:  

  17:         result.Appointments.Add(appointment);

  18:     }

  19: }

 

Zu guter Letzt habe ich mir noch eine Implementierung geschrieben, die in einem Dictionary die Zuordnung von State zu entsprechender Programmlogik hält (ebenfalls als Interface abstrahiert!):

   1: public class StateHandler : IAllocateHandlerToState

   2: {

   3:     private Dictionary<SelloutStates, ICreateOrderProposal> Handler { get; set; }

   4:  

   5:  

   6:     public StateHandler(IClock clock, IBuildDefaultProposalsValues defaultValues)

   7:     {

   8:         Handler = new Dictionary<SelloutStates, ICreateOrderProposal>

   9:                   {

  10:                       {SelloutStates.Oversupply, null},

  11:                       {SelloutStates.Predicted, new PredictedGap(defaultValues)},

  12:                       {SelloutStates.Urgent, new UrgentGap(defaultValues)},

  13:                       {SelloutStates.AppointmentWarnings, new AppointmentWarning(defaultValues, clock)},

  14:                       {SelloutStates.Unknown, null},

  15:                       {SelloutStates.Verify, new NoForecastPossible(defaultValues)},

  16:                       {SelloutStates.Ok, new UncomplicatedAppointment(defaultValues, clock)}

  17:                   };

  18:     }

  19:  

  20:     

  21:     public ICreateOrderProposal Handle(SelloutStates state)

  22:     {

  23:         return Handler[state];

  24:     }

  25: }

Anmerkung: Wie ihr seht verwende ich eine IClock. Das liegt daran, dass ich inzwischen überall im Code statt DateTime.Now immer auf eine Implementierung von IClock zugreife, um Unit Tests zu ermöglichen, die Datumsangaben prüfen.

 

Das Ergebnis ist, dass ich nur noch einen Zweizeiler habe:

   1: var state = _orderProposalState.Evaluate(entry, folder);

   2: _orderProposalFactory.Handle(state).CreateWith(result,entry,folder);

Falls ihr euch wundert, wo die Methode ‘decide’ abgeblieben ist: Diese habe ich im gleichen Zug ebenfalls in eine eigene Klasse extrahiert. Die Implementierung dazu erhalte ich über den Konstruktor vom IoC. Die Variable ‘_orderProposalState’ enthält nun diese Logik.

Vorteil? Ganz offensichtlich ist die Klasse übersichtlicher geworden und zwar erheblich. Natürlich könnte man jetzt argumentieren, dass ich die Logik auf weitere Klassen verteilt habe (was neben dem SRP übrigens auch dem Separation of concerns Prinzip entspricht). Das ist natürlich richtig, aber in dieser Klasse sehe ich auf Anhieb den Zusammenhang: Ich ermittle einen Status eines Artikels. Im Anschluss reagiere ich in einer speziell dafür vorgesehenen Geschäftslogik. Wenn ich nun wissen will, wie der Status ermittelt wird, dann gehe ich in die dafür zuständige Klasse. Will ich hingegen wissen, wie ein Artikel mit diesem speziellen Status verarbeitet wird, gehe ich in die dedizierte Klasse. Will ich das Ganze erweitern, dann baue ich einfach einen weiteren entsprechenden Handler. Bestehender Code muss nicht angefasst werden (Ausnahme: Die Klasse StateHandler, aber auch dies könnte man anders lösen!). Und zu guter Letzt gestalten sich die Tests deutlich einfacher.

Mit Tag(s) versehen:

One thought on “Strategy Pattern in der Praxis

  1. […] 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 […]

    Gefällt mir

Schreibe einen Kommentar

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: