(パート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の終わり)