Refactoring using an event-driven approach: part 2

    In the previous article we learnt what event-driven programming is and how you can leverage it to refactor your code. We also learnt circumstances under which you may use an event-driven approach instead of other conventional approaches. I highly recommend that you read that first.

    In this article, we shall learn how event-driven systems are built from the ground up. We shall use JavaScript for the examples but the principles still apply in other languages.

    In any Event System there must be a defined set of functions:

    1. Registration function

    This function is usually defined by an on() method. It registers an event listener which is an anonymous function or callback function.

    An event listener is a function that defines the actions to be taken once the event is dispatched at any given point in your program.

    The on() method may also take on an optional target context parameter that defines the context that the listener is called from.

    The usual form of an event registration function is as follows:

    EventEmitter.on(event, listener, target|context?)

    Creating our on() method is very straightforward. We just need to push the event and it’s listener to an array or object which we’ll use throughout the Event Emitter object.

    Create a EventEmitter.js file and add the following code:

    /**
     * Manages all events
     */
    export default {
        registeredEventListeners: [],
        /**
         * Registers an event 
         * @param {String} event 
         * @param {Function} handler 
         * @param {Object} target 
         * @returns void
         */
        on: function(event, handler, target) {
            /**
             * Apparently I can't get the callee.caller because of webpack's strict mode
            */
    
            // if (!target) {
            //     target = arguments.callee.caller;
            // }
            
            // check if event already exists
            if (!this.registeredEventListeners[event]) {
                this.registeredEventListeners[event] = [];
                //register event
                this.registeredEventListeners[event].push({handler: handler, target: target});
            }
            
        }
    }

    2. De-registration function

    This method is usually defined as an off() method. It basically removes the event listeners for a given event from the list of registered event listeners.

    I’ve added a helper method to reduce duplication as we add more methods to the Event Emitter.

        /**
         * Removes a listener for a given event 
         * @param {String} event 
         * @param {Function} handler 
         * @returns void
         */
        off: function(event, handler) {
            try{
                var listeners = this.getListeners(event);
                if (listeners) {
                    var index = listeners.indexOf(event);
                    listeners.slice().map(function(listener){
                        if(listener.handler === handler)
                            listeners.splice(index,1)
                    });
                }
            }catch(e){
                throw new Error("Listener not turned off. " + e);
            }
    
            return this;
        },
        /**
         * Helper function that gets all listeners for given event
         * @param {String} event
         * @returns Array
         */
        getListeners: function(event) {
            if (!this.registeredEventListeners[event]) {
                this.registeredEventListeners[event] = [];
            }
            return this.registeredEventListeners[event];
        }
    

    3. Dispatching function

    In order to fire/dispatch events, we have to call the listeners attached to the given event and also keep track of them.

    First, add a fireEvents array to the EventEmitter class. This array keeps track of all dispatched events.

    firedEvents: [],

    The following snippet shows the implementation of a dispatch function:

    firedEvents: [],
    /**
     * Calls/Fires a registered event
     * @param {String} event 
     */
    dispatch: function(event){
        try{
            // get all listeners for that event
            var listeners = this.getListeners(event);
            if(!listeners || !listeners[0]) return
            
            var args = [].slice.call(arguments, 1);
    
            var that = this;
            listeners.slice().map(function(i){
                i.handler.apply(i.target, args);
                that.firedEvents.push({event: event, handler: i.handler, target: i.target});
            });
        }catch(e){
            // console.log("Event not fired. "+ e);
            throw new Error("Event not dispatched. "+ e);
        }
    
        return this;
    }

    4. Clearing function

    A clearing function just deletes both the registered and fired listeners. It effectively resets everything back to default.

    Usually denoted as clear().

    /**
     * clear all trackers for listeners and fired events
     */
    clear: function(){
        this.registeredEventListeners = [];
        this.firedEvents = [];
    
        return this;
    }

    Conclusion

    We now have a simple but effective event manager that can help you clean up your code. This tutorial basically provides the building blocks of most event emitters in NodeJs, Laravel and many more.

    An Event Emitter is like a real-life event manager who ensures that the various events scheduled for the calendar are well organized with the right people (listeners/handlers) and occur at the right place (target/context).

    In the next tutorial, we’ll refactor the game and show how we can use this class to refactor some of the code in the little game we built.

    You can get the full code of a simple NPM package I built here. Feel free to contribute.


    Also published on Medium.