Ich arbeite an einem Projekt, das Anforderungen verarbeitet, und die Anforderung besteht aus zwei Komponenten: dem Befehl und den Parametern. Der Handler für jeden Befehl ist sehr einfach (<10 Zeilen, oft <5). Es gibt mindestens 20 Befehle und wahrscheinlich mehr als 50.
Ich habe ein paar Lösungen gefunden:
- ein großer Schalter / wenn-sonst auf Befehlen
- Zuordnung von Befehlen zu Funktionen
- Zuordnung von Befehlen zu statischen Klassen / Singletons
Jeder Befehl führt eine kleine Fehlerprüfung durch, und das einzige Bit, das herausgezogen werden kann, ist die Prüfung der Anzahl von Parametern, die für jeden Befehl definiert sind.
Was wäre die beste Lösung für dieses Problem und warum? Ich bin auch offen für alle Designmuster, die ich möglicherweise verpasst habe.
Ich habe mir für jedes die folgende Pro / Contra-Liste ausgedacht:
Schalter
- Profis
- behält alle Befehle in einer Funktion; Da sie einfach sind, handelt es sich um eine visuelle Nachschlagetabelle
- Sie müssen den Quellcode nicht mit Tonnen kleiner Funktionen / Klassen überladen, die nur an einem Ort verwendet werden
- Nachteile
- sehr lang
- Schwer programmgesteuert Befehle hinzuzufügen (Verkettung mit Standard-Groß- / Kleinschreibung erforderlich)
Kartenbefehle -> Funktion
- Profis
- kleine, mundgerechte Stücke
- Kann Befehle programmgesteuert hinzufügen / entfernen
- Nachteile
- Wenn dies in Reihe erfolgt, entspricht dies optisch dem Schalter
- Wenn nicht direkt ausgeführt, werden viele Funktionen nur an einem Ort verwendet
Kartenbefehle -> statische Klasse / Singleton
- Profis
- kann mit Polymorphismus einfache Fehlerüberprüfungen durchführen (nur wie 3 Zeilen, aber immer noch)
- ähnliche vorteile wie map -> function solution
- Nachteile
- Viele sehr kleine Klassen werden Projekt überladen
- Implementierung nicht alle am selben Ort, daher ist es nicht so einfach, Implementierungen zu scannen
Zusätzliche Hinweise:
Ich schreibe dies in Go, glaube aber nicht, dass die Lösung sprachspezifisch ist. Ich suche nach einer allgemeineren Lösung, weil ich in anderen Sprachen möglicherweise etwas sehr Ähnliches tun muss.
Ein Befehl ist eine Zeichenfolge, aber ich kann dies bei Bedarf problemlos einer Zahl zuordnen. Die Funktionssignatur ist so etwas wie:
Reply Command(List<String> params)
Go hat Funktionen der obersten Ebene, und andere Plattformen, die ich in Betracht ziehe, haben Funktionen der obersten Ebene, daher der Unterschied zwischen der zweiten und der dritten Option.
quelle
Antworten:
Dies passt hervorragend zu einer Karte (2. oder 3. Lösungsvorschlag). Ich habe es Dutzende Male benutzt und es ist einfach und effektiv. Ich unterscheide nicht wirklich zwischen diesen Lösungen; Der wichtige Punkt ist, dass es eine Karte mit Funktionsnamen als Tasten gibt.
Der Hauptvorteil des Kartenansatzes besteht meiner Meinung nach darin, dass es sich bei der Tabelle um Daten handelt. Dies bedeutet, dass es zur Laufzeit weitergegeben, erweitert oder auf andere Weise geändert werden kann. Es ist auch einfach, zusätzliche Funktionen zu codieren, die die Karte auf neue, aufregende Weise interpretieren. Dies wäre mit der Case / Switch-Lösung nicht möglich.
Ich habe die Nachteile, die Sie erwähnen, nicht wirklich erlebt, aber ich möchte einen zusätzlichen Nachteil erwähnen: Das Versenden ist einfach, wenn nur der Name der Zeichenfolge zählt, aber wenn Sie zusätzliche Informationen berücksichtigen müssen, um zu entscheiden, welche Funktion ausgeführt werden soll Es ist viel weniger sauber.
Vielleicht bin ich gerade noch nie auf ein Problem gestoßen, aber ich sehe wenig Wert sowohl im Befehlsmuster als auch in der Codierung des Versands als Klassenhierarchie, wie andere bereits erwähnt haben. Der Kern des Problems besteht darin, Anforderungen Funktionen zuzuordnen. Eine Karte ist einfach, offensichtlich und leicht zu testen. Eine Klassenhierarchie erfordert mehr Code und Design, erhöht die Kopplung zwischen diesem Code und kann Sie dazu zwingen, mehr Entscheidungen im Voraus zu treffen, die Sie später wahrscheinlich ändern müssen. Ich denke nicht, dass das Befehlsmuster überhaupt zutrifft.
quelle
Ihr Problem passt sehr gut zum Command-Entwurfsmuster . Sie werden also im Grunde eine Basisschnittstelle
Command
haben und dann würde es mehrereCommandImpl
Klassen geben, die diese Schnittstelle implementieren würden. Die Schnittstelle muss im Wesentlichen nur eine einzige Methode habendoCommand(Args args)
. Sie können die Argumente über eine Instanz derArgs
Klasse übergeben lassen. Auf diese Weise können Sie die Kraft des Polymorphismus nutzen, anstatt klobige if / else-Anweisungen zu verwenden. Auch dieses Design ist leicht erweiterbar.quelle
Immer wenn ich frage, ob ich eine switch-Anweisung oder einen OO-ähnlichen Polymorphismus verwenden soll, beziehe ich mich auf das Ausdrucksproblem . Grundsätzlich ist es sehr schwierig, ein System zu erstellen, mit dem Sie auf natürliche Weise sowohl neue Fälle als auch neue Aktionen hinzufügen können, wenn Ihre Daten unterschiedliche "Fälle" haben und der Zauberstab unterschiedliche "Aktionen" unterstützt (wobei jede Aktion für jeden Fall etwas anderes ausführt) in der Zukunft.
Wenn Sie switch-Anweisungen (oder das Besuchermuster) verwenden, ist es einfach, neue Aktionen hinzuzufügen (weil Sie alles in einer einzigen Funktion haben), aber schwierig, neue Fälle hinzuzufügen (weil Sie zurückgehen und die alten Funktionen bearbeiten müssen).
Umgekehrt, wenn Sie OO-artigen Polymorphismus verwenden, ist es einfach, neue Fälle hinzuzufügen (nur eine neue Klasse zu erstellen), aber es ist schwierig, Methoden zu einer Schnittstelle hinzuzufügen (da Sie dann zurückgehen und eine Reihe von Klassen bearbeiten müssen).
In Ihrem Fall haben Sie nur eine Methode, die Sie unterstützen müssen (eine Anfrage bearbeiten), aber viele mögliche Fälle (jeder unterschiedliche Befehl). Da es wichtiger ist, das Hinzufügen neuer Fälle zu vereinfachen als das Hinzufügen neuer Methoden, müssen Sie für jeden Befehl eine eigene Klasse erstellen.
Übrigens spielt es aus der Sicht der Erweiterbarkeit keine große Rolle, ob Sie Klassen oder Funktionen verwenden. Wenn wir mit einer switch-Anweisung vergleichen, kommt es darauf an, wie Dinge "verteilt" werden und wie Klassen und Funktionen auf die gleiche Weise verteilt werden. Verwenden Sie daher nur das, was in Ihrer Sprache praktischer ist (und da Go über lexikalische Gültigkeitsbereiche und Closures verfügt, ist der Unterschied zwischen Klassen und Funktionen tatsächlich sehr gering).
Beispielsweise können Sie in der Regel die Delegierung verwenden, um die Fehlerüberprüfung durchzuführen, anstatt sich auf die Vererbung zu verlassen.
In diesem Beispiel wird natürlich davon ausgegangen, dass es eine sinnvolle Möglichkeit gibt, die Argumente der Wrapper-Funktion oder der übergeordneten Klasse für jeden Fall überprüfen zu lassen. Abhängig davon, wie die Dinge laufen, kann es einfacher sein, den Aufruf von check_arguments in die Befehle selbst zu integrieren (da jeder Befehl die Prüffunktion aufgrund einer unterschiedlichen Anzahl von Argumenten, unterschiedlicher Befehlstypen usw. möglicherweise mit unterschiedlichen Argumenten aufrufen muss).
tl; dr: Es gibt keinen besten Weg, um alle Probleme zu lösen. Konzentrieren Sie sich aus der Perspektive, Dinge zum Laufen zu bringen, darauf, Ihre Abstraktionen auf eine Weise zu erstellen, die wichtige Invarianten erzwingt und Sie vor Fehlern schützt. Denken Sie im Hinblick auf die Zukunftssicherheit daran, welche Teile des Codes mit größerer Wahrscheinlichkeit erweitert werden.
quelle
Ich habe go noch nie benutzt, als ac # -Programmierer würde ich wahrscheinlich die folgende Zeile durchgehen, hoffentlich passt diese Architektur zu dem, was Sie tun.
Ich würde eine kleine Klasse / Objekt für jede mit der Hauptfunktion ausführen, jeder sollte seine Zeichenfolgendarstellung kennen. Auf diese Weise erhalten Sie eine Plug-in-Funktion, die sich anhört, als ob Sie möchten, da die Anzahl der Funktionen zunimmt. Beachten Sie, dass ich keine Statik verwenden würde, es sei denn, Sie müssten es wirklich tun, da dies keinen großen Vorteil bringt.
Ich hätte dann eine Factory, die weiß, wie man diese Klassen zur Laufzeit erkennt, indem man sie so ändert, dass sie aus verschiedenen Assembles usw. geladen werden. Dies bedeutet, dass Sie nicht alle im selben Projekt benötigen.
Dies macht es auch modularer zum Testen und sollte den Code schön und klein machen, was später einfacher zu warten ist.
quelle
Wie wählen Sie Ihre Befehle / Funktionen aus?
Wenn es einen "cleveren" Weg gibt, die richtige Funktion auszuwählen, ist dies der richtige Weg. Dies bedeutet, dass Sie neue Unterstützung hinzufügen können (möglicherweise in einer externen Bibliothek), ohne die Kernlogik zu ändern.
Auch das Testen einzelner Funktionen ist isolierter einfacher als eine enorme switch-Anweisung.
Schließlich wird es nur an einer Stelle verwendet - stellen Sie möglicherweise fest, dass bei Erreichen von 50 verschiedene Bits mit verschiedenen Funktionen wiederverwendet werden können?
quelle
Ich habe keine Ahnung, wie Go funktioniert, aber eine Architektur, die ich in ActionScript verwendet habe, ist eine doppelt verknüpfte Liste, die als Verantwortungskette fungiert. Jeder Link hat eine DeterminResponsibility-Funktion (die ich als Rückruf implementiert habe, aber Sie könnten jeden Link separat schreiben, wenn dies für das, was Sie versuchen, besser funktioniert). Wenn ein Link feststellt, dass er die Verantwortung trägt, ruft er meetResponsibility auf (erneut einen Rückruf), und das beendet die Kette. Wenn es keine Verantwortung hätte, würde es die Anfrage an das nächste Glied in der Kette weiterleiten.
Verschiedene Anwendungsfälle können einfach durch Hinzufügen eines neuen Glieds zwischen den Gliedern (oder am Ende) der vorhandenen Kette hinzugefügt und entfernt werden.
Dies ähnelt Ihrer Vorstellung von einer Funktionskarte, unterscheidet sich jedoch geringfügig von ihr. Das Schöne ist, dass Sie die Anfrage einfach weitergeben und es funktioniert, ohne dass Sie etwas anderes tun müssen. Der Nachteil ist, dass es nicht gut funktioniert, wenn Sie einen Wert zurückgeben müssen.
quelle