Here's what I would do. If you can use Guava (a Google library written and used by Google), I recommend scrolling down and looking at the other solution first.
Vanilla Java
First, start by adding a method to get the class from your subscribers:
public interface Subscriber<N extends News> {
void onNews(N news);
Class<N> getSupportedNewsType();
}
Then when implementing:
public class MySubscriber implements Subscriber<MyNews> {
// ...
public Class<MyNews> getSupportedNewsType() {
return MyNews.class;
}
}
In your aggregator, include a map where the keys and values aren't typed:
private Map<Class<?>, Set<Subscriber<?>> subscribersByClass = ... ;
Also note that Guava has a multimap implementation that will do this key to multiple values stuff for you. Just Google "Guava Multimap" and you'll find it.
To register a subscriber:
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);
}
And to dispatch:
@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);
}
}
Notice the cast here. This will be safe because of the nature of the generics between the register
method and the Subscriber
interface, provided no one does something ridiculously wrong, like raw-typing such as implements Subscriber
(no generic argument). The SuppressWarnings
annotation suppresses warnings about this cast from the compiler.
And your private method to retrieve subscribers:
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;
}
Your private
methods and fields do not need to be type safe. It won't cause any problems anyway since Java's generics are implemented via erasure, so all of the sets here will be just a set of objects anyway. Trying to make them type safe will only lead to nasty, unnecessary casts that have no bearing on its correctness.
What does matter is that your public
methods are type safe. The way the generics are declared in Subscriber
and the public methods on Aggregator
, the only way to break it is via raw types, like I stated above. In short, every Subscriber
passed to register is guaranteed to accept the types that you're registering it for as long as there's no unsafe casts or raw typing.
Using Guava
Alternatively, you can take a look at Guava's EventBus
. This would be easier, IMO, for what you're trying to do.
Guava's EventBus
class uses annotation-driven event dispatching instead of interface-driven. It's really simple. You won't have a Subscriber
interface anymore. Instead, your implementation will look like this:
public class MySubscriber {
// ...
@Subscribe
public void anyMethodNameYouWant(MyNews news) {
// Handle news
}
}
The @Subscribe
annotation signals to Guava's EventBus
that it should remember that method later for dispatching. Then to register it and dispatch events, use an EventBus
isntance:
public class Aggregator {
private EventBus eventBus = new EventBus();
public void register(Object obj) {
eventBus.register(obj);
}
public void dispatch(News news) {
eventBus.dispatch(news);
}
}
This will automatically find the methods that accept the news
object and do the dispatching for you. You can even subscribe more than once in the same class:
public class MySubscriber {
// ...
@Subscribe
public void anyMethodNameYouWant(MyNews news) {
// Handle news
}
@Subscribe
public void anEntirelyDifferentMethod(MyNews news) {
// Handle news
}
}
Or for multiple types within the same subscriber:
public class MySubscriber {
// ...
@Subscribe
public void handleNews(MyNews news) {
// Handle news
}
@Subscribe
public void handleNews(YourNews news) {
// Handle news
}
}
Lastly, EventBus
respects hierarchical structures, so if you have a class that extends MyNews
, such as MyExtendedNews
, then dispatching MyExtendedNews
events will also be passed to those that care about MyNews
events. Same goes for interfaces. In this way, you can even create a global subscriber:
public class GlobalSubscriber {
// ...
@Subscribe
public void handleAllTheThings(News news) {
// Handle news
}
}