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

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

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

но у меня будет много таких ситуаций. То есть у меня также будет объект Tiger, который будет иметь TigerListeners. Теперь TigerListeners и ElephantListeners совершенно разные:

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

пока

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

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


person Jake    schedule 04.06.2010    source источник


Ответы (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) { 
                ... 
            }
        }
    }
}

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

person matt b    schedule 04.06.2010
comment
Думаю, мой вопрос действительно касался того, какая реализация предпочтительнее. Механизм широковещания повторно реализован в моем коде трижды (не так много в схеме вещей), в то время как ваша версия требует совершенно новой иерархии объектов и операторов instanceof. Есть преимущества и недостатки, но как мне выбрать правильный метод в данной ситуации? - person Jake; 04.06.2010
comment
Кроме того, есть очень веский аргумент о потере читабельности при большом количестве типов событий. - person Jake; 04.06.2010
comment
Что ж, вы можете заменить instanceof на использование универсальных типов, если вам это действительно интересно, или какое-либо другое решение OO. Я не вижу в этом проблемы. Я интерпретировал ваш вопрос как недовольство повторяющимся определением интерфейсов слушателя в вашем коде, это подход к решению этой проблемы. И я действительно не понимаю, что вы думаете о потере читабельности - я думаю, что отделение концепции Events от интерфейса слушателя устраняет необходимость дублировать определения того, что такое Listener. - person matt b; 04.06.2010
comment
Лично я бы выбрал определение одного интерфейса и одного метода для работы с (listen(Event)), а не переопределение listenFoo() метода несколькими разными способами. - person matt b; 04.06.2010
comment
Старайтесь избегать использования дженериков. interface Listener<T>{ void listen(T event); } - person darkconeja; 07.03.2018

Это более общий ответ для людей, которые приходят сюда просто для того, чтобы стать слушателем. Я резюмирую Создание настраиваемых прослушивателей из 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. События прослушивателя триггера

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

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
    }
}
person Suragch    schedule 30.10.2017
comment
Обратите внимание, что NPE возможен, если MyClass.setMyClassListener(null) и MyClass.someMethod() могут вызываться из разных потоков. - person rhashimoto; 12.02.2019
comment
@rhashimoto, как лучше всего это предотвратить? - person Suragch; 12.02.2019
comment
Я бы использовал переменную члена AtomicReference для удержания слушателя. - person rhashimoto; 12.02.2019

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

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

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

Другой - java.util.Observable, который обеспечивает механизм трансляции, но это тоже не самое лучшее, имхо.

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

person mhaller    schedule 04.06.2010
comment
Семантическое значение, хотя и является допустимым аргументом, создает тесную связь (и риск изменения). Хороший глаз, но я не могу согласиться. - person Justin; 04.06.2010

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

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

person Robin    schedule 04.06.2010

Я создал библиотеку сигналов только для этой цели. Для удаления кода котла, задействованного в повторной реализации механизма вещания.

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

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

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)
person Ilya Gazman    schedule 16.03.2021

Попробуйте библиотеку 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" (локальная копия)

person Warren MacEvoy    schedule 13.08.2016
comment
Извините за -1, но это вопрос паттернов, а не использования некоторых библиотек. - person Kamil; 20.09.2018
comment
ссылка мертва - это была публикация в журнале Java Developer много лет назад. - person Warren MacEvoy; 26.09.2018