泛型和(超级?)类型令牌可以帮助构建类型安全的新闻聚合器吗?

Raf*_*ele 6 java generics reflection

我有这个基本的News界面

interface News {
    String getHeader();
    String getText();
}
Run Code Online (Sandbox Code Playgroud)

和具体类一样,SportsNewsFinancialNews提供特定的方法,如getStockPrice(),getSport()等等.新闻旨在发送给

interface Subscriber<N extends News> {
    void onNews(N news);
}
Run Code Online (Sandbox Code Playgroud)

问题是如何注册和维护订阅.我尝试的第一种方法是使用中心Aggregator,在Class<T>对象之间保持地图Set<Subscriber<T>>,但很快这种方法显示出不可行.这是所需的API

public class Aggregator {

    public <N extends News> void subscribe(Subscriber<N> subscriber) {
        // TODO somehow (super type token) extract N and 
        // add the item to the set retrieved by getSubscribersFor()
    }

    public <N extends News> void dispatch(N news) {
        for (Subscriber<N> subscriber: getSubscribersFor(news.getClass())) {
            subscriber.onNews(news);
        }
    }

    private <N extends News> Set<Subscriber<N>> getSubscribersFor(Class<N> k) {
        // TODO retrieve the Set for the specified key from the Map
    }
}
Run Code Online (Sandbox Code Playgroud)

有什么替代品可以安全吗?Java可以解决这个问题吗?我把这个小小的演示放在网上,以帮助你更好地理解问题的真正含义.

UPDATE

另一种方法是使Aggregator自己用实际新闻类型进行参数化.这没关系,除了它是一个鸡和蛋的问题:现在需要找到一种方法来检索聚合器.在Java中,没有办法表达以下内容

interface News {
    static Aggregator<CurrentClass> getAggregator();
}
Run Code Online (Sandbox Code Playgroud)
  • static 方法不能 abstract
  • 没有办法在类型参数中引用当前类型

Bri*_*ian 4

这就是我要做的。如果您可以使用 Guava(由 Google 编写和使用的 Google 库),我建议您向下滚动并首先查看其他解决方案。

香草爪哇

首先,首先添加一个从订阅者处获取类的方法:

public interface Subscriber<N extends News> {
    void onNews(N news);
    Class<N> getSupportedNewsType();
}
Run Code Online (Sandbox Code Playgroud)

然后在实施时:

public class MySubscriber implements Subscriber<MyNews> {

    // ...

    public Class<MyNews> getSupportedNewsType() {
        return MyNews.class;
    }
}
Run Code Online (Sandbox Code Playgroud)

在聚合器中,包含一个未键入键和值的映射:

private Map<Class<?>, Set<Subscriber<?>> subscribersByClass = ... ;
Run Code Online (Sandbox Code Playgroud)

另请注意,Guava 有一个多重映射实现,可以为您执行此键到多个值的操作。只要谷歌“Guava Multimap”,你就会找到它。

注册订阅者:

public <N extends News> void register(Subscriber<N> subscriber) {
    // The method used here creates a new set and puts it if one doesn't already exist
    Set<Subscriber<?>> subscribers = getSubscriberSet(subscriber.getSupportedNewsType());
    subscribers.add(subscriber);
}
Run Code Online (Sandbox Code Playgroud)

并发送:

@SuppressWarnings("unchecked");
public <N extends News> void dispatch(N news) {
    Set<Subscriber<?>> subs = subscribersByClass.get(news.getClass());
    if (subs == null)
        return;

    for (Subscriber<?> sub : subs) {
        ((Subscriber<N>) sub).onNews(news);
    }
}
Run Code Online (Sandbox Code Playgroud)

注意这里的演员阵容。register由于方法和接口之间的泛型的性质Subscriber,这将是安全的,只要没有人做一些可笑的错误,例如原始类型implements Subscriber(没有泛型参数)。该SuppressWarnings注释禁止编译器发出有关此转换的警告。

以及您检索订阅者的私有方法:

private Set<Subscriber<?>> getSubscriberSet(Class<?> clazz) {
    Set<Subscriber<?>> subs = subscribersByClass.get(news.getClass());
    if (subs == null) {
        subs = new HashSet<Subscriber<?>>();
        subscribersByClass.put(subs);
    }
    return subs;
}
Run Code Online (Sandbox Code Playgroud)

您的private方法和字段不需要类型安全。无论如何,它不会造成任何问题,因为 Java 的泛型是通过擦除实现的,所以这里的所有集合无论如何都只是一组对象。试图使它们类型安全只会导致令人讨厌的、不必要的强制转换,而这与其正确性无关。

重要是你的public方法是类型安全的。泛型的声明方式Subscriber和公共方法的方式Aggregator,打破它的唯一方法是通过原始类型,就像我上面所说的那样。简而言之,只要不存在不安全的强制转换或原始类型,每个Subscriber传递给寄存器的都保证接受您正在注册的类型。


使用番石榴

或者,您可以看看 Guava 的EventBus。IMO,对于您想要做的事情来说,这会更容易。

Guava 的EventBus类使用注释驱动的事件调度而不是接口驱动。这真的很简单。您将不再有Subscriber界面。相反,您的实现将如下所示:

public class MySubscriber {
    // ...

    @Subscribe
    public void anyMethodNameYouWant(MyNews news) {
        // Handle news
    }
}
Run Code Online (Sandbox Code Playgroud)

@Subscribe注释向 Guava 发出信号EventBus,表明它应该记住该方法以便稍后进行调度。然后要注册它并分派事件,请使用EventBus实例:

public class Aggregator {
    private EventBus eventBus = new EventBus();

    public void register(Object obj) {
        eventBus.register(obj);
    }

    public void dispatch(News news) {
        eventBus.dispatch(news);
    }
}
Run Code Online (Sandbox Code Playgroud)

这将自动找到接受该对象的方法news并为您进行调度。您甚至可以在同一课程中多次订阅:

public class MySubscriber {
    // ...

    @Subscribe
    public void anyMethodNameYouWant(MyNews news) {
        // Handle news
    }

    @Subscribe
    public void anEntirelyDifferentMethod(MyNews news) {
        // Handle news
    }
}
Run Code Online (Sandbox Code Playgroud)

或者对于同一订阅者内的多种类型:

public class MySubscriber {
    // ...

    @Subscribe
    public void handleNews(MyNews news) {
        // Handle news
    }

    @Subscribe
    public void handleNews(YourNews news) {
        // Handle news
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,EventBus尊重层次结构,因此如果您有一个扩展的类MyNews,例如MyExtendedNews,那么调度MyExtendedNews事件也将传递给那些关心MyNews事件的类。接口也是如此。通过这种方式,你甚至可以创建一个全局订阅者:

public class GlobalSubscriber {
    // ...

    @Subscribe
    public void handleAllTheThings(News news) {
        // Handle news
    }
}
Run Code Online (Sandbox Code Playgroud)