Disegno del modello di comando


11

Ho questa vecchia implementazione del modello di comando. È un po 'passare un contesto attraverso tutta l' implementazione DIOperation , ma in seguito mi sono reso conto, nel processo di apprendimento e apprendimento (che non si ferma mai), che non è ottimale. Penso anche che la "visita" qui non si adatti davvero e confonda.

In realtà sto pensando di refactoring del mio codice, anche perché un comando non dovrebbe sapere nulla degli altri e al momento condividono tutti le stesse coppie chiave-valore. È davvero difficile mantenere quale classe possiede quale valore-chiave, a volte portando a variabili duplicate.

Un esempio di caso d'uso: diciamo che CommandB richiede UserName impostato da CommandA . CommandA dovrebbe impostare la chiave UserNameForCommandB = John ? O dovrebbero condividere un UserName comune = valore-chiave John ? Cosa succede se UserName viene utilizzato da un terzo comando?

Come posso migliorare questo design? Grazie!

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

3
Non ho mai avuto fortuna usando il comando per impostare le proprietà (come il nome). Inizia a diventare molto dipendente. Se le proprietà delle impostazioni provano a utilizzare un'architettura di eventi o un modello di osservatore.
ahenderson,

1
1. Perché passare i parametri attraverso un visitatore separato? Cosa c'è di sbagliato nel passare un contesto come argomento di esibizione? 2. Il contesto è per la parte "comune" del comando (es. Sessione / documento corrente). Tutti i parametri specifici dell'operazione vengono passati meglio attraverso il costruttore dell'operazione.
Kris Van Bael

@KrisVanBael questa è la parte confusa che sto cercando di cambiare. Lo sto passando come Visitatore mentre in realtà è un contesto ...
Andrea Richiardi,

@ahenderson Intendi eventi tra i miei comandi giusto? Metteresti lì i tuoi valori-chiave (simile a quello che Android fa con Parcel)? Sarebbe lo stesso nel senso che CommandA dovrebbe creare un evento con le coppie chiave-valore accettate da CommandB?
Andrea Richiardi,

Risposte:


2

Sono un po 'preoccupato per la mutabilità dei tuoi parametri di comando. È davvero necessario creare un comando con parametri in costante cambiamento?

Problemi con il tuo approccio:

Vuoi che altri thread / comandi cambino i tuoi parametri mentre performè in corso?

Vuoi visitBeforee visitAfterdello stesso Commandoggetto ad essere chiamato con diversi DIParameteroggetti?

Vuoi che qualcuno dia dei parametri ai tuoi comandi, di cui i comandi non hanno idea?

Niente di tutto ciò è proibito dal tuo progetto attuale. Mentre a volte un concetto di parametro di valore-chiave generico ha i suoi meriti, non mi piace rispetto a una classe di comando generica.

Esempio di conseguenze:

Considera una realizzazione concreta della tua Commandclasse - qualcosa del genere CreateUserCommand. Ora ovviamente, quando richiedi la creazione di un nuovo utente, il comando avrà bisogno di un nome per quell'utente. Dato che so che le CreateUserCommande le DIParametersclassi, quale parametro devo impostare?

Potrei impostare il userNameparametro o username... trattate i parametri in modo insensibile? Non lo saprei davvero .. oh aspetta .. forse è solo name?

Come puoi vedere, la libertà che ottieni da una generica mappatura dei valori-chiave implica che usare le tue classi come qualcuno che non le ha implementate è ingiustamente difficile. Dovresti almeno fornire alcune costanti per i tuoi comandi per far sapere ad altri quali chiavi sono supportate da quel comando.

Possibili approcci di progettazione diversi:

  • Parametri immutabili: rendendo Parameterimmutabili le istanze è possibile riutilizzarle liberamente tra diversi comandi.
  • Classi di parametri specifici: data una UserParameterclasse che contiene esattamente i parametri di cui avrei bisogno per i comandi che coinvolgono un utente, sarebbe molto più semplice lavorare con questa API. Potresti comunque avere ereditarietà sui parametri, ma non avrebbe più senso che le classi di comando prendano parametri arbitrari - dal lato pro ovviamente questo significa che gli utenti API sanno quali parametri sono richiesti esattamente.
  • Un'istanza di comando per contesto: se hai bisogno che i tuoi comandi abbiano cose simili visitBeforee visitAfter, riutilizzandoli anche con parametri diversi, sarai aperto al problema di essere chiamato con parametri diversi. Se i parametri devono essere gli stessi su più chiamate di metodo, è necessario incapsularli nel comando in modo tale che non possano essere scambiati per altri parametri tra le chiamate.

Sì, mi sono sbarazzato di visitBefore e visitAfter. Fondamentalmente sto passando la mia interfaccia DIParameter nel metodo perform. Il problema con istanze DIParamters indesiderate sarà sempre presente, perché ho scelto di avere la flessibilità di passare l'interfaccia. Mi piace molto l'idea di poter sottoclassare e rendere immutabili i bambini DIParameters una volta riempiti. Tuttavia, una "autorità centrale" deve ancora passare il DIParameter corretto al comando. Questo è probabilmente il motivo per cui ho iniziato a implementare un modello di Visitatore ... Volevo avere l'inversione del controllo in qualche modo ...
Andrea Richiardi,

0

La cosa bella dei principi di progettazione è che prima o poi sono in conflitto tra loro.

Nella situazione descritta, penso che preferirei andare con un qualche tipo di contesto dal quale ogni comando può prendere informazioni e inserire informazioni (specialmente se quelle sono coppie chiave-valore). Questo si basa su un compromesso: non voglio accoppiare comandi separati solo perché sono una sorta di input reciproco. All'interno di CommandB, non mi interessa come è stato impostato UserName - solo che è lì per me da usare. Stessa cosa in CommandA: inserisco le informazioni, non voglio sapere cosa ne fanno gli altri, né chi sono.

Questo significa una sorta di contesto di passaggio, che puoi trovare male. Per me, l'alternativa è peggio, soprattutto se questo semplice contesto chiave-valore (può essere un semplice bean con getter e setter, per limitare un po 'il fattore "forma libera") può consentire alla soluzione di essere semplice e testabile, con comandi separati, ognuno con la propria logica aziendale.


1
Quali principi trovi contrastanti qui?
Jimmy Hoffa

Giusto per chiarire, il mio problema non è scegliere tra il contesto o il modello Visitatore. In pratica sto usando un modello di contesto chiamato Visitatore :)
Andrea Richiardi,

Ok, probabilmente ho frainteso la tua esatta domanda / problema.
Martin

0

Supponiamo che tu abbia un'interfaccia di comando:

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

E un argomento:

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

Ciò di cui hai bisogno è:

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

Imposta NameObserver& ocome riferimento a CommandB. Ora, ogni volta che CommandA cambia il nome del soggetto, CommandB può eseguire con le informazioni corrette. Se il nome viene utilizzato da più comandi, utilizzare astd::list<NameObserver>


Grazie per la risposta. Il problema con questo disegno imho è che abbiamo bisogno di un Setter + NameObserver per ogni parametro. Potrei passare un'istanza DIParameters (contesto) e notificarlo, ma di nuovo, probabilmente non risolverò il fatto che sto ancora accoppiando CommandA con CommandB, il che significa che CommandA deve mettere un valore-chiave che solo CommandB dovrebbe conoscere ... quello che ho provato è stato anche avere un'entità esterna (ParameterHandler) che è l'unica a sapere quale comando ha bisogno di quale parametro e imposta / ottiene di conseguenza sull'istanza DIParameters.
Andrea Richiardi,

@Kap "Il problema con questo imho di progettazione è che abbiamo bisogno di un Setter + NameObserver per ogni parametro" - il parametro in questo contesto è un po 'confuso per me, penso che tu intendessi campo. Nel qual caso dovresti già avere un setter per ogni campo che cambia. Dal tuo esempio sembra che ComamndA cambi il nome del Soggetto. Dovrebbe cambiare il campo tramite un setter. Nota: non è necessario un osservatore per campo, basta avere un getter e passare l'oggetto a tutti gli osservatori.
ahenderson,

0

Non so se questo è il modo giusto di gestirlo sui programmatori (nel qual caso mi scuso), ma, dopo aver verificato tutte le risposte qui (@ Frank's in particolare). Ho riformattato il mio codice in questo modo:

  • DIParametri caduti. Avrò singoli oggetti (generici) come input di DIOperation (immutabile). Esempio:
class RelatedObjectTriplet {
privato:
    std :: string const m_sPrimaryObjectId;
    std :: string const m_sSecondaryObjectId;
    std :: string const m_sRelationObjectId;

    RelatedObjectTriplet & operator = (RelatedObjectTriplet altro);

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

    RelatedObjectTriplet (RelatedObjectTriplet const e altro);


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

    ~ RelatedObjectTriplet ();
};
  • Nuova classe DIOperation (con esempio) definita come:
template <class T = void> 
classe DIOperazione {
pubblico:
    virtual int perform () = 0;

    T virtuale getResult () = 0;

    virtual ~ DIOperation () = 0;
};

class CreateRelation: public DIOperation <RelatedObjectTriplet> {
privato:
    statico std :: string const TYPE;

    // Params (immutabile)
    RelatedObjectTriplet const m_sParams;

    // Nascosto
    CreateRelation & operator = (const e origine di CreateRelation);
    CreateRelation (const & source di CreateRelation);

    // Interno
    std :: string m_sNewRelationId;

pubblico:
    CreateRelation (const & params di RelatedObjectTriplet);

    int perform ();

    RelatedObjectTriplet getResult ();

    ~ CreateRelation ();
};
  • Può essere usato in questo modo:
RelatedObjectTriplet triplet ("33333", "55555", "77777");
CreateRelation createRel (triplet);
createRel.perform ();
const RelatedObjectTriplet res = createRel.getResult ();

Grazie per l'aiuto e spero di non aver fatto errori qui :)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.