Domanda

So che avere un'eredità di diamanti è considerata una cattiva pratica.Tuttavia, ho 2 casi in cui ritengo che l'eredità dei diamanti potrebbe adattarsi molto bene.Vorrei chiederti se mi consiglieresti di utilizzare l'ereditarietà dei diamanti in questi casi o esiste un altro design che potrebbe essere migliore.

Caso 1: Voglio creare classi che rappresentino diversi tipi di "Azioni" nel mio sistema.Le azioni sono classificate in base a diversi parametri:

  • L'azione può essere "Leggi" o "Scrivi".
  • L'azione può essere con ritardo o senza ritardo (non è solo 1 parametro.Cambia il comportamento in modo significativo).
  • Il "tipo di flusso" dell'azione può essere FlowA o FlowB.

Intendo avere il seguente design:

// abstract classes
class Action  
{
    // methods relevant for all actions
};
class ActionRead      : public virtual Action  
{
    // methods related to reading
};
class ActionWrite     : public virtual Action  
{
    // methods related to writing
};
class ActionWithDelay : public virtual Action  
{
    // methods related to delay definition and handling
};
class ActionNoDelay   : public virtual Action  {/*...*/};
class ActionFlowA     : public virtual Action  {/*...*/};
class ActionFlowB     : public virtual Action  {/*...*/};

// concrete classes
class ActionFlowAReadWithDelay  : public ActionFlowA, public ActionRead, public ActionWithDelay  
{
    // implementation of the full flow of a read command with delay that does Flow A.
};
class ActionFlowBReadWithDelay  : public ActionFlowB, public ActionRead, public ActionWithDelay  {/*...*/};
//...

Naturalmente, obbedirò al fatto che nessuna delle due azioni (che eredita dalla classe Action) implementerà lo stesso metodo.

Caso 2: Implemento il modello di progettazione composito per un "comando" nel mio sistema.Un comando può essere letto, scritto, cancellato, ecc.Voglio anche avere una sequenza di comandi, che può anche essere letta, scritta, cancellata, ecc.Una sequenza di comandi può contenere altre sequenze di comandi.

Quindi ho il seguente disegno:

class CommandAbstraction
{
    CommandAbstraction(){};
    ~CommandAbstraction()=0;
    void Read()=0;
    void Write()=0;
    void Restore()=0;
    bool IsWritten() {/*implemented*/};
    // and other implemented functions
};

class OneCommand : public virtual CommandAbstraction
{
    // implement Read, Write, Restore
};

class CompositeCommand : public virtual CommandAbstraction
{
    // implement Read, Write, Restore
};

Inoltre, ho un tipo speciale di comandi, i comandi "moderni".Sia un comando che un comando composito possono essere moderni.Essere "Moderno" aggiunge un certo elenco di proprietà a un comando e a un comando composito (per lo più le stesse proprietà per entrambi).Voglio essere in grado di contenere un puntatore a CommandAbstraction e inizializzarlo (tramite new) in base al tipo di comando necessario.Quindi voglio fare il seguente progetto (oltre a quanto sopra):

class ModernCommand : public virtual CommandAbstraction
{
    ~ModernCommand()=0;
    void SetModernPropertyA(){/*...*/}
    void ExecModernSomething(){/*...*/}
    void ModernSomethingElse()=0;

};
class OneModernCommand : public OneCommand, public ModernCommand
{
    void ModernSomethingElse() {/*...*/};
    // ... few methods specific for OneModernCommand
};
class CompositeModernCommand : public CompositeCommand, public ModernCommand
{
    void ModernSomethingElse() {/*...*/};
    // ... few methods specific for CompositeModernCommand
};

Ancora una volta, mi assicurerò che nessuna delle due classi che ereditano dalla classe CommandAbstraction implementi lo stesso metodo.

Grazie.

È stato utile?

Soluzione

ereditarietà è rapporti secondo più forte (più accoppiamento) in C ++, preceduta solo da amici. Se si può ridisegnare in utilizzando solo la composizione vostro codice sarà più debolmente accoppiati. Se non è possibile, allora si dovrebbe considerare se tutte le classi in realtà dovrebbe ereditare dalla base. E 'a causa di esecuzione o semplicemente un'interfaccia? Will si desidera utilizzare qualsiasi elemento della gerarchia come elemento di base? O sono le foglie solo nella vostra gerarchia che sono reali azione di? Se solo foglie sono azioni e si stanno aggiungendo comportamento si può considerare di design con sede politica per questo tipo di composizione di comportamenti.

L'idea è che i comportamenti (ortogonali) diversi possono essere definiti in gruppi di classe piccola e poi in bundle insieme per fornire il comportamento reale completa. Nell'esempio mi prenderà in considerazione solo una politica che definisce se l'azione deve essere eseguita ora o in futuro, e il comando da eseguire.

I fornire una classe astratta in modo che diverse istanze del modello possono essere memorizzati (tramite puntatori) in un contenitore o passati alle funzioni come argomenti e vengono chiamati polimorfico.

class ActionDelayPolicy_NoWait;

class ActionBase // Only needed if you want to use polymorphically different actions
{
public:
    virtual ~Action() {}
    virtual void run() = 0;
};

template < typename Command, typename DelayPolicy = ActionDelayPolicy_NoWait >
class Action : public DelayPolicy, public Command
{
public:
   virtual run() {
      DelayPolicy::wait(); // inherit wait from DelayPolicy
      Command::execute();  // inherit command to execute
   }
};

// Real executed code can be written once (for each action to execute)
class CommandSalute
{
public:
   void execute() { std::cout << "Hi!" << std::endl; }
};

class CommandSmile
{
public:
   void execute() { std::cout << ":)" << std::endl; }
};

// And waiting behaviors can be defined separatedly:
class ActionDelayPolicy_NoWait
{
public:
   void wait() const {}
};

// Note that as Action inherits from the policy, the public methods (if required)
// will be publicly available at the place of instantiation
class ActionDelayPolicy_WaitSeconds
{
public:
   ActionDelayPolicy_WaitSeconds() : seconds_( 0 ) {}
   void wait() const { sleep( seconds_ ); }
   void wait_period( int seconds ) { seconds_ = seconds; }
   int wait_period() const { return seconds_; }
private:
   int seconds_;
};

// Polimorphically execute the action
void execute_action( Action& action )
{
   action.run();
}

// Now the usage:
int main()
{
   Action< CommandSalute > salute_now;
   execute_action( salute_now );

   Action< CommandSmile, ActionDelayPolicy_WaitSeconds > smile_later;
   smile_later.wait_period( 100 ); // Accessible from the wait policy through inheritance
   execute_action( smile_later );
}

L'uso dell'ereditarietà consente metodi pubblici dalle implementazioni di politiche che siano accessibili tramite il modello di istanza. Questo non consente l'uso di aggregazione per combinare le politiche come nessun nuovo membri funzioni potrebbero essere spinti nell'interfaccia classe. Nell'esempio, il modello dipende dalla politica di avere un metodo wait (), che è comune a tutte le politiche di attesa. Ora in attesa di un periodo di tempo ha bisogno di un periodo di tempo fisso che viene impostato attraverso il periodo () metodo pubblico.

Nell'esempio, la politica nowait è solo un esempio particolare della politica WaitSeconds con il periodo impostato a 0. Questo è stato intenzionale per marcare che l'interfaccia politica non ha bisogno di essere lo stesso. Un'altra implementazione politica attesa potrebbe essere in attesa di un numero di millisecondi, orologio zecche, o fino a qualche evento esterno, fornendo una classe che registra come un callback per l'evento dato.

Se non è necessario il polimorfismo si può prendere l'esempio della classe di base e dei metodi virtuali del tutto. Mentre questo può sembrare troppo complesso per l'esempio corrente, è possibile decidere di aggiungere altre politiche per il mix.

Mentre l'aggiunta di nuovi comportamenti ortogonali implicherebbe una crescita esponenziale del numero di classi, se l'eredità pianura viene utilizzato (con il polimorfismo), con questo approccio si può semplicemente implementare ogni parte diversa separatamente e incollare insieme nel modello di azione.

Per esempio, è possibile rendere la vostra azione periodica e aggiungere un'exit policy che determinano quando per uscire dal ciclo periodico. Le prime opzioni che vengono in mente sono LoopPolicy_NRuns e LoopPolicy_TimeSpan, LoopPolicy_Until. Questo metodo della politica (exit () nel mio caso) viene chiamato una volta per ogni ciclo. La prima implementazione conta il numero di volte che è stato chiamato un uscite dopo un numero fisso (fisso dall'utente, come periodo è stato fissato nell'esempio precedente). La seconda applicazione potrebbe eseguire periodicamente il processo per un determinato periodo di tempo, mentre l'ultimo verrà eseguito questo processo finché un determinato tempo (orologio).

Se si sta ancora seguendo me fino a qui, vorrei anzi apportare alcune modifiche. Il primo è che invece di utilizzare un parametro di comando modello che implementa un metodo execute () vorrei usare funtori e, probabilmente, un costruttore su modelli che prende il comando da eseguire come parametro. La logica è che questo renderà molto più estensibile in combinazione con altre librerie come boost :: bind o amplificare :: lambda poiché in questo caso i comandi potrebbero essere vincolate al punto di istanziazione a qualsiasi funzione, funtore o metodo membro gratuito di una classe.

Ora devo andare, ma se siete interessati posso provare la pubblicazione di una versione modificata.

Altri suggerimenti

C'è una differenza di progettazione di qualità tra eredità diamante implementazione orientata cui attuazione è ereditato (rischioso), e subtyping orientata eredità in cui sono ereditati interfacce o marcatori interfacce (spesso utile).

In generale, se si può evitare il primo, è meglio in quanto da qualche parte lungo la linea del metodo richiamato esatto può causare problemi, e l'importanza di basi virtuali, membri, ecc, inizia mattering. In realtà, Java non avrebbe permesso di tirare qualcosa di simile, supporta solo la gerarchia di interfaccia.

Credo che il "più pulita" DESIGN si può trovare per questo è di trasformare in modo efficace tutte le classi nel diamante in mock-interfacce (da non avere informazioni di stato, e avendo metodi virtuali puri). Ciò riduce l'impatto di ambiguità. E, naturalmente, è possibile utilizzare più e anche il diamante eredità per questo, proprio come si usa attrezzi in Java.

Quindi, avere una serie di implementazioni concrete di queste interfacce che possono essere implementate in modo diverso (ad esempio, l'aggregazione, anche ereditarietà).

Incapsula questo quadro in modo che i client esterni ottengono solo le interfacce e mai interagire direttamente con i tipi concreti, e assicurarsi di testare a fondo le vostre implementazioni.

Naturalmente, questo è un sacco di lavoro, ma se si sta scrivendo un API centrale e riutilizzabile, questo potrebbe essere la soluzione migliore.

Mi sono imbattuto in questo problema proprio questa settimana e ho trovato un articolo su DDJ che ha spiegato i problemi e quando si dovrebbe o non dovrebbe essere preoccupato per loro. Eccolo:

"ereditarietà multipla considerato utile"

I "diamanti" nella gerarchia di ereditarietà delle interfacce sono abbastanza sicuri: è l'eredità del codice che ti mette nei guai.

Per ottenere il riutilizzo del codice, ti consiglio di prendere in considerazione i mixin (google per C++ Mixins se non hai familiarità con la tecnica).Quando usi i mixin hai la sensazione di poter "andare a fare shopping" per i frammenti di codice necessari per implementare la tua classe senza utilizzare l'ereditarietà multipla delle classi con stato.

Quindi, il modello è: ereditarietà multipla delle interfacce e una singola catena di mixin (che fornisce il riutilizzo del codice) per aiutare a implementare la classe concreta.

Spero che aiuti!

Con il primo esempio .....

la sua se ActionRead ActionWrite bisogno di essere sottoclassi di azione a tutti.

dal momento che si sta andando a finire con una classe concreta che sarà un'azione in ogni modo, si può solo ereditare actionread e actionwrite senza che esse siano le azioni in loro stessi.

, però, si potrebbe inventare codice che richiedono loro di essere azioni. Ma in generale mi piacerebbe provare e azione separata, leggere, scrivere e Delay e solo la classe concreta mescola tutto insieme

Con fuori sapere più di quello che si sta facendo, Io probabilmente riorganizzare le cose un po '. Invece di ereditarietà multipla con tutte queste versioni di azione, Vorrei fare la lettura polimorfica e corsi di scrittura e la scrittura, istanziare come delegati.

Qualcosa di simile a quanto segue (che non ha ereditarietà diamante):

Qui presento uno dei tanti modi di attuazione opzionale Delay, e assumere la metodologia ritardo è lo stesso per tutti i lettori. ogni sottoclasse potrebbe avere una propria implementazione di ritardo nel qual caso si passa verso il basso per leggere e istanza del rispettiva classe derivata Delay.

class Action // abstract
{
   // Reader and writer would be abstract classes (if not interfaces)
   // from which you would derive to implement the specific
   // read and write protocols.

   class Reader // abstract
   {
      Class Delay {...};
      Delay *optional_delay; // NULL when no delay
      Reader (bool with_delay)
      : optional_delay(with_delay ? new Delay() : NULL)
      {};
      ....
   };

   class Writer {... }; // abstract

   Reader  *reader; // may be NULL if not a reader
   Writer  *writer; // may be NULL if not a writer

   Action (Reader *_reader, Writer *_writer)
   : reader(_reader)
   , writer(_writer)
   {};

   void read()
   { if (reader) reader->read(); }
   void write()
   { if (writer)  writer->write(); }
};


Class Flow : public Action
{
   // Here you would likely have enhanced version
   // of read and write specific that implements Flow behaviour
   // That would be comment to FlowA and FlowB
   class Reader : public Action::Reader {...}
   class Writer : public Action::Writer {...}
   // for Reader and W
   Flow (Reader *_reader, Writer *_writer)
   : Action(_reader,_writer)
   , writer(_writer)
   {};
};

class FlowA :public Flow  // concrete
{
    class Reader : public Flow::Reader {...} // concrete
    // The full implementation for reading A flows
    // Apparently flow A has no write ability
    FlowA(bool with_delay)
    : Flow (new FlowA::Reader(with_delay),NULL) // NULL indicates is not a writer
    {};
};

class FlowB : public Flow // concrete
{
    class Reader : public Flow::Reader {...} // concrete
    // The full implementation for reading B flows
    // Apparently flow B has no write ability
    FlowB(bool with_delay)
    : Flow (new FlowB::Reader(with_delay),NULL) // NULL indicates is not a writer
    {};
};

Per il caso 2, non è un OneCommand solo un caso speciale di CompositeCommand? Se si eliminano OneCommand e permettere CompositeCommands di avere un solo elemento, credo che il vostro disegno diventa più semplice:

              CommandAbstraction
                 /          \
                /            \
               /              \
        ModernCommand      CompositeCommand
               \               /
                \             /
                 \           /
             ModernCompositeCommand

Hai ancora il diamante temuta, ma credo che questo possa essere un caso accettabile per esso.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top