Избегайте instanceof при проверке типа сообщения

У меня есть следующая ситуация, когда клиентский класс выполняет различное поведение в зависимости от типа сообщения, которое он получает. Мне интересно, есть ли лучший способ сделать это, поскольку мне не нравятся операторы instanceof и if.

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

public class Client {
    messageReceived(IMessage message) {
        if(message instanceof concreteMessageA) {
            concreteMessageA msg = (concreteMessageA)message;
            //do concreteMessageA operations
        }
    }
        if (message instanceof concreteMessageB) {
            concreteMessageb msg = (concreteMessageB)message;
            //do concreteMessageB operations
        }
}

person volker238    schedule 10.08.2009    source источник
comment
Исходя из того, что вы используете JMS, что такое IMessage? JMS имеет предопределенные типы сообщений, поэтому вы не можете определять свои собственные, если у вас нет посредника, который их преобразовывает, что является просто головной болью, если вы не пытаетесь абстрагироваться от JMS.   -  person Robin    schedule 11.08.2009


Ответы (5)


Простой способ избежать тестирования instanceof - это полиморфная диспетчеризация; например

public class Client {
    void messageReceived(IMessage message) {
        message.doOperations(this);
    }
}

где каждый класс сообщений определяет соответствующий doOperations(Client client) метод.

РЕДАКТИРОВАТЬ: второе решение, которое лучше соответствует требованиям.

Альтернативой, заменяющей последовательность тестов instanceof оператором switch, является:

public class Client {
    void messageReceived(IMessage message) {
        switch (message.getMessageType()) {
        case TYPE_A:
           // process type A
           break;
        case TYPE_B:
           ...
        }
    }
}

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

person Stephen C    schedule 10.08.2009
comment
Второй подход может потребовать приведения, если разные блоки process необходимы для доступа к свойствам, доступным только для подтипов сообщений. - person oxbow_lakes; 10.08.2009
comment
Мне не нравится идея о том, что клиент определяет, как следует обрабатывать сообщение. Что произойдет, если вам нужно изменить doOperations - как вы получите этот обновленный код для всех клиентов? Думаю, в этом случае лучше использовать шаблон посетителя. Так что посетитель-клиент будет принят сообщением и сможет красться, чтобы делать то, что он хочет. - person pjp; 10.08.2009
comment
@pgp: эээ ... Я думаю, вы неправильно понимаете проблему. Способ постановки задачи, логика обработки сообщений ›› ‹** специфичны для класса Client. - person Stephen C; 10.08.2009
comment
Сначала я думал использовать полиморфизм и делать именно то, что Стивен говорит в своем первом примере кода, но я подумал, что это может нарушать инкапсуляцию, передавая Client сообщениям. Кроме того, если у меня было 5 клиентов, и каждый из них хочет сделать что-то свое с каждым сообщением, это будет 5 различных операций в каждом сообщении, по одной для каждого из клиентов. - person volker238; 10.08.2009
comment
@volker: передача self не нарушает инкапсуляцию. Инкапсуляция нарушается только в том случае, если Клиент предоставляет состояние / методы, которых он не должен. - person Stephen C; 11.08.2009
comment
@volker: вариант использования 5 клиентов может быть рассмотрен путем создания подкласса класса Client, каждый из которых перегружает метод messageReceived и, возможно, делегирует super.messageReceived для работы с общими типами сообщений. - person Stephen C; 11.08.2009

Один из вариантов - это цепочка обработчиков. У вас есть цепочка обработчиков, каждый из которых может обрабатывать сообщение (если применимо), а затем потреблять его, что означает, что оно не будет передано дальше по цепочке. Сначала вы определяете интерфейс Handler:

public interface Handler {
    void handle(IMessage msg);
}

И тогда логика цепочки обработчиков выглядит так:

List<Handler> handlers = //...
for (Handler h : handlers) {
    if (!e.isConsumed()) h.handle(e);
}

Затем каждый обработчик может решить обработать / потребить событие:

public class MessageAHandler implements Handler {
    public void handle(IMessage msg) {
        if (msg instanceof MessageA) {
            //process message
            //consume event 
            msg.consume();
        }
    }
}

Конечно, это не избавляет от instanceofs - но это означает, что у вас нет огромного if-elseif-else-if-instanceof блока, который может быть нечитаемым.

person oxbow_lakes    schedule 10.08.2009
comment
Читаемость - это вопрос мнения / вкуса. Создание новых классов обработчиков для каждого типа сообщений означает, что вам нужно снова поискать в другом месте (кроме класса Client и классов IMessage), чтобы найти логику. Я бы не назвал это большой победой в удобочитаемости. - person Stephen C; 10.08.2009
comment
Я бы тоже! Все зависит от того, ненавидите ли вы большие if-else-instanceof блоки. Звучит так, как будто OP делает, поэтому я предложил это. Цепочка обработчиков полезна, потому что она более гибкая, чем if-else. Обработчику не обязательно принимать событие, поэтому его может обрабатывать более одного обработчика. - person oxbow_lakes; 10.08.2009

Какой тип системы сообщений вы используете?

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

Я знаю, что вы можете сделать это в JMS или в службе событий OSGi.

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

  String filterMsg1 = "JMSType='messageType1'";
  String filterMsg2 = "JMSType='messageType2'";

  // Create a receiver using this filter
  Receiver receiverType1 = session.createReceiver(queue, filterMsg1);
  Receiver receiverType2 = session.createReceiver(queue, filterMsg2);

  receiverType1.setMessageHandler(messageType1Handler);
  receiverType2.setMessageHandler(messageType2Handler);

Теперь каждый обработчик будет получать только определенный тип сообщения (без instanceof или if-then), конечно, при условии, что отправитель устанавливает тип с помощью вызовов setJMSType () в исходящем сообщении.

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

person Robin    schedule 10.08.2009
comment
Я использую JMS, и мне нужно рассмотреть ваше предложение. - person volker238; 10.08.2009
comment
@ volker238 - он называется селектором, и вы можете использовать его для фильтрации на основе информации заголовка сообщения. - person Robin; 11.08.2009
comment
@ volker238 - Я добавил фрагмент кода, основанный на том, что вы используете JMS. Предполагая, что вы можете установить свойства заголовка, селектор - это абсолютно подходящий вариант, поскольку он позволяет реализации JMS выполнять всю маршрутизацию за вас. Нет необходимости в некрасивом коде обработки, который вы катите сами. - person Robin; 11.08.2009

Решение Java 8, использующее двойную отправку. Не избавляется от instanceof полностью, но требует только одной проверки для каждого сообщения вместо цепочки if-elseif.

public interface Message extends Consumer<Consumer<Message>> {};

public interface MessageA extends Message {

   @Override
   default void accept(Consumer<Message> consumer) {
      if(consumer instanceof MessageAReceiver){
        ((MessageAReceiver)consumer).accept(this);
      } else {
        Message.super.accept(this);
      }
   }
}

public interface MessageAReceiver extends Consumer<Message>{
   void accept(MessageA message);
}
person Dev    schedule 25.08.2016

С JMS 2.0 вы можете использовать:

consumer.receiveBody(String.class)

Для получения дополнительной информации см. здесь:

person t-jd    schedule 04.04.2019