Come garantire un metodo che gestisca ogni evento in un sistema di eventi?

6

Ho diversi tipi di eventi, ad esempio:

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

E molti ascoltatori che rispondono ad alcuni degli eventi di cui sopra registrandoli in un gestore di eventi. Attualmente è così:

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) { ... }
}

Ciò che non mi piace di questo è che non c'è nulla che forzi o controlli almeno la relazione tra gli eventi registrati e i controlli per loro e la gestione nel metodo di risposta. Questo continua a portare a bug degli sviluppatori. è anche un sacco di codice con instanceof s per poco beneficio (direi).

Quindi penso di fare qualcosa di "intelligente" come questo: creare una mappa tra i tipi di evento e i gestori in modo che ogni evento registrato per la gestione abbia un gestore:

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);
}

E poi:

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 -> ...;
}

Ovviamente questo non funziona a causa dei farmaci generici. apply dà un errore e capisco perché

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)

Non so come ottenere ciò che voglio funzionare correttamente. Alcune cose che avevo in mente:

  • Trasmetti il risultato di map.get a qualcosa che assicuri che funzioni correttamente
  • Cambia il metodo respond come generico protected abstract <T extends Event> boolean respond(T event); che dà l'errore simile:
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)

Qualcuno ha un suggerimento su come ottenere ciò che voglio in qualche modo?

    
posta Mark 26.02.2017 - 15:53
fonte

4 risposte

0

L'ho capito usando il mio approccio generico. Il mio secondo punto sulla modifica di respond a un generico <T extends Event> boolean respond(T event) è stato davvero l'approccio giusto. L'errore del compilatore per i tipi incompatibili può essere risolto con un cast sporco ma sicuro. Ecco il codice completo di lavoro

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;
}

L'output di esecuzione è

true
true
false

come richiesto.

Ci sono 3 cose che voglio sottolineare su questo approccio.

Il primo è la facilità d'uso e la sicurezza del tempo di compilazione. Accoppia la registrazione degli eventi (l'elenco degli eventi da ascoltare che in questo caso sono le chiavi della mappa) e la gestione degli eventi. Per ogni tipo di evento registrato è necessario fornire un gestore per quel tipo di evento e quando il listener deve rispondere a un evento utilizzato dallo stesso gestore. La creazione di un nuovo tipo di evento non comporta alcun lavoro aggiuntivo come i metodi dei visitatori o le nuove interfacce. È stato creato e se un ascoltatore vuole usarlo è pronto.

La seconda cosa è che il metodo di risposta può essere spostato nella superclasse astratta perché ha lo stesso comportamento per tutti gli ascoltatori: ottenere il gestore per l'evento, chiamarlo e restituire il risultato. Se un listener di sottoclassi vuole cambiare questo per un tipo di evento specifico, può sovrascriverlo, controllare il tipo di evento e per il resto chiamata super.

La terza cosa è quel cast e la perdita di informazioni tra l'inserimento della funzione evento nella mappa e il recupero della funzione dall'evento. La mappa è dichiarata con 2 caratteri jolly senza alcun modo per forzare una relazione tra loro per quanto ne so. questo a causa della mancanza di campi generici in java. se mai esistesse una cosa del genere, semplificerebbe questo approccio. La forzatura degli stessi tipi per l'evento e il relativo gestore viene eseguita nel metodo register che viene aggiunto alla mappa ma tale applicazione viene persa nella dichiarazione del campo. Ciò significa che quando si ottiene il valore dalla chiave non c'è più corrispondenza di tempo di compilazione e un cast deve essere fatto allo stesso tipo. Sappiamo che questo cast è sicuro se usassimo il metodo register per popolare la mappa altrimenti saremmo condannati. un campo generico eliminerebbe la necessità di un cast "sicuro".

Questo è tutto. Ho imparato molto dalle diverse risposte e dalla mia lotta con i generici di Java e pubblico qui in modo verbale a beneficio di tutti gli altri. Speriamo che questo aiuti qualcun altro in futuro.

    
risposta data 02.03.2017 - 03:54
fonte
2

Potresti usare un modello di visitatore, in questo modo:

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);
    }
}

e poi

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

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

Questa soluzione garantisce che

  1. Eviti tutte le instanceof necessarie per inviare il tipo di evento.
  2. Tutti gli eventi che non sono gestiti esplicitamente da un'implementazione di visitatore hanno una gestione predefinita nella classe astratta EventVisitor .
risposta data 27.02.2017 - 06:52
fonte
0

Potresti considerare di rendere AbstractListener parametrizzato dal calcestruzzo tipo di Event che desideri gestire.

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

Quindi hai:

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

Avresti bisogno di gestori di calcestruzzo separati, ma potresti averli entrambi inviati a qualcosa che potrebbe gestire l'elaborazione da diversi tipi di eventi.

    
risposta data 27.02.2017 - 06:10
fonte
0

Un'altra opzione è smettere di "combattere" il sistema di tipo limitato. La mia comprensione è che si desidera garantire che tutto ciò che si desidera ascoltare abbia un gestore e che il gestore destro venga chiamato. Quindi questi due passaggi dovrebbero essere combinati. Non hai mostrato cosa stai ascoltando esattamente, ma suppongo che tu abbia qualcosa observable .

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

Questo implica un cast, ma puoi usare assert per controllarlo durante lo sviluppo e il cast è proprio accanto alla registrazione. Ovviamente non devi usare una classe anonima:

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

Puoi modificare il tipo di ritorno in boolean se necessario.

Un esempio di come potrebbe funzionare:

// 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());
            }
        });
    }
}
    
risposta data 27.02.2017 - 15:49
fonte

Leggi altre domande sui tag