Java中避免使用'instanceof'方法

64

我遇到了以下(可能常见的)问题,目前这个问题困扰着我:

有几个生成的事件对象继承自抽象类Event,我想将它们分配到会话Bean中,例如

public void divideEvent(Event event) {
    if (event instanceof DocumentEvent) {
        documentGenerator.gerenateDocument(event);
    } else if (event instanceof MailEvent) {
        deliveryManager.deliverMail(event);
        ...
    }
    ...

}

但是未来可能会有两种以上的事件类型,这样if-else语句会变得很长,可能难以阅读。此外,我认为在这种情况下,instanceof并不是真正的“最佳实践”。

我可以将一个抽象方法添加到Event类型中,并让它们自己分离,但是这样我必须在每个实体中注入特定的Session Beans。

有没有任何提示可以解决这个问题的“漂亮”方案?

感谢任何帮助!


17
对于遵循良好的面向对象编程实践的渴望,我给出1+的评价。 - Hovercraft Full Of Eels
也许这篇文章涵盖了它?动态分派 - Dilum Ranatunga
8个回答

54

最简单的方法是让事件(Event)提供一个可供调用的方法,这样事件(Event)就知道该做什么了。

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

1
事件为什么要知道生成哪种类型的文档? - Charlie Martin
2
一个事件可能不会知道,但DocumentEvent可以知道要生成的文档的一些信息。 ;) 这个问题相当抽象,这种方法并不适用于所有情况,但如果可以使用,它将是最简单、最易于扩展的解决方案。 - Peter Lawrey
11
你所展示的是一个非常好的 访问者模式(Visitor Pattern) 的例子。 - Buhake Sindi
3
@精英们,这与访问者模式类似,但并不相同。访问者模式是为了根据封闭的对象图插入行为。在我看来,这更接近于标准的ActionListener.actionPerformed模式。 - Kirk Woll
7
我对此感到紧张。这似乎将一个简单的POJO数据对象与逻辑混合得太多了。事件不再是数据包,而开始成为其他方法的代理。 - TheLQ

11

多态是你的朋友。

class DocumentGenerator {
   public void generate(DocumentEvent ev){}
   public void generate(MainEvent ev){}
   //... and so on
}
然后只需要:
 DocumentGenerator dg = new DocumentGenerator();

 // ....
 dg.generate(event);

更新

有许多人提出反对意见,认为你“必须在编译时知道事件的种类”。是的,当你能够编写生成部分时,你显然必须在生成器的编译时期知道你正在解释什么事件。

这些竞争性的示例使用了命令模式,这很好,但意味着事件不仅需要知道它们的表示方式,还需要知道如何“打印”它们的表示方式。这意味着每个类可能有两种敏感要求的更改:事件表示方式的更改和事件在打印时表示方式的更改。

现在,例如,考虑需要国际化这个问题。在命令模式案例中,您必须转到 n 个不同的事件类型类并编写新的 do 方法。在多态案例中,更改只局限于一个类。

当然,如果您需要进行一次国际化,您可能需要许多语言,这将促使您向命令模式每个类添加类似于策略的东西,导致现在需要 n 个类 × m 种语言;而在多态案例中,您只需要一个策略和一个类。

选择任何一种方法都有理由,但声称多态方法是错误的就是不正确的。


4
乍一看这看起来不错,但可能不适合,因为它需要在编译时知道类型,而显然在所讨论的情况下并非如此。 - x4u
4
更具体地说,上述代码无法编译通过。dg.generate(event) 方法调用会失败,因为在 DocumentGenerator 上没有匹配 generate(Event) 的方法。多态在这种方法参数的情况下不起作用。 - David Harkness
1
这个答案是错误的。instanceof在运行时检查对象类型,而方法重载则根据不同类型在编译时进行反应。 - blubb
同意其他人说这个答案是错误的 - 它确实是。要调用重载方法,必须在编译时确定。你必须将你的事件对象显式转换为正确的子类对象(这违背了初衷),才能使其工作。你答案中的“更新”部分并没有改变这个事实。 - GreenieMeanie
这确实非常错误,甚至不是多态而是重载。这里没有运行时分派。 - KeatsPeeks

8
每个事件都有一个功能,比如说 do。 每个子类都会重写 do 函数,以执行相应的操作。 动态分派之后就会处理所有其他事情。 你只需要调用 event.do() 即可。

4

我没有评论的权限,也不知道确切的答案。但是是否只有我一个人建议使用重载(这发生在编译时并且只会生成编译错误)来解决此问题?

仅仅是一个例子,如您所见,它将无法编译。

package com.stackoverflow;

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

作为建议重载方法的人之一,我不明白在我的解决方案(或其他人的解决方案)中编译器会抱怨的地方,就像你所暗示的那样。你认为它会生成什么错误,并且为什么?(你可以编辑你的答案提供更多细节。) - Giulio Piancastelli
3
我指的是函数重载。一开始看到你的回答似乎是函数重载的解决方法,也许我漏看了什么。如果有一个事件对象的引用,但只有两个重载:x(MailEvent) 和 x(DocEvent),那么他无法调用方法x,因为在编译时没有匹配项。请注意,这句话中的"he"指的是事件对象的引用者。 - user124
你是对的,添加一个 dispatchEvent(Event e) 方法并不能解决问题,因为如果你将一个 Event 引用作为参数传递,该方法将 始终 被调用。但是,另一方面,如果你能够传递到特定事件类的引用,例如 d.dispatchEvent(new DocEvent()),重载就可以正常工作。 - Giulio Piancastelli
1
@Giulio - 如果你要这样做,那么此时你不会调用分发器--而是直接调用最终被分派到的方法。分发器的作用是能够传递一个未知的实例并使其执行正确的操作(商标)。 - David Harkness

3

利用方法解析顺序有什么问题?

public void dispatchEvent(DocumentEvent e) {
    documentGenerator.gerenateDocument(event);
}

public void dispatchEvent(MailEvent e) {
    deliveryManager.deliverMail(event);
}

让Java来匹配正确的参数类型,然后正确分派事件。

3
正如其他人(@ bigoldbrute在自己的答案中,@x4u在评论@Charlie Martin的答案中)所指出的那样,这种解决方案仅适用于编译时已知参数类型的情况,即参数不是对父抽象“Event”类的引用。(哦,这就是利用方法解析顺序的问题!) - Giulio Piancastelli

2

这是Sum types的典型用例,也称为标记联合。不幸的是,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());
}

在这里,我尽可能地分离关注点,以便每个类和接口的责任都是唯一的。在现实生活中的项目中,DocumentEventImplDocumentEvent 实现和 DocumentEvent 接口声明通常合并为单个类 DocumentEvent,但这会引入循环依赖,并强制一些具体类之间的依赖(而我们知道,应该优先使用接口)。

此外,通常应该用类型参数替换 void 以表示结果类型,就像这样:

interface EventVisitor<R> {
    R visitDocumentEvent(DocumentEvent event);
    ...
}

interface Event {
    <R> R accept(EventVisitor<R> visitor);
}

这使得我们可以使用无状态访问者,这非常方便处理。

这种技术允许我们(几乎?)总是机械地消除instanceof,而不必想出一个特定于问题的解决方案。


2
您可以将每个处理程序类注册到每个事件类型,并在事件发生时执行分派,如下所示。
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中。

1

你可以定义一个名为Dispatcher的接口,如下所示:

interface Dispatcher {
    void doDispatch(Event e);
}

使用类似于DocEventDispatcherMailEventDispatcher等实现。

然后定义一个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()));
    }
}

这是行不通的:dispatcherMap.get(event.getClass()) 总是会返回 null - Rotsor
你是对的 - 应该说“类似 (DocEvent.class, new DocEventDispatcher()) 的条目”。 - Ray
现在这有意义了。然而,这并不是避免instanceof,而是隐藏它。你仍然利用运行时类型信息,这就是instanceof的问题所在。 - Rotsor
这些解决方案中,哪一个不依赖于运行时类型信息? - Ray
所有使用虚方法进行分派的人。 - Rotsor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接