Brian's Waste of Time

Sat, 24 Mar 2007

Containers and Lifecycles

I've been playing with Guice, and to repeat myself, Bob and Kevin know how to write a clean client API, that is for sure :-) In terms of IoC/DI/Flooflah I think Guice has finally knocked Pico out for elegance. Spring does DI, and is fantastic as a library, but I have never liked its dependency resolution options and how it functions container-wise.

Looking at Guice I did run into the thing most folks latched on to immediately, it has no lifecycle hooks! Egads, how shall we live! This actually matters as almost everything I do, lately, is multithreaded and embarrassingly concurrent, though. Because of the oddities of the Java Memory Model, simple lifecycle hooks are awfully useful -- just a start/stop is enough, really. You can work around it by having bootstrapping classes, but... yuck. Anyway, I digress.

Then the obviousness of it smacks me upside my head -- lifecycle crap is orthogonal to dependency resolution, we have just gotten used to seeing them bundled together. So I started futzing with non-sucky ways to do lifecycles outside the container.

Lifecycle events are just events, and despite the trend away from it, I think first-class events trump implicit events. This means you pass the event to an event listener rather than have a no-arg method invoked. Heresy, probably, but oh well. So what is an event? Well, crap, really there is no reason to restrict it, so an event is any Java object.

Java's lack of closure support makes for craploads of one-method interfaces as event listeners. This is idiommatic, and frequently idiommatic is good as it reduces the number of concepts you have to understand to learn something new, but I always find it annoying. Spring and other containers have been pushing folks towards having the classes which would register lifecycle listeners just be the listener (via the no-arg method), so that heresy has already gained some traction. Bah, go with it. We can dispatch events based on annotating a method as an event listener and use the argument type to match which events it cares about. So, stab #1:

    public class EventBus
    {
        private final List<Object> listeners = new CopyOnWriteArrayList<Object>();

        public void register(Object listener)
        {
            listeners.add(listener);
        }

        public void fire(Object event) throws IllegalAccessException,
                                              InvocationTargetException
        {
            for (final Object listener : listeners) {
                for (final Method m : listener.getClass().getMethods()) {
                    if (m.getAnnotation(EventListener.class) != null) {
                        final Class[] params = m.getParameterTypes();
                        if (params.length == 1 && params[0].isAssignableFrom(event.getClass())) {
                            m.invoke(listener, event);
                        }
                    }
                }
            }
        }
    }

This is extremely inefficient, but it is a couple minutes of hacking. It is also easy to optimize by analyzing listeners when they are registered instead of when the event is fired. That also allows for detecting illegally shaped listener methods at registration time, instead of just ignoring them, blah blah blah. Regardless, it works for now. Our thing which wants to receive events looks like:

    public class RockingChair
    {
        private final List<String> events = new ArrayList<String>();

        @EventListener
        public void startRocking(Start event)
        {
            events.add("start");
        }

        @EventListener
        public void fallOver(Stop event)
        {
            events.add("stop");
        }

        public List<String> getEvents() {
            return Collections.unmodifiableList(events);
        }
    }

So here we have event methods on a listener class, and support arbitrary events. Driving it just looks something like this.

    public void testBasics() throws Exception
    {
        EventBus bus = new EventBus();
        RockingChair chair = new RockingChair();
        bus.register(chair);

        Start event = new Start();
        bus.fire(event);

        assertTrue(chair.getEvents().contains("start"));
    }

    public void testSubclass() throws Exception
    {
        EventBus bus = new EventBus();
        RockingChair chair = new RockingChair();
        bus.register(chair);

        Start event = new Start() {};
        bus.fire(event);

        assertTrue(chair.getEvents().contains("start"));
    }

Nothing fancy here, and it works fairly well. You can add lots of shiny knobs if you want to, but how do we go about hooking it into Guice? The obvious way is just to add the EventBus as a component and make things which depend on lifecycle events dependent upon it:

    public class Glider
    {
        private final List<String> events = new CopyOnWriteArrayList<String>();

        @Inject
        public Glider(EventBus bus)
        {
            bus.register(this);
        }

        @EventListener
        public void rock(Start event)
        {
            events.add("start");
        }

        @EventListener
        public void fallOver(Stop event)
        {
            events.add("stop");
        }

        public List<String> getEvents()
        {
            return Collections.unmodifiableList(events);
        }

    }

This bothers me though. I do like passing an event listener to the bus other than the Glider itself, it seems to be polluting the heck out of the Glider's API to have those event listener methods we refactor it just a touch:

    public class Glider
    {
        private final List<String> events = new CopyOnWriteArrayList<String>();

        @Inject
        public Glider(EventBus bus)
        {
            bus.register(new Object()
            {
                @EventListener
                public void rock(Start event)
                {
                    events.add("start");
                }

                @EventListener
                public void fallOver(Stop event)
                {
                    events.add("stop");
                }
            });
        }

        public List<String> getEvents()
        {
            return Collections.unmodifiableList(events);
        }

    }

By flagging the method as an EventListener and dispatching based on parameter type we can now have pretty painless multiple-event listener instances. If you could have an anonymous inner class implement more than one interface you could accomplish the same with interface based listeners. Hmm, you could also hide that behind a library where you do something like:

    bus.registerInterfaceBasedListener(new Object() {
        public void onFoo() {}
        publc void onBar() {}
    }, BarListener.class, FooListener.class);

But you are then dispatching on instanceof which isn't any prettier and is not really more optimizable. Need to think on it a bit more :-)

Regardless, I rather like argument-type based dispatch of events from an event bus which accepts arbitrary instances as events, and it does play pretty nicely with Guice, or anything else. Much more mucking to consider, I think :-)

2 writebacks [/src/java] permanent link