Conception de modèle de commande

11

J'ai cette ancienne implémentation du modèle de commande. C'est en quelque sorte passer un Contexte à travers toute l' implémentation de DIOperation , mais j'ai réalisé plus tard, dans le processus d'apprentissage et d'apprentissage (qui ne s'arrête jamais), que ce n'est pas optimal. Je pense aussi que la "visite" ici ne correspond pas vraiment et confond simplement.

Je pense en fait à refactoriser mon code, également parce qu'une commande ne devrait rien savoir des autres et, pour le moment, elles partagent toutes les mêmes paires clé-valeur. Il est vraiment difficile de conserver quelle classe possède quelle valeur-clé, ce qui conduit parfois à des variables en double.

Un exemple de cas d'utilisation: disons que CommandB nécessite UserName qui est défini par CommandA . CommandA doit-il définir la clé UserNameForCommandB = John ? Ou devraient-ils partager une valeur-clé UserName = John commune ? Que faire si le nom d'utilisateur est utilisé par une troisième commande?

Comment puis-je améliorer cette conception? Merci!

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
la source
3
Je n'ai jamais eu de chance en utilisant la commande pour définir les propriétés (comme le nom). Cela commence à devenir très dépendant. Si vos propriétés de configuration tentent d'utiliser une architecture d'événement ou un modèle d'observateur.
ahenderson
1
1. Pourquoi transmettre les paramètres à un visiteur distinct? Quel est le problème de passer un contexte comme argument de performance? 2. Le contexte concerne la partie «commune» de la commande (par exemple, la session / le document en cours). Tous les paramètres spécifiques à l'opération sont mieux transmis via le constructeur de l'opération.
Kris Van Bael
@KrisVanBael, c'est la partie déroutante que j'essaie de changer. Je le passe en tant que visiteur alors qu'il s'agit en fait d'un contexte ...
Andrea Richiardi
@ahenderson Voulez-vous dire les événements entre mes commandes à droite? Souhaitez-vous y mettre vos valeurs-clés (similaires à ce que fait Android avec Parcel)? Serait-ce la même chose dans le sens où CommandA devrait créer un événement avec les paires clé-valeur acceptées par CommandB?
Andrea Richiardi

Réponses:

2

Je m'inquiète un peu de la mutabilité de vos paramètres de commande. Est-il vraiment nécessaire de créer une commande avec des paramètres en constante évolution?

Problèmes avec votre approche:

Voulez-vous que d'autres threads / commandes modifient vos paramètres pendant le performdéroulement?

Voulez - vous visitBeforeet visitAfterdu même Commandobjet à être appelé avec différents DIParameterobjets?

Voulez-vous que quelqu'un fournisse des paramètres à vos commandes, dont les commandes n'ont aucune idée?

Rien de tout cela n'est interdit par votre conception actuelle. Bien qu'un concept générique de paramètre clé-valeur ait parfois ses mérites, je ne l'aime pas par rapport à une classe de commande générique.

Exemple de conséquences:

Considérez une réalisation concrète de votre Commandclasse - quelque chose comme CreateUserCommand. Maintenant, évidemment, lorsque vous demandez la création d'un nouvel utilisateur, la commande aura besoin d'un nom pour cet utilisateur. Étant donné que je connais les CreateUserCommandet les DIParametersclasses, paramètres dois - je régler?

Je pourrais régler le userNameparamètre, ou username.. traitez-vous les paramètres de manière insensible? Je ne sais pas vraiment ... oh attends ... c'est peut-être juste name?

Comme vous pouvez le voir, la liberté que vous gagnez d'un mappage de valeur-clé générique implique que l'utilisation de vos classes en tant que personne qui ne les a pas implémentées est indûment difficile. Vous auriez au moins besoin de fournir certaines constantes pour vos commandes afin que les autres sachent quelles clés sont prises en charge par cette commande.

Différentes approches de conception possibles:

  • Paramètres immuables: en rendant vos Parameterinstances immuables, vous pouvez les réutiliser librement parmi différentes commandes.
  • Classes de paramètres spécifiques: étant donné une UserParameterclasse qui contient exactement les paramètres dont j'avais besoin pour les commandes impliquant un utilisateur, il serait beaucoup plus simple de travailler avec cette API. Vous pouvez toujours avoir un héritage sur les paramètres, mais il ne serait plus logique que les classes de commandes prennent des paramètres arbitraires - côté pro, cela signifie bien sûr que les utilisateurs de l'API savent exactement quels paramètres sont requis.
  • Une instance de commande par contexte: si vous avez besoin que vos commandes aient des choses comme visitBeforeet visitAfter, tout en les réutilisant avec différents paramètres, vous serez ouvert au problème d'être appelé avec des paramètres différents. Si les paramètres doivent être identiques sur plusieurs appels de méthode, vous devez les encapsuler dans la commande de sorte qu'ils ne puissent pas être désactivés pour d'autres paramètres entre les appels.
Franc
la source
Oui, je me suis débarrassé de visitBefore et visitAfter. Je passe essentiellement mon interface DIParameter dans la méthode perform. Le problème avec les instances DIParamters indésirables sera toujours là, car j'ai choisi d'avoir la flexibilité de passer l'interface. J'aime vraiment l'idée de pouvoir sous-classer et rendre immuables les enfants DIParameters une fois qu'ils sont remplis. Cependant, une "autorité centrale" doit encore transmettre le bon DIParameter à la commande. C'est probablement pourquoi j'ai commencé à implémenter un modèle de visiteur ... Je voulais avoir une inversion de contrôle d'une manière ou d'une autre ...
Andrea Richiardi
0

Ce qui est bien avec les principes de conception, c'est que tôt ou tard, ils entrent en conflit les uns avec les autres.

Dans la situation décrite, je pense que je préférerais aller avec une sorte de contexte dans lequel chaque commande peut prendre des informations et les mettre (en particulier s'il s'agit de paires clé-valeur). Ceci est basé sur un compromis: je ne veux pas que des commandes séparées soient couplées simplement parce qu'elles sont une sorte d'entrée les unes aux autres. À l'intérieur de CommandB, je me fiche de la façon dont UserName a été défini - juste qu'il est là pour moi. Même chose dans CommandA: j'ai mis les informations dedans, je ne veux pas savoir ce que les autres en font - ni qui ils sont.

Cela signifie une sorte de contexte passager, que vous pouvez trouver mauvais. Pour moi, l'alternative est pire, surtout si ce contexte clé-valeur simple (peut être un simple bean avec des getters et setters, pour limiter un peu le facteur "forme libre") peut permettre à la solution d'être simple et testable, avec bien commandes séparées, chacune avec sa propre logique métier.

Martin
la source
1
Quels principes trouvez-vous contradictoires ici?
Jimmy Hoffa
Juste pour clarifier, mon problème n'est pas de choisir entre le contexte ou le modèle de visiteur. J'utilise essentiellement un modèle de contexte appelé Visiteur :)
Andrea Richiardi
Ok, j'ai probablement mal compris votre question / problème exact.
Martin
0

Supposons que vous ayez une interface de commande:

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

Et un sujet:

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

Ce dont vous avez besoin c'est:

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();
    }
};

Définir NameObserver& ocomme référence à CommandB. Désormais, chaque fois que CommandA change le nom des sujets, CommandB peut s'exécuter avec les informations correctes. Si le nom est utilisé par plusieurs commandes, utilisez unstd::list<NameObserver>

ahenderson
la source
Merci d'avoir répondu. Le problème avec cette conception à mon humble avis est que nous avons besoin d'un Setter + NameObserver pour chaque paramètre. Je pourrais passer une instance de DIParameters (contexte) et notifier, mais encore une fois, je ne résoudrai probablement pas le fait que je suis toujours en train de coupler CommandA avec CommandB, ce qui signifie que CommandA doit mettre une valeur-clé que seul CommandB devrait connaître ... ce que j'ai essayé était également d'avoir une entité externe (ParameterHandler) qui est la seule à savoir quelle commande a besoin de quel paramètre et définit / obtient en conséquence sur l'instance DIParameters.
Andrea Richiardi
@Kap "Le problème avec cette conception, c'est que nous avons besoin d'un Setter + NameObserver pour chaque paramètre" - le paramètre dans ce contexte est un peu déroutant pour moi, je pense que vous vouliez dire champ. Dans ce cas, vous devriez déjà avoir un setter pour chaque champ qui change. D'après votre exemple, il semble que ComamndA change le nom du sujet. Il devrait changer le champ via un setter. Remarque: vous n'avez pas besoin d'un observateur par champ, il suffit d'avoir un getter et de passer l'objet à tous les observateurs.
ahenderson
0

Je ne sais pas si c'est la bonne façon de gérer cela sur les programmeurs (auquel cas je m'excuse), mais après avoir vérifié toutes les réponses ici (@ Frank's en particulier). J'ai refactorisé mon code de cette façon:

  • DIP paramètres abandonnés. J'aurai des objets individuels (génériques) comme entrée de DIOperation (immuable). Exemple:
classe RelatedObjectTriplet {
privé:
    std :: string const m_sPrimaryObjectId;
    std :: string const m_sSecondaryObjectId;
    std :: string const m_sRelationObjectId;

    RelatedObjectTriplet & operator = (RelatedObjectTriplet autre);

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

    RelatedObjectTriplet (Const RelatedObjectTriplet & autre);


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

    ~ RelatedObjectTriplet ();
};
  • Nouvelle classe DIOperation (avec exemple) définie comme:
modèle <classe T = void> 
classe DIOperation {
Publique:
    virtual int perform () = 0;

    virtual T getResult () = 0;

    virtuel ~ DIOperation () = 0;
};

classe CreateRelation: public DIOperation <RelatedObjectTriplet> {
privé:
    statique std :: string const TYPE;

    // Paramètres (immuables)
    RelatedObjectTriplet const m_sParams;

    // Caché
    CreateRelation & operator = (CreateRelation const & source);
    CreateRelation (CreateRelation const & source);

    // Interne
    std :: string m_sNewRelationId;

Publique:
    CreateRelation (RelatedObjectTriplet const & params);

    int perform ();

    RelatedObjectTriplet getResult ();

    ~ CreateRelation ();
};
  • Il peut être utilisé comme ceci:
RelatedObjectTriplet triplet ("33333", "55555", "77777");
CreateRelation createRel (triplet);
createRel.perform ();
const RelatedObjectTriplet res = createRel.getResult ();

Merci pour l'aide et j'espère que je n'ai pas fait d'erreur ici :)

Andrea Richiardi
la source