13

私が書いている C++ MVC フレームワークでは、オブザーバー パターンを多用しています。私は Design Patterns (GoF、1995) の関連する章を徹底的に読み、記事や既存のライブラリ (Boost を含む) で多数の実装を見てきました。

しかし、パターンを実装していると、もっと良い方法があるに違いないという気持ちを抑えられませんでした。克服する方法を見つけることができれば、パターン自体にリファクタリングする必要があると感じたクライアント コードの行とスニペットが含まれていました。いくつかの C++ の制限。また、私の構文は、ExtJs ライブラリで使用されているものほど洗練されていません。

// Subscribing
myGridPanel.on( 'render', this.onRender );

// Firing
this.fireEvent( 'render', null, node );

そこで、コードの優雅さ、読みやすさ、パフォーマンスを優先しながら、一般化された実装にたどり着くためにさらなる調査を行うことにしました。5 回目の試行で大当たりしたと思います。

と呼ばれる実際の実装gxObserver、GitHub で入手できます。それは十分に文書化されており、readme ファイルには長所と短所が詳しく説明されています。その構文は次のとおりです。

// Subscribing
mSubject->gxSubscribe( evAge, OnAgeChanged );

// Firing
Fire( evAge, 69 );

余計な作業になってしまったので、自分の発見を SO コミュニティと共有するだけだと感じました。したがって、以下でこの質問に答えます。

プログラマーがオブザーバー パターンを実装する際に考慮する必要がある追加の考慮事項 (デザイン パターンで提示されているものに加えて) は何ですか?

C++ に焦点を当てていますが、以下に記述されている内容の多くはどの言語にも当てはまります。

注: SO は回答を 30000 語に制限しているため、私の回答は 2 つの部分に分けて提供する必要がありましたが、2 番目の回答 (「件名」で始まる回答) が最初に表示される場合があります。答えのパート 1 は、デザイン パターンのクラス図から始まるものです。

4

2 に答える 2

15

ここに画像の説明を入力してください

(パートIの開始)

前提条件

州だけではありません

デザインパターンは、オブザーバーパターンをオブジェクトの「状態」に結び付けます。上記のクラス図(デザインパターンから)に見られるように、サブジェクトの状態はSetState()メソッドを使用して設定できます。状態が変化すると、対象はすべてのオブザーバーに通知します。GetState()次に、オブザーバーはメソッドを使用して新しい状態を問い合わせることができます。

ただし、GetState()サブジェクト基本クラスの実際のメソッドではありません。代わりに、各具体的な主題は、独自の特殊な状態メソッドを提供します。実際のコードは次のようになります。

SomeObserver::onUpdate( aScrollManager )
{
    // GetScrollPosition() is a specialised GetState();
    aScrollPosition = aScrollManager->GetScrollPosition();
}

オブジェクトの状態とは何ですか?これを状態変数のコレクションとして定義します–永続化する必要のあるメンバー変数(後で復元するため)。たとえば、との両方BorderWidthFillColourFigureクラスの状態変数である可能性があります。

複数の状態変数を持つことができ、したがってオブジェクトの状態が複数の方法で変化する可能性があるという考えは重要です。これは、被験者が複数のタイプの状態変化イベントを発生させる可能性が高いことを意味します。GetState()また、サブジェクト基本クラスにメソッドを含めることがほとんど意味をなさない理由についても説明します。

ただし、状態の変化のみを処理できるオブザーバーパターンは不完全なパターンです。オブザーバーがステートレス通知、つまり状態に関連しない通知を監視するのは一般的です。たとえば、KeyPressまたはMouseMoveOSイベント。またはのようなイベントBeforeChildRemove。これは明らかに実際の状態変化を示すものではありません。これらのステートレスイベントは、プッシュメカニズムを正当化するのに十分です。オブザーバーがサブジェクトから変更情報を取得できない場合は、すべての情報を通知とともに提供する必要があります(これについては後ほど詳しく説明します)。

多くのイベントがあります

「実生活」で被験者が多くの種類のイベントをどのように発火させるかは簡単にわかります。ExtJsライブラリをざっと見ると、一部のクラスが30以上のイベントを提供していることがわかります。したがって、一般化されたサブジェクトオブザーバープロトコルは、デザインパターンが「関心」と呼ぶものを統合する必要があります。これにより、オブザーバーは特定のイベントにサブスクライブでき、サブジェクトは関心のあるオブザーバーにのみそのイベントを発生させることができます。

// A subscription with no interest.
aScrollManager->Subscribe( this );

// A subscription with an interest.
aScrollManager->Subscribe( this, "ScrollPositionChange" );

多対多の可能性があります

1人のオブザーバーが、多数のサブジェクトから同じイベントを観察する場合があります(オブザーバーとサブジェクトの関係が多対多になります)。たとえば、プロパティインスペクターは、選択された多くのオブジェクトの同じプロパティの変更をリッスンする場合があります。オブザーバーが通知を送信したサブジェクトに関心がある場合、通知には送信者を組み込む必要があります。

SomeSubject::AdjustBounds( aNewBounds )
{
    ...
    // The subject also sends a pointer to itself.
    Fire( "BoundsChanged", this, aNewBounds );
}

// And the observer receives it.
SomeObserver::OnBoundsChanged( aSender, aNewBounds )
{
}

ただし、多くの場合、オブザーバーは送信者のIDを気にしないことに注意してください。たとえば、サブジェクトがシングルトンである場合、またはオブザーバーによるイベントの処理がサブジェクトに依存しない場合です。したがって、送信者をプロトコルの一部にするのではなく、プロトコルの一部にすることを許可し、送信者のスペルをプログラマーに任せる必要があります。

オブザーバー

イベントハンドラー

イベントを処理するオブザーバーのメソッド(つまり、イベントハンドラー)は、オーバーライドまたは任意の2つの形式で提供されます。このセクションでは、オブザーバーの実装において重要で複雑な部分を提供し、この2つについて説明します。

オーバーライドされたハンドラー

オーバーライドされたハンドラーは、デザインパターンによって提示されるソリューションです。基本Subjectクラスは仮想OnEvent()メソッドを定義し、サブクラスはそれをオーバーライドします。

class Observer
{
public:
    virtual void OnEvent( EventType aEventType, Subject* aSubject ) = 0;
};

class ConcreteObserver
{
    virtual void OnEvent( EventType aEventType, Subject* aSubject )
    {
    }
};

サブジェクトは通常、複数のタイプのイベントを発生させるという考えをすでに説明していることに注意してください。ただし、メソッドですべてのイベント(特に、イベントが数十個ある場合)を処理するのOnEventは扱いにくいです。各イベントを独自のハンドラーで処理すると、より適切なコードを記述できます。事実上、これによりOnEvent、他のハンドラーへのイベントルーターが作成されます。

void ConcreteObserver::OnEvent( EventType aEventType, Subject* aSubject )
{
    switch( aEventType )
    {
        case evSizeChanged:
            OnSizeChanged( aSubject );
            break;
        case evPositionChanged:
            OnPositionChanged( aSubject );
            break;
    }
}

void ConcreteObserver::OnSizeChanged( Subject* aSubject )
{
}

void ConcreteObserver::OnPositionChanged( Subject* aSubject )
{
}

オーバーライドされた(基本クラス)ハンドラーを持つことの利点は、実装が非常に簡単なことです。サブジェクトにサブスクライブしているオブザーバーは、それ自体への参照を提供することによってそうすることができます。

void ConcreteObserver::Hook()
{
    aSubject->Subscribe( evSizeChanged, this );
}

次に、サブジェクトはObserverオブジェクトのリストを保持するだけで、起動コードは次のようになります。

void Subject::Fire( aEventType )
{
    for ( /* each observer as aObserver */)
    {
        aObserver->OnEvent( aEventType, this );
    }
}

オーバーライドされたハンドラーの欠点は、その署名が固定されていることです。これにより、(プッシュモデルでの)余分なパラメーターの受け渡しが難しくなります。さらに、イベントごとに、プログラマーはルーター(OnEvent)と実際のハンドラー(OnSizeChanged)の2ビットのコードを維持する必要があります。

任意のハンドラー

OnEventオーバーライドされたハンドラーの不足を克服するための最初のステップは…すべてを持たないことです!どの方法で各イベントを処理するかをサブジェクトに伝えることができれば便利です。そのようなもの:

void SomeClass::Hook()
{
    // A readable Subscribe( evSizeChanged, OnSizeChanged ) has to be written like this:
    aSubject->Subscribe( evSizeChanged, this, &ConcreteObserver::OnSizeChanged );
}

void SomeClass::OnSizeChanged( Subject* aSubject )
{
}

この実装では、クラスから継承するためにクラスが不要になっていることに注意してくださいObserver。実際、Observerクラスはまったく必要ありません。このアイデアは新しいものではなく、ハーブサッターの2003年のDrDobbsの記事「GeneralizingObserver」</a>で詳細に説明されています。ただし、C ++での任意のコールバックの実装は、単純なことではありません。ハーブはfunction彼の記事でこの機能を使用していましたが、残念ながら彼の提案の重要な問題は完全には解決されていませんでした。この問題とその解決策を以下に説明します。

C ++はネイティブデリゲートを提供しないため、メンバー関数ポインター(MFP)を使用する必要があります。C ++のMFPはクラス関数ポインターであり、オブジェクト関数ポインターではないため、(MFP)と(オブジェクトインスタンス)のSubscribe両方でメソッドを提供する必要がありました。この組み合わせをデリゲートと呼びます。&ConcreteObserver::OnSizeChangedthis

メンバー関数ポインター+オブジェクトインスタンス=委任

Subjectクラスの実装は、デリゲートを比較する機能に依存する場合があります。たとえば、特定の代理人にイベントを発生させたい場合や、特定の代理人の登録を解除したい場合です。ハンドラーが仮想ハンドラーではなく、(基本クラスで宣言されたハンドラーではなく)サブスクライブしているクラスに属している場合、デリゲートは同等である可能性があります。しかし、他のほとんどの場合、コンパイラーまたは継承ツリーの複雑さ(仮想継承または多重継承)により、それらは比較できないものになります。Don Clugstonは、この問題に関する素晴らしい詳細な記事を書いています。この記事では、問題を克服するC++ライブラリも提供しています。標準に準拠していませんが、ライブラリはほとんどすべてのコンパイラで動作します。

仮想イベントハンドラーが本当に必要なものであるかどうかを尋ねる価値があります。つまり、オブザーバーサブクラスがその(具体的なオブザーバー)基本クラスのイベント処理動作をオーバーライド(または拡張)したいシナリオがあるかどうかです。悲しいことに、答えはこれが十分に可能であるということです。したがって、一般化されたオブザーバーの実装では仮想ハンドラーが許可されるはずです。すぐにこの例を見ていきます。

更新プロトコル

デザインパターンの実装ポイント7では、プルモデルとプッシュモデルについて説明します。このセクションでは、説明を拡張します。

引く

プルモデルでは、サブジェクトは最小限の通知データを送信し、オブザーバーはサブジェクトからさらに情報を取得する必要があります。

プルモデルは、などのステートレスイベントでは機能しないことはすでに確立していBeforeChildRemoveます。プルモデルでは、プログラマーはプッシュモデルでは存在しない各イベントハンドラーにコード行を追加する必要があることにも言及する価値があります。

// Pull model
void SomeClass::OnSizeChanged( Subject* aSubject )
{
    // Annoying - I wish I didn't had to write this line.
    Size iSize = aSubject->GetSize();
}

// Push model
void SomeClass::OnSizeChanged( Subject* aSubject, Size aSize )
{
    // Nice! We already have the size.
}

覚えておく価値のあるもう1つの点は、プッシュモデルを使用してプルモデルを実装できることですが、その逆はできません。プッシュモデルは、必要なすべての情報をオブザーバーに提供しますが、プログラマーは、特定のイベントに関する情報を送信せず、オブザーバーに詳細についてサブジェクトに問い合わせさせることを希望する場合があります。

固定アリティプッシュ

固定アリティプッシュモデルでは、通知が運ぶ情報は、合意された量とタイプのパラメーターを介してハンドラーに配信されます。これは実装が非常に簡単ですが、イベントごとにパラメーターの量が異なるため、いくつかの回避策を見つける必要があります。この場合の唯一の回避策は、イベント情報を構造体(またはクラス)にパックし、それをハンドラーに配信することです。

// The event base class
struct evEvent
{
};

// A concrete event
struct evSizeChanged : public evEvent
{
    // A constructor with all parameters specified.
    evSizeChanged( Figure *aSender, Size &aSize )
      : mSender( aSender ), mSize( aSize ) {}

    // A shorter constructor with only sender specified.
    evSizeChanged( Figure *aSender )
      : mSender( aSender )
    {
        mSize = aSender->GetSize();
    }

    Figure *mSender;
    Size    mSize;
};

// The observer's event handler, it uses the event base class.
void SomeObserver::OnSizeChanged( evEvent *aEvent )
{
    // We need to cast the event parameter to our derived event type.
    evSizeChanged *iEvent = static_cast<evSizeChanged*>(aEvent);

    // Now we can get the size.
    Size iSize  = iEvent->mSize;
}

現在、サブジェクトとそのオブザーバー間のプロトコルは単純ですが、実際の実装はかなり長くなります。考慮すべきいくつかの欠点があります。

evSizeChangedまず、イベントごとにかなりの量のコード(を参照)を作成する必要があります。多くのコードは悪いです。

第二に、答えるのが簡単ではないいくつかの設計上の質問があります。クラスevSizeChangedと一緒に宣言するのSizeか、それともそれを実行する主題と一緒に宣言するのか。あなたがそれについて考えるならば、どちらも理想的ではありません。それでは、サイズ変更通知には常に同じパラメーターが含まれますか、それともサブジェクトに依存しますか?(回答:後者は可能です。)

第三に、誰かが発火する前にイベントのインスタンスを作成し、後でそれを削除する必要があります。したがって、サブジェクトコードは次のようになります。

// Argh! 3 lines of code to fire an event.
evSizeChanged *iEvent = new evSizeChanged( this );
Fire( iEvent );
delete iEvent;

または、これを行います。

// If you are a programmer looking at this line than just relax!
// Although you can't see it, the Fire method will delete this 
// event when it exits, so no memory leak!
// Yes, yes... I know, it's a bad programming practice, but it works.
// Oh.. and I'm not going to put such comment on every call to Fire(),
// I just hope this is the first Fire() you'll look at and just 
// remember.
Fire( new evSizeChanged( this ) );

第四に、キャスティングビジネスが進行中です。Fire()ハンドラー内でキャストを実行しましたが、サブジェクトのメソッド内でキャストすることも可能です。ただし、これには動的キャスト(パフォーマンスにコストがかかる)が含まれるか、静的キャストを実行するため、イベントが発生し、ハンドラーが予期するイベントが一致しない場合に大惨事が発生する可能性があります。

第五に、ハンドラーのアリティはほとんど読みにくいです:

// What's in aEvent? A programmer will have to look at the event class 
// itself to work this one out.
void SomeObserver::OnSizeChanged( evSizeChanged *aEvent )
{
}

これとは対照的に:

void SomeObserver::OnSizeChanged( ZoomManager* aManager, Size aSize )
{
}

次のセクションに進みます。

バリアリティプッシュ

コードを見る限り、多くのプログラマーはこのサブジェクトコードを見たいと思っています。

void Figure::AdjustBounds( Size &aSize )
{
     // Do something here.

     // Now fire
     Fire( evSizeChanged, this, aSize );
}

void Figure::Hide()
{
     // Do something here.

     // Now fire
     Fire( evVisibilityChanged, false );
}

そして、このオブザーバーコード:

void SomeObserver::OnSizeChanged( Figure* aFigure, Size aSize )
{
}

void SomeObserver::OnVisibilityChanged( aIsVisible )
{
}

サブジェクトのFire()メソッドとオブザーバーハンドラーは、イベントごとに異なるアリティを持っています。コードは読みやすく、私たちが期待していた限り短いものです。

この実装には非常にクリーンなクライアントコードが含まれますが、かなり複雑なSubjectコードが生成されます(多数の関数テンプレートと場合によっては他の機能が含まれます)。これは、ほとんどのプログラマーが取るトレードオフです。多くの(クライアントコード)よりも、1つの場所(Subjectクラス)に複雑なコードを配置する方が適切です。また、サブジェクトクラスが完全に機能することを考えると、プログラマーはそれをブラックボックスと見なし、実装方法についてはほとんど気にしないかもしれません。

Fire検討する価値があるのは、アリティとハンドラーのアリティが一致することをいつどのように確認するかです。実行時にそれを行うことができ、2つが一致しない場合は、アサーションを発生させます。ただし、コンパイル時にエラーが発生した場合は、次のように、各イベントのアリティを明示的に宣言する必要があります。

class Figure : public Composite, 
               public virtual Subject
{
public:
    // The DeclareEvent macro will store the arity somehow, which will
    // then be used by Subscribe() and Fire() to ensure arity match 
    // during compile time.
    DeclareEvent( evSizeChanged, Figure*, Size )
    DeclareEvent( evVisibilityChanged, bool )
};

これらのイベント宣言が別の重要な役割をどのように果たすかについては、後で説明します。

(パートIの終わり)

于 2013-01-31T19:42:33.957 に答える
11

(パートIIの開始)

科目

サブスクリプションプロセス

何が保存されていますか?

特定の実装に応じて、オブザーバーがサブスクライブするときに、サブジェクトは次のデータを保存する場合があります。

  • イベントID–関心、またはオブザーバーがサブスクライブするイベント。
  • オブザーバーインスタンス–最も一般的にはオブジェクトポインターの形式です。
  • メンバー関数ポインター–任意のハンドラーを使用する場合。

このデータは、サブスクライブメソッドのパラメーターを形成します。

// Subscription with an overridden handler (where the observer class has a base class handler method).
aSubject->Subscribe( "SizeChanged", this );

// Subscription with an arbitrary handler.
aSubject->Subscribe( "SizeChanged", this, &ThisObserverClass::OnSizeChanged );

任意のハンドラーが使用される場合、メンバー関数ポインターは、クラスまたは構造体のオブザーバーインスタンスと一緒にパックされてデリゲートを形成する可能性が高いことに注意してください。したがって、Subscribe()メソッドには次のシグネチャが含まれる可能性があります。

// Delegate = object pointer + member function pointer.
void Subject::Subscribe( EventId aEventId, Delegate aDelegate )
{
   //...
}

実際の保存(おそらく内std::map)には、キーとしてイベントIDが含まれ、値としてデリゲートが含まれます。

イベントIDの実装

それらを起動するサブジェクトクラスの外部でイベントIDを定義すると、これらのIDへのアクセスを簡素化できます。しかし、一般的に言えば、サブジェクトによって発生するイベントは、そのサブジェクトに固有のものです。したがって、ほとんどの場合、サブジェクトクラス内でイベントIDを宣言するのが論理的です。

イベントIDを宣言する方法はいくつかありますが、ここでは最も重要な3つだけを説明します。

列挙型は、一見すると、最も論理的な選択のようです。

class FigureSubject : public Subject
{
public:
    enum {
        evSizeChanged,
        evPositionChanged
    };
};

列挙型の比較(サブスクリプションと起動時に発生します)は迅速です。おそらく、この戦略の唯一の不便は、オブザーバーがサブスクリプション時にクラスを指定する必要があることです。

// 'FigureSubject::' is the annoying bit.
aSubject->Subscribe( FigureSubject::evSizeChanged, this );

文字列は列挙型に「ルーザー」オプションを提供します。通常、サブジェクトクラスは列挙型のようにそれらを宣言しないためです。代わりに、クライアントは以下を使用します。

// Observer code
aFigure->Subscribe( "evSizeChanged", this );

文字列の良いところは、ほとんどのコンパイラが他のパラメータとは異なる色でコード化することです。これにより、コードの可読性が向上します。

// Within a concrete subject
Fire( "evSizeChanged", mSize, iOldSize );

ただし、文字列の問題は、イベント名のスペルを間違えたかどうかを実行時に判断できないことです。また、文字列は文字ごとに比較する必要があるため、文字列の比較は列挙型の比較よりも時間がかかります。

タイプは、ここで説明する最後のオプションです。

class FigureSubject : public Subject
{
public:
    // Declaring the events this subject supports.
    class SizeChangedEventType     : public Event {} SizeChangedEvent;
    class PositionChangedEventType : public Event {} PositionChangedEvent;
};

型を使用する利点は、次のようなメソッドのオーバーロードが可能になることですSubscribe()(これは、オブザーバーの一般的な問題を解決できることがすぐにわかります)。

// This particular method will be called only if the event type is SizeChangedType
FigureSubject::Subscribe( SizeChangedType aEvent, void *aObserver )
{
    Subject::Subscribe( aEvent, aObserver );

    Fire( aEvent, GetSize(), aObserver );
}

しかし、繰り返しになりますが、オブザーバーはサブスクライブするために少し余分なコードが必要です。

// Observer code
aFigure->Subscribe( aFigure->SizeChangedEvent, this );

オブザーバーをどこに保存しますか?

デザインパターンの実装ポイント1は、各サブジェクトのオブザーバーをどこに保存するかを扱います。このセクションはその議論に追加し、3つのオプションを提供します。

  • グローバルハッシュ
  • 主題ごと
  • イベントごと

デザインパターンで提案されているように、サブジェクトオブザーバーマップを格納する場所の1つは、グローバルハッシュテーブルです。テーブルには、サブジェクト、イベント、オブザーバー(またはデリゲート)が含まれます。すべてのメソッドの中で、サブジェクトがオブザーバーのリストを格納するためにメンバー変数を消費しないため、これが最もメモリ効率が高くなります。グローバルリストは1つだけです。これは、ブラウザによって提供されるメモリが限られているために、パターンがjavascriptフレームワークに実装されている場合に役立つ可能性があります。この方法の主な欠点は、処理が最も遅いことです。発生するすべてのイベントについて、最初にグローバルハッシュから要求されたサブジェクトをフィルター処理し、次に要求されたイベントをフィルター処理してから、すべてのオブザーバーを反復処理する必要があります。

また、デザインパターンで提案されているのは、すべてのサブジェクトがオブザーバーのリストを保持していることです。これは(サブジェクトごとのメンバー変数の形式で)わずかに多くのメモリを消費しstd::mapますが、サブジェクトは要求されたイベントをフィルタリングし、このイベントのすべてのオブザーバーを反復処理するだけでよいため、グローバルハッシュよりも優れたパフォーマンスを提供します。コードは次のようになります。

class Subject
{
protected:    
    // A callback is represented by the event id and the delegate.
    typedef std::pair< EventId, Delegate > Callback;

    // A map type to store callbacks
    typedef std::multimap< EventId, Delegate > Callbacks;

    // A callbacks iterator
    typedef Callbacks::iterator CallbackIterator;

    // A range of iterators for use when retrieving the range of callbacks
    // of a specific event.
    typedef std::pair< CallbackIterator, CallbackIterator> CallbacksRange;

    // The actual callback list
    Callbacks mCallbacks;
public:
    void Fire( EventId aEventId )
    {
        CallbacksRange   iEventCallbacks;
        CallbackIterator iIterator;

        // Get the callbacks for the request event.
        iEventCallbacks = mCallbacks.equal_range( aEventId );

        for ( iIterator = iEventCallbacks.first; iIterator != iEventCallbacks.second; ++iIterator  )
        {
            // Do the firing.
        }
    }
};

デザインパターンでは、各イベントをメンバー変数として使用し、オブザーバーをイベント自体に格納するオプションは提案されていません。これは、各イベントがメンバー変数を消費するだけでなく、イベントごとstd::vectorにオブザーバーを格納するため、最もメモリを消費する戦略です。ただし、この戦略では、フィルタリングを実行する必要がなく、接続されたオブザーバーを反復処理できるため、最高のパフォーマンスが得られます。この戦略には、他の2つと比較して最も単純なコードも含まれます。これを実装するには、イベントでサブスクリプションと起動のメソッドを提供する必要があります。

class Event
{
public:
    void Subscribe( void *aDelegate );
    void Unsubscribe( void *aDelegate );

    void Fire();
};

件名は次のようになります。

class ConcreteSubject : public Subject
{
public:
    // Declaring the events this subject supports.
    class SizeChangedEventType     : public Event {} SizeChangedEvent;
    class PositionChangedEventType : public Event {} PositionChangedEvent;
};

オブザーバーは理論的にはイベントを直接サブスクライブできますが、代わりにサブジェクトを通過することにはメリットがあることがわかります。

// Subscribing to the event directly - possible but will limit features.
aSubject->SizeChangedEvent.Subscribe( this );

// Subscribing via the subject.
aSubject->Subscribe( aSubject->SizeChangedEvent, this );

3つの戦略は、ストアとコンピューティングのトレードオフの明確なケースを提供します。そして、次の表を使用して比較できます。

ここに画像の説明を入力してください

採用するアプローチでは、次のことを考慮する必要があります。

  • 被験者/オブザーバーの比率–特に典型的な被験者にオブザーバーがいないか、1人しかいない場合、オブザーバーが少なく、被験者が多いシステムでは、メモリペナルティが高くなります。
  • 通知の頻度–通知の頻度が高いほど、パフォーマンスの低下が大きくなります。

MouseMoveオブザーバーパターンを使用してイベントを通知する場合、実装のパフォーマンスをさらに検討する必要があります。メモリペナルティに関する限り、次の計算が役立つ場合があります。与えられた:

  • イベントごとの戦略の使用
  • 典型的な64ビットシステム
  • 各科目には平均8つのイベントがあります

800万のサブジェクトインスタンスは、1GBをわずかに下回るRAMを消費します(イベントメモリのみ)。

同じオブザーバー、同じイベント?

オブザーバーパターンの実装における重要な質問の1つは、同じオブザーバーが(同じ主題の)同じイベントに複数回サブスクライブすることを許可するかどうかです。

そもそも、許可すれば、std::multimapの代わりに使用する可能性がありstd::mapます。さらに、次の行には問題があります。

aSubject->Unsubscribe( evSizeChanged, this );

サブジェクトが以前のサブスクリプション(複数存在する可能性があります!)のどれからサブスクリプションを解除するかを知る方法がないためです。したがって、使用Subscribe()するトークンを返す必要がUnsubscribe()あり、実装全体がはるかに複雑になります。

一見すると、それはかなりばかげているように見えます。なぜ同じオブジェクトが同じイベントを複数回サブスクライブしたいのでしょうか。ただし、次のコードを検討してください。

class Figure
{
public:
    Figure( Subject *aSubject )
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Figure::OnSizeChanged );
    }

    void OnSizeChanged( Size aSize )
    {
    }
};

class Circle : public Figure
{
public:
    Circle( Subject *aSubject )
      : Figure( aSubject) 
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Circle::OnSizeChanged );
    }

    void OnSizeChanged( Size aSize )
    {
    }
};

この特定のコードは、同じイベントを2回サブスクライブする同じオブジェクトにつながります。メソッドは仮想ではないためOnSizeChanged()、メンバー関数ポインターは2つのサブスクリプション呼び出し間で異なることにも注意してください。したがって、この特定のケースでは、サブジェクトはメンバー関数ポインターを比較することもでき、サブスクライブ解除の署名は次のようになります。

aSubject->Unsubscribe( evSizeChanged, this, &Circle::OnSizeChanged );

ただし、OnSizeChanged()が仮想の場合、トークンなしで2つのサブスクリプション呼び出しを区別する方法はありません。

正直なところ、仮想の場合、呼び出されるのは独自のハンドラーであり、基本クラスのハンドラーでOnSizeChanged()はないため、クラスがイベントを再度サブスクライブする理由はありません。Circle

class Figure
{
public:
    // Constructor
    Figure( Subject *aSubject )
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Figure::OnSizeChanged );
    }

    virtual void OnSizeChanged( Size aSize )
    {
    }
};

class Circle : public Figure
{
public:
    // Constructor
    Circle( Subject *aSubject )
      : Figure( aSubject) { }

    // This handler will be called first when evSizeChanged is fired.
    virtual void OnSizeChanged( Size aSize )
    {
        // And we can call the base class handler if we want.
        Figure::OnSizeChanged( aSize );
    }
};

このコードは、基本クラスとそのサブクラスの両方が同じイベントに応答する必要がある場合に、おそらく最良の妥協点を表しています。ただし、ハンドラーは仮想であり、プログラマーは基本クラスがサブスクライブするイベントを知っている必要があります。

同じオブザーバーが同じイベントに複数回サブスクライブすることを禁止すると、パターンの実装が大幅に簡素化されます。これにより、メンバー関数ポインターを比較する必要がなくなり(トリッキーなビジネス)、これUnsubscribe()と同じくらい短くすることができます(MFPが提供されている場合でもSubscribe())。

aSubject->Unsubscribe( evSizeChanged, this );

サブスクリプション後の一貫性

オブザーバーパターンの主な目的の1つは、オブザーバーをサブジェクトの状態と一致させることです。状態変更イベントがまさにそれを行うことはすでに見てきました。

オブザーバーがサブジェクトをサブスクライブするとき、前者の状態が後者の状態とまだ一致していないと主張することがデザインパターンの作成者に誤解されたことは少し驚くべきことです。このコードを考えてみましょう:

class Figure
{
public:
    // Constructor
    Figure( FigureSubject *aSubject )
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Figure::OnSizeChanged );
    }

    virtual void OnSizeChanged( Size aSize )
    {
        mSize = aSize;

        // Refresh the view.
        Refresh();
    }
private:
    Size mSize;
};

作成時に、Figureクラスはそのサブジェクトでサブスクライブしますが、そのサイズはサブジェクトのサイズと一致していません。また、ビューを更新して正しいサイズを表示することもありません。

オブザーバーパターンを使用して状態変更イベントを発生させる場合、サブスクリプション後にオブザーバーを手動で更新する必要があることがよくあります。これを達成する1つの方法は、オブザーバー内です。

class Figure
{
public:
    Figure( FigureSubject *aSubject )
    {
        // We subscribe to the subject on size events
        aSubject->Subscribe( evSizeChanged, this, &Figure::OnSizeChanged );

        // Now make sure we're consistent with the subject.
        OnSizeChanged( aSubject->GetSize() );
    }

    // ...
};

しかし、12の状態変化イベントがあるサブジェクトを想像してみてください。すべてが自動的に行われ、サブスクリプション時にサブジェクトが正しいイベントをオブザーバーに送り返すと便利です。

Subscribe()これを実現する1つの方法は、具体的な主題でオーバーロードされたメソッドを必要とします。

// This method assumes that each event has its own unique class, so the method
// can be overloaded.
FigureSubject::Subscribe( evSizeChanged aEvent, Delegate aDelegate )
{
    Subject::Subscribe( aEvent, aDelegate );

    // Notice the last argument in this call.
    Fire( aEvent, GetSize(), aDelegate );
}

次に、オブザーバーコード:

class Figure
{
public:
    Figure( FigureSubject *aSubject )
    {
        // We subscribe to the subject on size events.
        // The subject will fire the event upon subscription
        aSubject->Subscribe( evSizeChanged, MAKEDELEGATE( this, &Figure::OnSizeChanged ) );
    }

    // ...
};

Fire呼び出しが追加のパラメーター(aDelegate)を受け取るようになったため、その特定のオブザーバーのみを更新でき、既にサブスクライブしているオブザーバーは更新できないことに注意してください。

gxObserverは、バインドされたイベントを定義することによってこのシナリオを処理します。これらは、(オプションの送信者以外の)唯一のパラメーターがゲッターまたはメンバー変数にバインドされているイベントです。

class Subject : virtual public gxSubject
{
public:
    gxDefineBoundEvent( evAge, int, GetAge() )

    int GetAge() { return mAge; }
private:
    int mAge;    
}

これにより、サブジェクトはイベントタイプのみを提供するイベントを発生させることもできます。

// Same as Fire( evAge, GetAge() );
Fire( evAge );

使用されるメカニズムに関係なく、覚えておく価値があります。

  • オブザーバーが州のイベントサブスクリプションの直後に彼らの主題と一致していることを保証する方法が必要です。
  • これは、オブザーバーコードではなく、サブジェクトクラスに実装されている方がよいでしょう。
  • このFire()メソッドは、単一のオブザーバー(サブスクライブしたばかりのオブザーバー)に対して起動できるように、追加のオプションのパラメーターを必要とする場合があります。

発砲プロセス

基本クラスからの火

次のコードスニペットは、JUCEでのイベント発生の実装を示しています。

void Button::sendClickMessage (const ModifierKeys& modifiers)
{
    for (int i = buttonListeners.size(); --i >= 0;)
    {
        ButtonListener* const bl = (ButtonListener*) buttonListeners[i];
        bl->buttonClicked (this);
    }
}

このアプローチにはいくつかの問題があります。

  • コードから、クラスが独自のリストを維持していることが明らかです。これは、クラスbuttonListenersにも独自のメソッドがあることを意味しAddListenerますRemoveListener
  • 具体的な主題は、オブザーバーのリストをループする主題です。
  • サブジェクトは、クラス(ButtonListener)とその中の実際のコールバックメソッド()の両方を知っているため、オブザーバーと高度に結合していますbuttonClicked

これらすべての点は、基本サブジェクトクラスがないことを意味します。このアプローチを採用する場合、具体的な主題ごとに、発砲/サブスクリプションのメカニズムを再実装する必要があります。これは、カウンターオブジェクト指向プログラミングです。

オブザーバーの管理、トラバーサル、および実際の通知をサブジェクトの基本クラスで行うのが賢明です。このように、下線を引くメカニズムへの変更(たとえば、スレッドセーフの導入)では、具体的な主題ごとに変更を加える必要はありません。これにより、具体的な対象に十分にカプセル化されたシンプルなインターフェイスが残り、発砲は1行に削減されます。

// In a concreate subject
Fire( evSize, GetSize() );

イベントの一時停止と再開

多くのアプリケーションとフレームワークでは、特定のサブジェクトのイベントの発生を一時停止する必要があります。一時停止されたイベントをキューに入れて、発生を再開したときに発生させたい場合もあれば、単に無視したい場合もあります。サブジェクトインターフェイスに関する限り、次のようになります。

class Subject
{
public:
    void SuspendEvents( bool aQueueSuspended );
    void ResumeEvents();
};

イベントの一時停止が役立つ1つの例は、複合オブジェクトの破棄中です。複合オブジェクトが破棄されると、最初にすべての子が破棄され、次にすべての子が破棄されます。これらの複合オブジェクトがモデルレイヤーにある場合は、ビューレイヤーの対応するオブジェクトに通知する必要があります(たとえば、evBeforeDestroyイベントを使用して)。

ここに画像の説明を入力してください

この特定のケースでは、各オブジェクトがevBeforeDestroyイベントを発生させる必要はありません。最上位のモデルオブジェクトのみが発生する場合に発生します(最上位のビューオブジェクトを削除すると、そのすべての子も削除されます)。したがって、コンポジット自体が破棄されるたびに、その子のイベントを(キューに入れずに)一時停止したいと考えています。

別の例は、多くのオブジェクトを含むドキュメントのロードであり、一部は他のオブジェクトを観察します。サブジェクトが最初にロードされ、ファイルデータに基づいてサイズが設定される場合がありますが、オブザーバーはまだロードされていないため、サイズ変更通知を受け取りません。この場合、ロードする前にイベントを一時停止しますが、ドキュメントが完全にロードされるまでイベントをキューに入れます。キューに入れられたすべてのイベントを発生させると、すべてのオブザーバーがサブジェクトと一致するようになります。

最後に、最適化されたキューは、同じサブジェクトの同じイベントを複数回キューに入れることはありません。通知が再開されると、後でキューに入れられたイベントが(20,20)に通知する場合、サイズが(10,10)に変更されたことをオブザーバーに通知する意味はありません。したがって、各イベントの最新バージョンは、キューが保持する必要があるバージョンです。

サブジェクト機能をクラスに追加するにはどうすればよいですか?

典型的なサブジェクトインターフェースは、これらの線に沿って何かを見るでしょう:

class Subject
{
public:
    virtual void Subscribe( aEventId, aDelegate );
    virtual void Unsubscribe( aEventId, aDelegate );
    virtual void Fire( aEventId );
}

問題は、このインターフェイスをさまざまなクラスにどのように追加するかです。考慮すべき3つのオプションがあります。

  • 継承
  • 構成
  • 多重継承

継承

デザインパターンでは、ConcreteSubjectクラスはクラスから継承しSubjectます。

class ScrollManager: public Subject
{
}

デザインパターンのクラス図とサンプルコードはどちらも、これがその方法であると簡単に思わせる可能性があります。しかし、まったく同じ本が相続に対して警告し、それよりも構成を優先することを推奨しています。これは賢明なことです。一部だけが対象である、多くのコンポジットを含むアプリケーションを検討してください。クラスはCompositeクラスから継承しSubjectますか?その場合、多くのコンポジットには必要のないサブジェクト機能があり、常に空のオブザーバーリスト変数の形式でメモリペナルティが発生する可能性があります。

構成

ほとんどのアプリケーションとフレームワークでは、サブジェクト機能を選択したクラスにのみ「プラグイン」する必要があります。これは必ずしも基本クラスではありません。構成はまさにそれを可能にします。実際には、クラスにはmSubject、次のように、すべてのサブジェクトメソッドへのインターフェイスを提供するメンバーがあります。

class ScrollManager: public SomeObject
{
public:
    Subject mSubject;
}

この戦略の問題の1つは、サブジェクトがサポートするクラスごとにメモリペナルティ(メンバー変数)が発生することです。もう1つは、サブジェクトプロトコルへのアクセスがやや面倒になることです。

// Notification within a class composed with the subject protocol.
mSubject.Fire( ... );

// Or the registration from an observer.
aScrollManager.mSubject.Subscribe( ... );

多重継承

多重継承により、サブジェクトプロトコルを自由にクラスに構成できますが、メンバー構成の落とし穴はありません。

class ScrollManager: public SomeObject,
                     public virtual Subject
{
}

このようにmSubjectして、前の例から削除しているので、次のようになります。

// Notification within a subject class.
Fire( ... );

// Or the registration from an observer.
aScrollManager.Subscribe( ... );

サブジェクトの継承に使用することに注意してくださいpublic virtual。したがって、のサブクラスがScrollManagerプロトコルを再継承することを決定した場合、インターフェイスを2回取得することはありません。しかし、プログラマーは基本クラスがすでにサブジェクトであることに気付くと想定するのが妥当です。したがって、それを再継承する理由はありません。

多重継承は一般的に推奨されておらず、すべての言語がそれをサポートしているわけではありませんが、この目的のために検討する価値は十分にあります。Javascriptに基づくExtJは、多重継承をサポートしていませんが、ミックスインを使用して同じことを実現します。

Ext.define('Employee', {
    mixins: {
        observable: 'Ext.util.Observable'
    },

    constructor: function (config) {
        this.mixins.observable.constructor.call(this, config);
    }
});

結論

この記事を締めくくるには、オブザーバーパターンの一般化された実装で次の重要なポイントを説明する必要があります。

  • 被験者はステートイベントとステートレスイベントの両方を発生させます。後者はプッシュモデルでのみ実現できます。
  • 被験者は通常、複数のタイプのイベントを発生させます。
  • オブザーバーは、多数のサブジェクトで同じイベントをサブスクライブできます。サブジェクトオブザーバープロトコルの意味は、送信者のスペルを許可する必要があります。
  • 任意のイベントハンドラーは、クライアントコードを大幅に簡素化し、優先される可変アリティプッシュモデルを容易にします。ただし、それらの実装は単純ではなく、より複雑なサブジェクトコードになります。
  • イベントハンドラーは仮想である必要がある場合があります。
  • オブザーバーは、グローバルハッシュ、サブジェクトごと、またはイベントごとに保存できます。この選択は、メモリ、パフォーマンス、およびコードの単純さの間のトレードオフを形成します。
  • 理想的には、オブザーバーは同じイベントの同じサブジェクトで一度だけサブスクライブします。
  • サブスクリプションの直後にオブザーバーを対象の状態と一致させることは、覚えておくべきことです。
  • イベントの発生は、キューイングオプションを使用して一時停止してから、再開する必要がある場合があります。
  • クラスにサブジェクト機能を追加する場合は、多重継承を検討する価値があります。

(パートIIの終わり)

于 2013-01-31T19:43:04.797 に答える