Koppeling van widgets door middel van een message bus
Inhoud
  1. Gestructureerde aanpak
  2. Voorbeeld van los gekoppelde widgets
  3. Concrete code voorbeelden
  4. Voorbeeld van een toepassing : jQuery-UI Accordion Widget met status persistentie


Gestructureerde aanpak

Een message broker is een architecturaal patroon voor validatie van berichten, bericht transformatie en message routing. Een broker bemiddelt de communicatie tussen applicaties, minimaliseert het wederzijdse besef dat de verschillende onderdelen (widgets) binnen een applicatie van elkaar moeten hebben om te kunnen berichten uit te wisselen, en implementeert effectief ontkoppeling. Het doel van een broker is om inkomende berichten te ontvangen en er eventueel actie op uit te voeren,en ze vervolgens door te sturen.

Een aantal voorbeelden van de meest belangrijke acties die ondernomen kunnen worden in een broker:

  1. Routeren van berichten naar een of meerdere bestemmingen
  2. Transformeren van berichten
  3. Bericht aggregatie en het splitsen van berichten in verschillende deelberichten
  4. Interactie met een externe repository om een bericht te valideren of op te slaan
  5. Beroep doen op webservices om gegevens op te halen
  6. Reageren op events of fouten
  7. Inhoud- en onderwerp-gebaseerde routing van berichten via het publish / subscribe-model.



De hier beschreven broker functioneert als een message bus, en werkt volgens het publish -> topic -> subscribe model.
De broker registreert de producers en consumers van de afzonderlijke widgets. De widgets kunnen zich als publisher van een topic en/of subscriber op een topic aanmelden en via hun procucers en consumers berichten versturen en ontvangen/verwerken. Elke producer en consumer heeft een uniek id.
Het grote voordeel is dat de architectuur eenvoudig gewijzigd kan worden, de widgets zijn loosely coupled binnen de applicatie waardoor een grote mate van flexibiliteit en uitwisselbaarheid gerealiseerd wordt.



Voorbeeld van los gekoppelde widgets

Onderstaande widgets kunnen via de message broker met elkaar communiceren. Elke widget kan zich op een topic subscriben, en via producers en consumers berichten versturen en ontvangen. De widgets hebben geen kennis van elkaar. Verstuur hier berichten en kijk hoe de widgets gekoppeld zijn.



Processor Widget
Acknowledge widget
Dummy widget
Updater widget
My widget


Concrete code voorbeelden

De Acknowledge wiidget is geabonneeerd op het 'processorwidget_message' topic en verstuurt zelf een reply als de message is ontvangen naar de publisher, in dit geval de ProcessorWidget.
De widgets en configuratie objecten zijn opgebouwd volgens het eerder behandelde singleton pattern.
De configuratie van de message bus wordt gedaan in het MessageBus singleton-object. De initialisatie van de Message Bus wordt gedaan door in het entrypoint van de applicatie de create() methode aan te roepen.
Als een widget als publisher en/of subscriber wil fungeren moet de widget uitgebreid worden met een EventBroker object. Daardoor komen de methoden addProducer(), publish(), removeProducer(), createConsumer() en destroyConsumer() beschikbaar. De aangemaakte producers en consumers zijn zichtbaar in een DOM inspector (bijvoorbeeld Mozilla FireBug)



Voorbeeld van bovenstaande toepassing van de message broker :

//WIDGETS============================================================================

var Acknowledge = (function(){

    //private widget methods (with preceding underscore)
    var _publisher = {
    
        //message producer for reply
        reply : function(message){
        
            self.publish({//replying message-event met custom message object
                            type        : 'acknowledge_reply',
                            reply       : message,
                            results     : { data : 'confirm processorwidget message',
                                                    sender : self.identifier
                                          }
            });
        },
        
        //message producer
        sendMessage : function(elem){
		
            self.publish({//publish message-event met custom message object
                            type        : 'acknowledge_message',
                            value       : elem.value,
                            results     : { data : 'some acknowledge results',
                                                    sender : self.identifier
                                          }
            });
        }
    
    };
    
    var _subscriber = {
    
        callback : function(event){
            event.receiver=Acknowledge.identifier;
            
            switch(event.type){
                //different type of listeners
                case 'processorwidget_message' :
                    //take actions as response to the incoming message
                    event.consumerID="ACK-consumer_01";
                    _publisher.reply('a reply message');
                    testForm.acDisplay(printResult(event));
                    //actions for processorwidget_message                   
                break;
            }
        }
    };
    
    //define public widget methods
    var self = {
    
        identifier:'Acknowledge',
        
        //make broker methods available inside this widget
        getEventBroker : function(){
            return new EventBroker(this.identifier); 
        },
        
        callback : function(event){
            _subscriber.callback(event); 
        }
    };
    $.extend(self,self.getEventBroker());
    return self;
    
})();

/*
 * more widgets here, omitted for simplicity ====================================================
 */
 
 //setup the message bus with the connections to establish

var MessageBus = (function(){
		
        var self = {
        
            create :  function(){
                //many to many
                ProcessorWidget.addProducer('processorwidget_message', Updater.createConsumer({
                    consumerID      : "UP-consumer_01",
                    producer        : 'processor_widget',
                    callback        : Updater.callback//callback defined in widget, best practice.
                                      Callback can also be defined here as anonymous function
                }));
                
                ProcessorWidget.addProducer('processorwidget_message', Acknowledge.createConsumer({
                    consumerID      : "AKC-consumer_01",
                    producer        : 'processor_widget',
                    callback        : Acknowledge.callback
                }));
                
                Dummy.addProducer('dummy_message', MyWidget.createConsumer({
                    consumerID      : "MW-consumer_02",
                    producer        : 'dummy',
                    callback        : MyWidget.callback
                }));
                
                 
                Dummy.addProducer('dummy_message', ProcessorWidget.createConsumer({
                    consumerID      : "PW-consumer_01",
                    producer        : 'dummy',
                    callback        : ProcessorWidget.callback
                }));
                
                Acknowledge.addProducer('acknowledge_reply', ProcessorWidget.createConsumer({
                    consumerID      : "PW-consumer_02",
                    producer        : 'acknowledge',
                    callback        : ProcessorWidget.callback
                }));
                
                Updater.addProducer('updater_message', Dummy.createConsumer({
                    consumerID      : "DU-consumer_01",
                    producer        : 'updater',
                    callback        : Dummy.callback
                }));
            
            
            }
			
        };
        return self;
})();
	
//APPLICATION ENTRY POINT===========================================================

$(document).ready(function(){
		
    $.extend(Util);//tools object
    $.extend(RequestManager);//xhr object
    $.logInit();//logger, arg=true then relative, arg='console' then output to firebug console
            
    MessageBus.create();//create message bus
    testForm.init(); // initializes the form presenter
            
});
		
            

De code van de message broker :

function EventBroker(id){
    this.producers = {};
    this.consumers = {};
    this.identifier=id;//publisher and / or subscriber id
}

EventBroker.prototype = {
    constructor: EventBroker,

    addProducer: function(type, subscriber){
        if (typeof this.producers[type] == "undefined"){
            this.producers[type] = [];
        }
        if(subscriber){
            this.producers[type].push(subscriber);
        
        }
    },
    
    publish: function(event){
        if (!event.target){
            event.target = this;
        }
        if (this.producers[event.type] instanceof Array){
            var producers = this.producers[event.type];
            for (var i=0, len=producers.length; i < len; i++){
                producers[i](event);
            }
        }            
    },

    removeProducer: function(type, subscriber){
        if (this.producers[type] instanceof Array){
            var producers = this.producers[type];
            for (var i=0, len=producers.length; i < len; i++){
                if (producers[i] === subscriber){
                    break;
                }
            }
            
            producers.splice(i, 1);
        }            
    },
    
    createConsumer : function(obj){
            if(!obj){
                obj={};
            }
            
            if(!obj.consumerID){
                alert("createSubscriber Error : No consumer ID definded");
            }
            
            if (typeof this.consumers[obj.consumerID] == "undefined"){
            this.consumers[obj.consumerID] = [];
        }

        
            
            if(!obj.callback){
            //generic callback, show warning
            var id = this.identifier;
                obj.callback= function(event){
                    alert('WARNING : NO CALLBACK DEFINED for '+id+' consumer ');
                };
            }
            this.consumers[obj.consumerID].push(obj);
            return obj.callback;
    },
        
    destroyConsumer: function(id){
       for(var i=0; len=consumers.length; i < len; i++){
            if(consumers[i]===id){
                break;
            }
       }
       consumers.splice(i, 1);
    }
};   
    


Voorbeeld van een toepassing : jQuery-UI Accordion Widget met status persistentie

Door middel van een messagebus is decoupling veel eenvoudiger te realiseren. Een voorbeeld daarvan is het gebruik van een widget die de wijzigingen in de status van een jQuery-UI accordion opslaat als een hash in de uri-component.
Daardoor is het mogelijk de status van de accordion te bookmarken en ook om door middel van browser's back- en forwardbuttons door de browserhistory te navigeren.

De persistentie widget (uri.persist.js) stuurt na een wijziging van de uri-hash een message via de messagebus. Een willekeurige andere widget kan zich subscriben op het topic 'url_persit_message' , en zo via een callback acties ondernemen.

De persistentie widget kan vooraf geconfigureerd worden.


De accordion in de projects pagina werkt op deze manier :
Projects


Voorbeeld van de index.js :

var MessageBus = (function(){
		
		var self = {
			create :  function(){
            
                Persist.addProducer('url_persist_message', MessageBus.accordionConsumer.createConsumer({
                    consumerID      : "acc-consumer_01",
                    producer        : 'persist',
                    callback        : MessageBus.accordionConsumer.callback
                }));   
			}
			
		};
		return self;
	
})();




$(document).ready(function(){
    
    $.extend(Util);//tools object
    $.logInit();//logger, arg=true then relative, arg='console' then output to firebug console
         
    var self = $('#accordion');
    var lock=true;
    
    MessageBus.accordionConsumer={};//interface object
    $.extend(MessageBus.accordionConsumer,new EventBroker("accordion")); //extends interface object with 
                                                                             eventBroker methods
    
    //callback for hash changes
    MessageBus.accordionConsumer.callback = function(event){
            
            event.receiver="accordion";
            
            switch(event.type){
                case 'url_persist_message' :
                    event.consumerID="self-consumer_01";
                    //open accordiontab with number received through the messagebus
                    lock=event.message.firstLoad;
                    self.accordion( "option", "active", event.message.data );
                break;
            }
            
    }

    // initiation of jQuery-UI accordion   
    $(function() {
		self.accordion({
                    collapsible  : true,
                    autoHeight   : false,
                    active       : -1 //mandatory, if omitted, 
                                        accordion does not open 
                                        up on the initial tab
		});
	});
	
	// accordion onChange eventhandler, gets triggered each time a tab is opened or closed
	self.bind('accordionchange', function(event, ui) {
	    //alert("change");
	    var active = self.accordion( "option", "active" );
        
	    //lock is true if the page is reloaded in the browser. The onChange event is triggered, 
	      but the accordion is not yet aware of it's state and will close all tabs. 
	      Locking the section beneath prevents this from happening
	      
        if(!lock){
            
            if(typeof(active)==='boolean' && !active){
                //all tabs closed
                location.hash="tabs=-1";
            }else{
                if(typeof(active)==='number'){
                     //range of [0-{tabs}]
                    location.hash="tabs="+active; 
                }
            }
        }
        lock=false
        /*
         * skip persisting here for one polling cycle , because reload from 
         * persist triggers accordionChange event 
         * also and causes feedback loop
         */
	    Persist.skip();    
    });
    
    //initiate persist module
    Persist.init({ 
        initHash : "tabs=0",  //inital value after the first page reload
        filter   : function(hash){  //optional fiter to preprocess the returned uri-hash object
            var aId=hash.split("=");
            var tabId=parseInt(aId[1]);
            return tabId;
        }
    });
    
    //messagebus creation
    MessageBus.create();
    
	//flag for detection of firstload of index page
	Persist.firstLoad=true;
    
	//start polling for hash changes
	Persist.start(true);
});