| |||
では,早速問題から見てみましょう. | |||
新着メール問題 | |||
あなたの使用している電子メールは,新着のメールをどのように知らせてくれ ますか? パソコンがLANに接続されている場合,おそらく周期的に新着メール の有無をチェックし,もし到着していればパソコン上に新着メールを示すポッ プアップを表示するでしょう. このようなプログラムは,伝統的にbiff(ビフ)と呼ばれています.最初に UNIXでこのプログラムを書いたプログラマが,郵便が届いた時に鳴いた犬の名 を付けたそうです.このプログラムは,X-Window環境に移植され,GUIの xbiffとなり,Windows環境でもwinbiffとして実装されました.見え方は替わっ ても,基本的に「メールの到着を知らせる」という機能は変わっていません. ここではbiffをjavaで書いて見ることにしましょう.実はあなたはネットワー クのソケットプログラミングやメールを受信するPOP3プロトコルには精通して いますが,GUIプログラムはあまり書いたことがなく,その部分は他のプログ ラマに任せることにします. | |||
まずは書いてみる | |||
第1バージョンのプログラムは次のようなものです. // メールビフ(第1バージョン) public class MailBiff implements Runnable { private boolean newMailArrived() { // pop3 プロトコルを使って // メールアカウントをチェックする } public void run() { // ... while (true) { // 新着のチェック if (newMailArrived()) { // あれば表示 youGotMailPopup(); } // 一定時間待つ Thread.sleep(interval); } } private void youGotMailPopup() { // awt を使って「新着メール」を表示する // このメソッドは他のプログラマにお願いする. } } youGotMailPopup()メソッドは他のプログラマが担当します.あとの部分は自分でコー ディングします. 最初youGotMailPopup()を簡単なプリント文,
System.out.println("You Got Mail !"); とし,他の部分を実装します.うまく動くようになった段階で,そのプログラ ムを信頼するGUIプログラマに渡して,youGotMailPopup()を実装してもらおう と考えました. この設計はどうでしょう.1つのクラスを2人で開発するのは不便そうです.途 中であなたのプログラムにバグを発見してそれを修正したい場合,GUI 実装者にも連絡しなくてはいけません.また,GUI実装者が複数いて,各々別 の実装をしている場合,さらに面倒なことになります.youGotMailPopup()の 担当毎にクラスを分けたいところです. 問題点
| |||
TemplateMethodパターン | |||
このプログラムでは全体の処理の流れが決まっています. その中で,youGotMailPopup()の部分のみの動作が変更できることが望まれています. ここで利用できるパターンを考えてみます.振舞に分類されるパターンのなか で,TemplateMethod と呼ばれるパターンがあります.GoFを参照すると, TemplateMethod
とあります.今回の例では,全体の処理の流れを規定するrun()メソッドが上 記の「スケルトン」に当たります.また,youGotMailPopup()が「いくつかの ステップ」に当てはまります. TemplateMethod パターンを使った解決は次のようなものです. まず自分でMailBiffクラスを書き,youGotMailPopup()を空にしておきます. youGotMailPopup()担当者はMailBiffを継承してGUIMailBiffという新たなクラ スを作り,youGotMailPopup()をオーバーライドして実装します.あたなたは 最初,youGotMailPopup()メソッドに簡単な1行のプリント文入れることで,自 分の POP3プロトコルでのメールチェックの部分に専念してテストができます. GUI実装者はMailBiffを継承してyouGotMailPopup()をオーバーライドしたクラ スを作ることで,GUI部分を実装します. 第2バージョンのプログラムは次のような2つのクラスになるでしょう.
// メールビフ(TemplateMethod) public class MailBiff implements Runnable { private boolean newMailArrived() { // ... } public void run() { while (true) { if (newMailArrived()) { notify(); } Thread.sleep(interval); } } public void notify() { // デバッグ用のプリント文 System.out.println("You Got Mail!"); } } // GUI担当者が作成するクラス public class GUIMailBiff extends MailBiff { public void notify() { // 知らせを受けたらGUIメソッドを呼ぶようにオーバーライド youGotMailPopup(); } private void youGotMailPopup() { // GUI実装者の腕の見せどころ! } } ここでは,run()メソッドがアルゴリズム(全体の処理の流れ)を記述し,その 中で notify()が変更可能なステップとなります.MailBiffクラスのメソッド 名を,youGotMailPopup()からnotify()に変更しました.通知方式はなんでも よく,たまたまGUIMailBiffクラスがGUIポップアップを notify()の方法とし て選択した,ということを強調するためです.すなわち,この設計では,「メー ルの着信をチェックする」という役割と,「GUIのポップアップを 表示する」という役割が2つのクラスにきっちり分かれているため,MailBiff クラスの実装者はもはやGUIによるユーザへの通知がどのような方法で行われ るかを知る必要はありません. クラス図では,GoFパターンのパターン名とそのパターンの中での役割を,UML のノートとして表現しています.ノートは,「パターン名:役割名」という形 式で書いています.また,通常のクラス図の右に,依存性を明示的に示したク ラス図も書いています.GUIMailBiffクラスはMailBiffクラスを継承しており, そのためGUIMailBiffクラスはMailBiffクラスに依存しています.しかし,逆 にMailBiffクラスはGUIMailBiffクラスに関して一切知識を持ちません.逆方 向の依存性は存在しません.ちなみに,UMLクラス図では,矢印的な表記が出 てくる時はその方向は必ず依存性の方向と一致していることを覚えておくとよ いでしょう.継承の矢印は依存性の矢印の方向と一致します.ここでは,依 存性を1つのテーマとしているため,すべてのクラス図は下に依存性の先が来 るように配置します.この約束に従うと,図の下の方から実装を進めないと, クラスのコードがコンパイルできないことが明示的にわかります. 2つのクラス間に依存性がある場合,依存の矢印の先にあるクラスのインター フェイス仕様が変更された場合,矢印の元にあるクラスの実装にその変更が影響し ます.仮にMailBiffクラスのnotify()メソッドの名前や引数が変更したとする と,GUIMailBiffクラスも,その変更に対応する必要が出てきます. TemplateMethodパターンの1つの特徴に,「依存方向と呼び出し方向の逆転」 があります.上記例では,依存方向はGUIMailBiff→MailBiffですが,実際に コードの呼び出しが起こる方向は,MailBiff→GUIMailBiffなのです.notify メソッドをオーバーライドすることで,このような逆転が発生します.notify メソッドは「フックメソッド」とか,「ホットスポット」と呼ばれることがあ ります.MailBiffクラスの中で,アルゴリズムのスケルトンであるrun()はフ ローズンな硬い部分,その中のnotify()が変更可能であり,ホットなフック部 分と言う事ができます. これは従来の手続き型言語では,「関数ポインタを用いたコールバック」のよ うな手法で実現されていたものです.この逆転によって,「フレームワーク」 と呼ばれるクラスライブラリを作成することができます.フレームワークは, 制御のアルゴリズムの大半を自身の内部に持ちます.フレームワークをカスタ マイズしたアプリケーションは,適当なフックを実装することでその制御を受 け取り,独自の動作を記述します.フレームワークの方が時間的に先に開発さ れ,アプリケーションに関する知識を持たないのですが,呼び出しの方向はフ レームワークからアプリケーションに起こるのです.この基本的な仕組みを支 えているのが,TemplateMethodパターンです. 新着メール問題では,TemplateMethodを使った設計がまずまずうまく行くよう です.クラスが2つに分かれ,それぞれの担当者が自分が関心のある部分に専 念してプログラムできるようになりました. | |||
複数の通知先 | |||
さて,次に複数のオブジェクトに同時に通知するような拡張を考えてみましょう. 例えば,複数の通知を同時に行うように設計変更してみます.具体的には音に よる着信メロディー通知を加えてみます.音によるnotifyも追加できるように するため,GUISoundMailBiff クラスを作成し,
// ポップアップおよび着信メロディーによるユーザインターフェイス public class GUISoundMailBiff extends MailBiff { public void notify() { youGotMailPopup(); youGotMailMelody(); } private void youGotMailPopup() { // GUI実装者の腕の見せどころ! } private void youGotMailMelody() { // Sound実装者の腕の見せどころ! } } とすることができます.しかし,これからも徐々に追加される通知方法には柔 軟に対応することは難しいでしょう.ランタイムに通知先を追加・変更するこ ともできません.また,この設計のもう1つの問題点として,各々の通知方法 の実装者は,MailBiffの実装がないと自分の実装を始めることができないこと が挙げられます.すなわち,GUISoundMailBiffがMailBiffに依存しています. 新着メールをチェックする側のプログラムとユーザに通知する側のプログラム は,TemplateMethodを利用して別々のクラスに分けることができました.し かしこれでもなお両者の結びつきが強すぎます.両者の直接の依存関係を断ち 切ることはできるでしょうか. 問題点
|
Observerパターン |
ここでは幾つかの通知方法を同時に自動的に呼び出せること,実行時に通知先 を変更できること,また,通知する側とされる側が直接の依存関係を持たない で開発できる,ということを実現する解を探ってみます. Observer
|
|
Java では,その名もObserverというインターフェイスとObservableというク ラスがjava.utilパッケージに用意されています.GoFでは,監視する側を Observerといい,状態変化する側をSubjectと呼びますが,JavaではSubject に対してはObservableという言葉を用います.GoFのObserverはJavaでも同じ くObserverというクラス名になっています.POSAでは,ObserverをSubscriber, SubjectをPublisherと呼びます. では,Observerパターンを利用した実装を考えてみます.
import java.util.*; // メールビフ(Observerパターン) public class MailBiff extends Observable implements Runnable { private boolean newMailArrived() { // ... } public void run() { // ... while (true) { if (newMailArrived()) { setChanged(); notifyObservers(); } Thread.sleep(interval); } } } MailBiffが変更通知側です.よってObservableを継承します. Observableクラスには,あらかじめnotifyObservers()メソッドが準備されています. また,自分自身の状態が変化したことをマークするsetChanged()メソッドがあります. notifyObservers()の中ではObserbableに登録されている 全Observableに通知を送る機能が用意されています. 通知を受け取るクラスは,次のように実装します.
// ポップアップによるユーザインターフェイス public class MailGUIPopup implements Observer { public void update(Observable o, Object arg) { youGotMailPopup(); } private void youGotMailPopup() { // ... } } // 着信メロディーによるユーザインターフェイス public class MailSoundMelody implements Observer { public void update(Observable o, Object arg) { youGotMailMelody(); } private void youGotMailMelody() { // ... } } |
|
ポップアップと着信メロディーの両方のユーザインターフェイスを考えてみま した.両者は,別のクラスとして実装します.Observer側は通知をupdate()メ ソッドで受け取ります.Observer側がObservable側からの通知を受け取るため には,ObservableにObserverを登録する必要があります.これはランタイムに プログラムで行われます.1つのサンプルアプリケーションは,以下のように なるでしょう.
// サンプルアプリケーション public class BiffSampleApplication { public static void main(String[] argv) { // ..... // MailBiff, GUIPopup, SoundMelody の生成 MailBiff mailBiff = new MailBiff(); MailGUIPopup gui = new MailGUIPopup(); MailSoundMelody sound = new MailSoundMelody(); // MailBiffにObserverとして登録 mailBiff.addObserver(gui); mailBiff.addObserver(sound); // メールチェックスレッドの開始 Thread mailThread = new Thread(mailBiff); mailThread.start(); } } これで複数の通知希望者に対して,同時にメールが着信したという通知を送る ことができました.また通知先はaddObserverメソッドによって登録するため, ランタイムに変更することも可能です. また,MailGUIPopupやMailSoundMelodyクラスは,もはやMailBiffに依存して いません.MailGUIPopupクラスおよびMailSoundMelodyクラスの実装者は, MailBiffクラスの実装を待たずに実装を始めることができます.クラス図上で は,依存関係が両者から消えています.これは,MailBiffクラスと MailGUIPopupおよびMailSoundMelodyクラスとの間の通知インターフェイスが, ObserverとObservableに押し出され,局所化されてしまったからです. これで,懸案の問題は一応すべて解決したようです. |
内容の通知 - Push型/Pull型のObserverパターン |
新たな要求を考えてみます.新着メールのポップアップの表示は,「新着メー ルです!」という単純なものを考えていましたが,差出人や表題を含めて「新 着メールです!差出人:○○,題名:○○」というポップアップを出して欲し い,という要望が出ました.MailGUIPopupはこの通知を行うためにMailBiffか らメールの差出人と題名に関する情報を受け取る必要があります.Observerパ ターンでは,タイミングの通知を主に置いているため,それ以上の情報を受け 渡すには工夫がいります.この問題に対処するには伝統的に2つの手法があり ます.
pull型はObserverが情報を引っ張り出すイメージ,push型は情報を Observableが押し出すイメージです. MailBiff, MailGUIPopup を変更して,まずpull型で情報を引き出してみます.
// メールビフ(pull型) public class MailBiff extends Observable implements Runnable { private String from; // 差出人アドレス private String subject; // 題名 private boolean newMailArrived() { // ... // 新着メールがあれば true を返す. // また,from, subject をセットする. } public void run() { // ... while (true) { if (newMailArrived()) { setChanged(); notifyObservers(); } Thread.sleep(interval); } } public String getFromAddress() { return from; } public String getSubject() { return subject; } } // ポップアップによるユーザインターフェイス(pull型) public class MailGUIPopup implements Observer { public void update(Observable o, Object arg) { if (o instanceof MailBiff) { MailBiff biff = (MailBiff)o; youGotMailPopup(biff.getFromAddress(),biff.getSubject()); } } private void youGotMailPopup(String from, String subject) { // 今度は,from/subject 情報も表示 } } 情報の受け渡しはなんとか可能なようです.しかし,MailGUIPopup のupdate の中で,キャストが発生してしまいました.通知元をObservableのインターフェ イスのみで扱いきれないため,MailGUIPopupが期待するMailBiffにキャストし てgetFromAddress()やgetSubject()メソッドを呼び出して情報を取り出す必要 があります.さらに弊害として,今までMailGUIPopupはMailBiffに依存していませんで したが,今回MailBiffのメソッドを呼び出しているため,MailBiffに依存して します.その結果MailGUIBiffはMailBiff無しではコンパイルできなくなって しまいました. では今度はpush型で実装してみます.push型では,情報を一気に Observerへ渡すため,必要情報をまとめたクラスを作ると便利です.一般にこ のような情報をイベントと呼びます.新着メールイベントは,次のような単純 なクラスです.
// 新着メールイベント public class NewMailEvent { public String from; // 差出人アドレス public String subject; // 題名 } 普通publicなインスタンス変数は良くない実装の典型例ですが,イベントがデー タの入れ物であるということを強調するために単純化しています. このクラスを使って,MailBiff に情報をpushしてみます.
// メールビフ(push型) public class MailBiff extends Observable implements Runnable { private NewMailEvent newMailArrived() { // ... // 新着メールがあればfrom, subject をセットした // NewMailEvent を返す.なければ null を返す. } public void run() { // ... while (true) { NewMailEvent newMail = newMailArrived(); if (newMail != null) { setChanged(); notifyObservers(newMail); } Thread.sleep(interval); } } } // ポップアップによるユーザインターフェイス(push型) public class MailGUIPopup implements Observer { public void update(Observable o, Object arg) { if (arg instanceof NewMailEvent) { NewMailEvent newMail = (NewMailEvent)arg; youGotMailPopup(newMail.from, newMail.Subject); } } private void youGotMailPopup(String from, String subject) { // 今度は,from/subject 情報も表示 } } push型を使うことで,MailGUIPopup 自身がMailBiffに依存することは避けら れました.ただし,NewMailEventという型に対しては,MailBiffもMailGUIも 依存しています.依存関係を示すのクラス図を見ると明らかなように,この設 計ではMailBiff中のMailGUIPopupが依存している部分をイベントクラスとして 切り出したことなります.NewMailEventに関して両クラスが合意していれば, 両クラスは独立して開発することができます. ただし,push型でもpull型でもMailGUIPopupクラスのupdate()ではキャストが 発生していることに注意してください.Observerパターンでは,タイミングの 通知以上の情報を伝達するには,どうしてもキャストが必要になってしまうのです. 問題点
|
複数タイプのイベント通知とMultiCastパターン |
新たな仕様拡張として,メールだけでなくニュースの新着も知らせられるよう にしてみましょう.MailBiff は新着メールを監視しますが,新着ニュースを 監視するNewsBiffを作成しました.将来的には,興味のあるインターネットホー ムページの更新を通知するHomePageBiffなども作れそうです.
// 新着ニュースイベント public class NewNewsEvent { pubic String group; // ニュースグループ pubic String from; // 差出人 pubic String subject; // 題名 } // ニュースビフ(push型) public class NewsBiff extends Observable implements Runnable { private NewNewsEvent newNewsArrived() { // ... // 新着ニュースがあればgroup, from, subjectをセットした // NewNewsEvent を返す.なければ null を返す. } public void run() { // ... while (true) { NewNewsEvent newNews = newNewsArrived(); if (newNews != null) { setChanged(); notifyObservers(newNews); } Thread.sleep(interval); } } } これらのユーザーインターフェイスを一手に受けるGUIPopupを考えてみます. すなわち,MailBiffもNewsBiffも監視するGUIPopupです.今までと同様の設計 ですと,Observableからの通知を受けたとき,イベントの型を見て分岐する必 要が出てきます.また,以前と同様,キャストによってイベントクラスから内 容を取り出す必要があります.
// ポップアップによるユーザインターフェイス(push型) public class GUIPopup implements Observer { public void update(Observable o, Object arg) { if (arg instanceof NewMailEvent) { // 新着メール NewMailEvent newMail = (NewMailEvent)arg; youGotMailPopup(newMail.from, newMail.Subject); } else if (arg instanceof NewNewsEvent) { // 新着ニュース NewNewsEvent newNews = (NewNewsEvent)arg; youGotNewsPopup(newNews.group, newNews.from, newNews.Subject); } } private void youGotMailPopup(String from, String subject) { // メールのポップアップ } private void youGotNewsPopup(String group, String from, String subject) { // ニュースのポップアップ } } このように,複数の違ったタイプのイベントを扱う場合にObserverパターンを 用いると,イベントタイプで分岐してキャストによって内容を取得するような コードがどうしても出てきてしまいます.ここでは,新たなパターンとして MultiCastパターンを採用してみます.
MultiCast
|
|
MultiCastパターンは,GoFの23パターンには含まれておらず,Pattern Hatching[Vlissides99] の中で紹介されているものです.同書の中には,GoF が発表される際にこれを1つの独立したパターンとするか,Observerパターン のバリエーションとするかで議論があったことが書かれています.MultiCast パターンはJava(Java 2 SDK 1.2)では,委譲ベースのイベントモデルとか,イベン トリスナーモデルなどと呼ばれている仕組みとして実装されています.また, 用語としてJavaではSender をイベントソース,Receiverをイベントリスナー と呼びます.
Java2 SDK 1.2 のMultiCastパターン(すなわちイベントモデル)は, ObserverパターンのようにGoFの構造を閉じたクラス群として提供していません. 実際はいくつかのクラスと,命名則の組み合わせになります. このモデルのためにJavaのコアライブラリに提供されているのは以下のクラスです.
特に,イベントソース(Sender)側としては,継承すべきクラスを提供して おらず,その都度規則に合わせてユーザがクラスを1から書くことになります. このパターンのJavaでのお作法は次のようなものです.
Javaでは,Observerパターンの抽象クラス側はライブラリとして閉(Closed)に 実装提供されていますが,MultiCastパターンはこのように半開(Semi-Open)に 実装提供されていると言えます.Javaは継承すべきクラスとコーディング規則 という形でこのパターンを提供しました. さて,ではこの仕組みを使ってメールおよびニュースの新着通知の問題を解い てみます.まずイベントクラスを定義します.作法通りにEventObjectを継承 します.
import java.util.*; // 新着メールイベント public class NewMailEvent extends EventObject { public String from; // 差出人アドレス public String subject; // 題名 } // 新着ニュースイベント public class NewNewsEvent extends EventObject { pubic String group; // ニュースグループ pubic String from; // 差出人 pubic String subject; // 題名 } 次に,リスナーインターフェイスを定義します.これも作法通り EventListener を拡張します. import java.util.*; // メールのリスナーインターフェイス interface MailEventListener extends EventListener { void newMailArrived(NewMailEvent e); } // ニュースのリスナーインターフェイス interface NewsEventListener extends EventListener { void newNewsArrived(NewNewsEvent e); } これらが,ソース側とリスナー側との共通合意です. すなわち,具体ソースと具体リスナーはこれらのイベントクラスと リスナーインターフェイスに依存しますが,これ以上にお互いに依存しません. ここまで決めれば独立して開発が可能になります. ソース側は以下のように実装されます
import java.util.*; // メールビフ(MultiCast) public class MailBiff implements Runnable { private List listeners = new ArrayList(); public void addMailEventListener(MailEventListener l) { listeners.add(l); } public void removeMailEventListener(MailEventListener l) { listeners.remove(l); } protected void fireNewMailEvent(NewMailEvent e) { Iterator i = listeners.iterator(); while (i.hasNext()) { MailEventListener l = (MailEventListener)i.next(); l.newMailArrived(e); } } private NewMailEvent newMailArrived() { // ... // 新着メールがあればfrom, subject をセットした // NewMailEvent を返す.なければ null を返す. } public void run() { // ... while (true) { NewMailEvent newMail = newMailArrived(); if (newMail != null) { fireNewMailEvent(newMail); } Thread.sleep(interval); } } } Newsのソースは以下のように実装されます.字句的にMailをNewsに置き換えた だけのものです.
import java.util.*; // ニュースビフ(MultiCast) public class NewsBiff implements Runnable { private List listeners = new ArrayList(); public void addNewsEventListener(NewsEventListener l) { listeners.add(l); } public void removeNewsEventListener(NewsEventListener l) { listeners.add(l); } protected fireNewNewsEvent(NewNewsEvent e) { Iterator i = listeners.iterator(); // 登録されている全リスナーにイベントを配送 while (i.hasNext()) { NewsEventListener l = (NewsEventListener)i.next(); l.newNewsArrived(e); } } private NewNewsEvent newNewsArrived() { // ... // 新着ニュースがあればgroup, from, subjectをセットした // NewNewsEvent を返す.なければ null を返す. } public void run() { // ... while (true) { NewNewsEvent newNews = newNewsArrived(); if (newNews != null) { fireNewNewsEvent(newNews); } Thread.sleep(interval); } } } ソース側は,特に継承するクラスが決まっている訳ではないことに注意してく ださい.前述のお作法に乗っ取ってクラスを定義します.先程定義した Listenerインターフェイスを引数に取る,add~Listener, remove~Listener というメソッドを作ることが必要です. 次に,リスナー側です.具体リスナーは,先程定義したリスナーインターフェ イスを実装する必要があります.ここでは,MailとNewsの両方に興味がある具 体リスナーを作成します.
// ユーザインターフェイス(MultiCast) public class GUIPopup implements MailEventListener, NewsEventListener { public void newMailArrived(NewMailEvent e) { youGotMailPopup(e.from, e.subject); } public void newNewsArrived(NewNewsEvent e) { youGotNewsPopup(e.group, e.from, e.subject); } private void youGotMailPopup(String from, String subject) { // メールのポップアップ } private void youGotNewsPopup(String group, String from, String subject) { // ニュースのポップアップ } } 2つのイベントに興味があるので,2つのリスナーをimplemnetし,それぞれ の対応するイベント受信メソッドを実装しています. |
|
再度,依存関係に注意してクラス図を見てください.イベントクラスとリスナー インターフェイスがGUIPopupとMailBiff,NewsBiffをつなぐ鍵となっています. GUIPopupとMailBiff,NewsBiffはお互いに依存していませんが,これらのイベ ントクラスとリスナーインターフェイスを通じてタイミングと情報を受け渡し ているのです. これらを統合したアプリケーションの例は,以下のようなものです.
// サンプルアプリケーション public class BiffSampleApplication2 { public static void main(String[] argv) { // ..... // MailBiff, NewsBiff, GUIPopup の生成 MailBiff mailBiff = new MailBiff(); NewsBiff newsBiff = new NewsBiff(); GUIPopup gui = new GUIPopup(); // リスナ登録 mailBiff.addMailEventListener(gui); newsBiff.addNewsEventListener(gui); // チェックスレッドの開始 Thread mailThread = new Thread(mailBiff); Thread newsThread = new Thread(newsBiff); mailThread.start(); newsThread.start(); } } |
メリット・デメリット |
さて,MultiCastパターンによる設計は, Observerパターンによる設計に比べて,以下のメリットがあります.
MultiCastパターンはObserverパターンのプッシュ型の1つのバリエーション であるとも言えます.特に,Smalltalkの様に静的な型チェックがない言語で は,この2つのパターンの区別にはあまり意味がないかもしれません.この2 つのパターンの差をTypedMessage というパターンで取り出すこともあります. しかし,JavaやC++のような言語では,この2つを別のパターンとして扱う方 が,実用的と思います.一般に,通知のタイミングのみが重要でありイベン トの内容が少ない場合,および,イベントの内容に意味があっても1種類のみ であり,複数の通知者やイベントタイプを区別する必要がない場合は, Observerパターンがシンプルです. 逆に,複数のイベント通知元やイベントタイプを区別する必要がある場合は, 少々コーディング量は増えますが,MultiCastパターンを利用するのが良いで しょう. |
C++ のテンプレートとMultiCast |
ここで,C++のテンプレートを利用したこのパターンのサポート実装を紹介し ます.Javaに比べて言語の表現能力が大きなC++では,MultiCastのサポートと してJavaのような半開(Semi-Open)ではなく,閉(Closed)な実装が可能です. Javaでは,イベントソース側はお作法に従ってクラスを作成することが必 要でした.可能ならばこのお作法を,「このクラスを継承しなければならない」 というコンパイラがチェック可能な形で提供できるとよいのです.Observerパ ターンでは,イベント型についての情報を扱う必要が無かったため,イベント ソース側をObservable抽象クラスとして提供することができました.しかし, Javaの継承やインターフェイスのみでは,型安全なMultiCastのイベントソー スの抽象クラスを定義することができません. C++では,「テンプレート」と呼ばれる型のパラメータ化機能を準備してます. これを使うと,継承とは違った方法で型についての動作の多重定義が可能です. 汎用ライブラリとして,イベント型を型引数とする,EventListenerクラス とEventSourceクラスを以下のように定義します.
#include <vector> #include <algorithm> using namespace std; // 汎用イベントリスナ template<class EventType> class EventListener { public: // イベントが起こった時のコールバック virtual void eventHappened(const EventType& ev) = 0; virtual ~EventListener() { } }; // 汎用イベントソースクラス template<class EventType> class EventSource { public: // リスナの型 typedef EventListener<EventType> ListenerType; protected: // リスナのリスト vector<ListenerType*> listeners; public: // イベントリスナを登録する void addEventListener(ListenerType* l) { listeners.push_back(l); } // イベントリスナを削除する void removeEventListener(ListenerType* l) { vector <ListenerType*>::iterator i = find(listeners.begin(), listeners.end(), l); if (i != listeners.end()) listeners.erase(i); } virtual ~EventSource() { } protected: // イベントリスナにイベントを通知する void fireEvent(const EventType& ev) { for (unsigned n = listeners.size(), i = 0; i < n; i++) listeners[i]->eventHappened(ev); } }; |
|
まず,イベントリスナを汎用化できます.これは,テンプレートのEventType型引 数をメソッドeventHappenedの引数とすることにより,このメソッドが多重定 義できる機能を利用してます.小さなことだが,C++ではクラス定義の最後に セミコロン(;)が必要である. また,イベントソースも汎用化できます. これも,テンプレートのEventType型引数で様々なメソッドを多重定義しています. これらのサポートテンプレートにより,イベント型を定義すれば, そのリスナインターフェイスは,EventListener < EventType >と自動的に定義できます. さらに,イベントソースの元になる抽象クラスが, EventSource < EventType > として自動的に定義され, その中には,リスナの追加・登録等のリスナ管理,さらに, イベントの発火メソッドの実装が統一的に含まれています. このように,Javaではできなかったイベントソース側の汎用化も実現しています. ではこのサポートテンプレートを利用したMultiCastパターンで,メールの問題を解いてみます.
#include <string> using namespace std; // 新着メールイベント class NewMailEvent { string from; // 差出人 string subject; // 題名 }; // 新着ニュースイベント class NewNewsEvent { string group; // ニュースグループ string from; // 差出人 string subject; // 題名 }; // メールビフ(MultiCast C++) class MailBiff : public EventSource<NewMailEvent> { private: bool newMailArrived(NewMailEvent* e) { // ... // 新着メールがあればNewMailEventにfrom, subject をセットし, // true を返す. } public: void run() { // ... while (true) { NewMailEvent newMail; if (newMailArrived(&newMail)) fireEvent(newMail); Thread.sleep(interval); } } }; // ニュースビフ(MultiCast C++) class NewsBiff : public EventSource <NewNewsEvent> { private: bool newNewsArrived(NewNewsEvent* e) { // ... // 新着ニュースがあればgroup, from, subject をセットした // NewNewsEvent を入れ,true を返す. } public: void run() { // ... while (true) { NewNewsEvent newNews; if (newNewsArrived(&newNews)) fireEvent(newNews); Thread.sleep(interval); } } }; // ユーザインターフェイス(MultiCast C++) class GUIPopup : public EventListener <NewMailEvent>, public EventListener <NewNewsEvent> { public: void eventHappened(const NewMailEvent& e) { youGotMailPopup(e.from, e.subject); } void eventHappened(const NewNewsEvent& e) { youGotNewsPopup(e.group, e.from, e.subject); } private: void youGotMailPopup(string from, string subject) { // メールのポップアップ } void youGotNewsPopup(string group, string from, string subject) { // ニュースのポップアップ } } 2つのイベントを受け取るユーザインターフェイスでは,2つのイベントリス ナーのインターフェイスクラスを多重継承しています. 注目したいのは,イベントの型を1つ定義するだけで,そのリスナーインター フェイスとイベントソースの抽象クラスがコンパイラによって自動生成される ということです.Javaに比べてプログラマが書くコードは大幅に減少します. C++のテンプレートのように,コンパイル時に型引数に応じてコードを自動生 成する機能は,コンパイルタイムポリモーフィズムと呼ばれています.例え ば,EventSourceのfireEventメソッドには,イベントの配送アルゴリズムが記 述されています.このアルゴリズムはイベントの型でパラメータ化されていま すが,具体的な型は分かりません.このイベント型引数が受け付けるメソッド は,特定のスーパークラスで規定されている訳ではありませんし,継承のツリー とは無関係なものです.ただし,fireEventのコードを読めば,リスナークラ スには,イベントクラスを引数に持つeventHappenedメソッドが期待されてい ることが分かります.コンパイラがMailBiffのコードをパースし, NewMailEventを型引数とするEventSource<NewMailEvent>を継承していること が分かり,fireEventメソッドが利用されていることが分かった段階で, fireEventをNewMailEvent用に自動生成します.自動生成の過程で型引数に期 待されているメソッドが無ければ,コンパイルエラーとなります. コンパイルタイムポリモーフィズムは,プログラマが書くコードを大幅に減 らす可能性があります.しかし,コンパイルされたオブジェクトコードは直感 に反して巨大になる可能性があることに注意してください.ランタイムのポリ モーフィズムのように,継承やインターフェイスによってアルゴリズムが共有 されている訳ではなく,必要なすべての型についてアルゴリズムを複製してい るのですから.これは,型安全性とのトレードオフになる問題です. 最近のANSI/ISO 標準C++では,標準ライブラリにSTLが取り入れられるなど, テンプレートをふんだんに利用したライブラリに焦点があたっています.コン パイルタイムポリモーフィズムは,継承を使わないでアルゴリズムを再利用す る,「ジェネリックプログラミング」という新たなプログラミングパラダイム を指向しているようです.オブジェクト指向プログラミングとジェネリックプ ログラミングは,未だ有用な融合方法が研究途上です.Jim Coplien の Curiously Recurring Template パターン[Coplien95]のようなイディオム, 高木幹夫の「修飾」(MCT - Modifier Class Template)[高木95]による,コン パイルタイムのデコレータパターンなどいくつかのおもしろい融合例が発見 されています. |
補記: Java Genericsを使った実装(JSR14) |
2001/10現在,JSR14(Adding Generics to the Java Programming Language: Participant Draft Specification, April 27, 2001)において C++ のテンプ レートのような総称性をJavaに導入する提案がなされています.ここでは,上記 C++による実装に近いものを,Java Genericsを用いて実装してみよう.まず, リスナーの総称表現です.
public interface EventListener<EventType> extends java.util.EventListener { public void eventHappened(EventType event); } これはイベント型を型パラメータとした,リスナーインターフェイスの Generic表現です.C++版とほとんど同じでです.次に,イベント型を型パラメー タとした,イベントソースのGeneric表現を示します. |
import java.util.Vector; public class EventSource<EventType> { protected Vector<EventListener<EventType>> listeners = new Vector<EventListener<EventtType>>(); public void addListener(EventListener<EventType> l) { listeners.add(l); } public void removeListener(EventListener<EventType> l) { listeners.remove(l); } protected void fireEvent(EventType e) { for (int i = 0; i < listeners.size(); i++) listeners.get(i).eventHappened(e); // イベント配送 } } |
これも,意味は C++ バージョンと全く同じです. イベントオブジェクトは,全述の NewMailEvent と NewNewsEvent をそのまま利用します. では,メールビフとニュースビフをこれらを使って実装してみましょう. |
// メールビフ(MultiCast Java Generics) public class MailBiff extends EventSource<NewMailEvent> implements Runnable { private NewMailEvent newMailArrived() { // 新着メールがあればfrom, subject をセットした // NewMailEvent を返す.なければ null を返す. } public void run() { // ... while (true) { NewMailEvent newMail = newMailArrived(); if (newMail != null) fireEvent(newMail); Thread.sleep(interval); } } } // ニュースビフ(MultiCast Java Generics) public class NewsBiff extends EventSource<NewNewsEvent> implements Runnable { private NewNewsEvent newNewsArrived() { // 新着ニュースがあればgroup, from, subject をセットした // NewNewsEvent を返す.なければ null を返す. } public void run() { // ... while (true) { NewNewsEvent newNews = newNewsArrived(); if (newNews != null) fireEvent(newNews); Thread.sleep(interval); } } } そして最後に,両方のイベントを受けるユーザーインターフェイスを示します.
// ユーザインターフェイス(MultiCast C++) public class GUIPopup { public GUIPopup(EventSource<NewMailEvent> mailbiff, EventSource<NewMailEvent> newsbiff) { mailbiff.addListener( new EventListener<NewMailEvent>() { public void eventHappened(NewMailEvent e) { youGotMailPopup(e.from, e.subject); } } ); newsbiff.addListener( new EventListener<NewNewsEvent>() { public void eventHappened(NewNewsEvent e) { youGotNewsPopup(e.group, e.from, e.subject); } } ); } protected void youGotMailPopup(String from, String subject) { // メールのポップアップ } protected void youGotNewsPopup(String group, String from, String subject) { // ニュースのポップアップ } } |
実はJSR14で提案されているJava Genericsでは, 同じGenericクラス/インターフェイスは型引数が違っても2つは同時に implements/extendsすることができません. そこで,ここではJavaらしく,匿名クラスを利用して別々のインナーオブジェクトを作成して, リスナーオブジェクトとしています. インナークラスのオブジェクトからはその外側のクラスが参照可能なため, 各 eventHappened コールバックの中で,youGotMailPopup/youGotNewsPopup などの メソッドを利用することができるのです. なお,ソースとリスナーとの対応づけは,GUIPopupクラスのコンスタラクタで行われています. 例えば,以下のようなメインプログラムがあれば,動作するでしょう. |
public class BiffSampleApplication3 { public static void main(String[] args) { // ... // ソースの生成 MailBiff mailbiff = new MailBiff(); NewsBiff newsbiff = new NewsBiff(); // 受信GUIの生成 GUIPopup gui = new GUIPopup(mailbiff, newsbiff); // チェックスレッドの開始 Thread mailThread = new Thread(mailbiff); Thread newsThread = new Thread(newsbiff); mailThread.start(); newsThread.start(); // ... } } |
補記2: C#を使った実装(eventとdelegate) |
C# では,イベント処理を event と delegate という機構を使って書くことができます. eventはイベントの発生源(イベントソース)であり, delegateはそのイベントを受信するハンドラを表現しています. これも典型的なマルチキャストといえるでしょう (この機能はMicrosoftからJavaに提案され不採用になった経緯があります). まず,イベントオブジェクトを定義する. C#ではイベント引数(EventArgs)という用語を使います. これは,イベントが発生したときにdelegateに引き渡されるイベント引数となります. System.EventArgs を継承することが約束となっています.
// イベント引数クラス(mail) public class NewMailEventArgs : System.EventArgs { public string from; public string subject; } // イベント引数クラス(news) public class NewNewsEventArgs : System.EventArgs { public string group; public string from; public string subject; } 次に,イベントハンドラとなる delegate を定義します. これは,Java のリスナーインターフェイスに相当します. C#には関数のシグニチャをカプセル化する delegate というオブジェクトがあり, これを使ってコールバックなどを表現できます. これは,C言語の関数ポインタのようなものだと考えてもいいですし, メソッドが1つしかないインスタンス化可能なインターフェイスだと考えることもできるでしょう.
// イベントハンドラの delegate public delegate void NewMailEventHandler(object sender, NewMailEventArgs e); public delegate void NewsNewEventHandler(object sender, NewNewsEventArgs e); では,具体的にMailBiffとNewsBiffを定義しましょう.
public class MailBiff { public event NewMailEventHandler Event; private NewMailEventArgs NewMailArrived() { // 新着メールがあればfrom, subject をセットした // NewMailEvent を返す.なければ null を返す. } public void Run() { // ... while (true) { NewMailEventArgs newMail = NewMailArrived(); if (newMail != null) Event(this, newMail); Thread.Sleep(interval); } } } public class NewsBiff { public event NewNewsEventHandler Event; private NewNewsEventArgs NewNewsArrived() { // 新着メールがあればgroup, from, subject をセットした // NewMailEventArgs を返す.なければ null を返す. } public void Run() { // ... while (true) { NewMailEventArgs newNews = NewMailArrived(); if (newNews != null) Event(this, newNews); Thread.Sleep(interval); } } } Java版とほとんど同じですが,event が重要な働きをしています. ここでは,MailBiff, NewsBiffが共にEventという名前のeventをpublicに 保持していることに注意してください.Eventにはハンドラとなるdelegateを追加(+=)することで, 実際のイベントハンドラが登録できます.また,eventは,Event(sender, args)とすることで イベントを発火し,各delegateに配送することができます.
public class GUIPopup { // mail 到着イベントハンドラ public void NewMailArrived(object sender,MailEventArgs e) { YouGotMailPopup(e.from, e.subject); } // news 到着イベントハンドラ public void NewNewsArrived(object sender,NewsEventArgs e) { YouGotNewsPopup(e.group, e.from, e.subject); } protected void YouGotMailPopup(string from, string subject) { // メールのポップアップ } protected void YouGotNewsPopup(string group, string from, string subject) { // ニュースのポップアップ } } では,これらのパーツを組み合わせてアプリケーションにしてみよう.
public class BiffSampleApplication3 { static void Main() { // ソースの生成 MailBiff mailBiff = new MailBiff(); NewsBiff newsBiff = new NewsBiff(); // 受信GUIの生成 GUIPopup gui = new GUIPopup(); mailBiff.Event += new NewMailEventHandler(gui.NewMailArrived); newsBiff.Event += new NewNewsEventHandler(gui.NewNewsArrived); // チェックスレッドの開始 Thread mailThread = new Thread(new ThreadStart(mailBiff.Run)); Thread newsThread = new Thread(new ThreadStart(newsBiff.Run)); } } 前述したように,イベントハンドラdelegateの登録は,Eventへの+=で行っています. delegateは,どんな名前のメソッドでも引数のシグニチャが同じであれば その場でeventに登録できてとってもクールです. |
謝辞 |
C# のソースコードは,岡島幸男さんに提供いただきました.また, 記事中に参考意見をくださった,加藤立朗さん,前川@フリーダムさん,岡村敏弘 さん,真鍋和久さん,佐々木邦彦さん,水越明哉さん,ありがとうございました. |
参考文献 |
以上 |