Ereditarietà del diamante (C ++)

So che avere ereditarietà del diamante è considerata una ctriggers pratica. Tuttavia, ho 2 casi in cui sento che l’ereditarietà del diamante potrebbe adattarsi molto bene. Voglio chiedere, mi consiglieresti di usare l’ereditarietà del diamante in questi casi, o c’è un altro disegno che potrebbe essere migliore.

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

  • L’azione può essere “Lettura” o “Scrittura”.
  • L’azione può essere con ritardo o senza ritardo (non è solo un parametro, ma modifica significativamente il comportamento).
  • Il “tipo di stream” dell’azione può essere FlowA o FlowB.

Intendo avere il seguente design:

// abstract classs 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 classs 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 {/*...*/}; //... 

Ovviamente obbedirò che nessuna azione (ereditata dalla class 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 possono anche essere letti, scritti, cancellati, ecc. Una sequenza di comandi può contenere altre sequenze di comandi.

Quindi ho il seguente design:

 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, comandi “moderni”. Sia un comando sia un comando composito possono essere moderni. Essere “Modern” aggiunge un certo elenco di proprietà a un comando e un comando composito (per lo più stesse proprietà per entrambi). Voglio essere in grado di tenere un puntatore a CommandAbstraction e inizializzarlo (tramite nuovo) in base al tipo di comando necessario. Quindi voglio fare il seguente disegno (in aggiunta 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 }; 

Di nuovo, mi assicurerò che nessuna class che erediti dalla class CommandAbstraction implementerà lo stesso metodo.

Grazie.

L’ereditarietà è la seconda più forte (più accoppiamento) delle relazioni in C ++, preceduta solo dall’amicizia. Se riesci a riprogettare usando solo la composizione, il tuo codice sarà più facilmente accoppiato. Se non puoi, allora dovresti considerare se tutte le tue classi dovrebbero realmente ereditare dalla base. È dovuto all’implementazione o solo un’interfaccia? Vuoi utilizzare qualsiasi elemento della gerarchia come elemento base? O sono solo delle foglie nella tua gerarchia che sono delle vere Azioni? Se solo le foglie sono azioni e si sta aggiungendo un comportamento, è ansible considerare la progettazione basata su criteri per questo tipo di composizione dei comportamenti.

L’idea è che comportamenti diversi (ortogonali) possono essere definiti in insiemi di classi di piccole dimensioni e quindi raggruppati insieme per fornire il vero comportamento completo. Nell’esempio considererò solo una politica che definisce se l’azione deve essere eseguita ora o in futuro e il comando da eseguire.

Fornisco una class astratta in modo che le diverse istanze del modello possano essere archiviate (tramite puntatori) in un contenitore o passate a funzioni come argomenti e vengano richiamate in modo 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 ai metodi pubblici dall’implementazione delle politiche di essere accessibili tramite l’istanziazione del modello. Ciò non consente l’uso dell’aggregazione per combinare le politiche in quanto non è ansible inserire nuovi membri della funzione nell’interfaccia di class. Nell’esempio, il modello dipende dal criterio che ha un metodo wait (), che è comune a tutti i criteri di attesa. Ora l’attesa di un periodo di tempo richiede un periodo di tempo fisso impostato tramite il metodo public ().

Nell’esempio, il criterio NoWait è solo un esempio particolare del criterio WaitSeconds con il periodo impostato su 0. Questo è stato intenzionale per indicare che l’interfaccia della politica non deve necessariamente essere la stessa. Un’altra implementazione della politica di attesa potrebbe essere in attesa su un numero di millisecondi, tick di clock o fino a qualche evento esterno, fornendo una class che si registra come callback per l’evento specificato.

Se non hai bisogno di polimorfismo, puoi eliminare dall’esempio la class base e i metodi virtuali del tutto. Sebbene questo possa sembrare eccessivamente complesso per l’esempio corrente, puoi decidere di aggiungere altre politiche al mix.

L’aggiunta di nuovi comportamenti ortogonali implicherebbe una crescita esponenziale del numero di classi se viene utilizzata l’ereditarietà semplice (con polimorfismo), con questo approccio è ansible implementare ogni singola parte separatamente e incollarla insieme nel modello di azione.

Ad esempio, è ansible rendere periodica l’azione e aggiungere una politica di uscita che determina quando uscire dal ciclo periodico. Le prime opzioni che vengono in mente sono LoopPolicy_NRuns e LoopPolicy_TimeSpan, LoopPolicy_Until. Questo metodo di policy (exit () nel mio caso) viene chiamato una volta per ogni ciclo. La prima implementazione conta il numero di volte in cui è stata definita una uscita dopo un numero fisso (fissato dall’utente, come è stato fissato nell’esempio precedente). La seconda implementazione eseguirà periodicamente il processo per un determinato periodo di tempo, mentre l’ultimo eseguirà questo processo fino a un determinato intervallo di tempo (orologio).

Se mi stai ancora seguendo fino qui, farei davvero delle modifiche. Il primo è che invece di usare un parametro template Command che implementa un metodo execute (), userei i functor e probabilmente un costruttore di template che accetta il comando da eseguire come parametro. La logica è che questo renderà molto più estensibile in combinazione con altre librerie come boost :: bind o boost :: lambda, poiché in quel caso i comandi potrebbero essere vincolati al punto di istanziazione a qualsiasi funzione libera, functor o metodo membro di una class.

Ora devo andare, ma se sei interessato posso provare a pubblicare una versione modificata.

Esiste una differenza nella qualità del design tra l’ereditarietà del diamante orientata all’implementazione dove l’implementazione è ereditata (rischiosa) e l’ereditarietà subtyping-oriented in cui le interfacce o le interfacce marker sono ereditate (spesso utili).

Generalmente, se puoi evitare il primo, stai meglio perché da qualche parte sulla linea il metodo invocato esatto può causare problemi, e l’importanza delle basi virtuali, degli stati, ecc., Inizia a fare la differenza. Infatti, Java non ti permetterebbe di estrarre qualcosa del genere, supporta solo la gerarchia dell’interfaccia.

Penso che il design “più pulito” che puoi ottenere sia quello di trasformare efficacemente tutte le tue classi nel diamante in interfacce fittizie (non avendo informazioni sullo stato e avendo metodi virtuali puri). Questo riduce l’impatto dell’ambiguità. E naturalmente, puoi usare l’ereditarietà di più e persino diamanti proprio come faresti con le implementazioni in Java.

Quindi, disporre di una serie di implementazioni concrete di queste interfacce che possono essere implementate in modi diversi (ad esempio, aggregazione, persino ereditarietà).

Incapsula questo framework in modo che i client esterni ottengano solo le interfacce e non interagiscano mai direttamente con i tipi concreti e assicurati di testare accuratamente le implementazioni.

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

Mi sono imbattuto in questo problema proprio questa settimana e ho trovato un articolo su DDJ che spiegava i problemi e quando dovevi o non dovevi preoccuparti di loro. Ecco qui:

“Eredità multipla considerata utile”

“Diamanti” nella gerarchia di ereditarietà delle interfacce è abbastanza sicuro: è l’ereditarietà del codice che ti porta in acqua calda.

Per ottenere il riutilizzo del codice, ti consiglio di prendere in considerazione i mix (google per C ++ Mixins se non hai familiarità con tequnique). Quando usi i mixin ti sembra di poter “fare shopping” per i frammenti di codice necessari per implementare la tua class senza utilizzare l’ereditarietà multipla delle classi stateful.

Quindi, il modello è – l’ereditarietà multipla delle interfacce e una singola catena di mixin (che ti dà il riutilizzo del codice) per aiutare a implementare la class concreta.

Spero possa aiutare!

Con il primo esempio …..

è se ActionRead ActionWrite debba essere una sottoclass di azione.

poiché finirai con una class concreta che sarà comunque un’azione, potresti ereditare actionread e actionwrite senza che siano azioni in se stesse.

tuttavia, potresti inventare un codice che richiederebbe che fossero azioni. Ma in generale proverei a separare Action, Read, Write e Delay e solo la class concreta mescola tutto ciò insieme

Senza sapere di più di quello che stai facendo, probabilmente riorganizzerei le cose un po ‘. Invece di ereditarietà multipla con tutte queste versioni di azione, farei lezioni di lettura e scrittura e scrittura polimorfe, instanciati come delegati.

Qualcosa come il seguente (che non ha ereditarietà del diamante):

Qui presento uno dei molti modi per implementare il Delay opzionale e presumo che la metodologia di delay sia la stessa per tutti i lettori. ciascuna sottoclass potrebbe avere una propria implementazione del ritardo, nel qual caso si passerebbe a Read e all’istanza della rispettiva class Delay derivata.

 class Action // abstract { // Reader and writer would be abstract classs (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 elimini OneCommand e consenti a CompositeCommand di avere solo un elemento, penso che il tuo design sia più semplice:

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

Hai ancora il diamante temuto, ma penso che questo possa essere un caso accettabile per questo.