20

ダイヤモンドの継承を持つことは悪い習慣と見なされていることを私は知っています. ただし、ダイヤモンドの継承が非常にうまく適合すると感じるケースが 2 つあります。これらの場合にダイヤモンドの継承を使用することをお勧めしますか、それともより良い別のデザインがありますか.

ケース 1:システムでさまざまな種類の「アクション」を表すクラスを作成したいと考えています。アクションは、いくつかのパラメーターによって分類されます。

  • アクションは「読み取り」または「書き込み」です。
  • アクションは、遅延ありでも遅延なしでもかまいません (1 つのパラメーターだけではありません。動作が大幅に変わります)。
  • アクションの「フロー タイプ」は、FlowA または FlowB です。

以下のデザインを予定しています。

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

もちろん、2 つのアクション (Action クラスから継承) が同じメソッドを実装することはありません。

ケース 2:システムに「コマンド」の複合設計パターンを実装します。コマンドは、読み取り、書き込み、削除などを行うことができます。また、読み取り、書き込み、削除などを行うことができるコマンドのシーケンスも必要です。コマンドのシーケンスには、他のコマンドのシーケンスを含めることができます。

だから私は次のデザインを持っています:

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

さらに、特別な種類のコマンド、「モダン」コマンドがあります。1 つのコマンドと複合コマンドの両方を最新にすることができます。「モダン」であることにより、特定のプロパティ リストが 1 つのコマンドと複合コマンドに追加されます (両方のプロパティはほとんど同じです)。CommandAbstraction へのポインターを保持し、必要なコマンドの種類に応じて (new を介して) 初期化できるようにしたいと考えています。だから私は(上記に加えて)次のデザインをしたい:

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

繰り返しますが、CommandAbstraction クラスから継承する 2 つのクラスが同じメソッドを実装しないようにします。

ありがとうございました。

4

7 に答える 7

20

継承は、C ++で2番目に強力な(より結合性の高い)関係であり、その前に友情のみがあります。コンポジションのみを使用するように再設計できる場合、コードはより緩く結合されます。できない場合は、すべてのクラスが本当にベースから継承する必要があるかどうかを検討する必要があります。それは実装によるものですか、それとも単なるインターフェースによるものですか?階層の任意の要素を基本要素として使用しますか?それとも、実際のアクションである階層内の葉だけですか?葉だけがアクションであり、動作を追加する場合は、このタイプの動作の構成に対するポリシーベースの設計を検討できます。

アイデアは、さまざまな(直交する)動作を小さなクラスセットで定義し、それらをバンドルして実際の完全な動作を提供できるということです。この例では、アクションを現在実行するか将来実行するかを定義する1つのポリシーと、実行するコマンドについて検討します。

テンプレートのさまざまなインスタンス化を(ポインターを介して)コンテナーに格納したり、引数として関数に渡して多態的に呼び出したりできるように、抽象クラスを提供します。

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

継承を使用すると、ポリシー実装のパブリックメソッドにテンプレートのインスタンス化を通じてアクセスできるようになります。これにより、新しい関数メンバーをクラスインターフェイスにプッシュできないため、ポリシーを組み合わせるための集計の使用が許可されなくなります。この例では、テンプレートは、すべての待機中のポリシーに共通のwait()メソッドを持つポリシーに依存しています。現在、期間を待機するには、period()パブリックメソッドを介して設定された固定期間が必要です。

この例では、NoWaitポリシーは、期間が0に設定されたWaitSecondsポリシーの特定の例にすぎません。これは、ポリシーインターフェイスが同じである必要がないことを示すためのものです。別の待機ポリシーの実装では、特定のイベントのコールバックとして登録するクラスを提供することにより、ミリ秒数、クロックティック、または外部イベントまで待機することができます。

ポリモーフィズムが必要ない場合は、例から基本クラスと仮想メソッドをすべて取り出すことができます。これは現在の例では非常に複雑に見えるかもしれませんが、他のポリシーをミックスに追加することを決定できます。

新しい直交動作を追加すると、プレーン継承が(ポリモーフィズムで)使用される場合、クラスの数が指数関数的に増加することを意味しますが、このアプローチでは、それぞれの異なる部分を個別に実装し、アクションテンプレートに接着することができます。

たとえば、アクションを定期的にして、定期的なループをいつ終了するかを決定する終了ポリシーを追加できます。頭に浮かぶ最初のオプションは、LoopPolicy_NRunsとLoopPolicy_TimeSpan、LoopPolicy_Untilです。このポリシーメソッド(私の場合はexit())は、ループごとに1回呼び出されます。最初の実装は、固定数(上記の例では期間が固定されているため、ユーザーによって固定された)の後に出口と呼ばれた回数をカウントします。2番目の実装は、指定された期間、定期的にプロセスを実行しますが、最後の実装は、指定された時間(クロック)までこのプロセスを実行します。

あなたがまだここまで私をフォローしているなら、私は確かにいくつかの変更を加えるでしょう。1つ目は、execute()メソッドを実装するテンプレートパラメーターCommandを使用する代わりに、関数を使用し、おそらくコマンドをパラメーターとして実行するテンプレートコンストラクターを使用することです。理論的根拠は、これにより、boost::bindまたはboost::lambdaとして他のライブラリと組み合わせてはるかに拡張可能になることです。その場合、コマンドはインスタンス化の時点で任意のフリー関数、ファンクター、またはメンバーメソッドにバインドされる可能性があるためです。クラスの。

今、私は行かなければなりません、しかしあなたが興味を持っているなら、私は修正されたバージョンを投稿することを試みることができます。

于 2008-12-18T23:36:11.310 に答える
9

実装が継承される (危険な) 実装指向のダイヤモンド継承と、インターフェースまたはマーカーインターフェースが継承される (しばしば有用な) サブタイプ指向の継承の間には、設計品質の違いがあります。

一般に、前者を避けることができれば、どこかで正確に呼び出されたメソッドが問題を引き起こす可能性があり、仮想ベース、状態などの重要性が問題になり始めるため、より良い結果が得られます。実際、Java ではそのようなものをプルすることはできません。Java はインターフェイス階層のみをサポートします。

このために思いつくことができる「最もクリーンな」設計は、(状態情報を持たず、純粋な仮想メソッドを持つことによって) ダイヤモンド内のすべてのクラスを効果的にモック インターフェイスに変えることだと思います。これにより、あいまいさの影響が軽減されます。そしてもちろん、Java で実装を使用するのと同じように、複数のひし形の継承を使用することもできます。

次に、さまざまな方法で実装できるこれらのインターフェイスの具体的な実装のセットを用意します (たとえば、集約、さらには継承)。

このフレームワークをカプセル化して、外部クライアントがインターフェイスのみを取得し、具象型と直接やり取りしないようにし、実装を徹底的にテストしてください。

もちろん、これは大変な作業ですが、中心的で再利用可能な API を作成している場合は、これが最善の策かもしれません。

于 2008-12-18T20:17:25.283 に答える
4

インターフェイスの継承階層にある「ダイアモンド」は非常に安全です。コードの継承によって、お湯に浸かることができます。

コードを再利用するには、ミックスインを検討することをお勧めします(tequniqueに慣れていない場合は、C ++ミックスインをグーグルで検索してください)。ミックスインを使用すると、ステートフルクラスの多重継承を使用せずに、クラスを実装するために必要なコードスニペットを「購入」できるように感じます。

つまり、パターンは次のとおりです。具体的なクラスの実装を支援するための、インターフェイスの多重継承とミックスインの単一チェーン(コードの再利用を可能にする)。

お役に立てば幸いです。

于 2008-12-18T23:49:46.930 に答える
4

ちょうど今週、この問題に遭遇し、DDJ に関する記事を見つけて、問題と、問題を気にする必要がある場合と気にしない場合を説明しました。ここにあります:

「便利だと思われる多重継承」

于 2008-12-18T20:18:39.603 に答える
1

あなたが何をしているのかをもっと知ることなく、私はおそらく物事を少し再編成するでしょう。これらすべてのバージョンのアクションで複数の継承を行う代わりに、私は多形の読み取り、書き込み、および書き込みクラスを作成し、デリゲートとしてインスタンス化します。

次のようなもの(ダイヤモンドの継承はありません):

ここでは、オプションの遅延を実装する多くの方法の1つを紹介し、遅延の方法論がすべてのリーダーで同じであると想定します。各サブクラスには独自のdelayの実装がある場合があります。その場合、Readとそれぞれの派生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
    {};
};
于 2008-12-18T22:35:41.383 に答える
1

最初の例では.....

ActionRead ActionWrite がアクションのサブクラスである必要があるかどうか。

とにかくアクションになる1つの具象クラスになるので、それ自体がアクションになることなく、actionreadとactionwriteを継承することができます。

ただし、それらがアクションであることを必要とするコードを発明することはできます。しかし、一般的には、アクション、読み取り、書き込み、および遅延を分離しようとし、具象クラスだけがそれらすべてを混ぜ合わせます

于 2008-12-18T20:29:50.127 に答える
-1

ケース 2 については、OneCommand単なる特別なケースではありませCompositeCommandんか? を削除OneCommandしてCompositeCommands が 1 つの要素のみを持つようにすると、設計がよりシンプルになると思います。

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

あなたはまだ恐ろしいダイヤモンドを持っていますが、これは許容できるケースだと思います.

于 2008-12-18T20:49:43.750 に答える