Diseño de patrón de comando

11

Tengo esta antigua implementación del patrón de comando. Es como pasar un contexto a través de toda la implementación de DIOperation , pero más tarde me di cuenta, en el proceso de aprendizaje y aprendizaje (que nunca se detiene), que no es óptimo. También creo que la "visita" aquí realmente no encaja y simplemente confunde.

Realmente estoy pensando en refactorizar mi código, también porque un Comando no debe saber nada acerca de los demás y en este momento todos comparten los mismos pares clave-valor. Es realmente difícil mantener qué clase posee qué valor clave, lo que a veces conduce a variables duplicadas.

Un ejemplo de caso de uso: digamos que CommandB requiere UserName, que se establece mediante CommandA . ¿Debería CommandA establecer la clave UserNameForCommandB = John ? ¿O deberían compartir un nombre de usuario común = valor-clave de John ? ¿Qué pasa si el nombre de usuario es utilizado por un tercer comando?

¿Cómo puedo mejorar este diseño? ¡Gracias!

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
fuente
3
Nunca he tenido suerte usando el comando para establecer propiedades (como el nombre). Comienza a volverse muy dependiente. Si sus propiedades de configuración intente utilizar una arquitectura de eventos o un patrón de observación.
ahenderson
1
1. ¿Por qué pasar los parámetros a través de un visitante separado? ¿Qué tiene de malo pasar un contexto como argumento de realizar? 2. El contexto es para la parte 'común' del comando (por ejemplo, sesión / documento actual). Todos los parámetros específicos de la operación se pasan mejor a través del constructor de la operación.
Kris Van Bael
@KrisVanBael esa es la parte confusa que estoy tratando de cambiar. Lo estoy pasando como visitante mientras que en realidad es un contexto ...
Andrea Richiardi
@ahenderson ¿Te refieres a eventos entre mis comandos, verdad? ¿Pondría sus valores clave allí (similar a lo que Android hace con Parcel)? ¿Sería lo mismo en el sentido de que CommandA debería construir un Evento con los pares clave-valor que CommandB acepta?
Andrea Richiardi

Respuestas:

2

Estoy un poco preocupado por la mutabilidad de sus parámetros de comando. ¿Es realmente necesario crear un comando con parámetros que cambian constantemente?

Problemas con su enfoque:

¿Desea que otros hilos / comandos cambien sus parámetros mientras performcontinúa?

¿Quieres visitBeforey visitAfterdel mismo Commandobjeto ser llamado con diferentes DIParameterobjetos?

¿Desea que alguien alimente los parámetros de sus comandos, sobre los cuales los comandos no tienen idea?

Nada de esto está prohibido por su diseño actual. Si bien un concepto de parámetro clave-valor genérico tiene sus ventajas a veces, no me gusta con respecto a una clase de comando genérico.

Ejemplo de consecuencias:

Considere una realización concreta de su Commandclase, algo así CreateUserCommand. Ahora, obviamente, cuando solicita que se cree un nuevo usuario, el comando necesitará un nombre para ese usuario. Dado que yo sé los CreateUserCommandy las DIParametersclases, qué parámetro he de poner?

Podría establecer el userNameparámetro, o username... ¿trata los parámetros sin distinción entre mayúsculas y minúsculas? Realmente no lo sabría ... oh, espera ... tal vez es solo name?

Como puede ver, la libertad que obtiene de un mapeo genérico de valores clave implica que usar sus clases como alguien que no las implementó es injustificadamente difícil. Al menos necesitaría proporcionar algunas constantes para sus comandos para que otros sepan qué teclas son compatibles con ese comando.

Posibles enfoques de diferentes diseños:

  • Parámetros inmutables: al convertir sus Parameterinstancias en inmutables, puede reutilizarlas libremente entre diferentes comandos.
  • Clases de parámetros específicos: dada una UserParameterclase que contiene exactamente los parámetros que necesitaría para los comandos que involucran a un usuario, sería mucho más simple trabajar con esta API. Todavía podría tener herencia en los parámetros, pero ya no tendría sentido que las clases de comando tomen parámetros arbitrarios; por supuesto, esto significa que los usuarios de API saben qué parámetros se requieren exactamente.
  • Una instancia de comando por contexto: si necesita que sus comandos tengan elementos similares visitBeforey visitAfter, al mismo tiempo que los reutiliza con diferentes parámetros, estará abierto al problema de ser llamado con diferentes parámetros. Si los parámetros deben ser los mismos en llamadas a varios métodos, debe encapsularlos en el comando de modo que no puedan cambiarse por otros parámetros entre las llamadas.
Franco
fuente
Sí, me deshice de visitBefore y visitAfter. Básicamente estoy pasando mi interfaz DIParameter en el método de ejecución. El problema con las instancias de DIParamters no deseadas siempre estará ahí, porque elegí tener la flexibilidad de pasar la interfaz. Realmente me gusta la idea de poder subclasificar y hacer que los niños de DIParameters sean inmutables una vez que están llenos. Sin embargo, una "autoridad central" todavía necesita pasar el DIParameter correcto al comando. Esta es probablemente la razón por la que comencé a implementar un patrón Visitante ... Quería tener una inversión de control de alguna manera ...
Andrea Richiardi
0

Lo bueno de los principios de diseño es que tarde o temprano entran en conflicto entre sí.

En la situación descrita, creo que preferiría ir con algún tipo de contexto en el que cada comando pueda tomar información y poner información (especialmente si son pares clave-valor). Esto se basa en una compensación: no quiero que se unan comandos separados solo porque son algún tipo de entrada entre sí. Dentro de CommandB, no me importa cómo se configuró UserName, solo que está ahí para que lo use. Lo mismo en CommandA: configuro la información, no quiero saber qué están haciendo otros con ella, ni quiénes son.

Esto significa una especie de contexto pasajero, que puedes encontrar malo. Para mí, la alternativa es peor, especialmente si este contexto clave-valor simple (puede ser un simple bean con getters y setters, para limitar un poco el factor de "forma libre") puede permitir que la solución sea simple y comprobable, con comandos separados, cada uno con su propia lógica de negocios.

Martín
fuente
1
¿Qué principios encuentras en conflicto aquí?
Jimmy Hoffa
Solo para aclarar, mi problema no es elegir entre el contexto o el patrón de visitante. Básicamente estoy usando un patrón de Contexto llamado Visitante :)
Andrea Richiardi
Ok, probablemente entendí mal tu pregunta / problema exacto.
Martin
0

Supongamos que tiene una interfaz de comando:

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

Y un tema:

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

Lo que necesitas es:

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

Establecer NameObserver& ocomo referencia a CommandB. Ahora, cuando CommandA cambia, el nombre de Sujetos CommandB puede ejecutarse con la información correcta. Si el nombre es usado por más comandos, use unstd::list<NameObserver>

Ahenderson
fuente
Gracias por la respuesta. El problema con este diseño es que necesitamos un Setter + NameObserver por cada parámetro. Podría pasar una instancia de DIParameters (contexto) y notificar, pero de nuevo, probablemente no resolveré el hecho de que todavía estoy acoplando CommandA con CommandB, lo que significa que CommandA tiene que poner un valor clave que solo CommandB debería saber ... lo que intenté fue también tener una entidad externa (ParameterHandler) que es la única que sabe qué comando necesita qué parámetro y establece / obtiene en consecuencia en la instancia de DIParameters.
Andrea Richiardi
@Kap "El problema con este diseño en mi humilde opinión es que necesitamos un Setter + NameObserver por cada parámetro" - el parámetro en este contexto es un poco confuso para mí, creo que querías decir campo. En cuyo caso, ya debería tener un setter para cada campo que cambia. Según su ejemplo, parece que ComamndA cambia el nombre del Asunto. Debería cambiar el campo a través de un setter. Nota: no necesita un observador por campo, solo tenga un captador y pase el objeto a todos los observadores.
ahenderson
0

No sé si esta es la forma correcta de manejar esto en los Programadores (en cuyo caso me disculpo), pero, después de haber revisado todas las respuestas aquí (@ Frank en particular). Refactoré mi código de esta manera:

  • DIParameters caídos. Tendré objetos individuales (genéricos) como entrada de DIOperation (inmutable). Ejemplo:
clase RelatedObjectTriplet {
privado:
    std :: string const m_sPrimaryObjectId;
    std :: string const m_sSecondaryObjectId;
    std :: string const m_sRelationObjectId;

    RelatedObjectTriplet & operator = (RelatedObjectTriplet otro);

público:
    RelatedObjectTriplet (std :: string const & sPrimaryObjectId,
                         std :: string const & sSecondaryObjectId,
                         std :: string const & sRelationObjectId);

    RelatedObjectTriplet (RelatedObjectTriplet const y otros);


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

    ~ RelatedObjectTriplet ();
};
  • Nueva clase DIOperation (con ejemplo) definida como:
plantilla <clase T = vacío> 
clase DIOperation {
público:
    virtual int perform () = 0;

    virtual T getResult () = 0;

    virtual ~ DIOperation () = 0;
};

clase CreateRelation: public DIOperation <RelatedObjectTriplet> {
privado:
    static std :: string const TYPE;

    // Parámetros (inmutables)
    RelatedObjectTriplet const m_sParams;

    // Oculto
    CreateRelation & operator = (CrearRelación constante y fuente);
    CreateRelation (CreateRelation const y fuente);

    // interno
    std :: string m_sNewRelationId;

público:
    CreateRelation (RelatedObjectTriplet const & params);

    int realizar ();

    RelatedObjectTriplet getResult ();

    ~ CreateRelation ();
};
  • Se puede usar así:
RelatedObjectTriplet triplet ("33333", "55555", "77777");
CreateRelation createRel (triplete);
createRel.perform ();
const RelatedObjectTriplet res = createRel.getResult ();

Gracias por la ayuda y espero no haber cometido errores aquí :)

Andrea Richiardi
fuente