Джава. Правильный шаблон для реализации слушателей

Очень часто у меня возникает ситуация, когда у данного объекта должно быть много слушателей. Например, я мог бы иметь

class Elephant {
  public void addListener( ElephantListener listener ) { ... }
}

но у меня будет много таких ситуаций. То есть у меня тоже будет Tiger объект, который будет иметь TigerListeners. Сейчас, TigerListenerс и ElephantListenerс довольно разные:

interface TigerListener {
  void listenForGrowl( Growl qrowl );
  void listenForMeow( Meow meow );
}

в то время как

interface ElephantListener {
  void listenForStomp( String location, double intensity );
}

Я нахожу, что мне всегда нужно повторно реализовывать механизм вещания в каждом классе животных, и реализация всегда одинакова. Есть ли предпочтительный шаблон?

6 ответов

Вместо каждого Listener имея конкретные методы для каждого типа события, вы можете отправить его, измените интерфейс, чтобы принять общий Event учебный класс. Затем вы можете подкласс Event к определенным подтипам, если вам нужно, или они содержат состояние, такое как double intensity,

TigerListener и ElephentListener затем становятся

interface TigerListener {
    void listen(Event event);
}

Фактически, вы можете в дальнейшем преобразовать этот интерфейс в обычный Listener:

interface Listener {
    void listen(Event event);
}

Ваш Listener реализации могут содержать логику, которая им нужна для конкретных событий, которые им нужны

class TigerListener implements Listener {
    @Overrides
    void listen(Event event) {
        if (event instanceof GrowlEvent) {
            //handle growl...
        }
        else if (event instance of MeowEvent) {
            //handle meow
        }
        //we don't care about any other types of Events
    }
}

class ElephentListener {
    @Overrides
    void listen(Event event) {
        if (event instanceof StompEvent) {
            StompEvent stomp = (StompEvent) event;
            if ("north".equals(stomp.getLocation()) && stomp.getDistance() > 10) { 
                ... 
            }
        }
    }
}

Ключевое отношение между подписчиком и издателем заключается в том, что издатель может отправлять события подписчикам, необязательно, что он может отправлять ему определенные типы событий - этот тип рефакторинга выталкивает эту логику из интерфейса в конкретные реализации.,

Это более общий ответ для людей, которые приходят сюда, просто желая сделать слушателя. Я суммирую создание пользовательских слушателей из CodePath. Прочтите эту статью, если вам нужно больше объяснений.

Вот шаги.

1. Определите интерфейс

Это в дочернем классе, который должен общаться с каким-то неизвестным родителем.

public class MyClass {

    // interface
    public interface MyClassListener {
        // add whatever methods you need here
        public void onSomeEvent(String title);
    }
}

2. Создайте Установщик Слушателя

Добавьте личную переменную-член слушателя и метод открытого сеттера к дочернему классу.

public class MyClass {

    // add a private listener variable
    private MyClassListener mListener = null;

    // provide a way for another class to set the listener
    public void setMyClassListener(MyClassListener listener) {
        this.mListener = listener;
    }


    // interface from Step 1
    public interface MyClassListener {
        public void onSomeEvent(String title);
    }
}

3. Триггер события прослушивателя

Дочерний объект теперь может вызывать методы в интерфейсе слушателя. Убедитесь, что проверили на ноль, потому что там может быть никто не слушает (То есть родительский класс, возможно, не вызвал метод setter для нашего слушателя.)

public class MyClass {

    public void someMethod() {
        // ...

        // use the listener in your code to fire some event
        if (mListener != null) 
            mListener.onSomeEvent("hello");
    }


    // items from Steps 1 and 2

    private MyClassListener mListener = null;

    public void setMyClassListener(MyClassListener listener) {
        this.mListener = listener;
    }

    public interface MyClassListener {
        public void onSomeEvent(String myString);
    }
}

4. Реализуйте обратные вызовы слушателя в родительском

Теперь родитель может использовать прослушиватель, который мы настроили в дочернем классе.

Пример 1

public class MyParentClass {

    private void someMethod() {

        MyClass object = new MyClass();
        object.setMyClassListener(new MyClass.MyClassListener() {
            @Override
            public void onSomeEvent(String myString) {
                // handle event
            }
        });
    }
}

Пример 2

public class MyParentClass implements MyClass.MyClassListener {

    public MyParentClass() {
        MyClass object = new MyClass();
        object.setMyClassListener(this);
    }

    @Override
    public void onSomeEvent(String myString) {
        // handle event
    }
}

Другие варианты - это шаблон доски. Это отключает издателя и подписчика друг от друга, и ни один из них не будет содержать какой-либо широковещательный код. Они оба просто используют механизм обмена сообщениями для pub/sub, и ни один из них не имеет прямого отношения к другому.

Это распространенная модель для обмена сообщениями на платформе OSGi.

Я думаю, что вы делаете это правильно, так как ваши интерфейсы имеют семантическую ценность и выражают то, что они слушают (например, рычит и мяукает вместо ударов). При использовании общего подхода вы можете повторно использовать широковещательный код, но вы можете потерять читабельность.

Например, есть java.beans.PropertyChangeSupport которая является утилитой для реализации Oberservers, слушающих изменения значений. Он выполняет широковещательную рассылку, но вам все еще нужно реализовать метод в своем доменном классе и делегировать объекту PropertyChangeSupport. Методы обратного вызова сами по себе не имеют смысла, а передаваемые события основаны на строках:

public interface PropertyChangeListener extends java.util.EventListener {
     void propertyChange(PropertyChangeEvent evt);
}

Еще один java.util.Observable который обеспечивает механизм вещания, но это также не лучшая вещь imho.

мне нравится ElephantListener.onStomp()

Именно для этого я создал библиотеку сигналов . Чтобы удалить код котла, участвующий в «повторной реализации механизма вещания».

Сигнал - это объект, автоматически созданный из интерфейса. У него есть методы для добавления слушателей и отправки / трансляции событий.

Выглядит это так:

      interface Chat{
    void onNewMessage(String s);    
}

class Foo{
    Signal<Chat> chatSignal = Signals.signal(Chat.class);
    
    void bar(){
        chatSignal.addListener( s-> Log.d("chat", s) ); // logs all the messaged to Logcat
    }
}

class Foo2{
    Signal<Chat> chatSignal = Signals.signal(Chat.class);
    
    void bar2(){
        chatSignal.dispatcher.onNewMessage("Hello from Foo2"); // dispatches "Hello from Foo2" message to all the listeners
    }
}

В этом примере Foo2 транслирует новые сообщения через Chat интерфейс. Foo затем послушайте их и зарегистрируйте в logcat.

  • Обратите внимание, что нет ограничений на то, какие интерфейсы вы можете использовать.
  • У вас также есть сахарный API для регистрации только для первой трансляции и отмены регистрации для всех сигналов сразу (через SignalsHelper)

Попробуйте библиотеку java kiss, и вы сделаете это быстрее и правильнее.

import static kiss.API.*;

class Elephant {
  void onReceiveStomp(Stomp stomp) { ... }
}

class Tiger {
  void onReceiveMeow(Meow meow) { ... }
  void onReceiveGrowl(Growl growl) { ... }
}

class TigerMeowGenerator extends Generator<Meow> {
   // to add listeners, you get: 
   //    addListener(Object tiger); // anything with onReceiveMeow(Meow m);
   //    addListener(meow->actions()); // any lambda
   // to send meow's to all listeners, use 
   //    send(meow)
}

Генератор является поточно-ориентированным и эффективным (написание правильных генераторов является самой сложной частью). Это реализация идей в Java Dev. Журнал - Умение слушать на Java (локальная копия)

Другие вопросы по тегам