Wie kann eine Anwendung mit mehreren Switch-Fällen umgestaltet werden?

10

Ich habe eine Anwendung, die eine Ganzzahl als Eingabe verwendet und basierend auf der Eingabe statische Methoden verschiedener Klassen aufruft. Jedes Mal, wenn eine neue Nummer hinzugefügt wird, müssen wir einen weiteren Fall hinzufügen und eine andere statische Methode einer anderen Klasse aufrufen. Es gibt jetzt 50 Fälle im Schalter und jedes Mal, wenn ich einen weiteren Fall hinzufügen muss, schaudere ich. Gibt es einen besseren Weg, dies zu tun.

Ich habe nachgedacht und bin auf diese Idee gekommen. Ich benutze das Strategiemuster. Anstatt einen Schalterfall zu haben, habe ich eine Karte von Strategieobjekten, wobei der Schlüssel die Eingabe-Ganzzahl ist. Sobald die Methode aufgerufen wird, sucht sie das Objekt und ruft die generische Methode für das Objekt auf. Auf diese Weise kann ich die Verwendung des Switch-Case-Konstrukts vermeiden.

Was denkst du?

Kaushik Chakraborty
quelle
2
Was ist das eigentliche Problem mit dem aktuellen Code?
Philip Kendall
Was passiert, wenn Sie eine dieser Änderungen vornehmen müssen? Müssen Sie einen switchFall hinzufügen und eine bereits vorhandene Methode in Ihrem komplexen System aufrufen, oder müssen Sie sowohl die Methode als auch ihren Aufruf erfinden?
Kilian Foth
@KilianFoth Ich habe dieses Projekt als Wartungsentwickler geerbt und musste noch keine Änderungen vornehmen. Ich werde jedoch bald Änderungen vornehmen, daher möchte ich jetzt umgestalten. Aber um Ihre Frage zu beantworten, ja zu letzterer.
Kaushik Chakraborty
2
Ich denke, Sie müssen ein komprimiertes Beispiel dafür zeigen, was los ist.
Whatsisname
1
@KaushikChakraborty: dann mache ein Beispiel aus dem Gedächtnis. Es gibt Situationen, in denen ein Über-Switch mit mehr als 250 Fällen angemessen ist, und es gibt Fälle, in denen der Switch schlecht ist, egal wie wenige Fälle er hat. Der Teufel steckt im Detail und wir haben keine Details.
Whatsisname

Antworten:

13

Es gibt jetzt 50 Fälle im Schalter und jedes Mal, wenn ich einen weiteren Fall hinzufügen muss, schaudere ich.

Ich liebe Polymorphismus. Ich liebe SOLID. Ich liebe reine objektorientierte Programmierung. Ich hasse es, wenn diese einen schlechten Ruf bekommen, weil sie dogmatisch angewendet werden.

Sie haben sich nicht gut dafür ausgesprochen, die Strategie umzugestalten. Das Refactoring hat übrigens einen Namen. Es heißt Bedingt durch Polymorphismus ersetzen .

Ich habe auf c2.com einige relevante Ratschläge für Sie gefunden :

Es ist wirklich nur dann sinnvoll, wenn dieselben oder sehr ähnliche bedingte Tests häufig wiederholt werden. Bei einfachen, selten wiederholten Tests würde das Ersetzen einer einfachen Bedingung durch die Ausführlichkeit mehrerer Klassendefinitionen und wahrscheinlich das Entfernen des Codes, der tatsächlich die bedingt erforderliche Aktivität erfordert, zu einem Lehrbuchbeispiel für die Verschleierung von Code führen. Ziehen Sie Klarheit der dogmatischen Reinheit vor. - DanMuller

Sie haben einen Schalter mit 50 Fällen und Ihre Alternative besteht darin, 50 Objekte zu produzieren. Oh und 50 Zeilen Objektkonstruktionscode. Dies ist kein Fortschritt. Warum nicht? Da dieses Refactoring nichts dazu beiträgt, die Anzahl von 50 zu verringern. Sie verwenden dieses Refactoring, wenn Sie feststellen, dass Sie eine andere switch-Anweisung für dieselbe Eingabe an einer anderen Stelle erstellen müssen. Dann hilft dieses Refactoring, weil es 100 wieder in 50 verwandelt.

Solange Sie sich auf "den Schalter" beziehen, als wäre er der einzige, den Sie haben, empfehle ich dies nicht. Der einzige Vorteil des Refactorings besteht darin, dass die Wahrscheinlichkeit verringert wird, dass ein Goofball Ihren 50-Fall-Schalter kopiert und einfügt.

Was ich empfehle, ist, diese 50 Fälle genau auf Gemeinsamkeiten zu untersuchen, die herausgerechnet werden können. Ich meine 50? "Ja wirklich?" Bist du sicher, dass du so viele Fälle brauchst? Vielleicht versuchen Sie hier zu viel zu tun.

candied_orange
quelle
Ich stimme dem zu, was Sie sagen. Der Code weist viele Redundanzen auf. Es kann sein, dass viele Fälle nicht einmal notwendig sind, aber auf den ersten Blick scheint dies nicht der Fall zu sein. Jeder Fall ruft eine Methode auf, die mehrere Systeme aufruft, die Ergebnisse aggregiert und zum aufrufenden Code zurückkehrt. Jede Klasse ist in sich geschlossen, erledigt einen Job und ich fürchte, ich werde gegen das Prinzip des hohen Zusammenhalts verstoßen, wenn ich die Anzahl der Fälle reduzieren würde.
Kaushik Chakraborty
2
Ich kann 50 bekommen, ohne den hohen Zusammenhalt zu verletzen und die Dinge in sich geschlossen zu halten. Ich kann es einfach nicht mit einer Nummer machen. Ich würde eine 2, eine 5 und eine weitere 5 brauchen. Deshalb nennt man das Ausklammern. Im Ernst, schauen Sie sich Ihre gesamte Architektur an und sehen Sie, ob Sie keinen Ausweg aus dieser Hölle finden, in der Sie sich befinden. Beim Refactoring geht es darum, schlechte Entscheidungen rückgängig zu machen. Sie nicht in neuen Formen verewigen.
candied_orange
Wenn Sie nun einen Weg finden, die 50 mithilfe dieses Refactorings zu reduzieren, entscheiden Sie sich dafür. So nutzen Sie die Idee von Doc Browns: Eine Karte mit Karten kann zwei Schlüssel enthalten. Etwas zum Nachdenken.
candied_orange
1
Ich stimme dem Kommentar von Candied zu. Das Problem sind nicht die 50 Fälle in der switch-Anweisung, sondern das übergeordnete Architekturdesign, bei dem Sie eine Funktion aufrufen, die zwischen 50 Optionen entscheiden muss. Ich habe einige sehr große und komplexe Systeme entworfen und war noch nie in eine solche Situation gezwungen.
Dunk
@Candied "Sie verwenden dieses Refactoring, wenn Sie feststellen, dass Sie eine andere switch-Anweisung für denselben Eingang an einer anderen Stelle erstellen müssen." Können Sie dies näher erläutern? Da ich einen ähnlichen Fall habe, in dem ich switch-Fälle habe, aber auf verschiedenen Ebenen, wie wir sie in unserem haben Projekt zuerst Autorisierung, Validierung, CRUD-Verfahren dann dao. In jeder Schicht befinden sich also die Schalterfälle am selben Eingang, dh eine Ganzzahl, die jedoch unterschiedliche Funktionen wie auth ausführen, gültig. Sollen wir also eine Klasse für jeden Typ erstellen, der unterschiedliche Methoden hat? Passt unser Fall zu dem, was Sie sagen möchten, indem Sie "denselben Schalter an demselben Eingang wiederholen"?
Siddharth Trikha
9

Eine Karte mit Strategieobjekten allein, die in einer Funktion Ihres Codes initialisiert wird, in der mehrere Codezeilen aussehen

     myMap.Add(1,new Strategy1());
     myMap.Add(2,new Strategy2());
     myMap.Add(3,new Strategy3());

erfordert, dass Sie und Ihre Kollegen die Funktionen / Strategien, die aufgerufen werden sollen, einheitlicher in separaten Klassen implementieren (da Ihre Strategieobjekte alle dieselbe Schnittstelle implementieren müssen). Ein solcher Code ist oft etwas umfassender als

     case 1:
          MyClass1.Doit1(someParameters);
          break;
     case 2:
          MyClass2.Doit2(someParameters);
          break;
     case 3:
          MyClass3.Doit3(someParameters);
          break;

Sie werden jedoch immer noch nicht von der Last befreit, diese Codedatei zu bearbeiten, wenn eine neue Nummer hinzugefügt werden muss. Die wirklichen Vorteile dieses Ansatzes sind anders:

  • die Initialisierung der Karte wird nun aus dem Dispatch Code getrennt , die tatsächlich ruft die Funktion auf eine bestimmte Nummer zugeordnet, und dieser ist nicht jene 50 Wiederholungen nicht mehr enthalten, wird es einfach aussehen myMap[number].DoIt(someParameters). Dieser Versandcode muss also nicht bei jedem Eintreffen einer neuen Nummer berührt werden und kann nach dem Open-Closed-Prinzip implementiert werden. Wenn Sie Anforderungen erhalten, bei denen Sie den Versandcode selbst erweitern müssen, müssen Sie nicht mehr 50 Stellen ändern, sondern nur eine.

  • Der Inhalt der Karte wird zur Laufzeit bestimmt (während der Inhalt des Switch-Konstrukts vor der Kompilierungszeit bestimmt wird), sodass Sie die Initialisierungslogik flexibler oder erweiterbarer gestalten können.

Ja, es gibt einige Vorteile, und dies ist sicherlich ein Schritt in Richtung mehr SOLID-Code. Wenn sich die Umgestaltung jedoch auszahlt, müssen Sie oder Ihr Team selbst entscheiden. Wenn Sie nicht erwarten, dass der Versandcode geändert wird, die Initialisierungslogik geändert wird und die Lesbarkeit von switchkein echtes Problem darstellt, ist Ihr Refactoring jetzt möglicherweise nicht so wichtig.

Doc Brown
quelle
Obwohl ich nicht gerne blind jeden Schalter durch Polymorphismus ersetze, werde ich sagen, dass die Verwendung einer Karte, wie sie Doc Brown hier vorschlägt, in der Vergangenheit für mich sehr gut funktioniert hat. Wenn Sie die gleiche Schnittstelle implementieren bitte ersetzen Doit1, Doit2etc. mit einer DoitMethode , die viele verschiedene Implementierungen hat.
candied_orange
Wenn Sie die Kontrolle über den Typ des als Schlüssel verwendeten Eingabesymbols haben, können Sie einen Schritt weiter gehen, indem Sie doTheThing()eine Methode für das Eingabesymbol festlegen. Dann müssen Sie die Karte nicht mehr pflegen.
Kevin Krumwiede
1
@ KevinKrumwiede: Was Sie vorschlagen, bedeutet einfach, die Strategieobjekte selbst im Programm weiterzugeben, als Ersatz für die ganzen Zahlen. Wenn das Programm jedoch eine Ganzzahl als Eingabe von einer externen Datenquelle verwendet, muss mindestens an einer Stelle des Systems eine Zuordnung von der Ganzzahl zur zugehörigen Strategie erfolgen.
Doc Brown
Erweitern Sie den Vorschlag von Doc Brown: Sie können auch eine Factory erstellen, die die Logik zum Erstellen der Strategieobjekte enthält, falls Sie sich für diesen Weg entscheiden. Trotzdem ist die Antwort von CandiedOrange für mich am sinnvollsten.
Vladimir Stokic
@DocBrown Das war es, worauf ich mich einließ "wenn Sie die Kontrolle über den Typ des Eingabesymbols haben".
Kevin Krumwiede
0

Ich bin stark für die Strategie, die in der Antwort von @DocBrown beschrieben ist .

Ich werde eine Verbesserung der Antwort vorschlagen.

Die Anrufe

 myMap.Add(1,new Strategy1());
 myMap.Add(2,new Strategy2());
 myMap.Add(3,new Strategy3());

kann verteilt werden. Sie müssen nicht zur selben Datei zurückkehren, um eine weitere Strategie hinzuzufügen, die dem Open-Closed-Prinzip noch besser entspricht.

Angenommen, Sie implementieren Strategy1in der Datei Strategy1.cpp. Sie können den folgenden Codeblock darin haben.

namespace Strategy1_Impl
{
   struct Initializer
   {
      Initializer()
      {
         getMap().Add(1, new Strategy1());
      }
   };
}
using namespace Strategy1_Impl;

static Initializer initializer;

Sie können denselben Code in jeder StategyN.cpp-Datei wiederholen. Wie Sie sehen können, wird dies eine Menge wiederholter Codes sein. Um die Codeduplizierung zu reduzieren, können Sie eine Vorlage verwenden, die in eine Datei eingefügt werden kann, auf die alle StrategyKlassen zugreifen können.

namespace StrategyHelper
{
   template <int N, typename StrategyType> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new StrategyType());
      }
   };
}

Danach müssen Sie in Strategy1.cpp nur noch Folgendes verwenden:

static StrategyHelper::Initializer<1, Strategy1> initializer;

Die entsprechende Zeile in StrategyN.cpp lautet:

static StrategyHelper::Initializer<N, StrategyN> initializer;

Sie können die Verwendung von Vorlagen auf eine andere Ebene heben, indem Sie eine Klassenvorlage für die konkreten Strategieklassen verwenden.

class Strategy { ... };

template <int N> class ConcreteStrategy;

Und dann anstelle von Strategy1verwenden ConcreteStrategy<1>.

template <> class ConcreteStrategy<1> : public Strategy { ... };

Ändern Sie die Hilfsklasse, um Strategys zu registrieren, in:

namespace StrategyHelper
{
   template <int N> struct Initializer
   {
      Initializer()
      {
         getMap().Add(N, new ConcreteStrategy<N>());
      }
   };
}

Ändern Sie den Code in Strateg1.cpp in:

static StrategyHelper::Initializer<1> initializer;

Ändern Sie den Code in StrategN.cpp in:

static StrategyHelper::Initializer<N> initializer;
R Sahu
quelle