Design de padrão de comando

11

Eu tenho essa implementação antiga do padrão de comando. É meio que passar um Contexto por toda a implementação da DIOperation , mas percebi mais tarde, no processo de aprendizado e aprendizado (que nunca para), que não é o ideal. Eu também acho que a "visita" aqui realmente não se encaixa e apenas confunde.

Na verdade, estou pensando em refatorar meu código, também porque um Comando não deve saber nada sobre os outros e, no momento, todos compartilham os mesmos pares de valores-chave. É realmente difícil manter qual classe possui qual valor-chave, às vezes levando à duplicação de variáveis.

Um exemplo de caso de uso: digamos que CommandB exija UserName, definido por CommandA . CommandA deve definir a chave UserNameForCommandB = John ? Ou eles devem compartilhar um valor-chave comum UserName = John ? E se o UserName for usado por um terceiro comando?

Como posso melhorar esse design? Obrigado!

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
fonte
3
Eu nunca tive sorte usando o comando para definir propriedades (como nome). Começa a se tornar muito dependente. Se suas propriedades de configuração tentarem usar uma arquitetura de eventos ou um padrão de observador.
precisa saber é o seguinte
1
1. Por que passar os parâmetros através de um visitante separado? O que há de errado em passar um contexto como argumento de execução? 2. O contexto é para a parte 'comum' do comando (por exemplo: Sessão / documento atual). Todos os parâmetros específicos da operação são melhor passados ​​pelo construtor da operação.
Kris Van Bael #
@KrisVanBael é a parte confusa que estou tentando mudar. Estou passando-o como um visitante, enquanto ele realmente é um contexto ...
Andrea Richiardi
@ahenderson Você quer dizer eventos entre os meus comandos, certo? Você colocaria seus valores-chave (como o Android faz com o Parcel)? Seria o mesmo no sentido em que CommandA deve criar um Evento com os pares de valores-chave que CommandB aceita?
precisa

Respostas:

2

Estou um pouco preocupado com a mutabilidade dos seus parâmetros de comando. É realmente necessário criar um comando com parâmetros em constante mudança?

Problemas com sua abordagem:

Deseja que outros threads / comandos alterem seus parâmetros enquanto performestá acontecendo?

Deseja visitBeforee visitAfterdo mesmo Commandobjeto ser chamado com DIParameterobjetos diferentes ?

Deseja que alguém alimente parâmetros aos seus comandos, dos quais os comandos não têm idéia?

Nada disso é proibido pelo seu design atual. Embora um conceito genérico de parâmetro de valor-chave tenha seus méritos às vezes, não gosto disso em relação a uma classe de comando genérica.

Exemplo de consequências:

Considere uma realização concreta de sua Commandclasse - algo assim CreateUserCommand. Agora, obviamente, quando você solicita a criação de um novo usuário, o comando precisará de um nome para esse usuário. Dado que eu sei que os CreateUserCommande as DIParametersaulas, qual parâmetro devo definir?

Eu poderia definir o userNameparâmetro, ou username... você trata os parâmetros sem distinção de maiúsculas e minúsculas? Eu realmente não saberia ... oh espera ... talvez seja só name?

Como você pode ver a liberdade que você ganha com um mapeamento genérico de valores-chave, implica que o uso de suas classes como alguém que não os implementou é injustificadamente difícil. Você precisaria pelo menos fornecer algumas constantes para seus comandos, para que outras pessoas saibam quais chaves são suportadas por esse comando.

Possíveis abordagens de projetos diferentes:

  • Parâmetros imutáveis: ao tornar suas Parameterinstâncias imutáveis, você pode reutilizá-las livremente entre diferentes comandos.
  • Classes de parâmetro específicas: dada uma UserParameterclasse que contém exatamente os parâmetros que eu precisaria para comandos que envolvem um usuário, seria muito mais simples trabalhar com essa API. Você ainda pode ter herança nos parâmetros, mas não faria mais sentido que as classes de comando usassem parâmetros arbitrários - do lado profissional, é claro que isso significa que os usuários da API sabem exatamente quais parâmetros são necessários.
  • Uma instância de comando por contexto: se você precisar que seus comandos tenham coisas como visitBeforee visitAfter, embora também os reutilize com parâmetros diferentes, você estará aberto ao problema de ser chamado com parâmetros diferentes. Se os parâmetros devem ser os mesmos em várias chamadas de método, é necessário encapsulá-los no comando para que eles não possam ser alternados para outros parâmetros entre as chamadas.
Frank
fonte
Sim, eu me livrei de visitBefore e visitAfter. Basicamente, estou passando minha interface DIParameter no método perform. O problema com instâncias DIParamters indesejadas sempre estará lá, porque eu escolhi ter a flexibilidade de passar a interface. Eu realmente gosto da ideia de poder subclassificar e tornar os filhos dos parâmetros DIP imutáveis ​​quando eles são preenchidos. No entanto, uma "autoridade central" ainda precisa passar o DIParameter correto para o comando. Este é provavelmente por isso que eu começou a implementar um pattern..I Visitor queria ter inversão de controle, de alguma forma ...
Andrea Richiardi
0

O que é legal nos princípios de design é que, mais cedo ou mais tarde, eles entram em conflito.

Na situação descrita, acho que prefiro ir com algum tipo de contexto em que cada comando possa obter e colocar informações (principalmente se forem pares de valores-chave). Isso se baseia em uma troca: não quero que comandos separados sejam acoplados apenas porque são algum tipo de entrada um para o outro. Dentro do CommandB, eu não me importo como o UserName foi definido - apenas existe para eu usar. A mesma coisa no CommandA: coloquei as informações, não quero saber o que os outros estão fazendo com elas - nem quem são elas.

Isso significa um tipo de contexto passageiro, que você pode achar ruim. Para mim, a alternativa é pior, especialmente se esse contexto simples de valor-chave (pode ser um bean simples com getters e setters, para limitar um pouco o fator "forma livre") pode permitir que a solução seja simples e testável, com comandos separados, cada um com sua própria lógica de negócios.

Martin
fonte
1
Quais princípios você considera conflitantes aqui?
Jimmy Hoffa
Apenas para esclarecer, meu problema não é escolher entre o padrão de contexto ou visitante. Estou basicamente usando um padrão Contexto chamado Visitor :)
Andrea Richiardi
Ok, provavelmente eu não entendi sua pergunta / problema exato.
Martin
0

Vamos supor que você tenha uma interface de comando:

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

E um assunto:

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

O que você precisa é:

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

Defina NameObserver& ocomo uma referência ao CommandB. Agora, sempre que o CommandA mudar, o nome do sujeito CommandB poderá ser executado com as informações corretas. Se o nome for usado por mais comando, use umstd::list<NameObserver>

ahenderson
fonte
Obrigado pela resposta. O problema com esse design é que precisamos de um Setter + NameObserver para cada parâmetro. Eu poderia passar uma instância DIParameters (context) e notificar, mas, novamente, provavelmente não resolverei o fato de que ainda estou acoplando CommandA a CommandB, o que significa que o CommandA precisa colocar um valor-chave que apenas o CommandB deve conhecer ... o que tentei foi também ter uma entidade externa (ParameterHandler), a única a saber qual comando precisa de qual parâmetro e define / obtém de acordo na instância DIParameters.
precisa
@ Kap "O problema com este design imho é que precisamos de um Setter + NameObserver para cada parâmetro" - parâmetro neste contexto é um pouco confuso para mim, acho que você quis dizer campo. Nesse caso, você já deve ter um setter para cada campo que muda. Do seu exemplo, parece que ComamndA muda o nome do Assunto. Deve alterar o campo através de um levantador. Nota: você não precisa de um observador por campo, basta ter um getter e passar o objeto a todos os observadores.
ahenderson
0

Não sei se essa é a maneira correta de lidar com isso nos programadores (nesse caso, peço desculpas), mas depois de ter verificado todas as respostas aqui (@ do Frank em particular). Refatorei meu código dessa maneira:

  • Parâmetros DIP descartados. Vou ter objetos individuais (genéricos) como entrada da DIOperation (imutável). Exemplo:
classe RelatedObjectTriplet {
privado:
    std :: string const m_sPrimaryObjectId;
    std :: string const m_sSecondaryObjectId;
    std :: string const m_sRelationObjectId;

    RelatedObjectTriplet & operator = (RelatedObjectTriplet outro);

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

    RelatedObjectTriplet (const; RelatedObjectTriplet e outros);


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

    ~ RelatedObjectTriplet ();
};
  • Nova classe DIOperation (com exemplo) definida como:
modelo <classe T = vazio> 
classe DIOperation {
público:
    virtual int perform () = 0;

    T virtual getResult () = 0;

    virtual ~ DIOperation () = 0;
};

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

    // Parâmetros (imutáveis)
    RelatedObjectTriplet const m_sParams;

    // Oculto
    CreateRelation & operator = (Const e origem de CreateRelation);
    CreateRelation (Const e origem de CreateRelation);

    // Interno
    std :: string m_sNewRelationId;

público:
    CreateRelation (const & params relacionados de ObjectObjectTripleto);

    int perform ();

    RelatedObjectTriplet getResult ();

    ~ CreateRelation ();
};
  • Pode ser usado assim:
Trigêmeo RelatedObjectTriplet ("33333", "55555", "77777");
CreateRelation createRel (trigêmeo);
createRel.perform ();
const RelatedObjectTriplet res = createRel.getResult ();

Obrigado pela ajuda e espero não ter cometido erros aqui :)

Andrea Richiardi
fonte