I have this basic News
interface
interface News {
String getHeader();
String getText();
}
and concrete classes like SportsNews
and FinancialNews
to provide specific methods like getStockPrice()
, getSport()
and so on. News are intended to be dispatched to a
interface Subscriber<N extends News> {
void onNews(N news);
}
The problem is how to register and maintain subscriptions. The first approach I tried was using a central Aggregator
, keeping a map between Class<T>
objects and Set<Subscriber<T>>
, but soon this approach revealed unviable. Here is the desired 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
}
}
Is there any alternative to be type safe? Can Java solve this problem at all? I put this little demo online to help you better understand what the problem really is.
UPDATE
An alternative would be to make Aggregator
itself parameterized with the actual news type. This would be ok, except that it's a chicken and egg problem: now one needs to find a way to retrieve the aggregator. In Java there's no way to express the following
interface News {
static Aggregator<CurrentClass> getAggregator();
}
static
method can't be abstract
By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type safe and easier to read.
Java labels every object by putting a class tag next to the object. One simple way to enforce type safety is to check the class tag of the object before every operation on the object. This will help make sure the object's class allows the operation. This approach is called dynamic type checking.
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.
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.
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
}
}
You will need to send the class
parameter to dispatch
. The following compiles for me, not sure if that meets your needs:
import java.util.Set;
interface News {
String getHeader();
String getText();
}
interface SportsNews extends News {}
interface Subscriber<N extends News> {
void onNews(N news);
}
class Aggregator {
public <N extends News> void subscribe(Subscriber<N> subscriber, Class<N> clazz) {
// TODO somehow (super type token) extract N and
// add the item to the set retrieved by getSubscribersFor()
}
public <N extends News> void dispatch(N item, Class<N> k) {
Set<Subscriber<N>> l = getSubscribersFor(k);
for (Subscriber<N> s : l) {
s.onNews(item);
}
}
private <N extends News> Set<Subscriber<N>> getSubscribersFor(Class<N> k) {
return null;
// TODO retrieve the Set for the specified key from the Map
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With