Frage

Ich weiß, dass Diamant Erbe aufweist, ist eine schlechte Praxis betrachtet. Allerdings habe ich zwei Fälle, in denen ich, dass Diamant Erbe fühle mich sehr gut passen könnte. Ich möchte fragen, würden Sie mir empfehlen in diesen Fällen Diamant Vererbung zu verwenden, oder gibt es eine andere Konstruktion, die besser sein könnte.

Fall 1: Ich mag Klassen erstellen, die verschiedenen Arten von "Aktionen" in meinem System darstellen. Die Aktionen werden durch verschiedene Parameter klassifiziert:

  • Die Aktion kann sein "Lesen" oder "Schreiben".
  • Die Aktion kann mit Verzögerung oder ohne Verzögerung sein (Es ist nicht nur ein Parameter. Es ändert sich das Verhalten signifikant).
  • Die Handeln "Bewegungsart" kann Flowa oder FlowB sein.

Ich beabsichtige, die folgende Konstruktion haben:

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

Natürlich, ich will gehorchen, dass keine zwei Aktionen (erbt von der Klasse Aktion) wird die gleiche Methode implementieren.

Fall 2: ich das zusammengesetzte Entwurfsmuster für ein "Command" in meinem System implementieren. Ein Befehl kann gelesen, geschrieben, gelöscht werden usw. Ich möchte auch eine Folge von Befehlen haben, die auch gelesen werden können, geschrieben, gelöscht, usw. Eine Folge von Befehlen andere Sequenzen von Befehlen enthalten.

Also ich habe folgendes 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
};

Darüber hinaus habe ich eine besondere Art von Befehlen, „Moderne“ Befehle. Sowohl ein Befehl und Composite-Befehl kann modern sein. „Moderne“ zu sein, fügt eine bestimmte Liste von Eigenschaften zu einem Befehl und Composite-Befehl (meist gleiche Eigenschaften für beide). Ich möchte einen Zeiger auf CommandAbstraction halten zu können, und initialisieren (über neu) nach der benötigten Art des Befehls. Deshalb mag ich das folgende Design tun (zusätzlich zu dem oben):

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

Auch hier werde ich sicherstellen, dass keine zwei Klassen von CommandAbstraction Klasse erben werden die gleiche Methode implementieren.

Danke.

War es hilfreich?

Lösung

Vererbung ist die zweitstärkste (mehrere Kopplungs) Beziehungen in C ++, voran nur durch Freundschaft. Wenn Sie Ihren Code neu gestalten zu können nur Zusammensetzung verwendet werden mehr lose gekoppelt. Wenn Sie nicht können, dann sollten Sie prüfen, ob alle Klassen wirklich von der Basis erben sollte. Ist es aufgrund der Implementierung oder nur eine Schnittstelle? Werden Sie ein beliebiges Element der Hierarchie als Basiselement verwendet werden soll? Oder sind nur Blätter in der Hierarchie, die wirkliche Aktion der? Wenn nur Blätter Aktionen sind und Sie hinzufügen Verhalten können Sie Policy-basiertes Design für diese Art der Zusammensetzung von Verhaltensweisen zu berücksichtigen.

Die Idee ist, dass verschiedene (orthogonal) Verhaltensweisen können in kleinen Klassensätzen definiert werden und dann gebündelt zusammen das reale vollständige Verhalten zu schaffen. Im Beispiel werde ich nur eine Politik prüfen, die festlegt, ob die Aktion jetzt oder in der Zukunft ausgeführt werden soll, und den Befehl auszuführen.

Ich biete eine abstrakte Klasse, so dass unterschiedliche Ausprägungen der Vorlage können (über Zeiger) in einem Behälter oder weitergegeben Funktionen als Argumente gespeichert werden und polymorph aufgerufen.

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

Die Verwendung der Vererbung erlaubt öffentliche Methoden von den politischen Implementierungen zugänglich über die Template-Instantiierung zu sein. Dieser verbietet die Verwendung von Aggregation zum Kombinieren der Politik, da keine neuen Funktionselemente in die Klassenschnittstelle geschoben werden können. Im Beispiel hängt die Vorlage von der Politik eine Methode wait () mit, die für alle Warte Politik üblich ist. Jetzt für einen Zeitraum warten muss eine feste Zeitdauer, die durch die Zeit festgelegt ist () public-Methode.

Im Beispiel ist die NOWAIT Politik nur ein besonderes Beispiel für die WaitSeconds Politik mit der Zeit auf 0 gesetzt Diesen beabsichtigt war zu markieren, dass die Politik Schnittstelle nicht das gleiche sein muss. Eine weitere Warte Umsetzung der Politik auf eine Reihe von Millisekunden warten könnte, Uhr tickt, oder bis ein externes Ereignis, indem Sie eine Klasse bereitstellt, die für das gegebene Ereignis als Rückruf registriert.

Wenn Sie nicht Polymorphismus müssen Sie aus dem Beispiel die Basisklasse und die virtuellen Methoden ganz herausnehmen können. Während dies für das aktuelle Beispiel zu komplex erscheinen mag, können Sie über das Hinzufügen von anderen Maßnahmen zur Mischung entscheiden.

Während des Hinzufügen neues orthogonales Verhaltens würde ein exponentielles Wachstum in der Anzahl der Klassen bedeuten, wenn Ebene Vererbung verwendet wird (mit Polymorphismus), mit diesem Ansatz kann man einfach jeden anderen Teil separat implementieren und sie zusammen in der Aktionsvorlage kleben.

Zum Beispiel könnten Sie Ihre Aktion regelmäßig machen und eine Exit-Politik hinzufügen, die bestimmen, wann die periodische Schleife zu verlassen. Erste-Optionen, die in den Sinn kommen, sind LoopPolicy_NRuns und LoopPolicy_TimeSpan, LoopPolicy_Until. Diese Politik-Methode (exit () in meinem Fall) wird einmal für jede Schleife genannt. Die erste Implementierung zählt die Anzahl, wie oft sie eine Ausfahrt nach einer festen Anzahl aufgerufen wurde (Fest durch den Benutzer, wie Zeitraum im Beispiel oben fixiert wurde). Die zweite Implementierung würde den Prozess für einen bestimmten Zeitraum in regelmäßigen Abständen ausgeführt werden, während die letzten, diesen Prozess bis zu einem bestimmten Zeitpunkt ausgeführt wird (Uhr).

Wenn Sie noch hinter mir bis hier, würde ich in der Tat einige Änderungen vornehmen. Der erste ist, dass stattdessen ein Template-Parameter-Befehl zu verwenden, die eine Methode auszuführen, implementiert () Ich functors und wahrscheinlich einen Templat-Konstruktor verwenden würde, der den Befehl als Parameter auszuführen nimmt. Der Grund ist, dass diese es viel dehnbarer in Kombination machen mit anderen Bibliotheken als boost :: bind oder boost :: lambda, da in diesem Fall Befehle an dem Punkt der Instanziierung an jedes freien Funktion, Funktors oder Mitglied Verfahren gebunden werden könnten eine Klasse.

Jetzt muss ich gehen, aber wenn Sie daran interessiert sind, ich kann versuchen, eine modifizierte Version veröffentlichen.

Andere Tipps

Es gibt einen Design-Qualität Unterschied zwischen umsetzungsorientierten Diamanten Vererbung, wo Implementierung vererbt wird (riskant) und Subtypisierung orientierte Vererbung, wo Schnittstellen oder Marker-Schnittstellen vererbt werden (oft nützlich).

Im Allgemeinen, wenn Sie die frühere vermeiden können, sind Sie da irgendwo besser auf der ganzen Linie die genaue aufgerufene Methode kann zu Problemen führen, und die Bedeutung der virtuellen Basen, Zustände, usw., beginnt Ausmachen. In der Tat, Java würden Sie nicht zulassen, dass so etwas ziehen, es unterstützt nur die Schnittstellenhierarchie.

Ich denke, dass die „saubersten“ entwerfen Sie kommen können dies effektiv ist, um alle Ihre Klassen in dem Diamanten in Mock-Schnittstellen zu drehen (durch keine Zustandsinformationen aufweisen, und mit rein virtuellen Methoden). Dies reduziert die Auswirkungen der Mehrdeutigkeit. Und natürlich können Sie einfach mehr und sogar Diamanten Vererbung dafür verwenden, wie Sie Geräte in Java verwenden würden.

Dann hat eine Reihe konkreter Implementierungen dieser Schnittstellen, die auf unterschiedliche Weise umgesetzt werden können (zum Beispiel, Aggregation, sogar Vererbung).

kapseln diesen Rahmen, so dass externe Clients nur die Schnittstellen erhalten und interagieren nicht direkt mit den konkreten Typen, und stellen Sie sicher, dass Ihre Implementierungen gründlich zu testen.

Natürlich, das ist eine Menge Arbeit, aber wenn Sie eine zentrale und wieder verwendbare API schreiben, könnte dies die beste Wahl sein.

ich in dieses Problem lief gerade in dieser Woche und einen Artikel über DDJ gefunden, die die Probleme erklärt und wenn Sie sollten oder nicht über sie betroffen sein. Hier ist sie:

"Mehrfachvererbung als nützlich"

„Diamonds“ in der Vererbungshierarchie von Schnittstellen ist ziemlich sicher -. Es ist Vererbung von Code, den Sie die in heißes Wasser bekommen

Wiederverwendung von Code zu erhalten, rate ich Ihnen, Mixins zu betrachten (google für C ++ Mixins, wenn Sie mit dem tequnique nicht vertraut sind). Wenn Mixins mit Ihnen das Gefühl, „einkaufen gehen“ können Sie für den Code-Schnipsel, die Sie benötigen Sie Klasse zu implementieren, ohne Mehrfachvererbung von Stateful-Klassen zu verwenden.

So

, ist das Muster - Mehrfachvererbung von Schnittstellen und eine einzige Kette von Mixins (geben Sie die Wiederverwendung von Code), um die konkrete Klasse zu implementieren

.

Ich hoffe, das hilft!

Mit dem ersten Beispiel .....

seine ob ActionRead ActionWrite müssen Subklassen von Maßnahmen sein, überhaupt.

, da Sie mit einer konkreten Klasse am Ende gehen, die sowieso eine Aktion sein werden, könnten Sie einfach actionread und actionwrite erben, ohne sie zu Handlungen in sich selbst zu sein.

obwohl, könnten Sie Code erfinden, die sie benötigen würden Aktionen sein. Aber im Allgemeinen würde ich versuchen und eine separate Aktion, Lesen, Schreiben und Delay und nur die konkrete Klasse mischt alles zusammen

Mit dem Wissen mehr von dem, was man tut, Ich würde wahrscheinlich die Dinge ein wenig neu zu organisieren. Statt mehr inheritence mit all diesen Versionen von Action, Ich würde polymorphe Lesen und Schreiben und Schreibkurse machen, instanziiert als Delegierte.

So etwas wie die folgenden (die keinen Diamanten inheritence haben):

Hier stelle ich eine von vielen Möglichkeiten optional Verzögerung der Umsetzung, und übernimmt die Verzögerungs Methodik gleich für alle Leser. Jede Unterklasse kann ihre eigene Implementierung von Verzögerung in diesem Fall würde passieren Sie nach unten zu lesen und Instanz der jeweilige abgeleitete Verzögerungsklasse.

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

Für Fall 2 ist kein OneCommand nur ein Spezialfall von CompositeCommand? Wenn Sie OneCommand beseitigen und CompositeCommands erlauben nur ein Element zu haben, ich glaube, Ihr Design wird einfacher:

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

Sie noch die gefürchtete Diamant, aber ich denke, das ist für sie ein akzeptabler Fall sein kann.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top