Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can generics and (super?) type tokens help to build a type-safe news aggregator?

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
  • there's no way to reference the current type in a type argument
like image 414
Raffaele Avatar asked Oct 23 '12 20:10

Raffaele


People also ask

Are generics used for type safety?

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.

How do you add type safety in Java?

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.


2 Answers

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
    }
}
like image 141
Brian Avatar answered Sep 30 '22 03:09

Brian


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
    }
}
like image 25
Miserable Variable Avatar answered Sep 30 '22 03:09

Miserable Variable