Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Decoupled Large-scale Applications Using JavaScript (And jQuery)

Building Decoupled Large-scale Applications Using JavaScript (And jQuery)

These are the slides for a talk I gave at jQuery Conf UK, where I explored a number of effective design patterns for scaling applications using JavaScript and jQuery. Unlike some of my previous talks on this subject, today I focused on the importance of decoupling.

"In computer science, coupling is the degree to which each module relies on a number of other modules. By decoupling your application, you're able to easily extend and develop one layer without affecting the other."

As a JavaScript developer (or a heavy jQuery user) it's important to remember that you can easily take advantage of patterns as simple as Pub/Sub (or a Mediator) for decoupling your application. In addition to helping solve the problem of tight coupling, patterns such as these can encourage separating your application logic into simpler, more independent modules that can be more easily reused. For anyone wishing to read up more about Pub/Sub, I wrote a detailed article on it for ScriptJunkie here: http://msdn.microsoft.com/en-us/scriptjunkie/hh201955.aspx

You can also benefit from patterns that can be found in libraries like jQuery (such as the Facade) to implement all kinds of fun concepts like limited public APIs or application-level security. Later in the talk I show how a Mediator can also be used to gain greater control over modules, like being able to start, stop and restart them to enable building applications that can 'fix' breakages. This helps create a user-experience that is potentially a lot more robust than what we see in many applications today. My more detailed article on these topics can be found at http://addyosmani.com/largescalejavascript/.

Note: Remember that the architecture presented is flexible and you're free to use as few or as many of the patterns covered in your applications.

For those looking for the demo application shown near the end of the talk, the source to it can be found here: https://github.com/addyosmani/largescale-demo.

Links to articles I've written related to this talk can be found in the 'Further Reading' section.

A gist I posted with a few different ways to implement Pub/Sub with jQuery can be found at https://gist.github.com/1321768 and I'm also happy to recommend the Pub/Sub solution in AppendTo's AmplifyJS (http://amplifyjs.com/). Finally, simple Mediator pattern implementations can be found here: https://gist.github.com/1794823.

Addy Osmani

February 10, 2012
Tweet

More Decks by Addy Osmani

Other Decks in Programming

Transcript

  1. COUPLED, DIRECT CALLS //Coupled $.when( $.ajax("mail.php") ) .then(function( ajaxArgs ){

    var jqXHR = ajaxArgs[2], data = jqXHR.responseText; // Display notification and preview modals.show('New Message', data); // What happens if something breaks here? // Play new message sound audioManager.play('newMessage'); // Update the unread message count messages.increaseMessageCount('unread'); });
  2. DECOUPLED, EVENT-DRIVEN // Decoupled, Event-driven $.when( $.ajax("mail.php") ) .then(function( ajaxArgs

    ){ var jqXHR = ajaxArgs[2], data = jqXHR.responseText; // Tell the application there is a new message, share the data $.publish('newMessage', data); // Other modules can now just listen out for 'newMessage' }); $.subscribe('newMessage', ...); // modals.show() // How about now? $.subscribe('newMessage', ...); // audioManager.play() $.subscribe('newMessage', ...); // messages.increaseMessageCount()
  3. LOGICALLY DECOUPLE OBJECTS GENERATING EVENTS FROM THOSE REACTING TO THEM

    NEW MESSAGE ‘HELLO WORLD’ THERE’S A NEW MESSAGE!
  4. OPTION 1: JQUERY CUSTOM EVENTS $(document).trigger('eventName'); //equivalent to $.publish('eventName') $(document).on('eventName',...);

    //equivalent to $.subscribe('eventName',...) // Example: $(document).on('newMessage', function(){ displayNewMailNotification(); }); $(document).trigger('newMessage');
  5. OPTION 2: PUB/SUB WRAPPER // Using .on()/.off() from jQuery 1.7.1

    (function($) { var o = $({}); $.subscribe = function() { o.on.apply(o, arguments); }; $.unsubscribe = function() { o.off.apply(o, arguments); }; $.publish = function() { o.trigger.apply(o, arguments); }; }(jQuery));
  6. OPTION 3: $.CALLBACKS // Multi-purpose callbacks list object // Pub/Sub

    implementation: var topics = {}; jQuery.Topic = function( id ) { var callbacks, topic = id && topics[ id ]; if ( !topic ) { callbacks = jQuery.Callbacks(); topic = { publish: callbacks.fire, subscribe: callbacks.add, unsubscribe: callbacks.remove }; if ( id ) { topics[ id ] = topic; } } return topic; };
  7. OPTION 3: $.CALLBACKS // Usage: // Subscribers $.Topic( 'mailArrived' ).subscribe(

    fn1 ); $.Topic( 'mailArrived' ).subscribe( fn2 ); $.Topic( 'mailSent' ).subscribe( fn1 ); // Publishers: $.Topic( 'mailArrived' ).publish( 'hello world!' ); $.Topic( 'mailSent' ).publish( 'woo! mail!' );
  8. OPTION 4: BACKBONE.JS EVENTS var myObject = {}; _.extend( myObject,

    Backbone.Events ); myObject.on('eventName', function( msg ) { console.log( 'triggered:' + msg ); }); myObject.trigger('eventName', 'some event'); // Similarly.. person.on('change:name change:age', function( msg ) { console.log('name and age changed'); });
  9. IF ONE PART OF YOUR APPLICATION BREAKS CAN THE APP

    FIX THIS BREAK ITSELF? BUT THERE ARE PROBLEMS:
  10. HOW MUCH OF WHAT WE CREATE IS EASILY REUSABLE? HOW

    SECURE IS YOUR APPLICATION FROM ITSELF? CAN OUR MODULES BE TESTED INDEPENDENTLY? MORE PROBLEMS
  11. “THE SECRET TO BUILDING LARGE APPS IS NEVER BUILD LARGE

    APPS. BREAK YOUR APP INTO SMALL PIECES. THEN, ASSEMBLE THOSE TESTABLE, BITE-SIZED PIECES INTO YOUR BIG APPLICATION” - JUSTIN MEYER
  12. $( el ).css() $( el ).animate(..) $( el ).attributes( key,

    value ) ... EXAMPLES YOU USE ALL THE TIME
  13. var module = (function() { var _private = { i:5,

    get : function() { console.log('current value:' + this.i); }, set : function( val ) { this.i = val; }, run : function() { console.log('running'); }, jump: function(){ console.log('jumping'); } }; return { facade : function( args ) { _private.set(args.val); _private.get(); if ( args.run ) { _private.run(); } } } }()); module.facade({run: true, val:10}); //outputs current value: 10, running A limited API allowing access to a more complex implementation FACADE IMPLEMENTATION
  14. return { facade : function( args ) { // set

    values of private properties _private.set(args.val); // test setter _private.get(); // optional: provide a simple interface // to internal methods through the // facade signature if ( args.run ) { _private.run(); } } } module.facade({run: true, val:10}); Users are only concerned with using the facade FACADE IMPLEMENTATION
  15. bindReady: function() { ... // One snippet for event listeners

    in jQuery core where the user // isn't exposed to any of the cross-browser implementation // handling behind the scenes if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded",DOMContentLoaded, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", jQuery.ready, false ); // If IE event model is used } else if ( document.attachEvent ) { document.attachEvent( "onreadystatechange", DOMContentLoaded); and another .bindReady() used for $(document).ready()
  16. TOWER HANDLES WHAT PLACES CAN TAKE OFF OR LAND ALL

    COMMUNICATION DONE FROM PLANES TO TOWER, NOT PLANE TO PLANE A CENTRALISED CONTROLLER IS KEY TO SUCCESS HERE ANALOGY: AIR TRAFFIC CONTROL
  17. var mediator = (function(){ var subscribe = function(channel, fn){ if

    (!mediator.channels[channel])mediator.channels[channel] = []; mediator.channels[channel].push({ context: this, callback:fn }); return this; }, publish = function(channel){ if (!mediator.channels[channel]) return false; var args = Array.prototype.slice.call(arguments, 1); for (var i = 0, l = mediator.channels[channel].length; i <l; i++) { var subscription = mediator.channels[channel][i]; subscription.callback.apply(subscription.context,args); } return this; }; return { channels: {}, publish: publish, subscribe: subscribe, installTo: function(obj){ obj.subscribe = subscribe; obj.publish = publish; } }; }()); MEDIATOR IMPLEMENTATION:
  18. // Publish-Subscribe via a centralised mediator mediator.subscribe('messageReceived', function( data ){

    // Awesome! a new message // I can do something with the data }); mediator.publish('messageReceived', { sender: '[email protected]', preview: 'hello world' }); MEDIATOR EXAMPLE:
  19. MESSAGES CHAT NOTIFICATIONS A new message is available A new

    notification has been posted Any new emails? Paul sent a new IM Any new instant messages? Are any contacts online to chat? 5 contacts are available to chat
  20. MODULE PATTERN OBJECT LITERALS AMD ASYNCHRONOUS MODULES - UNTIL WE

    GET ES HARMONY SIMULATED PRIVACY MODULAR OBJECTS
  21. WHAT IS A LARGE APP? A NON-TRIVIAL APPLICATION REQUIRING SIGNIFICANT

    DEVELOPER EFFORT TO MAINTAIN, WHERE MOST OF THE HEAVY LIFTING OF DATA MANIPULATION AND DISPLAY FALLS TO THE BROWSER IT ISN’T ABOUT LOC!
  22. NOTIFY US WHEN SOMETHING INTERESTING HAPPENS DON’T TOUCH ANYTHING THEY

    DON’T HAVE TO CAN’T CAUSE THE ENTIRE APP TO STOP WORKING MODULES:
  23. DEDICATE SUFFICIENT TIME TO PLANNING THE ARCHITECTURE THE MAKES THE

    MOST SENSE FOR YOUR APP - IT’S OFTEN MORE COMPLEX THAN WE INITIALLY THINK.
  24. 1. THE APPLICATION CORE USING THE MEDIATOR PATTERN MANAGES THE

    MODULE LIFECYCLE: STARTS, STOPS, RESTARTS IF NECESSARY
  25. CODE SAMPLE: app.core = (function(){ var data = {}; return{

    define: function( id, constructor ){ //... type checking not show var c = constructor(app.facade.define(this,id)); //..further instance checks not shown data[id] = { define: constructor, instance:null; }, start: function( id ){ var module = data[id]; module.instance =module.define(app.facade.define(this, id)); module.instance.initialize(); }, stop: function( id ){ var module = data[id]; if(module.instance){ module.instance.destroy(); } ...
  26. 1. THE APPLICATION CORE USING THE MEDIATOR PATTERN REACTS TO

    ACTIONS PASSED BACK FROM A SANDBOX (FACADE) - HANDLES LOGIC
  27. CODE SAMPLE: // The sandbox has publish() and subscribe() methods

    // which the mediator implements // subscribe register:function(events, module){ if(app.core.dom.isObject(events) && module){ if(data[module]){ data[module].events = events; } } }, // publish trigger: function(events){ if(app.core.dom.isObject(events)){ var mod; for(mod in data){ if(data.hasOwnProperty(mod)){ mod = data[mod]; if(mod.events && mod.events[events.type]){ ...
  28. 1. THE APPLICATION CORE USING THE MEDIATOR PATTERN ENABLES ADDING

    AND REMOVING MODULES WITHOUT CAUSING BREAKS
  29. CODE SAMPLE: // Summary definitions of a simple todo app

    // Easy to add/remove/start/stop app.core.define('#todo-counter', ...); app.core.define('#todo-field',...); app.core.define('#validate', ...); app.core.define("#todo-list", ...); app.core.define("#todo-entry", ...); app.core.define('#error', ...); app.core.define('#validate', ...);
  30. 2. THE SANDBOX USING THE FACADE PATTERN AN ABSTRACTION OF

    THE CORE THAT’S AN API FOR COMMON TASKS, USED BY MODULES
  31. CODE SAMPLE: app.sandbox = { define: function( el, module ){

    return{ publish: function( eventType ){ app.events.trigger( eventType ); }, subscribe: function( eventType ){ app.events.register( eventType, module); }, find: function( selector ){ return app.dom.query(el).find(selector); }, ignore: function( eventType ){ app.events.remove( eventType, module); } } } };
  32. 2. THE SANDBOX USING THE FACADE PATTERN INTERFACE FOR ENSURING

    MODULES DON’T DIRECTLY ACCESS THE CORE/LIBRARIES
  33. CODE SAMPLE: // Note: all input validation done by the

    mediator animate:function( props ){ return core.dom.animate(props); }, bind:function( el , type, fn ){ dom.bind(el, type, fn); }, unbind:function( el , type, fn ){ dom.unbind(el, type, fn); }, ignore:function( e ){ events.remove(e, module); }, createElement:function( el, config ){
  34. CODE SAMPLE: // A permissions structure can support checking //

    against subscriptions prior to allowing them // to clear. This enforces a flexible security // layer for your application. var permissions = { newMessage: { messageComposer:true, notifications: true, offlineStore: false }, trashMessage:{ messageComposer: true, notifications: false }, ...
  35. CODE SAMPLE (STATUS): app.core.define('#status-widget', function( sandbox ){ var status =

    "none"; return{ initialize: function(){ var el = sandbox.find('.editor'); sandbox.subscribe({ 'new-entry': this.updateStatus }); }, destroy: function(){ status = ""; }, updateStatus: function(data){ status = "edited"; sandbox.find('.status').html(status); sandbox.publish('status-updated'); } } });
  36. 3. MODULES: SUBSCRIBE TO NOTIFICATIONS OF INTEREST, RAPIDLY REACT TO

    CHANGES USING AMD/OBJECT LITERALS/MODULE PATTERN
  37. CODE SAMPLE: app.core.define('#status-widget', function( sandbox ){ var status = "none";

    return{ initialize: function(){ var el = sandbox.find('.editor'); sandbox.subscribe({ 'new-entry': this.updateStatus }); }, destroy: function(){ status = ""; }, updateStatus: function(data){ status = "edited"; sandbox.find('.status').html(status); sandbox.publish('status-updated'); } } });
  38. CODE SAMPLE: app.core.define('#status-widget', function( sandbox ){ var status = "none";

    return{ initialize: function(){ var el = sandbox.find('.editor'); sandbox.subscribe({ 'new-entry': this.updateStatus }); }, destroy: function(){ status = ""; }, updateStatus: function(data){ .. sandbox.publish({ type : 'status-updated', data : {value: ‘A new entry has been added’} }); } }
  39. • Modules have fewer dependencies • They can be managed

    so that the application doesn’t (or shouldn’t) fall over. • You can theoretically pick up any module, drop it in a page and start using it in another project AND ONE LAST REMINDER ABOUT ‘WHY?’
  40. • Patterns For Large-scale JavaScript Application Architecture http://addyosmani.com/largescalejavascript/ • Understanding

    The Publish/Subscribe Pattern http:// msdn.microsoft.com/en-us/scriptjunkie/hh201955.aspx • Writing Modular JavaScript with AMD, CommonJS & ES Harmony http://addyosmani.com/writing-modular-js • Understanding MVC & MVP For JS Developers http:// bit.ly/wJRR59 MORE INFORMATION
  41. • Backbone Fundamentals (book): http:// backbonefundamentals.com • Essential JavaScript Design

    Patterns (e-book) http:// bit.ly/bMyoQ9 • Building Large-Scale jQuery Applications http:// addyosmani.com/blog/large-scale-jquery/ FREE BOOKS/ARTICLES