- PR -

複数のリスナーを登録してイベントを発生させたい

投稿者投稿内容
tazi
会議室デビュー日: 2006/06/17
投稿数: 11
投稿日時: 2008-12-20 02:26
イベントを管理するクラス(イベントハンドラー)を作成しているのですが、設計で行き詰っています。
複数のリスナーを登録してイベントを発生させたい場合、どのように実装すべきなのでしょうか?
どなたかアドバイス頂けないでしょうか?

コード:
public interface Foo1Listener {
    public void occurred1(FooEvent1 ev);
}

public interface Foo2Listener {
    public void occurred2(FooEvent2 ev);
}

public class EventManager {
    private List listeners = new ArrayList();

    public void addFoo1Listener(Foo1Listener l) {
        listeners.add(l);
    }

    public void addFoo2Listener(Foo2Listener l) {
        listeners.add(l);
    }

    protected void fire(FooEvent1 ev) {
        Iterator i = ((List)listeners.clone()).iterator();
        while (i.hasNext()) {
            // Foo2Listener.occurred2メソッドが呼び出せない
            ((FooListener1)i.next()).occurred1(ev);
        }
    }
}



宜しくお願い致します。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-12-20 16:10
ちなみに、この問題はイベントには直接は関係ないと思います。(でも、ご質問の目的としてイベントがあることを明確に書かれることは、具体的になって良いことだと思います。)
あくまでも、型をどう扱うかという問題でしょう。
単純に fire を fire1 と fire2 に分ければ解決すると思いますが、これではダメなのですよね? 1 と 2 を同じものとして扱いたいことがあるのですよね?もしそうならば、やはり 1 と 2 に共通の親である型を作って、そこから implements/extends させるべきだと思います。

また、必須ではないものの、まずコーディングの際は Generics は使われたほうが良いと思います。
tazi
会議室デビュー日: 2006/06/17
投稿数: 11
投稿日時: 2008-12-20 19:11
ご返答頂きましてありがとうございます。
unibonさんの助言を元にコードを書いてみました。

コード:

public interface IEvent {
}

public class FooEvent1 implements IEvent {
}

public class FooEvent2 implements IEvent {
}

public interface IListener {
public void occurred(IEvent ev);
}

public interface Foo1Listener extends IListener {
public void occurred(IEvent ev);
}

public interface Foo2Listener extends IListener {
public void occurred(IEvent ev);
}

public class EventManager {
private List listeners = new ArrayList();

public void addFoo1Listener(Foo1Listener l) {
listeners.add(l);
}

public void addFoo2Listener(Foo2Listener l) {
listeners.add(l);
}

protected void fire(IEvent ev) {
Iterator i = ((List)listeners.clone()).iterator();
while (i.hasNext()) {
((IListener)i.next()).occurred(ev);
}
}
}



上記の場合、複数のリスナーを扱うことができるようになりましたが、異なる名前のメソッドを呼び出すことができないかと思います。
例えば KeyListener クラスは keyPressed メソッド、MouseListener クラスは mouseClicked メソッドというように異なる名前のメソッドを持つリスナーを同一の fire メソッドでイベントを発生させたいのですが、可能でしょうか?
java.awt.event パッケージは Generics が導入される前の JDK からあるのでできると思うのですが・・・

引き続き、宜しくお願い致します。

[ メッセージ編集済み 編集者: tazi 編集日時 2008-12-20 20:16 ]
あしゅ
ぬし
会議室デビュー日: 2005/08/05
投稿数: 613
投稿日時: 2008-12-20 19:49
単にinstanceofで分岐する、じゃダメなんですか?
もしくはリスナを格納するリストを二種類用意するとか。

そういう問題ではなくて、

動的にリスナのインタフェースの種類が増えるとかなら、
リフレクションで何とかする、という方法もあります。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-12-20 23:31
引用:

taziさんの書き込み (2008-12-20 19:11) より:
例えば KeyListener クラスは keyPressed メソッド、MouseListener クラスは mouseClicked メソッドというように異なる名前のメソッドを持つリスナーを同一の fire メソッドでイベントを発生させたいのですが、可能でしょうか?
java.awt.event パッケージは Generics が導入される前の JDK からあるのでできると思うのですが・・・


まず、私見になりますが、あまり AWT を参考にしないほうが良いと思います。AWT の作りが完璧にキレイだとは私は思いません。泥臭い部分もあるでしょう。
(批判は簡単にできますが。)

つぎに、「異なる名前のメソッド」を呼ぶ方法としては、私だったらつぎのようなコードにします。
コード:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

interface IEvent {
}

class FooKeyEvent implements IEvent {
}

class FooMouseEvent implements IEvent {
}

interface IListener {
public void occurred(IEvent ev);
}

abstract class FooKeyListener implements IListener {
public abstract void clicked(FooKeyEvent ev);

@Override
public void occurred(IEvent ev) {
clicked((FooKeyEvent)ev);
}
}

abstract class FooMouseListener implements IListener {
public abstract void pressed(FooMouseEvent ev);

@Override
public void occurred(IEvent ev) {
pressed((FooMouseEvent)ev);
}
}

public class EventManager {
private List<IListener> listeners = new ArrayList<IListener>();

public void addListener(IListener l) {
listeners.add(l);
}

protected void fire(IEvent ev) {
for (Iterator<IListener> i = listeners.iterator(); i.hasNext(); ) {
IListener listener = i.next();
listener.occurred(ev);
}
}

public static void main(String[] args) {
EventManager em = new EventManager();
em.addListener(new FooKeyListener() {
@Override
public void clicked(FooKeyEvent ev) {
//
}
});
em.addListener(new FooMouseListener() {
@Override
public void pressed(FooMouseEvent ev) {
//
}
});
em.fire(new FooMouseEvent());
}
}



ただ、このコードを書いていて思いましたが、Key と Mouse は、モノが違いますから、同じリスト(listeners)に詰め込む必要性がはたして高いのでしょうか?という疑問を持ちます。ただ、Key と Mouse だけなら2つですが、これにウィンドウのイベントやメニューのイベントなど何十種類、何百種類ものイベントが出てきたら、たしかに同じリストに詰め込むほうが良さそうです。
ただ、こうするとどこかでダウンキャストしたり instanceof で判定しないといけなくなりそうです。上記のコードでもダウンキャストしている箇所があり、マウスのイベントをキーのリスナーが受け取ると、そこで例外が起こるでしょう。
難しいですね。

[ メッセージ編集済み 編集者: unibon 編集日時 2008-12-20 23:32 ]
tazi
会議室デビュー日: 2006/06/17
投稿数: 11
投稿日時: 2008-12-21 12:08
あしゅさん、unibonさんご返答ありがとうございます。

unibonさんにご提示頂いたコードを実行してみましたが、そのままでは例外が発生してしまうようです。
Exception in thread "main" java.lang.ClassCastException: FooMouseEvent
at FooKeyListener.occurred(EventManager.java:23)
at EventManager.fire(EventManager.java:46)
at EventManager.main(EventManager.java:67)

また、イベントの種類が増えたときもそうですが、リスナーのメソッドの種類が増えた場合、例えば FooMouseListener クラスに mouseReleased メソッドが追加された場合のことを考えるとこのままでは問題がありそうです。
これらを解決しようとするとやはり instanceof で型を判定する必要がありそうです。
それか unibonさんの言われるように KeyEventManager、MouseEventManager と分けてやった方が良さそうですね。
未記入
大ベテラン
会議室デビュー日: 2008/02/07
投稿数: 115
投稿日時: 2008-12-21 14:17
まず「複数のリスナー」という表現が曖昧ですね。1つのイベントに対してイベントリスナー(つまりイベントを受け取る者)が複数ということならなんら難しいことではないですよね? 提示されているソースコードを読むと、どうやら「複数のリスナー」ではなく「複数のイベント」のことを言っているように思います。FooEvent1 と FooEvent2 が出てきていることからも、イベントを複数にしたいのだと考えました。

以下、イベントソースに「複数のイベント」を持たせるための一般的な実装方法について説明します。

まず、イベントが複数あるのに、イベントを発行する fire メソッドがひとつしかないという設計がおかしいです。イベントの数だけ add/remove/fire を用意します。つまり、

  protected void fireFoo1Occurred(FooEvent1 ev);
  protected void fireFoo2Occurred(FooEvent2 ev);

を用意するのが正道だと思います。そして、上記の流れで考えるとリスナー管理リストである ArrayList もイベントの数だけ用意しないといけないように思えてきます。(unibonさんが言っているのがそういうこと)

ところが、ちゃんと便利なクラスが用意されているんですね。それが、javax.swing.event.EventListenerList クラスです。これは指定したクラスに合致する要素だけを取り出すことができるリストだと思ってくれたら良いです。

EventListenerList を使用した場合、Foo1Occurred イベントと Foo2Occurred イベントを持つイベントソースクラスは以下のようになります。

コード:
private EventListenerList listenerList = new EventListenerList();

public void addFoo1Listener(Foo1Listener l) {
	this.listenerList.add(Foo1Listener.class, l);
}

public void removeFoo1Listener(Foo1Listener l) {
	this.listenerList.remove(Foo1Listener.class, l);
}

public Foo1Listener[] getFoo1Listeners() {
	return this.listenerList.getListeners(Foo1Listener.class);
}

protected void fireFoo1Occurred(FooEvent1 ev) {
	for(Foo1Listener listener : this.listenerList.getListeners(Foo1Listener.class)) {
		listener.occurred1(ev);
	}
}

public void addFoo2Listener(Foo2Listener l) {
	this.listenerList.add(Foo2Listener.class, l);
}

public void removeFoo2Listener(Foo2Listener l) {
	this.listenerList.remove(Foo2Listener.class, l);
}

public Foo2Listener[] getFoo2Listeners() {
	return this.listenerList.getListeners(Foo2Listener.class);
}

protected void fireFoo2Occurred(FooEvent2 ev) {
	for(Foo2Listener listener : this.listenerList.getListeners(Foo2Listener.class)) {
		listener.occurred2(ev);
	}
}



tazi
会議室デビュー日: 2006/06/17
投稿数: 11
投稿日時: 2008-12-21 20:15
ご返答ありがとうございます。
EventListenerList クラスを知らなかったので大変勉強になりました。

しかし、一つ疑問に思うことがあります。
イベントを発生させる側からすれば下記のコードのようにを EventManager クラス(EventListenerList を保持しているクラス) から fire メソッドを呼び出すだけで良く非常に便利だと思うのですが、もし新たに扱うイベントを増やしたい場合はこの EventManager クラスも変更しなくてはいけなくなってしまうため、拡張するのが難しいように思えます。
一般的には KeyEvent と MouseEvent のように性質の異なるイベントを同じクラスで管理しても良いのでしょうか?

コード:
EventManager em = new EventManager();
em.fireKeyPressed(new KeyEventPressed()); // KeyListener.pressed メソッドを呼び出す
em.fireMousePressed(new MouseEventPressed()); // MouseListener.pressed メソッドを呼び出す
em.fireMouseReleased(new MouseEventReleased()); // MouseListener.released メソッドを呼び出す



スキルアップ/キャリアアップ(JOB@IT)