Kann das Strategiemuster ohne nennenswerte Verzweigung umgesetzt werden?

14

Das Strategy-Muster eignet sich gut, um große if ... else-Konstrukte zu vermeiden und das Hinzufügen oder Ersetzen von Funktionen zu vereinfachen. Dennoch bleibt meiner Meinung nach ein Fehler. Es scheint, dass es in jeder Implementierung noch ein Verzweigungskonstrukt geben muss. Es kann sich um eine Fabrik oder eine Datendatei handeln. Ein Beispiel ist ein Bestellsystem.

Fabrik:

// All of these classes implement OrderStrategy
switch (orderType) {
case NEW_ORDER: return new NewOrder();
case CANCELLATION: return new Cancellation();
case RETURN: return new Return();
}

Der Code danach muss sich keine Sorgen machen, und es gibt nur einen Ort, an dem Sie jetzt einen neuen Auftragstyp hinzufügen können, aber dieser Codeabschnitt ist immer noch nicht erweiterbar. Das Herausziehen in eine Datendatei trägt etwas zur Lesbarkeit bei (fraglich, ich weiß):

<strategies>
   <order type="NEW_ORDER">com.company.NewOrder</order>
   <order type="CANCELLATION">com.company.Cancellation</order>
   <order type="RETURN">com.company.Return</order>
</strategies>

Dies fügt jedoch noch zusätzlichen Code für die Verarbeitung der Datendatei hinzu - gewährter, einfacher zu testender und relativ stabiler Code, aber dennoch zusätzliche Komplexität.

Außerdem ist diese Art von Konstrukt nicht gut für den Integrationstest geeignet. Jede einzelne Strategie ist jetzt möglicherweise einfacher zu testen, aber jede neue Strategie, die Sie hinzufügen, ist eine zusätzliche zu testende Komplexität. Es ist weniger als Sie es hätten, wenn Sie das Muster nicht verwendet hätten, aber es ist immer noch da.

Gibt es eine Möglichkeit, das Strategiemuster zu implementieren, das diese Komplexität verringert? Oder ist das so einfach wie es nur geht und der Versuch, weiter zu gehen, würde nur eine weitere Abstraktionsebene für wenig oder gar keinen Nutzen hinzufügen?

Michael K
quelle
Hmmmmm .... es könnte möglich sein, Dinge zu vereinfachen mit Dingen wie eval... könnte nicht in Java funktionieren, aber vielleicht in anderen Sprachen?
FrustratedWithFormsDesigner
1
@FrustratedWithFormsDesigner Reflexion ist das Zauberwort in Java
Ratschenfreak
2
Sie werden diese Bedingungen noch irgendwo brauchen. Wenn Sie sie an eine Factory senden, halten Sie sich einfach an DRY, da sonst die if- oder switch-Anweisung möglicherweise an mehreren Stellen aufgetreten ist.
Daniel B
1
Der Fehler, den Sie erwähnen, wird oft als Verletzung des Open-Closed-
Prinzips bezeichnet
akzeptierte Antwort in Verbindung stehenden Frage schlägt als Alternative Wörterbuch / Karte if-else und Schalter
gnat

Antworten:

15

Natürlich nicht. Selbst wenn Sie einen IoC-Container verwenden, müssen Sie irgendwo Bedingungen haben, die entscheiden, welche konkrete Implementierung injiziert werden soll. Dies ist die Natur des Strategiemusters.

Ich verstehe nicht wirklich, warum die Leute denken, dass dies ein Problem ist. In einigen Büchern wie Fowlers Refactoring heißt es , dass Sie, wenn Sie einen Schalter / Fall oder eine Kette von if / elses in der Mitte eines anderen Codes sehen, diesen als Geruch betrachten und versuchen sollten, ihn auf seine eigene Methode zu verschieben. Wenn der Code in jedem Fall mehr als eine Zeile, möglicherweise zwei, enthält, sollten Sie in Betracht ziehen, diese Methode zu einer Factory-Methode zu machen und Strategien zurückzugeben.

Einige Leute haben dies so verstanden, dass ein Schalter / Fall schlecht ist. Das ist nicht der Fall. Aber es sollte, wenn möglich, für sich bleiben.

pdr
quelle
1
Ich sehe es auch nicht als schlecht an. Ich bin jedoch immer auf der Suche nach Möglichkeiten, die Menge an Code zu reduzieren, die bei der Beibehaltung des Codes anfällt, was die Frage aufwirft.
Michael K
Switch-Anweisungen (und lange if / else-if-Blöcke) sind fehlerhaft und sollten nach Möglichkeit vermieden werden, um den Code wartbar zu halten. Das besagte "wenn überhaupt möglich" bestätigt, dass es einige Fälle gibt, in denen der Schalter vorhanden sein muss, und in diesen Fällen versuchen Sie, ihn auf einen einzigen Ort zu beschränken, und einer, der es weniger lästig macht (das ist so einfach) versehentlich eine der 5 Stellen im Code übersehen, die Sie benötigen, um synchron zu bleiben, wenn Sie den Code nicht richtig isolieren.
Shadow Man
10

Kann das Strategiemuster ohne nennenswerte Verzweigung umgesetzt werden?

Ja , indem Sie eine Hashmap / ein Wörterbuch verwenden, unter der sich jede Strategieimplementierung registriert. Die Fabrikmethode wird so etwas wie

Class strategyType = allStrategies[orderType];
return runtime.create(strategyType);

Jede Strategieimplementierung muss die Factory mit ihrem orderType und einigen Informationen zum Erstellen einer Klasse registrieren.

factory.register(NEW_ORDER, NewOrder.class);

Sie können den statischen Konstruktor für die Registrierung verwenden, wenn Ihre Sprache dies unterstützt.

Die register-Methode fügt lediglich der Hashmap einen neuen Wert hinzu:

void register(OrderType orderType, Class class)
{
   allStrategies[orderType] = class;
}

[Update 2012-05-04]
Diese Lösung ist viel komplexer als die ursprüngliche "Switch-Lösung", die ich die meiste Zeit bevorzugen würde.

In einer Umgebung, in der sich Strategien häufig ändern (dh die Preisberechnung hängt vom Kunden, der Zeit usw. ab), kann diese Hashmap-Lösung in Kombination mit einem IoC-Container eine gute Lösung sein.

k3b
quelle
3
Also hast du jetzt eine ganze Hashmap von Strategien, um einen Wechsel / Fall zu vermeiden? Wie genau sind Reihen factory.register(NEW_ORDER, NewOrder.class);sauberer oder weniger eine Verletzung von OCP als Reihen von case NEW_ORDER: return new NewOrder();?
pdr
5
Es ist überhaupt nicht sauberer. Es ist nicht intuitiv. Es opfert das Keep-it-Stimple-Dummheitsprinzip, um das Open-Closed-Prinzip zu verbessern. Gleiches gilt für Inversion-of-Control und Dependency-Injection: Diese sind schwieriger zu verstehen als eine einfache Lösung. Die Frage lautete "implementieren ... ohne signifikante Verzweigung" und nicht "wie man eine intuitivere Lösung erstellt"
k3b
1
Ich sehe auch nicht, dass Sie es vermieden haben, sich zu verzweigen. Sie haben gerade die Syntax geändert.
pdr
1
Gibt es mit dieser Lösung kein Problem in Sprachen, in denen Klassen erst geladen werden, wenn sie benötigt werden? Der statische Code wird erst ausgeführt, wenn die Klasse geladen wurde, und die Klasse wird niemals geladen, da keine andere Klasse darauf verweist.
Kevin Cline
2
Persönlich mag ich diese Methode, weil Sie damit Ihre Strategien als veränderbare Daten behandeln können, anstatt sie als unveränderlichen Code zu behandeln.
Tacroy
2

Bei "Strategie" geht es darum, mindestens einmal zwischen alternativen Algorithmen wählen zu müssen , nicht weniger. Irgendwo in Ihrem Programm muss jemand eine Entscheidung treffen - vielleicht der Benutzer oder Ihr Programm. Wenn Sie eine IoC verwenden, ändert Reflection, ein DataFile-Evaluator oder ein Switch / Case-Konstrukt nichts an der Situation.

Doc Brown
quelle
Ich würde Ihrer Aussage von einmal nicht zustimmen . Strategien können zur Laufzeit getauscht werden. Wenn beispielsweise eine Anforderung nicht mit einer Standard-ProcessingStrategy verarbeitet werden kann, kann eine VerboseProcessingStrategy ausgewählt und die Verarbeitung erneut ausgeführt werden.
dmux
@dmux: na klar, meine antwort entsprechend bearbeitet.
Doc Brown
1

Das Strategiemuster wird verwendet, wenn Sie mögliche Verhaltensweisen angeben, und eignet sich am besten zum Festlegen eines Handlers beim Start. Festlegen, welche Instanz der Strategie verwendet werden soll, kann über einen Mediator, Ihren Standard-IoC-Container, eine von Ihnen beschriebene Factory oder einfach durch Verwendung der richtigen, basierend auf dem Kontext erfolgen (da die Strategie häufig als Teil einer umfassenderen Verwendung bereitgestellt wird) der Klasse, die es enthält).

Für diese Art von Verhalten, bei dem verschiedene Methoden basierend auf Daten aufgerufen werden müssen, würde ich vorschlagen, die Polymorphismuskonstrukte zu verwenden, die von der jeweiligen Sprache bereitgestellt werden. nicht das Strategiemuster.

Telastyn
quelle
1

Im Gespräch mit anderen Entwicklern, bei denen ich arbeite, habe ich eine andere interessante Lösung gefunden - spezifisch für Java, aber ich bin sicher, dass die Idee in anderen Sprachen funktioniert. Konstruieren Sie eine Enumeration (oder eine Map, egal welche) mit den Klassenreferenzen und instanziieren Sie sie mit Reflection.

enum FactoryType {
   Type1(Type1.class),
   Type2(Type2.class);

   private Class<? extends Type> clazz;

   private FactoryType(Class<? extends Type> clazz) {
      this.clazz = clazz;
   }

   public Class<? extends Type> getTypeClass() {
      return clazz;
   }
}

Dies reduziert den Werkscode drastisch:

public Type create(FactoryType type) throws Exception {
   return type.getTypeClass().newInstance();
}

(Bitte ignorieren Sie die schlechte Fehlerbehandlung - Beispielcode :))

Es ist nicht die meisten flexibel, wie ein Build noch erforderlich ist , ... aber es reduziert den Code Änderung einer Zeile. Mir gefällt, dass die Daten vom Werkscode getrennt sind. Sie können sogar Parameter festlegen, indem Sie entweder abstrakte Klassen / Schnittstellen verwenden, um eine allgemeine Methode bereitzustellen, oder Anmerkungen zur Kompilierungszeit erstellen, um bestimmte Konstruktorsignaturen zu erzwingen.

Michael K
quelle
Wir sind uns nicht sicher, was dazu geführt hat, dass die Startseite erneut aufgerufen wurde. Dies ist jedoch eine gute Antwort. In Java 8 können Sie jetzt Konstruktorreferenzen ohne Reflektion erstellen.
JimmyJames
1

Die Erweiterbarkeit kann verbessert werden, indem für jede Klasse ein eigener Auftragstyp definiert wird. Dann wählt Ihre Fabrik die passende aus.

Beispielsweise:

public interface IOrderStrategy
{
    OrderType OrderType { get; }
}

public class NewOrder : IOrderStrategy
{
    public OrderType OrderType { get; } = OrderType.NewOrder;
}

public class OrderFactory
{
    private IEnumerable<IOrderStrategy> _strategies;

    public OrderFactory(IEnumerable<IOrderStrategy> strategies) // Injected by IoC container
    {
        _strategies = strategies;
    }

    public IOrderStrategy Create(OrderType orderType)
    {
        IOrderStrategy strategy = _strategies.FirstOrDefault(s => s.OrderType == orderType);

        if (strategy == null)
            throw new ArgumentException("Invalid order type.", nameof(orderType));

        return strategy;
    }
}
Eric Eskildsen
quelle
1

Ich denke, es sollte eine Grenze geben, wie viele Branchen akzeptabel sind.

Wenn meine switch-Anweisung beispielsweise mehr als acht Fälle enthält, bewerte ich meinen Code neu und suche, was ich neu faktorisieren kann. Sehr oft stelle ich fest, dass bestimmte Fälle in einer separaten Fabrik zusammengefasst werden können. Ich gehe davon aus, dass es eine Fabrik gibt, die Strategien entwickelt.

Auf jeden Fall können Sie dies nicht vermeiden und müssen irgendwo den Status oder den Typ des Objekts überprüfen, mit dem Sie arbeiten. Sie werden dann eine Strategie dafür entwickeln. Gute alte Trennung von Bedenken.

CodeART
quelle