Question

Je sais que d'avoir l'héritage de diamant est considéré comme une mauvaise pratique. Cependant, j'ai 2 cas où je sens que l'héritage de diamant pourrait correspondre très bien. Je veux demander, voulez-vous me recommander d'utiliser l'héritage de diamant dans ces cas, ou est-il une autre conception qui pourrait être mieux.

Cas 1: Je veux créer des classes qui représentent différents types de "Actions" dans mon système. Les actions sont classées par plusieurs paramètres:

  • L'action peut être "Lire" ou "Write".
  • L'action peut être avec retard ou sans délai (Il est à seulement 1 paramètre. Il modifie le comportement de manière significative).
  • peut être FLOWA ou FlowB "type de flux" de l'action.

Je l'intention d'avoir la conception suivante:

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

Bien sûr, je vais obéir à ce qu'aucun 2 actions (héritant de la classe d'action) mettra en œuvre la même méthode.

Cas n ° 2: Je mets en œuvre le modèle de conception composite pour une "commande" dans mon système. Une commande peut être lu, écrit, supprimé, etc. Je veux aussi avoir une séquence de commandes, qui peut aussi être lu, écrit, supprimé, etc. Une séquence de commandes peut contenir d'autres séquences de commandes.

J'ai donc la conception suivante:

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

En outre, j'ai un type spécial de commandes, les commandes « modernes ». Les deux une commande et commande composite peut être moderne. Être « moderne », ajoute une certaine liste de propriétés à une commande et commande composite (propriétés essentiellement identiques pour les deux d'entre eux). Je veux être en mesure de tenir un pointeur vers CommandAbstraction, et l'initialiser (via nouveau) en fonction du type de commande nécessaire. Donc, je veux faire la conception suivante (en plus de ce qui précède):

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

Encore une fois, je veillerai à ce qu'il n'y a pas 2 classes héritant de la classe CommandAbstraction mettra en œuvre la même méthode.

Merci.

Était-ce utile?

La solution

La transmission est le deuxième plus fort relations (plus) en couplage C ++, précédés seulement par amitié. Si vous pouvez redessiner en utilisant uniquement la composition de votre code sera plus lâchement couplé. Si vous ne pouvez pas, alors vous devriez examiner si toutes vos classes devraient vraiment hériter de la base. Est-ce en raison de la mise en œuvre ou juste une interface? Voulez-vous utiliser tout élément de la hiérarchie comme un élément de base? Ou sont les feuilles juste dans la hiérarchie qui sont réels d'action de? Si seulement les feuilles sont des actions et que vous ajoutez des comportements que vous pouvez envisager la conception à base de règles pour ce type de composition des comportements.

L'idée est que les différents comportements (orthogonaux) peuvent être définies dans de petits ensembles de classe puis regroupés pour fournir le comportement réel complet. Dans l'exemple que je considérerai juste une politique qui définit si l'action doit être exécutée dès à présent ou à l'avenir, et la commande à exécuter.

Je donne une classe abstraite de sorte que les différentes instanciations du modèle peuvent être stockées (par le biais des pointeurs) dans un récipient ou transmis à des fonctions comme arguments et s'appellent polymorphically.

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'utilisation de l'héritage permet aux méthodes publiques des mises en œuvre de la politique pour être accessible par le modèle instanciation. Ceci interdit l'utilisation de l'agrégation pour combiner les politiques comme aucun nouveau membre de fonction pourraient être poussés dans l'interface de classe. Dans l'exemple, le modèle dépend de la politique ayant une méthode d'attente (), qui est commun à toutes les politiques d'attente. attend maintenant une période de temps a besoin d'une période de temps fixe qui est fixé par la période () méthode public.

Dans l'exemple, la politique de NoWait est juste un exemple particulier de la politique WaitSeconds avec la période à 0. Ce fut intentionnel pour marquer que l'interface de la politique n'a pas besoin d'être le même. Une autre mise en œuvre de la politique d'attente pourrait être en attente sur un certain nombre de millisecondes, les tiques horloge, ou jusqu'à ce qu'un événement extérieur, en fournissant une classe qui enregistre comme un rappel pour l'événement donné.

Si vous n'avez pas besoin polymorphisme, vous pouvez prendre l'exemple de la classe de base et les méthodes virtuelles tout à fait. Même si cela peut sembler trop complexe pour l'exemple actuel, vous pouvez décider d'ajouter d'autres politiques au mélange.

Lors de l'ajout de nouveaux comportements orthogonaux impliquerait une croissance exponentielle du nombre de classes si l'héritage ordinaire est utilisé (avec polymorphisme), avec cette approche, vous pouvez simplement mettre en œuvre chaque différentes parties séparément et coller ensemble dans le modèle d'action.

Par exemple, vous pouvez faire votre action périodique et ajouter une stratégie de sortie qui déterminent quand sortir de la boucle périodique. premières options qui viennent à l'esprit sont LoopPolicy_NRuns et LoopPolicy_TimeSpan, LoopPolicy_Until. Cette méthode politique (sortie () dans mon cas) est appelée une fois pour chaque boucle. La première mise en œuvre compte le nombre de fois où il a été appelé après les sorties d'un nombre fixe (fixé par l'utilisateur, comme période a été fixée dans l'exemple ci-dessus). La deuxième mise en œuvre courrait périodiquement le processus pour une période de temps donnée, tandis que le dernier se déroulera ce processus jusqu'à un moment donné (horloge).

Si vous êtes toujours me suivez jusqu'ici, je fait faire quelques changements. La première est qu'au lieu d'utiliser un paramètre modèle de commande qui implémente une méthode d'exécution () j'utiliser foncteurs et probablement un constructeur qui prend la templated commande à exécuter en tant que paramètre. La raison est que cela fera beaucoup plus extensible en combinaison avec d'autres bibliothèques comme boost :: bind ou boost :: lambda, puisque dans cette commande de cas pourraient être liés au point de instanciation à une fonction libre, foncteur ou méthode membre d'une classe.

Maintenant, je dois y aller, mais si vous êtes intéressé je peux essayer de poster une version modifiée.

Autres conseils

Il y a une différence de qualité de conception entre l'héritage de diamant axé sur la mise en œuvre, où la mise en œuvre est héritée (risqué) et l'héritage orienté sous-typage où les interfaces ou les interfaces de marqueurs génétiques sont héritées (souvent utile).

En général, si vous pouvez éviter le premier, vous êtes mieux depuis quelque part sur la ligne de la méthode invoquée exacte peut causer des problèmes, et l'importance des bases virtuelles, états, etc., commence mattering. En fait, Java ne vous permettra de tirer quelque chose comme ça, il ne supporte que la hiérarchie d'interface.

Je pense que le « plus propre » conception que vous pouvez venir pour cela est de transformer efficacement toutes vos classes dans le diamant en interfaces maquette (en ayant aucune information d'état, et ayant des méthodes virtuelles pures). Cela réduit l'impact de l'ambiguïté. Et bien sûr, vous pouvez utiliser plusieurs et même héritage diamant pour cela juste comme vous utilisez des outils en Java.

Ensuite, un ensemble d'implémentations concrètes de ces interfaces qui peuvent être mises en œuvre de différentes façons (par exemple, l'agrégation, même héritage).

Encapsulate ce cadre afin que les clients externes ne reçoivent que les interfaces et ne jamais interagir directement avec les types de béton, et assurez-vous de bien tester vos implémentations.

Bien sûr, cela est beaucoup de travail, mais si vous écrivez une API centrale et réutilisable, cela pourrait être votre meilleur pari.

Je suis tombé sur ce problème cette semaine et a trouvé un article sur DDJ qui a expliqué les problèmes et quand vous devriez ou ne devrait pas se préoccuper d'eux. Ici, il est:

"héritage multiple jugé utile"

« Diamonds » dans la hiérarchie d'héritage des interfaces est tout à fait sûr -. Il est l'héritage de code qui est vous obtenez dans l'eau chaude

Pour la réutilisation de code, je vous conseille de considérer mixins (google pour Mixins C ++ si vous n'êtes pas familier avec le tequnique). Lors de l'utilisation mixins vous sentez que vous pouvez « faire du shopping » pour les extraits de code que vous devez mettre en œuvre vous classe sans utiliser l'héritage multiple de classes stateful.

Ainsi, le modèle est - l'héritage multiple d'interfaces et une seule chaîne de mixins (vous donnant la réutilisation du code) pour aider à mettre en œuvre la classe concrète

.

L'espoir qui aide!

Avec le premier exemple .....

si son ActionRead ActionWrite besoin d'être sous-classes d'action du tout.

puisque vous allez finir avec une classe concrète qui sera une action de toute façon, vous pourriez hériter actionread et actionwrite sans les actions étant en elles-mêmes.

cependant, vous pouvez inventer le code qui les obligerait à être des actions. Mais en général, je vais essayer et d'action séparée, Read, Write et Delay et juste la classe concrète mixes tout cela ensemble

En savoir plus sur ce que vous faites, Je réorganiser probablement les choses un peu. Au lieu de toutes les héritage multiple de ces versions d'action, Je voudrais faire la lecture polymorphes et cours d'écriture et d'écriture, instancié en tant que délégués.

Quelque chose comme ce qui suit (qui n'a pas inheritence de diamant):

Je présente ici une des nombreuses façons d'application de retard en option, et supposons que la méthode de retard est le même pour tous les lecteurs. chaque sous-classe peut avoir leur propre mise en œuvre de retard dans ce cas, vous passeriez pour lire et instance de la classe dérivée retard respectifs.

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

Pour le cas 2, n'est pas un OneCommand juste un cas particulier de CompositeCommand? Si vous éliminez OneCommand et permettent CompositeCommands d'avoir seulement un élément, je pense que votre conception devient plus simple:

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

Vous avez encore le diamant redoutée, mais je pense que cela peut être un cas acceptable pour elle.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top