【问题标题】:Avoiding 'instanceof' in Java避免在 Java 中使用“instanceof”
【发布时间】:2011-09-03 17:02:46
【问题描述】:

我有以下(可能是常见的)问题,这让我很困惑:

有几个生成的事件对象扩展了抽象类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 类型添加一个抽象方法并让它们自行划分,但是我必须在每个实体中注入特定的会话 Bean。

有什么提示可以为这个问题找到一个“漂亮”的解决方案吗?

感谢您的帮助!

【问题讨论】:

  • 1+ 表示希望坚持良好的 OOP 实践。
  • 也许这篇文章涵盖了它? Dynamic Dispatching

标签: java oop instanceof


【解决方案1】:

最简单的方法是让 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);
    }
}

【讨论】:

  • 为什么事件会知道要生成什么样的文档?
  • 一个事件不会,但一个 DocumentEvent 可能知道一些关于要生成的文档的信息。 ;) 这个问题相当抽象,而且这种方法并非在所有情况下都有效,但如果有效,它将是最简单、最容易扩展的解决方案。
  • 您所展示的是一个非常好的访问者模式示例。
  • @The Elite,和访客模式很像,但又不一样。访问者模式用于基于封闭对象图的插入行为。 IMO,这更接近标准 ActionListener.actionPerformed 模式。
  • 我对此很紧张。这似乎是一个简单的 POJO 数据对象与逻辑的过多混合。事件不再成为数据包,而是开始成为其他方法的委托
【解决方案2】:

多态是你的朋友。

class DocumentGenerator {
   public void generate(DocumentEvent ev){}
   public void generate(MainEvent ev){}
   //... and so on
}

那么就

 DocumentGenerator dg = new DocumentGenerator();

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

更新

许多人提出了反对意见,即您“必须在编译时知道事件的种类”。而且,是的,您显然必须知道在生成器部分的编译时要解释哪些事件,否则何时才能编写生成部分?

这些竞争示例使用命令模式,这很好,但意味着事件不仅要了解其表示的细节,还要了解如何打印他们的表示。这意味着每个类可能有两种敏感的需求变化:事件表示的变化,以及事件在打印中表示的方式的变化。

现在,例如,考虑需要将其国际化。在命令模式的情况下,您必须为 n 个不同的事件类型转到 n 个类并编写新的 do 方法。在多态的情况下,变化被局限在一个类中。

当然,如果您需要国际化一次,您可能需要多种语言,这会驱使您在命令模式案例中为每个类添加类似策略 的内容,现在需要 n 类 × m 语言;同样,在多态情况下,您只需要一个策略和一个类。

有理由选择这两种方法,但声称多态方法是错误的是不正确的。

【讨论】:

  • 这看起来不错,但可能不合适,因为它需要在编译时知道类型,而在所讨论的情况下显然不是这种情况。
  • 更具体地说,上面的内容不会编译。 dg.generate(event) 调用将失败,因为在 DocumentGenerator 上没有匹配 generate(Event) 的方法。多态性不适用于这样的方法参数。
  • 这个答案是错误的instanceof 在运行时检查对象类型,而重载方法在编译时对不同类型做出反应。
  • 同意其他人说这个答案是错误的 - 它是。要调用的重载方法是在编译时确定。您必须将您的 Event 对象显式转换为正确的子类对象(这违背了目的)才能使其正常工作。您答案的“更新”部分并没有改变这一事实。
  • 确实大错特错,甚至不是多态而是重载。这里没有运行时调度。
【解决方案3】:

每个事件都有一个功能,比如说做。 每个子类都覆盖 do, to do (:P) 适当的操作。 动态调度之后会做所有其他事情。 您需要做的就是调用 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);
        }
    

    【讨论】:

    • 作为建议重载方法的人之一,我看不出编译器在我的解决方案(或其他解决方案)中的哪个位置会抱怨,正如您所暗示的那样。你认为它会产生什么错误,为什么? (您可以编辑您的答案以提供更多详细信息。)
    • 我指的是重载。乍一看,您的答案看起来像是重载解决方案。也许我在那里遗漏了一些东西。如果一个人引用了Event obj,那么如果只有两个重载:x(MailEvent) 和x(DocEvent),他就不能调用方法x。 @编译时根本没有匹配。
    • 您是对的,添加dispatchEvent(Event e) 并不能解决问题,因为如果您将Event 引用作为参数传递,该方法将总是 被调用。但是,另一方面,如果您能够传递对特定事件类的引用,例如d.dispatchEvent(new DocEvent()),超载工作正常。
    • @Giulio - 如果你打算这样做,你不会在那个时候调用调度程序——你会调用它最终直接被调度到的方法。调度程序的重点是能够传递一个未知实例并让它做正确的事情 (tm)。
    【解决方案5】:

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

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

    让 Java 完成匹配正确参数类型的工作,然后正确调度事件。

    【讨论】:

    • 正如其他人(@bigoldbrute 在他自己的回答中,@x4u 在对@Charlie Martin 回答的评论中)所指出的那样,这种解决方案只有在编译时知道参数的类型时才有效,即参数不是对父抽象 Event 类的引用。 (哦,这就是利用方法解析顺序的问题!)
    【解决方案6】:

    这是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,而不必找出特定问题的解决方案。

    【讨论】:

      【解决方案7】:

      您可以针对每种事件类型注册每个处理程序类,并在这样的事件发生时执行调度。

      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。

      【讨论】:

        【解决方案8】:

        你可以有一个Dispatcher 接口,定义如下

        interface Dispatcher {
            void doDispatch(Event e);
        }
        

        使用 DocEventDispatcherMailEventDispatcher 等实现。

        然后定义一个Map&lt;Class&lt;? extends Event&gt;, Dispatcher&gt;,其中包含(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
        • 你是对的——应该说“像(DocEvent.class, new DocEventDispatcher())这样的条目”
        • 现在说得通了。但是,这并不是回避instanceof,而是隐藏它。您仍然使用运行时类型信息,这是 instanceof 的坏处。
        • 哪些解决方案不依赖运行时类型信息?
        • 所有使用虚拟方法进行调度的人。
        猜你喜欢
        • 2013-03-05
        • 2012-04-18
        • 2022-11-15
        • 1970-01-01
        • 1970-01-01
        • 2012-08-13
        • 1970-01-01
        • 1970-01-01
        • 2020-08-30
        相关资源
        最近更新 更多