$30 off During Our Annual Pro Sale. View Details »

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. LARGE-SCALE
    USING JAVASCRIPT AND JQUERY
    BUILDING
    WITH @ADDYOSMANI
    APPLICATIONS
    #JQCON #LARGESCALE

    View Slide

  2. I WORK AT A COMPANY
    CALLED

    View Slide

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

    View Slide

  4. WHAT DOES THAT INVOLVE?

    View Slide

  5. DECOUPLING
    BASIC CONCEPTS

    View Slide

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

    View Slide

  7. FILTER
    FORMULA EDITOR
    GRID
    SPREADSHEET
    WIDGET/MODULE

    View Slide

  8. PROMOTES REUSABILITY
    LESS COUPLING = EASIER REUSE

    View Slide

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

    View Slide

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

    View Slide

  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');
    });

    View Slide

  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()

    View Slide

  13. PUB/SUB
    BASIC CONCEPTS

    View Slide

  14. LOGICALLY DECOUPLE OBJECTS
    GENERATING EVENTS
    FROM THOSE REACTING
    TO THEM
    NEW MESSAGE
    ‘HELLO WORLD’
    THERE’S A NEW
    MESSAGE!

    View Slide

  15. BAKED INTO MVC, MV*
    MODELS VIEWS CONTROLLERS
    MODELS CAN BE OBSERVED FOR CHANGES

    View Slide

  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');

    View Slide

  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));

    View Slide

  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;
    };

    View Slide

  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!' );

    View Slide

  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');
    });

    View Slide

  21. CHECK-UP

    View Slide

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

    View Slide

  23. IF ONE PART OF YOUR APPLICATION
    BREAKS
    CAN THE APP FIX THIS BREAK ITSELF?
    BUT THERE ARE PROBLEMS:

    View Slide

  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

    View Slide

  25. THINK LONG TERM!
    WHAT HAVEN’T YOU
    FACTORED IN YET?
    WHAT MIGHT CHANGE?

    View Slide

  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

    View Slide

  27. PATTERNS

    View Slide

  28. FACADE PATTERN

    View Slide

  29. SIMPLIFIES USAGE OF A MODULE
    THROUGH A LIMITED API

    View Slide

  30. $( el ).css()
    $( el ).animate(..)
    $( el ).attributes( key, value )
    ...
    EXAMPLES YOU USE ALL THE TIME

    View Slide

  31. HIDES IMPLEMENTATION
    LEVEL DETAILS

    View Slide

  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

    View Slide

  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

    View Slide

  34. ENABLES COMPLEXITY BEHIND
    THE SCENES

    View Slide

  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()

    View Slide

  36. MEDIATOR PATTERN

    View Slide

  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

    View Slide

  38. ALLOWS MODULES TO BROADCAST
    OR LISTEN FOR NOTIFICATIONS
    A MEDIATOR:

    View Slide

  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 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:

    View Slide

  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: '[email protected]',
    preview: 'hello world'
    });
    MEDIATOR EXAMPLE:

    View Slide

  41. NOTIFICATIONS CAN BE HANDLED
    BY ANY NUMBER OF MODULES
    CENTRALISED PUB/SUB

    View Slide

  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

    View Slide

  43. AS CALLS PASS-THROUGH, IT CAN
    PERFORM VALIDATION AND MUCH MORE
    MORE THAN JUST PUBLISH/SUBSCRIBE

    View Slide

  44. MODULES

    View Slide

  45. MODULE PATTERN
    OBJECT LITERALS
    AMD
    ASYNCHRONOUS MODULES - UNTIL WE GET ES HARMONY
    SIMULATED PRIVACY
    MODULAR OBJECTS

    View Slide

  46. LARGE APPLICATIONS

    View Slide

  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!

    View Slide

  48. GWT->JAVASCRIPT, LOTS OF COMPONENTS
    GMail

    View Slide

  49. MODULARIZED HOMEPAGE
    Yahoo!

    View Slide

  50. BRAINSTORM

    View Slide

  51. LOOSELY COUPLED
    ARCHITECTURE
    SMALLER
    INDEPENDENT
    MODULES
    FLEXIBILITY TO
    CHANGE
    WHAT ARE WE LOOKING FOR?

    View Slide

  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:

    View Slide

  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.

    View Slide

  54. SOLUTION: COMBO!
    + +

    View Slide

  55. 1. THE APPLICATION CORE
    USING THE MEDIATOR PATTERN
    MANAGES THE MODULE
    LIFECYCLE: STARTS, STOPS,
    RESTARTS IF NECESSARY

    View Slide

  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();
    }
    ...

    View Slide

  57. 1. THE APPLICATION CORE
    USING THE MEDIATOR PATTERN
    REACTS TO ACTIONS PASSED
    BACK FROM A SANDBOX
    (FACADE) - HANDLES LOGIC

    View Slide

  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]){
    ...

    View Slide

  59. 1. THE APPLICATION CORE
    USING THE MEDIATOR PATTERN
    ENABLES ADDING AND
    REMOVING MODULES
    WITHOUT CAUSING BREAKS

    View Slide

  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', ...);

    View Slide

  61. 1. THE APPLICATION CORE
    USING THE MEDIATOR PATTERN
    HANDLES ERROR DETECTION
    AND MANAGEMENT

    View Slide

  62. 2. THE SANDBOX
    USING THE FACADE PATTERN
    AN ABSTRACTION OF THE
    CORE THAT’S AN API FOR
    COMMON TASKS, USED BY
    MODULES

    View Slide

  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);
    }
    }
    }
    };

    View Slide

  64. 2. THE SANDBOX
    USING THE FACADE PATTERN
    INTERFACE FOR ENSURING
    MODULES DON’T DIRECTLY
    ACCESS THE CORE/LIBRARIES

    View Slide

  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 ){

    View Slide

  66. 2. THE SANDBOX
    USING THE FACADE PATTERN
    PERMISSIONS MANAGER,
    SECURING WHAT MODULES
    CAN/CAN’T ACCESS

    View Slide

  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
    },
    ...

    View Slide

  68. 3. MODULES:
    UNIQUE (INDEPENDENT)
    BLOCKS OF FUNCTIONALITY
    FOR YOUR APPLICATION
    USING AMD/OBJECT LITERALS/MODULE PATTERN

    View Slide

  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');
    }
    }
    });

    View Slide

  70. 3. MODULES:
    SUBSCRIBE TO
    NOTIFICATIONS OF INTEREST,
    RAPIDLY REACT TO CHANGES
    USING AMD/OBJECT LITERALS/MODULE PATTERN

    View Slide

  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');
    }
    }
    });

    View Slide

  72. 3. MODULES:
    NOTIFY THE APP WHEN
    SOMETHING INTERESTING
    HAPPENS (PUBLISH)
    USING AMD/OBJECT LITERALS/MODULE PATTERN

    View Slide

  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’}
    });
    }
    }

    View Slide

  74. DEMO TIME!
    SEE: https://github.com/addyosmani/largescale-demo

    View Slide

  75. REVIEW

    View Slide

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

    View Slide

  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?’

    View Slide

  78. FURTHER READING

    View Slide

  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

    View Slide

  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

    View Slide

  81. Blog
    Twitter
    GitHub
    http://addyosmani.com
    @addyosmani or @addy_osmani
    http://github.com/addyosmani
    THAT’S IT! FOR MORE FROM ME:

    View Slide