Befehlsmusterdesign

11

Ich habe diese alte Implementierung des Befehlsmusters. Es ist eine Art Kontext durch die gesamte DIOperation- Implementierung zu führen, aber ich habe später im Lern- und Lernprozess (der niemals aufhört) festgestellt, dass dies nicht optimal ist. Ich denke auch, dass der "Besuch" hier nicht wirklich passt und nur verwirrt.

Ich denke tatsächlich darüber nach, meinen Code umzugestalten, auch weil ein Befehl nichts über die anderen wissen sollte und im Moment alle dieselben Schlüssel-Wert-Paare haben. Es ist wirklich schwer zu behaupten, welche Klasse welchen Schlüsselwert besitzt, was manchmal zu doppelten Variablen führt.

Ein Anwendungsbeispiel: Angenommen , CommandB benötigt UserName, der von CommandA festgelegt wird . Sollte CommandA den Schlüssel UserNameForCommandB = John setzen ? Oder sollten sie einen gemeinsamen Benutzernamen = John- Schlüsselwert haben? Was ist, wenn der Benutzername von einem dritten Befehl verwendet wird?

Wie kann ich dieses Design verbessern? Vielen Dank!

class DIParameters {
public:
   /**
    * Parameter setter.
    */
    virtual void setParameter(std::string key, std::string value) = 0;
    /**
    * Parameter getter.
    */
    virtual std::string getParameter(std::string key) const = 0;

    virtual ~DIParameters() = 0;
};

class DIOperation {
public:
    /**
     * Visit before performing execution.
     */
    virtual void visitBefore(DIParameters& visitee) = 0;
    /**
     * Perform.
     */
    virtual int perform() = 0;
    /**
     * Visit after performing execution.
     */
    virtual void visitAfter(DIParameters& visitee) = 0;

    virtual ~DIOperation() = 0;
};
Andrea Richiardi
quelle
3
Ich hatte noch nie Glück, Befehle zum Festlegen von Eigenschaften (wie Name) zu verwenden. Es beginnt sehr abhängig zu werden. Wenn Ihre Einstellungseigenschaften eine Ereignisarchitektur oder ein Beobachtermuster verwenden.
Ahenderson
1
1. Warum die Parameter durch einen separaten Besucher weitergeben? Was ist falsch daran, einen Kontext als Argument für die Leistung zu übergeben? 2. Der Kontext ist für den 'allgemeinen' Teil des Befehls (z. B. Aktuelle Sitzung / Dokument). Alle betriebsspezifischen Parameter werden besser über den Konstruktor der Operation übergeben.
Kris Van Bael
@KrisVanBael das ist der verwirrende Teil, den ich zu ändern versuche. Ich gebe es als Besucher weiter, während es tatsächlich ein Kontext ist ...
Andrea Richiardi
@ahenderson Meinst du Ereignisse zwischen meinen Befehlen richtig? Würden Sie dort Ihre Schlüsselwerte eintragen (ähnlich wie bei Android mit Parcel)? Wäre es in dem Sinne dasselbe, dass CommandA ein Ereignis mit den Schlüssel-Wert-Paaren erstellen sollte, die CommandB akzeptiert?
Andrea Richiardi

Antworten:

2

Ich bin etwas besorgt über die Veränderbarkeit Ihrer Befehlsparameter. Ist es wirklich notwendig, einen Befehl mit sich ständig ändernden Parametern zu erstellen?

Probleme mit Ihrem Ansatz:

Möchten Sie, dass andere Threads / Befehle Ihre Parameter ändern, während performsie ausgeführt werden?

Wollen Sie visitBeforeund visitAfterdes gleichen CommandObjekts mit unterschiedlichen aufgerufen werden DIParameterObjekte?

Möchten Sie, dass jemand Ihren Befehlen Parameter zuführt, von denen die Befehle keine Ahnung haben?

Nichts davon ist durch Ihr aktuelles Design verboten. Während ein generisches Schlüsselwert-Parameterkonzept manchmal seine Vorzüge hat, mag ich es in Bezug auf eine generische Befehlsklasse nicht.

Beispiel für Konsequenzen:

Betrachten Sie eine konkrete Verwirklichung Ihrer CommandKlasse - so etwas wie CreateUserCommand. Wenn Sie nun die Erstellung eines neuen Benutzers anfordern, benötigt der Befehl natürlich einen Namen für diesen Benutzer. Welchen Parameter sollte ich setzen, wenn ich die CreateUserCommandund die DIParametersKlassen kenne ?

Ich könnte den userNameParameter einstellen oder username... behandeln Sie Parameter ohne Berücksichtigung der Groß- und Kleinschreibung? Ich würde es nicht wirklich wissen ... oh warte ... vielleicht ist es nur so name?

Wie Sie sehen können, bedeutet die Freiheit, die Sie durch eine generische Schlüsselwertzuordnung erhalten, dass die Verwendung Ihrer Klassen als jemand, der sie nicht implementiert hat, ungerechtfertigt schwierig ist. Sie müssten zumindest einige Konstanten für Ihre Befehle angeben, damit andere wissen, welche Schlüssel von diesem Befehl unterstützt werden.

Mögliche unterschiedliche Designansätze:

  • Unveränderliche Parameter: Indem Sie Ihre ParameterInstanzen unveränderlich machen, können Sie sie unter verschiedenen Befehlen frei wiederverwenden.
  • Spezifische Parameterklassen: Bei einer UserParameterKlasse, die genau die Parameter enthält, die ich für Befehle mit einem Benutzer benötigen würde, wäre die Arbeit mit dieser API viel einfacher. Sie könnten immer noch Vererbung für die Parameter haben, aber es wäre für Befehlsklassen nicht mehr sinnvoll, beliebige Parameter zu verwenden - auf der Pro-Seite bedeutet dies natürlich, dass API-Benutzer genau wissen, welche Parameter erforderlich sind.
  • Eine Befehlsinstanz pro Kontext: Wenn Ihre Befehle Dinge wie visitBeforeund enthalten müssen visitAfter, während Sie sie gleichzeitig mit verschiedenen Parametern wiederverwenden, sind Sie offen für das Problem, mit unterschiedlichen Parametern aufgerufen zu werden. Wenn die Parameter bei mehreren Methodenaufrufen gleich sein sollen, müssen Sie sie in den Befehl einkapseln, damit sie zwischen den Aufrufen nicht für andere Parameter ausgeschaltet werden können.
Frank
quelle
Ja, ich habe visitBefore und visitAfter losgeworden. Grundsätzlich übergebe ich meine DIParameter-Schnittstelle in der perform-Methode. Das Problem mit unerwünschten DIParamters-Instanzen wird immer bestehen bleiben, da ich mich für die Flexibilität entschieden habe, die Schnittstelle zu übergeben. Ich mag die Idee wirklich, DIParameter-Kinder nach ihrer Füllung unveränderlich machen zu können. Eine "zentrale Behörde" muss jedoch weiterhin den richtigen DIP-Parameter an den Befehl übergeben. Dies ist wahrscheinlich der Grund, warum ich angefangen habe, ein Besuchermuster zu implementieren. Ich wollte auf irgendeine Weise eine Umkehrung der Kontrolle haben ...
Andrea Richiardi
0

Das Schöne an Designprinzipien ist, dass sie früher oder später miteinander in Konflikt stehen.

In der beschriebenen Situation würde ich es vorziehen, mit einem Kontext zu arbeiten, aus dem jeder Befehl Informationen entnehmen und in den er Informationen einfügen kann (insbesondere wenn es sich um Schlüssel-Wert-Paare handelt). Dies basiert auf einem Kompromiss: Ich möchte nicht, dass separate Befehle gekoppelt werden, nur weil sie eine Art Eingabe zueinander sind. In CommandB ist es mir egal, wie UserName eingestellt wurde - nur, dass es für mich da ist. Das Gleiche in CommandA: Ich setze die Informationen ein, ich möchte nicht wissen, was andere damit machen - noch wer sie sind.

Dies bedeutet eine Art vorübergehenden Kontext, den Sie schlecht finden können. Für mich ist die Alternative schlechter, insbesondere wenn dieser einfache Schlüsselwertkontext (kann eine einfache Bohne mit Gettern und Setzern sein, um den "Freiform" -Faktor ein wenig einzuschränken) es ermöglichen kann, dass die Lösung einfach und testbar ist getrennte Befehle, jeder mit seiner eigenen Geschäftslogik.

Martin
quelle
1
Welche Prinzipien finden Sie hier widersprüchlich?
Jimmy Hoffa
Zur Verdeutlichung besteht mein Problem nicht darin, zwischen Kontext- oder Besuchermuster zu wählen. Ich benutze im Grunde ein Kontextmuster namens Besucher :)
Andrea Richiardi
Ok, ich habe Ihre genaue Frage / Ihr Problem wahrscheinlich falsch verstanden.
Martin
0

Nehmen wir an, Sie haben eine Befehlsschnittstelle:

class Command {
public:
    void execute() = 0;
};

Und ein Thema:

class Subject {
    std::string name;
public:
    void setName(const std::string& name) {this->name = name;}
}

Was Sie brauchen ist:

class NameObserver {
public:
    void update(const std::string& name) = 0;
};

class Subject {
    NameObserver& o;
    std::string name;
private:
    void setName(const std::string& name) {
        this->name = name;
        o.update(name);
    }
};

class CommandB: public Command, public NameObserver {
    std::string name;
public:
    void execute();
    void update(const std::string& name) {
        this->name = name;
        execute();
    }
};

Stellen Sie NameObserver& oals Verweis auf CommandB. Wenn CommandA jetzt den Betreffnamen ändert, kann CommandB mit den richtigen Informationen ausgeführt werden. Wenn der Name von einem weiteren Befehl verwendet wird, verwenden Sie astd::list<NameObserver>

ahenderson
quelle
Danke für die Antwort. Das Problem bei diesem Design ist, dass wir für jeden Parameter einen Setter + NameObserver benötigen. Ich könnte eine DIParameters (Kontext) -Instanz übergeben und benachrichtigen, aber ich werde wahrscheinlich auch nicht die Tatsache lösen, dass ich CommandA immer noch mit CommandB kopple, was bedeutet, dass CommandA einen Schlüsselwert eingeben muss, den nur CommandB kennen sollte ... Ich habe auch versucht, eine externe Entität (ParameterHandler) zu haben, die als einzige weiß, welcher Befehl welchen Parameter benötigt und in der DIParameters-Instanz entsprechend setzt / erhält.
Andrea Richiardi
@Kap "Das Problem bei diesem Design ist, dass wir für jeden Parameter einen Setter + NameObserver benötigen" - Parameter in diesem Zusammenhang ist für mich etwas verwirrend, ich denke, Sie meinten Feld. In diesem Fall sollten Sie bereits einen Setter für jedes Feld haben, das sich ändert. Aus Ihrem Beispiel geht hervor, dass ComamndA den Namen des Betreffs ändert. Es sollte das Feld über einen Setter ändern. Hinweis: Sie benötigen keinen Beobachter pro Feld, haben nur einen Getter und geben das Objekt an alle Beobachter weiter.
Ahenderson
0

Ich weiß nicht, ob dies der richtige Weg ist, um mit Programmierern umzugehen (in diesem Fall entschuldige ich mich), aber nachdem ich alle Antworten hier überprüft habe (insbesondere @ Franks). Ich habe meinen Code folgendermaßen überarbeitet:

  • DIParameter fallen gelassen. Ich werde einzelne (generische) Objekte als Eingabe von DIOperation haben (unveränderlich). Beispiel:
Klasse RelatedObjectTriplet {
Privat:
    std :: string const m_sPrimaryObjectId;
    std :: string const m_sSecondaryObjectId;
    std :: string const m_sRelationObjectId;

    RelatedObjectTriplet & operator = (RelatedObjectTriplet other);

Öffentlichkeit:
    RelatedObjectTriplet (std :: string const & sPrimaryObjectId,
                         std :: string const & sSecondaryObjectId,
                         std :: string const & sRelationObjectId);

    RelatedObjectTriplet (RelatedObjectTriplet const & other);


    std :: string const & getPrimaryObjectId () const;
    std :: string const & getSecondaryObjectId () const;
    std :: string const & getRelationObjectId () const;

    ~ RelatedObjectTriplet ();
};
  • Neue DIOperation-Klasse (mit Beispiel) definiert als:
Vorlage <Klasse T = nichtig> 
Klasse DIOperation {
Öffentlichkeit:
    virtual int perform () = 0;

    virtuelles T getResult () = 0;

    virtual ~ DIOperation () = 0;
};

Klasse CreateRelation: public DIOperation <RelatedObjectTriplet> {
Privat:
    statisch std :: string const TYPE;

    // Parameter (unveränderlich)
    RelatedObjectTriplet const m_sParams;

    // Versteckt
    CreateRelation & operator = (CreateRelation const & source);
    CreateRelation (CreateRelation const & source);

    // Intern
    std :: string m_sNewRelationId;

Öffentlichkeit:
    CreateRelation (RelatedObjectTriplet const & params);

    int perform ();

    RelatedObjectTriplet getResult ();

    ~ CreateRelation ();
};
  • Es kann folgendermaßen verwendet werden:
RelatedObjectTriplet-Triplett ("33333", "55555", "77777");
CreateRelation createRel (Triplett);
createRel.perform ();
const RelatedObjectTriplet res = createRel.getResult ();

Danke für die Hilfe und ich hoffe ich habe hier keine Fehler gemacht :)

Andrea Richiardi
quelle