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.

96270e4c3e5e9806cf7245475c00b275?s=128

Addy Osmani

February 10, 2012
Tweet

Transcript

  1. LARGE-SCALE USING JAVASCRIPT AND JQUERY BUILDING WITH @ADDYOSMANI APPLICATIONS #JQCON

    #LARGESCALE
  2. I WORK AT A COMPANY CALLED

  3. BUILDING JAVASCRIPT APPS THAT HAVE TO SCALE FROM THE GET-GO

  4. WHAT DOES THAT INVOLVE?

  5. DECOUPLING BASIC CONCEPTS

  6. SEPARATES UNITS OF CODE THAT DON’T DEPEND ON EACH OTHER

  7. FILTER FORMULA EDITOR GRID SPREADSHEET WIDGET/MODULE

  8. PROMOTES REUSABILITY LESS COUPLING = EASIER REUSE

  9. BUT THAT’S NOT ALL! DECOUPLING CAN BE TAKEN FURTHER

  10. REDUCING THE RISK OF BREAKAGE WHEN OTHER MODULES FAIL EXAMPLE

  11. 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'); });
  12. 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()
  13. PUB/SUB BASIC CONCEPTS

  14. LOGICALLY DECOUPLE OBJECTS GENERATING EVENTS FROM THOSE REACTING TO THEM

    NEW MESSAGE ‘HELLO WORLD’ THERE’S A NEW MESSAGE!
  15. BAKED INTO MVC, MV* MODELS VIEWS CONTROLLERS MODELS CAN BE

    OBSERVED FOR CHANGES
  16. 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');
  17. 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));
  18. 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; };
  19. 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!' );
  20. 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'); });
  21. CHECK-UP

  22. YOUR CURRENT ARCHITECTURE MODULES WIDGETS MVC LIBRARIES TOOLKITS APPLICATION CORE

    TEMPLATES
  23. IF ONE PART OF YOUR APPLICATION BREAKS CAN THE APP

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

    SECURE IS YOUR APPLICATION FROM ITSELF? CAN OUR MODULES BE TESTED INDEPENDENTLY? MORE PROBLEMS
  25. THINK LONG TERM! WHAT HAVEN’T YOU FACTORED IN YET? WHAT

    MIGHT CHANGE?
  26. “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
  27. PATTERNS

  28. FACADE PATTERN

  29. SIMPLIFIES USAGE OF A MODULE THROUGH A LIMITED API

  30. $( el ).css() $( el ).animate(..) $( el ).attributes( key,

    value ) ... EXAMPLES YOU USE ALL THE TIME
  31. HIDES IMPLEMENTATION LEVEL DETAILS

  32. 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
  33. 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
  34. ENABLES COMPLEXITY BEHIND THE SCENES

  35. 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()
  36. MEDIATOR PATTERN

  37. 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
  38. ALLOWS MODULES TO BROADCAST OR LISTEN FOR NOTIFICATIONS A MEDIATOR:

  39. 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:
  40. // 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: 'hello@jquery.com', preview: 'hello world' }); MEDIATOR EXAMPLE:
  41. NOTIFICATIONS CAN BE HANDLED BY ANY NUMBER OF MODULES CENTRALISED

    PUB/SUB
  42. 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
  43. AS CALLS PASS-THROUGH, IT CAN PERFORM VALIDATION AND MUCH MORE

    MORE THAN JUST PUBLISH/SUBSCRIBE
  44. MODULES

  45. MODULE PATTERN OBJECT LITERALS AMD ASYNCHRONOUS MODULES - UNTIL WE

    GET ES HARMONY SIMULATED PRIVACY MODULAR OBJECTS
  46. LARGE APPLICATIONS

  47. 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!
  48. GWT->JAVASCRIPT, LOTS OF COMPONENTS GMail

  49. MODULARIZED HOMEPAGE Yahoo!

  50. BRAINSTORM

  51. LOOSELY COUPLED ARCHITECTURE SMALLER INDEPENDENT MODULES FLEXIBILITY TO CHANGE WHAT

    ARE WE LOOKING FOR?
  52. 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:
  53. DEDICATE SUFFICIENT TIME TO PLANNING THE ARCHITECTURE THE MAKES THE

    MOST SENSE FOR YOUR APP - IT’S OFTEN MORE COMPLEX THAN WE INITIALLY THINK.
  54. SOLUTION: COMBO! + +

  55. 1. THE APPLICATION CORE USING THE MEDIATOR PATTERN MANAGES THE

    MODULE LIFECYCLE: STARTS, STOPS, RESTARTS IF NECESSARY
  56. 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(); } ...
  57. 1. THE APPLICATION CORE USING THE MEDIATOR PATTERN REACTS TO

    ACTIONS PASSED BACK FROM A SANDBOX (FACADE) - HANDLES LOGIC
  58. 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]){ ...
  59. 1. THE APPLICATION CORE USING THE MEDIATOR PATTERN ENABLES ADDING

    AND REMOVING MODULES WITHOUT CAUSING BREAKS
  60. 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', ...);
  61. 1. THE APPLICATION CORE USING THE MEDIATOR PATTERN HANDLES ERROR

    DETECTION AND MANAGEMENT
  62. 2. THE SANDBOX USING THE FACADE PATTERN AN ABSTRACTION OF

    THE CORE THAT’S AN API FOR COMMON TASKS, USED BY MODULES
  63. 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); } } } };
  64. 2. THE SANDBOX USING THE FACADE PATTERN INTERFACE FOR ENSURING

    MODULES DON’T DIRECTLY ACCESS THE CORE/LIBRARIES
  65. 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 ){
  66. 2. THE SANDBOX USING THE FACADE PATTERN PERMISSIONS MANAGER, SECURING

    WHAT MODULES CAN/CAN’T ACCESS
  67. 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 }, ...
  68. 3. MODULES: UNIQUE (INDEPENDENT) BLOCKS OF FUNCTIONALITY FOR YOUR APPLICATION

    USING AMD/OBJECT LITERALS/MODULE PATTERN
  69. 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'); } } });
  70. 3. MODULES: SUBSCRIBE TO NOTIFICATIONS OF INTEREST, RAPIDLY REACT TO

    CHANGES USING AMD/OBJECT LITERALS/MODULE PATTERN
  71. 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'); } } });
  72. 3. MODULES: NOTIFY THE APP WHEN SOMETHING INTERESTING HAPPENS (PUBLISH)

    USING AMD/OBJECT LITERALS/MODULE PATTERN
  73. 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’} }); } }
  74. DEMO TIME! SEE: https://github.com/addyosmani/largescale-demo

  75. REVIEW

  76. A PATTERN FOR LARGE-SCALE APPS CORE SANDBOX MODULES

  77. • 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?’
  78. FURTHER READING

  79. • 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
  80. • 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
  81. Blog Twitter GitHub http://addyosmani.com @addyosmani or @addy_osmani http://github.com/addyosmani THAT’S IT!

    FOR MORE FROM ME: