List 2 の使用例 * stepl : 単純な TaIker/Listener if ( listener— ) { void changed( ) { listener— = listener; VOid setListener ( Listener* listener—(0) ( } Talker() ・ public: Listener* listener— private: class TaIker { v 辷セ u 引 void update(TaIker* talker) = virtual -Listener( ) { } public: c ー ass Listener { class Talker; using namespace std ー # inc lude <iostream> listene て ) ー→ update ( this public: 土 n セ va ー ue— private : * お試しコード listener 80 c lass Counter : pub lic Tal ker { counter( ) : value 」 0) { } class DigitPrinter : public Listener { changed( 十十 value— void increment( ) { int getValue( ) const { return value— cout < く static—cast<Counter*>(talker)—>getValue( ) くく endl; v 土てセ u 引 void update(TaIker* talker) ( List 4 class Listener ( public: v 土て加引 -Listener( ) { ) v 土てし u 引 void update(TaIker* talker) = の class TaIker ( private : std : : vector<Listener*> ー isteners— public: TaIker() { } if ( std: :find( listeners-.begin( ) , listeners— void addListener(Listener* listener) { void changed( ) { listeners—.clear( void removeListeners ( ) { listeners— . erase(it); if ( ! = listeners-. end( ) ) { ー isteners— std: :find( listeners— . begin( ) , std: :vector く Listener*>.• :iterator 土し = void removeListener(Listener* listener) ( listeners-.push—back( listener); listeners-. end( ) ) { (*辻) → update(this); while ( ! = listeners— . end( ) ) { std: :vector く Listener*>: :iterator it = listeners— listener) listener); . begin ( 十十土 t ー * お試しコード C ー ass Counter private : int value— public: : public TaIker { int main( ) ( Counter c ー DigitPrinter d ー C. setListener(&d); fo て ( 土北土 = 土く c. increment( return 0 ー 十十 i vecto 「を使って 1 つの Ta ⅸ e 「に複数の Listene 「を対応させる * Step2 #include #include : ひとつの T 引 ke てに複数の L 土 s セ ener # inc ー ude く iOSt て eam> く vector> #include く algorithm> く string> counter() : vaIue-(0) { } int getVaIue( ) const { return value—; } void increment( ) { 十十 value— changed( using namespace std; class DigitPrinter : public Listener ( virtual void update(TaIker* talker) { cout くく static—cast<Counter*>(talker)—>getValue( ) くく endl; cout くく string(static—cast く Counter*>(talker)->getValue( ) , ' * りくく endl; virtual void update(TaIker* talker) { class Starprinter : public Listener { return 0 ー c. increment( fo て ( int i = の i く十十土 ) { c. addListener(&s); c. addListener(&d); starprinter s ー Digitprinter Counter C ー int main( ) ( class Talker; C MAGAZINE 2001 4
オプジェクト工房 List 7 template を用いた Talker/Listene 「 class Counter : public Talker<int> ( private: 土 n し value— public: : more generic! (using template' ) * Step4 #inc ー ude く iostream> #incl ude く vector> #incl ude く set> #inc ー ude く a ー gorithm> #inc lude く string> * ListenerBase C ー ass ListenerBase { private : static std : : set く ListenerBase*> va ー ids— public: Listene て Base( ) { valids— . insert(this); } virtual -ListenerBase ( ) { va lids— . erase ( this ); } static bo 引 isVaIid(ListenerBase* listener) { return valids— . find( listener) ! = valids— . end( counter( ) : value 」 0) 0 void increment( ) ( 十十 va lue— changed(value—); using namespace std; class DigitPrinter : public Listener く int> { virtual VOid update(const int& value) { cout くく value くく endl ー cl ass StarPrinter : pub lic Listener<int> { v 土てセ u 引 void 土れセ & value) ( cout くく string(value, ' * ' ) くく endl; std : : set<ListenerBase*> : : va ー ids— template く class Event> class Talker; / / forward decl . * Listener<Event> temp ー ate<C ー ass Event> class Listener : public ListenerBase public: virtua ー VOid update ( const Event& ) * Ta lker<Event> template«class Event> class TaIker { private: std : : vector く Li stener<Event>*> ー isteners— public: Talker() 0 void addListener ( Li stener<Event>* listener ) { if ( std: :find( listeners-. begin( ) , ー isteners— listeners— . end( ) ) { listeners— . push—back(listener); int main( ) { Counter c ー DigitPrinter d; StarPrinter* S = new StarPrinter ー c. addListener(&d); 0. addListener(s); c. increment ( / / ここで不慮の死を遂げても delete 町 c. increment(); / / 迷惑をかけない . return 0 ー Ta ⅸ e 「から導出しない Counte 「 * Step4' : T 引 ke てから導出しない coun セ e て 〃 Talker, Listener, DigitPrinter, starp て inter はほかのコードと同じ ass Counter { private : int value— Tal ker く int> tal ker public: listener ) VOid removeListener ( Listener<Event>* ー istener ) std : : vector<Listener<Event>*> : : 土し era と 0 て it ー istener ); std : : find ( listeners—. begin ( ) , listeners- if ( it ! = listeners-. end( ) ) { listeners— . erase(it); counter() : vaIue-(0) { } TaIker<int>& talker( ) { return talker—; ) void increment( ) { 十十 va ー ue— talker( ) . changed(value—); void removeListeners( ) { listeners-.clear( VOid changed ( const Event& event ) { . beg 土 n ( Std : : vector<Listener<Event>*> : : 土セ e て atO て listeners— while ( 土セ ! = listeners— . end( ) ) { if ( ListenerBase: :isValid(*it) ) ( (*it)->update(event); 十十 it; } else ( it = listeners-.erase(it); 加セ main( ) { Counter c ー DigitPrinter StarPrinter* s = new StarPrinter ー c. talker( ) . addListener(&d); c. talker( ) . addListener(s); c. increment( delete 町 0. increment( return 0 ー コ し 試 お 8 イ 2001 4 C MAGAZINE
対し removeListener (this) すればいいでし よう。そうすれば changed ( ) のたびに生死 を確認する必要がなくなります。こちらは ぜひみなさん自身の手で挑戦してください。 ・ Java の場合 こでちょっと道草をします。これと同 じことを Java でやるとどうなるでしようか。 Java クラスライプラリが提供する java. util. Observable , java. util. Observer が TaIker , Listener と機能的にはまったく同じです。 Counter は ()bservable を extends , DigitPri nter は Observer を implements することで実 現できます。 Java ではオプジェクトを new 安全な方法にした Talker/Listene 「 く string> #include <algorithm> く set> く vector> く iostream> : 安全な Talker/Listener #include #inc lude # 土 n ude #incl ude * Step3 virtual ¯Listener( ) { valids— . erase(this) Listener( ) { valids— . insert(this); } public: static std; :set<Listener*> valids— private: friend class Talker; class Listener { listeners—.push—back( listener); listeners— . end( ) ) { if ( std: :find( listeners-.begin( ) , VOid addListener(Listener* listener) TaIker() { } public: std : : vector<Listener*> ー isteners— private: class Talker { std : : set く Listener*> Listener : : va ー ids— virtual void update(TaIker* セ引 ke て ) = の ー isteners— listene て s— std: :find( listeners—.begin( ) , std: :vector<Listener*>: :iterator it VOid removeListener(Listener* listener) ( することはあっても明示的に delete するこ とはありません。オプジェクトが使われて いないと Java 仮想マシンが判断した時点 で , 自動的にあと始末が行われます。 List 6 が Java で書いた Counter/DigitPrinte r です。メインルーチンは , Counter c = new Counter ( new DigitPrinter ( DigitPrinter d C. increment ( ) ー c. addObserver (d); d = nul 嵭 } 引 se { C. increment ( ) ー のように途中で DigitPrinter d がスコープ から外れています。これを実行すると・・ あら不思議 , 最後の increment で飛ぶこと なくカウント値がプリントされます。 時点で d はまだ死んでいないようです。 C. a この すかもしれないからです。 でも生き残り , じわじわとメモリを食い潰 い終わったつもりのオプジェクトがいつま 面 , ちょっと怖い気がします。自分では使 Java 仮想マシンって賢いなと感心する反 き残っているんですね。 ているので , あと始末の対象にならずに生 ddObserver(d) の時点で d は c から参照され it = listeners— . erase(it); * お試しコード c 1 ass Counter private : int value— public: : public TaIker ( listener) listener); if ( 辻 ! = listeners-. end( ) ) { listeners— . erase(it); void removeListeners ( ) { listeners-. clear( void changed ( ) { std: :vector<Listener*>: : 土 te て a セ 0 て it = while ( 土セ ! = listeners-.end( ) ) { if ( Listener: :valids— . find( *it) ! = Listener: :valids— . end( ) ( *it)->update(this); listeners—. begin ( counter() : value-(o) { } int getValue( ) const { return value— void increment( ) { 十十 v 引 ue changed( using namespace std ー class DigitPrinter : public Listener { cout くく static—cast<Counter*>(talker)—>getValue( ) くく endl; virtual void update(TaIker* talker) { return 0 ー c ・ increment(); / / 迷惑をかけない . delete 町 / / ここで不慮の死を遂げても 0. increment ( c. addListener(s); c. addListener(&d); StarPrinter* s = new starprinter ー Digitprinter d ー Counter c ー 土北 main( ) { cout くく string(static—cast<Counter*>(talker)->getValue( ) , ' * ' ) くく endl; v 土てに u 引 VOid update(TaIker* セ引 ke て ) { class S セ a て P て土 n し e て : public Listener { 82 十十 it; C MAGAZINE 2001 4
り " から始めます。イベントの発行側と受 理側とをそれぞれクラス TaIker/Listener と 名付け , Talker のメソッド changed() の呼 び出しによって Listener のメソッド update() が起動されるからくりを作ります ( List2 ) 。 これでできあがり , じつに単純です。 試しに使ってみましよう。 Talker から導 出したクラス Counter を用意し , メソッド increment ( ) によって内包するカウント値 を + 1 します。それによって生じるカウント 値の変化を Listener から導出したクラス Di gitprinter に伝え , カウント値をプリント します (List3)0 当然ながら前述の List1 に 示した Document/V1ew とそっくりです。 Step2* 複数の Listener 前述の Step1 では試しにカウント値をプ リントする DigitPrinter を Counter に関連付 けました。同様にカウント値と同じ数の * ' をプリントする StarPrinter を作り , の 2 つ (DigitPrinter/StarPrinter) を 1 つの Counter に関連付けたいとします。・・・・・・現 Talker はイベント通知先である Listener を 1 つしか管理していませんから , これに対応 するには複数個の Listener に対応できるよ う手を加えなければいけません。 こで標準 C + + ライプラリの vector を使 わせてもらいましよう。メソッド setListen er ( ) を addListener ( ) に改め , removeListen er() , removeListeners ( ) を追加します (Lis Fig. 1 List 4 の実行例 1 2 3 4 5 (4)0 実際に動かしてみて Fig. 1 のようにプ リントされましたか ? Step3 、不測の事態に備える ここで List4 にある main() を , c. addListener (s); c. addListener (&d); StarPrinter * s = new StarPrinter; DigitPrinter d; Counter C ー int main ( ) { Counter C ー 土北 main ( ) { あるいは , re turn の C. increment ( ) ー delete 町 C. increment ( ) ー return の c. increment ( / / ※ C. increment ( ) ー c. addListener(&s); StarPrinter s ー コンパイル時には何のエラーも出ません のいずれかに変更してみます。 c. addListener (&d); DigitPrinter d ー いのではないでしようか。 してくれたほうがカッコイイというか美し 誰にも迷惑をかけないよう自分の痕跡を消 のですが , 自分 (Listener) が死ぬときには moveListener(s) しなかったのがいけない からです。もちろん delete に先立って c. re インスタンスのメソッドを呼び出している ているというのに , Counter は存在しない s が delete / スコープから外れていなくなっ とが起こるでしようね。 StarPrinter である が , 実行時におそらく※の地点でヤノヾいこ し彎々 オフジェクト工房 ・ Listene 「クラスを安全にする そこで Listener のコンストラクタ / デスト ラクタにひねりを加えます。どうやるかと いうと , 世の中 ( プログラム中 ) で生きてい る ( 存在している ) 全 Listener を管理する集 合すなわち。戸籍 ' set く Listener * > を Listener の sta ⅱ c メンバとして用意します。そして Li stener のコンストラクタで出生届 ( 戸籍に 追加 ) , デストラクタで死亡届 ( 戸籍から抹 消 ) を出しておくんです。そして , TaIker:: update() で各 Listener を起動するに先立っ てその Listener が、戸籍 ' に登録されている かを調べます (List 5 ) 。こうすればプログ ラマが明示的に Talker::removeListener ( ) を呼び出さなくても Listener のデストラク タがやってくれたことになるわけです。 ただし , この解決策には若干の問題があ ります。つまり Talker::update() のたびに 戸籍 ' の確認を行わなくてはならないため , パフォーマンスが少し落ちてしまうでしよ う。 Listener の総数がそれほど多くなけれ ば速度劣化はほとんどないでしようが , 大 量の Listener を必要とするシチュエーショ ンでは気になります。・戸籍 ' として使って いる set の検索に要する時間計算量は , set の要素数 N の ( 2 を底とする ) 対数に比例し ますから。戸籍 ' への登録は必要最小限に抑 えたほうがいいはずです。そのため , List 5 では Listener のコンストラクト時に登録 していますが , これを改めて TaIker::addLi stener ( ) 呼び出し時にやったほうが。ほん の少しですが be れ er でしよう。そうすれば " 生きてはいるけど T ker に関連付けられて いない "Listener は・戸籍 ' に登録されていな いので , 検索時間がそれだけ向上するはず です。とはいっても微々たるモノでしよう けどね ^ ^ ; 。 それでも 4 秒オーダのパフォーマンスの 低下でも我慢がならないならどうするのか Listener の各インスタンスに addLis tener() した Talker の集合を持たせ , デス トラクタの中でこの集合にある各 Talker に どびてククのオブジェクト工房 81
オフジェクト工房 temp ー ate<C ー ass Event> class TaIker { Step4 がほしいのは というわけで , 複数の Listener にも存在 しない Listener にも対応できました。です が , ぼくにはこれでもまだ不満が残るんで す。それというのも , Listener::update() の 引数が気に入りませんム 。フ , Listener::upd ate ( ) の引数は TaIker * となっていて , イ べントの発行元である Talker へのポインタ が引き渡されます。ですから , たとえば dig itprinter::update (Talker * talker) では引数 として渡された TaIker * から Counter * に キャストし , さらに Counter のメソッド get VaIue ( ) でカウント値を取得し , プリント ・・カッコ悪いとは思いませ しています。 んか ? だってこんなコードを書いたとた ん , DigitPrinter は Counter からのイベント しか受理できないことになってしまうじゃ ないですか。ⅲ t 値をプリントするのが Digi tprinter の仕事であり , こいつを update ( ) するのが誰であろうが , プリントすべき int 値さえ手に入ればいいのであって , イ べントの発行元なんか知る必要はないはず です。 DigitPrinter::update() の引数として必 要なのは , プリントすべき int 値です。そ うでしよ ? だから DigitPrinter::update ( ) の望ましいインタフェイスは , void DigitPrinter : :update ( Java で書いた Counte 「 /DigitPrinter const int&) なんです。しかしそうすると基底クラスの 仮想メソッド Listener::update() も同じシグ ニチャ , すなわち , class Listener { virtua ー VOid update (const int&) = の て自由に選びたいのに , int 決め打ちでは 引数は Listener の導出クラスの求めに応じ でなくてはならなくなります。 update() の public: VOid VOid VOid addListener (Listener く Event * > * て emoveListener (Listener く Event * > * ) ー changed (const Event&) ー 致悪 ' でしかありません。 らんものでしようか。 ・ template を使う ・・なんとかな 解決策は存在します。 template を使うん です。 Listener を , update() に引き渡した い型を template 引数とした class template と します。 temp late く c ー ass Event> class Listener { virtua ー void update (const Event&) = 0 ー こうすれば DigitPrinter は Listener く int> か ら導出できます。 Listener の template 化に対応して , Talker にも同様の変更を施します。次のような感 じです。 最終的にできあがった template 版 Talker /Listener を List 7 に示します。 Talker から L istener への引数が Talker でなくなったこと で , イベント発行元は必ずしも Talker から 導出する必要がなくなり , より自由になり ました (List 8 ) 。 いかがでしようか。イベント駆動のきわ めて単純なからくりからここまで、凝る ' とができます。ばくのディスクの中にはさ らにもう一歩進めて・ update ' というメソッ ド名までも自由に取り替えることのできる TaIker/Listener が眠っています。いつか機 会があればお披露目しましよう。こうやっ ていじくり回して遊んでいるもんだからオ プジェクト工房は・試作品 ' ばっかりで , い つまでたっても・完成品 ' が生み出されてこ 終わりに ないんだよなあ・・ * stepl : 単純な TaIker/Listener - import java. util . ObservabIe; import java. util.Observer; ー Java version class Counter extends ObservabIe { private int value— public counter( ) ( value— public 土 n セ getvalue( ) { return value—; ) public void increment( ) ( 十十 value—; setChanged( notifyObservers( class DigitPrinter implements Observer ( public void update(ObservabIe from, Object arg) ( system. out. println( ( (counter)from) . getValue( ) public class stepl { public static void セ r 土記 (Counter c) { DigitPrinter d = new DigitPrinter ( c. addObserver(d); c. increment( public static void main(string[ ] args) { Counter c = new Counter( tria Ⅱ c c. increment( どびてククのオブジェクト工房 83
た方は少なくないと思います。標準入出力 によるオーソドックスなプログラム , いわ ゆるコンソールアプリケーションでは , ま ずメインルーチンからスタートし , そこか らサプルーチンを何重にも呼び出しながら 処理が進行しています。たくさんの関数が メインルーチンを頂点とする木構造をなし ていて , どの関数であってもメインルーチ ンを起点に呼び出しを追いかけていけば必 ずやたどり着くことでしよう。 ところが GUI アプリケーションは違いま す。 ・ボタンが押されたら XX する ・キー入力があったら 00 する のように , プログラムは " 何かが起こる ( イ べント ) " と " そのとき何をする ( アクション ) " の集合体であり , 木構造にはなっていませ ん。たくさんのアクションの断片が浮島の ごとくコード上に散らばっているだけで , 「こんなのが何で動くんだ ? 」とさえ思えて きます。 イベントとアクションを結び付けるから くりは , V1sual Basic や Borland Delphi , あ るいは Borland C + + Builder などの RAD ツー ルでは , 密かにどこかに埋め込まれ , さほ どおおっぴらにはコード上に姿を現しませ ん。 VisuaI C + + が提供するフレームワーク である MFC では , メッセージマップと呼 ばれるマクロ群がイベントとアクションと MFC に見るイベント駆動の仕組み public: MyDoc() : value-(0) { protected : int va ー ue— private: class MYDOC : public CDocument ( - Document-side * MFC's Document/View sample ー view-side UpdateAllViews(0); / / View を更新 ! 十十 value— void increment( ) { 〃 MyDoc 内のデータに変化を及ぼすメソッド 土 n セ getvalue( ) const { return value—; の対応表として機能しています。 ・イベント駆動の使われ方 アクションがイベントによって起動され る「イベント駆動」のからくりは , マウスや キー入力のようなハードウェアに起因する イベントだけでなく , ソフトウェアがイベ ントを起こしてアクションに火をつけると いう使われ方がされることもあります。例 としては , MFC の Document/View アーキ テクチャにこれを見ることができます。 アプリケーションが取り扱うデータをつ かさどる Document と , その表現 ( 見てくれ ) であるⅥ ew を用意して , その両者を関連付 けておきます。 Document は内包するデー タに変化が起こったときにイベントを発し , それによって関連付けられた View 側のア クションが起動されます。 MFC では CDocument のメソッド Update AllViews ( ) をコールすることがイベントの 発行であり , それをトリガとして CView の メソッド OnUpdate ( ) が呼び出されます。 OnUpdate ( ) 内の処理によって CDocument 内にあるデータの変化を見てくれに反映さ せるわけです (List 1 ) 。このからくりのお かげでデータの本体とその見てくれを分離 し , お互いの変更や拡張が他方に及ぼす影 響を小さく抑えることができます。 ・・要はコッチに何かが起こったことを オフジェクト工房 Talker クラスと Listener クラス アッチに伝えるということです。なんてこ とはないからくりなんだけど , いろんな応 用が考えられるし , さまざまな " ひねり " を 加えることができて , これがけっこうおも しろいんですよ。イベント駆動のきわめて 単純なからくりをこさえ , それに少しずつ " ひねり " を加えながら設計と実装の多様性 を楽しんでみようと思います。 class Talker; class Listener { public: virtual -Listener ( ) 0 virtual void talker) = の class TaIker { private: Listener* listener— public: Talker( ) void setListener(Listener* listener) { ー istener— = ー istener ー void changed( ) { i f ( listener— ) { listener—->update(this); Iistener-(0) { } stepl 単純な Talker/Listener では , まず元ネタとなる " 単純なからく class MyView : public CView { public: MyDOC* GetDocument ( / / たとえばマウスクリック時に MyDoc : : 土 nc て ement ( ) すると、 VOid OnLButtonDown(UINT, CPoint) { GetDocument ( ) —>increment ( / / MyDoc: :increment( ) 内での UpdateAIIViews( ) により . virtual VOid OnUpdate(CView* , LPARAM, CObject* ) { lnvalidate( / / 再描画 - ー OnD て aw ( ) が呼ばれる / / Doc の値 ( getv 引 ue ( ) ) を表示する virtual void OnDraw(CDC* pDC) { CString va 嵭 val . Format( d ” ,GetDocument( ) → getvalue( ) pDC->TextOut(0,0, val 嶹 ど 0 てククのオブジェクト工房 / 9