Large Switch-Anweisungen: Bad OOP?

77

Ich war immer der Meinung, dass große switch-Anweisungen ein Symptom für schlechtes OOP-Design sind. In der Vergangenheit habe ich Artikel gelesen, in denen dieses Thema behandelt wird, und sie haben alternative OOP-basierte Ansätze bereitgestellt, die normalerweise auf Polymorphismus basieren, um das richtige Objekt für die Behandlung des Falls zu instanziieren.

Ich bin jetzt in einer Situation, die eine monströse switch-Anweisung hat, die auf einem Datenstrom von einem TCP-Socket basiert, in dem das Protokoll im Grunde aus einem Befehl mit Zeilenumbruch besteht, gefolgt von Datenzeilen, gefolgt von einer Endmarkierung. Der Befehl kann einer von 100 verschiedenen Befehlen sein, daher möchte ich einen Weg finden, diese Monster-Switch-Anweisung auf etwas Verwaltbareres zu reduzieren.

Ich habe ein bisschen gegoogelt, um die Lösungen zu finden, an die ich mich erinnere, aber leider ist Google heutzutage für viele Arten von Abfragen zu einer Einöde irrelevanter Ergebnisse geworden.

Gibt es Muster für diese Art von Problem? Anregungen zu möglichen Implementierungen?

Ein Gedanke, den ich hatte, war die Verwendung einer Wörterbuchsuche, bei der der Befehlstext dem zu instanziierenden Objekttyp zugeordnet wurde. Dies hat den schönen Vorteil, dass lediglich ein neues Objekt erstellt und ein neuer Befehl / Typ für neue Befehle in die Tabelle eingefügt wird.

Dies hat jedoch auch das Problem der Typenexplosion. Ich brauche jetzt 100 neue Klassen und muss einen Weg finden, sie sauber mit dem Datenmodell zu verbinden. Ist die "One True Switch-Anweisung" wirklich der richtige Weg?

Ich würde mich über Ihre Gedanken, Meinungen oder Kommentare freuen.

Erik Funkenbusch
quelle
Wie wäre es mit Ausdruck verwenden? Suchen Sie also die Funktion anhand ihres Namens und rufen Sie sie auf. Der Name kann leicht mit übergebenen Variablen abgeglichen werden, sodass wir keinen Schalter verwenden müssen.
Schöpfer

Antworten:

34

Sie können einige Vorteile aus einem Befehlsmuster ziehen .

Bei OOP können Sie möglicherweise mehrere ähnliche Befehle zu einer einzigen Klasse zusammenfassen, wenn die Verhaltensvariationen klein genug sind, um eine vollständige Klassenexplosion zu vermeiden (ja, ich kann die OOP-Gurus bereits darüber schreien hören). Wenn das System jedoch bereits OOP ist und jeder der über 100 Befehle wirklich eindeutig ist, machen Sie sie einfach zu eindeutigen Klassen und nutzen Sie die Vererbung, um die allgemeinen Elemente zu konsolidieren.

Wenn das System nicht OOP ist, würde ich OOP nicht nur dafür hinzufügen ... Sie können das Befehlsmuster einfach mit einer einfachen Wörterbuchsuche und Funktionszeigern oder sogar dynamisch generierten Funktionsaufrufen basierend auf dem Befehlsnamen verwenden, abhängig von die Sprache. Anschließend können Sie logisch verknüpfte Funktionen einfach in Bibliotheken gruppieren, die eine Sammlung ähnlicher Befehle darstellen, um eine überschaubare Trennung zu erreichen. Ich weiß nicht, ob es einen guten Begriff für diese Art der Implementierung gibt ... Ich betrachte ihn immer als "Dispatcher" -Stil, basierend auf dem MVC-Ansatz für den Umgang mit URLs.

nezroy
quelle
Stimmen Sie dem Wörterbuch zu und delegieren Sie die Idee. Sie werden wahrscheinlich auch eine Leistungsverbesserung erhalten, wenn Sie Dictionary <string, Action> über eine große switch-Anweisung verwenden. Die Schwierigkeit besteht darin, es zu initialisieren, obwohl dies auf Wunsch mithilfe von Reflexion automatisiert werden kann.
Zooba
Möglicherweise könnte die Initialisierung aus der Konfiguration gezogen werden.
Davogones
Das Problem mit der Reflexion ist, dass viele Webhosts auf einer Vertrauensstufe ausgeführt werden, die es Ihnen nicht erlaubt, sie zu verwenden :(
Interessanterweise beschreiben Sie eine Fabrik, wenn der einzige Unterschied zwischen den Zweigen darin besteht, dass es sich um verschiedene Unterklassen einer Basis handelt.
Thedric Walker
24

Ich sehe zwei switch-Anweisungen als Symptom für ein Nicht-OO-Design, bei dem der Einschalt-Enum-Typ durch mehrere Typen ersetzt werden kann, die unterschiedliche Implementierungen einer abstrakten Schnittstelle bereitstellen. zum Beispiel die folgenden ...

switch (eFoo)
{
case Foo.This:
  eatThis();
  break;
case Foo.That:
  eatThat();
  break;
}

switch (eFoo)
{
case Foo.This:
  drinkThis();
  break;
case Foo.That:
  drinkThat();
  break;
}

... sollte vielleicht umgeschrieben werden als ...

IAbstract
{
  void eat();
  void drink();
}

class This : IAbstract
{
  void eat() { ... }
  void drink() { ... }
}

class That : IAbstract
{
  void eat() { ... }
  void drink() { ... }
}

Jedoch eine switch - Anweisung ist nicht imo so ein starker Indikator dafür , dass die Switch - Anweisung sollte mit etwas anderes ersetzt werden.

ChrisW
quelle
8
Das Problem ist, dass Sie nicht wissen, ob Sie über das Netzwerk einen Dies- oder einen Das-Befehl erhalten. Irgendwo muss man sich entscheiden, ob man new This () oder new That () aufruft. Nach dieser Abstraktion mit OO ist ein Kinderspiel.
17

Der Befehl kann einer von 100 verschiedenen Befehlen sein

Wenn Sie eine von 100 verschiedenen Aufgaben ausführen müssen, können Sie eine 100-Wege-Verzweigung nicht vermeiden. Sie können es im Kontrollfluss (switch, if-elseif ^ 100) oder in Daten (eine 100-Elemente-Zuordnung von Zeichenfolge zu Befehl / Factory / Strategie) codieren. Aber es wird da sein.

Sie können versuchen, das Ergebnis des 100-Wege-Zweigs von Dingen zu isolieren, die dieses Ergebnis nicht kennen müssen. Vielleicht sind nur 100 verschiedene Methoden in Ordnung; Sie müssen keine Objekte erfinden, die Sie nicht benötigen, wenn der Code dadurch unhandlich wird.

Jonas Kölker
quelle
10
+1. Ich werde entmutigt, wenn Leute sich über stinkende Schalter beschweren, aber dann eine Alternative vorschlagen, die wirklich nur das abstrakte Schalterverhalten ist, das über das gesamte System verteilt ist, im Gegensatz zu einem ordentlichen Stapel.
1
Das mag pedantisch sein, aber ist eine Karte (string-> command) wirklich ein Zweig? Konzeptionell mag es sein, dass abhängig vom Wert einer Variablen mehrere Dinge passieren können, aber wenn ich an eine Verzweigung denke, denke ich an etwas, das eine Variable explizit auf mögliche Werte überprüft. Während eine Karte eine direkte Suche ist. Mit anderen Worten, die Zuordnung der Bedingung zum Ergebnis erfolgt einmal zu Beginn (bei Kartenpopulation) und nicht jedes Mal (durch Testen der Variablen). Unter dieser Definition entfernt die Befehlskarte den Zweig.
Cam Jackson
1
@CamJackson: Sicher, wenn Sie eine Karte verwenden, können Sie sie mit Code verwenden, der selbst keine Verzweigung ausführt - sie hat die Verzweigung an eine beliebige map_lookupFunktion ausgelagert, die sie aufruft (die verzweigt). Ich bin mir nicht sicher, was diese Beobachtung für Sie bedeutet. "Es ist nicht wirklich ein Zweig, wenn es woanders passiert"? Die Zuordnung zwischen Eingabe und Ausgabe erfolgt zur Kompilierungszeit für Switch-Anweisungen (und andere Steuerungsflussanweisungen) und zur Laufzeit für Datenstrukturen (höchstwahrscheinlich). Es ist nicht so, dass sich die Form des Schalters zur Laufzeit ändert. Beide Arten der Suche erfordern (und eine ist) einen Zweig. Ich hoffe das hilft :-)
Jonas Kölker
@ JonasKölker Das ist ein guter Punkt. Ich habe wohl nicht genau über die tatsächliche Datenstruktur nachgedacht und wie sie funktioniert. Der einzige wirkliche Weg, um den Zweig zu entfernen, wäre, wenn Ihre 100 Befehle zufällig 0, 1, 2 usw. heißen würden, damit Sie sie direkt als Indizes verwenden könnten.
Cam Jackson
@CamJackson: Ich glaube, ich stimme zu - in diesem Fall ist die einzige Verzweigung, die auftreten wird, die Tatsache, dass das, was in Ihrem 100-Elemente-Array gespeichert ist, in den Programmzähler gestellt wird (in Java geschieht dies indirekt und dahinter die Szenen). Hier definiere ich Verzweigung so, dass der Programmzähler, nachdem er einen bestimmten Wert angenommen hat, mehrere verschiedene nächste Werte annehmen kann (bei normaler Programmausführung keine kosmischen Strahlen). (Total nitpicking: Wenn Ihre Befehle 0..17 und 19..100 heißen würden, könnten Sie ein Array A mit 101 Elementen haben, wobei A [18] ein Fehlerbehandler ist. Sie können weiter verallgemeinern.)
Jonas Kölker
3

Ich denke, dies ist einer der wenigen Fälle, in denen große Schalter die beste Antwort sind, es sei denn, es bietet sich eine andere Lösung an.

Loren Pechtel
quelle
2

Ich sehe das Strategiemuster. Wenn ich 100 verschiedene Strategien habe ... so sei es. Die riesige switch-Anweisung ist hässlich. Sind alle Befehle gültige Klassennamen? Verwenden Sie in diesem Fall einfach die Befehlsnamen als Klassennamen und erstellen Sie das Strategieobjekt mit Activator.CreateInstance.

Jason Punyon
quelle
2

Wenn Sie über eine große switch-Anweisung sprechen, fallen Ihnen zwei Dinge ein:

  1. Es verstößt gegen OCP - Sie könnten kontinuierlich eine große Funktion aufrechterhalten.
  2. Sie könnten eine schlechte Leistung haben: O (n).

Andererseits kann eine Kartenimplementierung OCP entsprechen und möglicherweise mit O (1) arbeiten.

Quamrana
quelle
1
"eine Karte [kann O (1) sein]" - Wenn die Suche k Bits liest, kann sie zwischen 2 ** k verschiedenen Schlüsseln unterscheiden. Daher müssen Sie für n Schlüssel mindestens log (n) Bits lesen. Wie können Sie das in O (1) tun, wenn log (n) unbegrenzt ist? In welchem ​​Modell?
Jonas Kölker
@ JonasKölker: Eine Map kann mit einer Hash-Tabelle implementiert werden, mit der der richtige Schlüssel gefunden werden kann O(1). Siehe diese Antwort: stackoverflow.com/a/1055261/4834
quamrana
Die Hash-Funktion muss diese log (n) Bits noch verarbeiten. Hash-Tabellen (und einige Arten von Suchbäumen) sind O (1) in Bezug auf die enthaltenen Werte, aber O (n) oder schlechter in Bezug auf die Bitgröße der Schlüsselwerte.
CA McCann
4
Jeder anständige Compiler verwendet eine Tabelle für switch-Anweisungen mit aufeinanderfolgenden Fallwerten oder einen binären Entscheidungsbaum, in dem dies nicht funktioniert oder in dem der binäre Entscheidungsbaum aufgrund der guten Kenntnisse der Prozessorarchitektur schneller ist. Bei case-Anweisungen mit vielen Werten, die weit voneinander entfernt sind, wird ein guter Compiler den Wert hashen, um ihn auf eine Tabelle zu reduzieren, wenn dies von Vorteil ist. Unabhängig davon verwendet der Compiler die Strategie, mit der die switch-Anweisung auf schnellstmögliche Weise implementiert wird. Entschuldigen Sie, aber es ist dumm, eine switch-Anweisung aus Leistungsgründen abzulehnen.
Gnasher729
1

Ich würde sagen, dass das Problem nicht die große switch-Anweisung ist, sondern die Verbreitung des darin enthaltenen Codes und der Missbrauch von Variablen mit falschem Gültigkeitsbereich.

Ich habe dies in einem Projekt selbst erlebt, als immer mehr Code in den Switch einging, bis er nicht mehr wartbar war. Meine Lösung bestand darin, eine Parameterklasse zu definieren, die den Kontext für die Befehle enthielt (Name, Parameter, was auch immer, vor dem Wechsel gesammelt), eine Methode für jede case-Anweisung zu erstellen und diese Methode mit dem Parameterobjekt aus dem case aufzurufen.

Natürlich ist ein vollständig OOP-Befehls-Dispatcher (basierend auf Magie wie Reflexion oder Mechanismen wie Java-Aktivierung) schöner, aber manchmal möchten Sie einfach nur Dinge reparieren und die Arbeit erledigen;)

devio
quelle
1

Sie können ein Wörterbuch (oder eine Hash-Map, wenn Sie in Java codieren) verwenden (es wird von Steve McConnell als tabellengesteuerte Entwicklung bezeichnet).

Nicolas Dorier
quelle
0

Eine Möglichkeit, wie Sie sich verbessern könnten, würde dazu führen, dass Ihr Code von den Daten gesteuert wird. So stimmen Sie beispielsweise für jeden Code mit etwas überein, das damit umgeht (Funktion, Objekt). Sie können Reflection auch verwenden, um Zeichenfolgen abzubilden, die die Objekte / Funktionen darstellen, und sie zur Laufzeit aufzulösen. Möglicherweise möchten Sie jedoch einige Experimente durchführen, um die Leistung zu bewerten.

Otávio Décio
quelle
0

Der beste Weg, um dieses spezielle Problem zu lösen: Serialisierung und Protokolle sauber, besteht darin, eine IDL zu verwenden und den Marshalling-Code mit switch-Anweisungen zu generieren. Da alle Muster (Prototype Factory, Befehlsmuster usw.), die Sie anderweitig verwenden möchten, müssen Sie eine Zuordnung zwischen einer Befehls-ID / Zeichenfolge und einem Klassen- / Funktionszeiger initialisieren, die seitdem langsamer als switch-Anweisungen ausgeführt wird Der Compiler kann die perfekte Hash-Suche für switch-Anweisungen verwenden.

ididak
quelle
Ich bin gespannt, welcher Compiler switch-Anweisungen von Strings mit perfektem Hashing implementiert.
paxos1977
0

Ja, ich denke, große case-Anweisungen sind ein Symptom dafür, dass der eigene Code verbessert werden kann ... normalerweise durch Implementierung eines objektorientierteren Ansatzes. Wenn ich zum Beispiel die Art der Klassen in einer switch-Anweisung bewerte, bedeutet dies fast immer, dass ich wahrscheinlich Generics verwenden könnte, um die switch-Anweisung zu entfernen.

Cyclo
quelle
1
Das Problem ist, dass diese Daten von einem anderen Ort stammen und nicht objektorientiert sein können. Es muss etwas getan werden, um sie in Objektorientierung umzuwandeln, und im Allgemeinen ist dies ein großer Schalter.
Loren Pechtel
0

Sie können hier auch einen Sprachansatz wählen und die Befehle mit den zugehörigen Daten in einer Grammatik definieren. Sie können dann ein Generator-Tool verwenden, um die Sprache zu analysieren. Ich habe Ironie für diesen Zweck verwendet. Alternativ können Sie das Interpreter-Muster verwenden.

Meiner Meinung nach besteht das Ziel nicht darin, das reinste OO-Modell zu erstellen, sondern ein flexibles, erweiterbares, wartbares und leistungsstarkes System zu schaffen.

Rine le Comte
quelle
0

Ich habe kürzlich ein ähnliches Problem mit einer riesigen switch-Anweisung und habe den hässlichen Schalter durch die einfachste Lösung, eine Nachschlagetabelle und eine Funktion oder Methode, die den erwarteten Wert zurückgibt, beseitigt. Das Befehlsmuster ist eine gute Lösung, aber 100 Klassen zu haben, finde ich nicht gut. Also hatte ich so etwas wie:

switch(id)
    case 1: DoSomething(url_1) break;
    case 2: DoSomething(url_2) break;
    ..
    ..
    case 100 DoSomething(url_100) break;

und ich habe mich geändert für:

string url =  GetUrl(id);  
DoSomthing(url);

GetUrl kann in die Datenbank wechseln und die gesuchte URL zurückgeben, oder es kann sich um ein Wörterbuch im Speicher handeln, das die 100 URLs enthält. Ich hoffe, dies könnte jedem da draußen helfen, wenn er eine riesige monströse switch-Anweisung ersetzt.

Darmis
quelle
0

Denken Sie daran, wie Windows ursprünglich in der Anwendungsnachrichtenpumpe geschrieben wurde. Es saugte. Anwendungen werden mit den mehr hinzugefügten Menüoptionen langsamer ausgeführt. Da der gesuchte Befehl immer weiter unten in der switch-Anweisung endete, wurde das Warten auf die Antwort immer länger. Es ist nicht akzeptabel, lange switch-Anweisungen zu haben. Ich habe einen AIX-Daemon als POS-Befehlshandler erstellt, der 256 eindeutige Befehle verarbeiten kann, ohne zu wissen, was in dem über TCP / IP empfangenen Anforderungsdatenstrom enthalten ist. Das allererste Zeichen des Streams war ein Index in ein Funktionsarray. Jeder nicht verwendete Index wurde auf einen Standardnachrichtenhandler festgelegt. loggen Sie sich ein und verabschieden Sie sich.

Robert Achmann
quelle
2
Das stimmt einfach nicht. Switch-Anweisungen verwenden eine Sprungtabelle, sodass das Einschalten von 2 Elementen nicht mehr wie im Jahr 2000 dauert. Es handelt sich um eine einzelne Tabellensuche.
Erik Funkenbusch
Auch wenn die switch-Anweisung Zeichenfolgenwerte vergleicht?
Robert Achmann
Nun, natürlich hängt es von der Länge des String-Werts ab, aber ansonsten ja. Windows verwendet C, das Zeichenfolgen sowieso nicht einschalten kann, in C # jedoch. Es ist immer noch eine Nachschlagetabelle.
Erik Funkenbusch