web-dev-qa-db-ja.com

イベントシステムで各イベントを処理するメソッドを確実にする方法は?

私はいくつかのタイプのイベントを持っています、例えば:

_abstract class Event {
    static class KeyPress extends Event { ... }
    static class KeyRelease extends Event { ... }
    static class KeyHold extends Event { ... }
    // ...
}
_

そして、上記のイベントのいくつかにイベントハンドラーに登録することによってそれらに応答する多くのリスナー。現在、次のようになっています。

_abstract class AbstractListener {
    Set<Class<? extends Event>> eventTypes;
    protected abstract boolean respond(Event event);
}

class DownKeyListener exteneds AbstractListener {
    DownKeyListener () {
        // just prepares to register to receive these events. doesn't matter how exactly.
        eventTypes.add(KeyPress.class);
        eventTypes.add(KeyHold.class);
        //no KeyRelease e.g.
    }

    boolean respond(Event event) {
        if (event instanceof KeyPress)
            return handleKeyPress(event);
        else if (event instanceof KeyHold)
            return handleKeyHold(event);
        return false;
    }

    private boolean handleKeyPress(KeyPress e) { ... }
    private boolean handleKeyHold(KeyHold e) { ... }
}
_

これについて私が気に入らないのは、少なくとも登録されたイベントとそれらのチェックとの関係、および応答メソッドでの処理の間の関係を強制またはチェックしないことです。これは開発者のバグを引き起こし続けます。 instanceofsを含む多くのコードでもあり、ほとんどメリットがありません(私はそう思います)。

だから私はこのような「スマート」なことをすることを考えました:イベントタイプとハンドラーの間のマップを作成して、処理のために登録された各イベントがハンドラーを持つようにします:

_abstract class AbstractListener {
    Map<Class<? extends Event>, Function<? extends Event, Boolean> map = new ...
    //             ^ doesn't ensure these event ^: are the same but at least
    //                                             that someone responds
    // to ensure same event i can do
    protected <T extends Event> void register(Class<T> event, Function<T, Boolean> function) {
        map.put(event, function);
    }

    protected abstract boolean respond(Event event);
}
_

その後:

_class DownKeyListener exteneds AbstractListener {
    DownKeyListener () {
        Map.put(KeyPress.class, keyPressFunction);
        Map.put(KeyHold.class, keyHoldFunction);
    }

    boolean respond(Event event) {
        Function<? extends Event, Boolean> f = map.get(event.getClass());
        return f == null ? false : f.apply(event);
    }

    Function<KeyPress, Boolean> keyPressFunction = event -> ...;
    Function<KeyHold, Boolean> keyHoldFunction = event -> ...;
}
_

もちろん、これはジェネリックスのために機能しません。 applyはエラーを出し、その理由を理解しています

_The method apply(capture#4-of ? extends Event) in the type Function<capture#4-of ? extends Event,Boolean> is not applicable for the arguments (capture#5-of ? extends Event)
_

自分が欲しいものを正しく動作させる方法がわかりません。私が考えていたいくつかのこと:

  • _map.get_の結果を、正しく機能することを保証するものにキャストします。
  • respondメソッドを一般的なprotected abstract <T extends Event> boolean respond(T event);に変更すると、同様のエラーが発生します。
_The method apply(capture#3-of ? extends Event) in the type Function<capture#3-of ? extends Event,Boolean> is not applicable for the arguments (T)
_

誰かが私が望むものを何らかの方法で達成することについての提案がありますか?

7
Mark

私はジェネリックのアプローチを使ってそれを理解しました。 respondをジェネリック<T extends Event> boolean respond(T event)に変更することについての私の2番目のポイントは、確かに正しいアプローチでした。互換性のない型のコンパイラエラーは、ダーティだが安全なキャストで解決できます。これが完全に機能するコードです

public class Test {

    public static void main(String[] args) {
        DownKeyListener dkl = new DownKeyListener();
        System.out.println(dkl.respond(new KeyPress()));
        System.out.println(dkl.respond(new KeyHold()));
        System.out.println(dkl.respond(new KeyRelease()));
    }
}

abstract class Event {
    static class KeyPress extends Event {}
    static class KeyHold extends Event {}
    static class KeyRelease extends Event {}
}

abstract class AbstractListener {

    private Map<Class<? extends Event>, Function<? extends Event, Boolean>> map = new HashMap<>();

    protected <T extends Event> void register(Class<T> event, Function<T, Boolean> function) {
        map.put(event, function);
    }

    <T extends Event> boolean respond(T event) {
        Function<? extends Event, Boolean> f = map.get(event.getClass());
        return f == null ? false : ((Function<T, Boolean>) f).apply(event);
    }
}

class DownKeyListener extends AbstractListener {

    DownKeyListener() {
        register(KeyPress.class, keyPressHandler);
        register(KeyHold.class, keyHoldHandler);
    }

    Function<KeyPress, Boolean> keyPressHandler = e -> true;
    Function<KeyHold, Boolean> keyHoldHandler = e -> true;
}

これを実行した結果は

本当
true
false

要求に応じ。

このアプローチについて指摘したいことが3つあります。

1つ目は、使いやすさとコンパイル時の安全性です。これは、イベントの登録(この場合はマップのキーとなるリッスンするイベントのリスト)とイベント処理を結び付けます。登録されているすべてのイベントタイプについて、そのイベントタイプのハンドラーを指定する必要があります。リスナーがイベントに応答する必要がある場合は、同じハンドラーが使用されます。新しいイベントタイプを作成しても、ビジターメソッドや新しいインターフェースなどの追加の作業は必要ありません。それが作成され、一部のリスナーがそれを使用したい場合は、準備ができています。

2つ目は、すべてのリスナーに対して同じ動作をするため、responseメソッドを抽象スーパークラスに移動できることです。つまり、イベントのハンドラーを取得して呼び出し、結果を返します。サブクラスリスナーが特定のイベントタイプに対してこれを変更したい場合は、それをオーバーライドできます。イベントタイプと残りの呼び出しスーパーを確認してください。

3つ目は、イベント関数をマップに配置してからイベントから関数を取得するまでのキャストと情報の損失です。マップは2つのワイルドカードで宣言されており、私が知る限り、それらの間の関係を強制する方法はありません。これは、Javaに汎用フィールドがないためです。もしそのようなものが存在するようになったなら、それはこのアプローチを単純化するでしょう。イベントとそのハンドラーの同じタイプの強制はregisterメソッドで行われ、マップに追加されますが、その強制はフィールド宣言で失われます。つまり、キーから値を取得する場合、コンパイル時の対応はなくなり、同じ型にキャストする必要があります。 registerメソッドのみを使用してマップにデータを入力した場合、このキャストは安全であることがわかっています。一般的なフィールドでは、「安全な」キャストの必要性がなくなります。

それでおしまい。私はたくさんをさまざまな答えと私の戦いから学んだJava Genericsと私は他の皆のためにここに冗長に投稿します。これが将来の誰かを助けることを願っています。

0
Mark

次のようなビジターパターンを使用できます。

abstract class Event {
    boolean accept(EventVisitor visitor);

    KeyPress extends Event {
        boolean accept(EventVisitor visitor)
        {
            return visitor.process(this);
        }
    }
    KeyRelease extends Event { ... }
    KeyHold extends Event { ... }
    // ...
}

abstract class EventVisitor
{
    boolean process(KeyPress event)
    {
        return false;
    }

    boolean process(KeyRelease event)
    {
        return false;
    }

    // One default process() method for each subclass of Event.

    boolean respond(Event event)
    {
        return event.accept(this);
    }
}

その後

class DownKeyListener extends EventVisitor
{
    void process(KeyPress event)
    {
        return handleKeyPress(event);
    }

    void process(KeyHold event)
    {
        return handleKeyHold(event);
    }
}

このソリューションにより、

  1. イベントのタイプでディスパッチするために必要なすべてのinstanceofを回避します。
  2. ビジター実装によって明示的に処理されないすべてのイベントには、抽象クラスEventVisitorでデフォルトの処理があります。
2
Giorgio

別のオプションは、制限された型システムを「戦う」ことをやめることです。私が理解しているのは、リッスンしたいものには必ずハンドラーがあり、正しいハンドラーが呼び出されるようにすることです。次に、これらの2つのステップを組み合わせる必要があります。あなたは正確にあなたが聞いているものを示していませんが、あなたは何かobservableを持っていると思います。

observable.register<KeyPress>(new EventListener() {
    void handleEvent(Event e) {
        assert e instanceof KeyPress;
        KeyPress kpe = (KeyPress)e;
        //Do stuff with kpe
    }
});

これにはキャストが含まれますが、開発中にアサートを使用してチェックでき、キャストは登録のすぐ隣にあります。もちろん、匿名クラスを使用する必要はありません。

observable.register<KeyPress>(new MyKeyPressHandler());

必要に応じて、戻り値の型をbooleanに変更できます。


それがどのように機能するかの例:

// Could also be abstract class
interface Event {
}

interface EventListener {
    void handleEvent(Event e);
}

interface Observable {
    public void register(Class<? extends Event> event, EventListener handler);
}

// An example of something observable - here how a keyboard might give events.
class KeyDownEvent {
    private Key key;
    KeyDownEvent(Key k) {
        key = k;
    }
    public Key getKey() {
        return key;
    }
}

class Keyboard implements Observable {
    private Map<Class<? extends Event>, Set<EventListener>> handlers = new HashMap<>();

    public void register(Class<? extends Event> event, EventListener handler) {
        // not handling if the set has not been defined yet, but it should be, (or in constructor).
        handlers.put(event, handler);
    }

    private notifyListeners(Event event) {
        Class eventClass = event.getClass();
        for (EventListener listener : handlers.get(eventClass)) {
            listener.handleEvent(event);
        }
    }

    private void updateKeyboardState(KeyboardState ks) {
        //Suppose this is called by some code to update the keyboard
        // get state
        for (Key k : keys) {
            if (ks.isPressed(k) && lastState.get(k) == KeyState.UP) {
                notifyListeners(new KeyDownEvent(k));
            }
        }
    }
}

class LogKeyPressesToConsole {
    LogKeyPressesToConsole(Keyboard keyboard) {
        keyboard.register(KeyDownEvent, new EventListener() {
            void handleEvent(Event event) {
                assert event instanceof KeyDownEvent;
                KeyDownEvent kde = (KeyDownEvent)event;
                System.out.printf(kde.getKey().toString());
            }
        });
    }
}
1
senevoldsen

AbstractListenerを、処理したいEventの具象型によってパラメーター化することを検討する場合があります。

abstract class AbstractListener<T extends Event> {
    ...
    protected abstract boolean respond(T event);
}

次に持っている:

class KeyPressListener extends AbstractListener<KeyPressEvent> {
    ....
}

個別の具象ハンドラーが必要ですが、両方を異なるタイプのイベントからの処理を処理できる何かにディスパッチすることができます。

0
Andy Dalton