Избегать "instanceof" в Java
У меня есть следующая (может быть, общая) проблема, и на данный момент она меня совершенно озадачивает:
Есть пара сгенерированных объектов событий, которые расширяют абстрактный класс Event
и я хочу разделить их на сессионные бины, как
public void divideEvent(Event event) {
if (event instanceof DocumentEvent) {
documentGenerator.gerenateDocument(event);
} else if (event instanceof MailEvent) {
deliveryManager.deliverMail(event);
...
}
...
}
Но в будущем может быть более двух типов событий, поэтому if-else будет длинным и, возможно, нечитаемым. Дополнительно я думаю instanceof
на самом деле не является "лучшей практикой" в этом случае.
Я мог бы добавить абстрактный метод к Event
напечатайте и попросите их разделить себя, но затем я должен внедрить конкретные сессионные компоненты в каждую сущность.
Есть ли подсказка для достижения "симпатичного" решения этой проблемы?
Спасибо за любую помощь!
8 ответов
Самый простой подход состоит в том, чтобы Событие предоставляло метод, который вы можете вызвать, чтобы Событие знало, что делать.
interface Event {
public void onEvent(Context context);
}
class DocumentEvent implements Event {
public void onEvent(Context context) {
context.getDocumentGenerator().gerenateDocument(this);
}
}
class MailEvent implements Event {
public void onEvent(Context context) {
context.getDeliveryManager().deliverMail(event);
}
}
class Context {
public void divideEvent(Event event) {
event.onEvent(this);
}
}
Полиморфизм твой друг.
class DocumentGenerator {
public void generate(DocumentEvent ev){}
public void generate(MainEvent ev){}
//... and so on
}
Тогда просто
DocumentGenerator dg = new DocumentGenerator();
// ....
dg.generate(event);
Обновить
Многие люди высказали возражение, что вам "нужно знать типы событий во время компиляции". И, да, вам явно необходимо знать, какие события вы интерпретируете во время компиляции генераторной части, когда еще вы сможете написать генерирующую часть?
Эти конкурирующие примеры используют шаблон Command, что хорошо, но означает, что события должны знать детали не только своего представления, но и того, как печатать свое представление. Это означает, что каждый класс может иметь два вида изменений требований, к которым они чувствительны: изменения в том, что представляют события, и изменения в том, как события представлены в печати.
Теперь рассмотрим, например, необходимость интернационализации этого. В случае с шаблоном Command вам нужно перейти к n классам для n различных типов событий и написать новые методы do. В случае полиморфизма изменения локализуются в одном классе.
Естественно, если вам нужно один раз интернационализировать, вам может понадобиться много языков, которые заставляют вас добавлять что-то вроде Стратегии к каждому классу в случае с шаблоном Command, требуя теперь n классов × m языков; опять же, вам нужна только одна стратегия и один класс в случае полиморфизма.
Есть причины выбрать любой из этих подходов, но утверждать, что подход полиморфизма неверен, просто неверен.
Каждое событие имеет функцию, скажем, сделать. Каждый подкласс переопределяет do, чтобы выполнить (:P) соответствующее действие. Динамическая отправка делает все остальное потом. Все, что вам нужно сделать, это вызвать event.do()
У меня нет прав на комментирование, и я не знаю точного ответа. Но только я или какой-то ppl здесь предлагаем использовать перегрузку (которая происходит во время компиляции и, следовательно, просто генерирует ошибку компиляции) для решения этой проблемы?
Просто пример. Как видите, он не скомпилируется.
package com.stackru;
public class Test {
static abstract class Event {}
static class MailEvent extends Event {}
static class DocEvent extends Event {}
static class Dispatcher {
void dispatchEvent(DocEvent e) {
System.out.println("A");
}
void dispatchEvent(MailEvent e) {
System.out.println("B");
}
}
public static void main(String[] args) {
Dispatcher d = new Dispatcher();
Event e = new DocEvent();
d.dispatchEvent(e);
}
В чем проблема с использованием порядка разрешения метода?
public void dispatchEvent(DocumentEvent e) {
documentGenerator.gerenateDocument(event);
}
public void dispatchEvent(MailEvent e) {
deliveryManager.deliverMail(event);
}
Пусть Java выполнит работу по сопоставлению правильного типа аргумента, а затем просто отправит событие правильно.
Вы можете зарегистрировать каждый из ваших классов-обработчиков для каждого типа события и выполнять диспетчеризацию, когда событие происходит следующим образом.
class EventRegister {
private Map<Event, List<EventListener>> listerMap;
public void addListener(Event event, EventListener listener) {
// ... add it to the map (that is, for that event, get the list and add this listener to it
}
public void dispatch(Event event) {
List<EventListener> listeners = map.get(event);
if (listeners == null || listeners.size() == 0) return;
for (EventListener l : listeners) {
l.onEvent(event); // better to put in a try-catch
}
}
}
interface EventListener {
void onEvent(Event e);
}
А затем получите ваши конкретные обработчики для реализации интерфейса и зарегистрируйте эти обработчики в EventRegister.
Это типичный вариант использования для типов сумм, также известных как теговые объединения. К сожалению, Java не поддерживает их напрямую, поэтому они должны быть реализованы с использованием некоторого варианта шаблона посетителя.
interface DocumentEvent {
// stuff specific to document event
}
interface MailEvent {
// stuff specific to mail event
}
interface EventVisitor {
void visitDocumentEvent(DocumentEvent event);
void visitMailEvent(MailEvent event);
}
class EventDivider implements EventVisitor {
@Override
void visitDocumentEvent(DocumentEvent event) {
documentGenerator.gerenateDocument(event);
}
@Override
void visitMailEvent(MailEvent event) {
deliveryManager.deliverMail(event);
}
}
Здесь мы определили наши EventDivider
Теперь, чтобы обеспечить механизм отправки:
interface Event {
void accept(EventVisitor visitor);
}
class DocumentEventImpl implements Event {
@Override
void accept(EventVisitor visitor) {
visitor.visitDocumentEvent(new DocumentEvent(){
// concrete document event stuff
});
}
}
class MailEventImpl implements Event { ... }
public void divideEvent(Event event) {
event.accept(new EventDivider());
}
Здесь я использовал максимально возможное разделение задач, чтобы ответственность каждого класса и интерфейса была одна и только одна. В реальных проектах DocumentEventImpl
, DocumentEvent
реализация и DocumentEvent
объявление интерфейса обычно объединяется в один класс DocumentEvent
, но это вводит циклические зависимости и вызывает некоторые зависимости между конкретными классами (и, как мы знаем, следует предпочитать зависеть от интерфейсов).
Дополнительно, void
обычно следует заменить параметром типа для представления типа результата, например так:
interface EventVisitor<R> {
R visitDocumentEvent(DocumentEvent event);
...
}
interface Event {
<R> R accept(EventVisitor<R> visitor);
}
Это позволяет использовать посетителей без гражданства, с которыми очень приятно иметь дело.
Эта техника позволяет (почти?) Всегда устранять instanceof
механически, а не выяснять решение конкретной проблемы.
Вы могли бы иметь Dispatcher
интерфейс, определенный как
interface Dispatcher {
void doDispatch(Event e);
}
с реализациями, такими как DocEventDispatcher
, MailEventDispatcher
, так далее.
Затем определите Map<Class<? extends Event>, Dispatcher>
с записями вроде (DocEvent, new DocEventDispatcher())
, Тогда ваш способ отправки может быть уменьшен до:
public void divideEvent(Event event) {
dispatcherMap.get(event.getClass()).doDispatch(event);
}
Вот модульный тест:
public class EventDispatcher {
interface Dispatcher<T extends Event> {
void doDispatch(T e);
}
static class DocEventDispatcher implements Dispatcher<DocEvent> {
@Override
public void doDispatch(DocEvent e) {
}
}
static class MailEventDispatcher implements Dispatcher<MailEvent> {
@Override
public void doDispatch(MailEvent e) {
}
}
interface Event {
}
static class DocEvent implements Event {
}
static class MailEvent implements Event {
}
@Test
public void testDispatcherMap() {
Map<Class<? extends Event>, Dispatcher<? extends Event>> map = new HashMap<Class<? extends Event>, Dispatcher<? extends Event>>();
map.put(DocEvent.class, new DocEventDispatcher());
map.put(MailEvent.class, new MailEventDispatcher());
assertNotNull(map.get(new MailEvent().getClass()));
}
}