SlideShare a Scribd company logo
Writing
Maintainable
 JavaScript


                   Andrew Dupont
               http://andrewdupont.net
I help maintain these.
I write ugly JavaScript all the time.
I work here.
We write ugly JavaScript all the time.
“What’s the problem?”
A JavaScript codebase
gets uglier as it grows.
Day 1



$("p.neat").addClass("ohmy").show("slow");
Day 31
var trip = Gowalla.trip;
$.each(trip.spots, function(i, spot) {
  var marker = new GMarker(
    new GLatLng(spot.lat, spot.lng), {
       icon: Gowalla.createLetterIcon(i),
       title: h(spot.name)
     }
  );
  GEvent.addListener(marker, "click", function() {
       marker.openInfoWindowHtml('<div class="map-bubble"><img src="' +
        spot.image_url + '" width="50" height="50" /><b><a href="' +
        spot.url + '" style="color: #37451e;">' + h(spot.name) +
        '</a></b></div>');
       return false;
  });
  Gowalla.map.addOverlay(marker);
});
Gowalla.zoomAndCenter(trip.spots);
Day 90
options = options || {};
var params = this.getSearchParams(options);
Paginator.currentPage = 1;
Paginator.handler = Gowalla.displaySpots;
Paginator.paginate('/spots', params);
if (Gowalla.filterOptions["l"] || Gowalla.filterOptions["sw"] ||
  Gowalla.filterOptions["lat"]) {
   $('#map-wrapper').show();
   $('#spots_search_l').removeClass('off');
   if (options.l) $('#spots_search_l').val(unescape(options.l));
} else {
   $('#map-wrapper').hide();
}
if (Gowalla.mapVisible()) $('#map-placeholder').show();
$('#heading').hide();
$('#featured_spots').hide();
$('#new_spots').hide();
$.getJSON('/spots', this.getSearchParams(options), function(spots) {
   if (spots.length > 0) {
     $('.paging').show();
     $('#filter').show();
     $('#results').show();
     $('#map-placeholder').hide();
     if (Gowalla.mapVisible() && !Gowalla.map) {
       $('#map-placeholder').addClass("transparent");
       Gowalla.createMap();
       GEvent.addListener(Gowalla.map, "dragend", function() {
         var sw = this.getBounds().getSouthWest().toString();
         var ne = this.getBounds().getNorthEast().toString();
         Gowalla.searchSpots({sw:sw, ne:ne, limit:'150'});
       });
     }
   }
   Gowalla.displaySpots(spots);
});
Ugliness of Code over Time




                             (Source: gut feeling)
design patterns
    recipes
     ideas
The solution:
Use existing so ware principles
    to make your codebase
      more maintainable.
Wishes:
WISH #1:
Code that accomplishes a single task
should all live together in one place.
WISH #2:
We should be able to rewrite a component
   without affecting things elsewhere.
WISH #3:
Troubleshooting should be somewhat easy
  even if you’re unfamiliar with the code.
Plan of attack
WISH:
    Code that accomplishes a single task
    should all live together in one place.



             THEREFORE:
Divide your codebase into components,
      placing each in its own file.
“What’s a component?”
WISH:
     We should be able to rewrite a component
       without breaking things elsewhere.



                THEREFORE:
  A component should be whatever size is
necessary to isolate its details from other code.
A “component” is
   something you could
    rewrite from scratch
without affecting other stuff.
Law of Demeter:
“Each unit should have
only limited knowledge
  about other units.”
The fewer “friends”
    a component has,
the less it will be affected
 by changes elsewhere.
Gowalla.Location
handles all client-side geolocation.



 Gowalla.Location.getLocation();
 //=> [30.26800, -97.74283]

 Gowalla.Location.getLocality();
 //=> "Austin, TX"
Gowalla.ActivityFeed
handles all feeds of user activity.
Gowalla.Flash
            handles the display of
          transient status messages.

Gowalla.Flash.success("Your settings were updated.");
Gowalla.Map
handles all interaction
  with Google Maps.
Example: Gowalla.Map

function addSpotsToMap(spots) {
  Gowalla.Map.clearSpots();
  $.each(spots, function(i, spot) {
    Gowalla.Map.addSpot(spot);
  });
}
Example: Gowalla.Map

function addSpotsToMap(spots) {
  Gowalla.Map.clearSpots();
  $.each(spots, function(i, spot) {
    Gowalla.Map.addSpot(spot, { infoWindow: true });
  });
}
WISH:
We should be able to rewrite a component
  without breaking things elsewhere.



           THEREFORE:
  We should standardize the way
  components talk to one another.
Have components communicate
 through a central message bus.
       (“custom events”)
Publisher and subscriber
  don’t need to know
  about one another.
Instead, they only know about
    a central event broker.
WISH:
Troubleshooting should be somewhat easy
  even if you’re unfamiliar with the code.



            THEREFORE:
        Embrace conventions.
“Files are named according to
     their module names.”
“Componets have a
standard way of initializing.”
“Why custom events?”
Every major framework
      has them:
jQuery


$(document).bind('customevent', function(event, data) {
  // stuff
});

$('#troz').trigger('customevent', [someAssociatedData]);
Prototype

$(document).observe('custom:event', function(event) {
  var customData = event.memo;
  // stuff
});

$('troz').fire('custom:event', { foo: "bar" });
Dojo
                 (“pub-sub”)


dojo.subscribe('some-event', function(data) {
  // stuff
});

dojo.publish('some-event', someData);
A custom event is an interface that
publisher and subscriber adhere to.
As long as the interface
remains the same, either part
  can be safely rewritten.
“So I should replace
all my method calls
with custom events?
    Fat chance.”
A consistent public API
  is also an interface.
It’s OK for a subscriber
to call methods on a broadcaster,
         but not vice-versa.
Example: script.aculo.us 2.0
Writing Maintainable JavaScript
The auto-completer knows
    about the menu…

   var menu = new S2.UI.Menu();
   menu.addChoice("Foo");
   menu.addChoice("Bar");
   someElement.insert(menu);
   menu.open();
…but the menu doesn’t know
        about the auto-completer

menu.observe('ui:menu:selected', function(event) {
  console.log('user clicked on:', event.memo.element);
});
“What does a rewrite
    look like?”
Instead of:
function showNearbySpotsInMenu() {
  $.ajax({
    url: '/spots',
    params: { lat: someLat, lng: someLng },
    success: function(spots) {
      var html = $.map(spots, function(spot) {
        return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>';
      });
      $('#spot_menu').html(html.join(''));
    }
  });
}
Do this:
function getNearbySpotsFromServer(lat, lng) {
  $.ajax({
    url: '/spots',
    params: { lat: lat, lng: lng },
    success: function(spots) {
      $(document).trigger('nearby-spots-received', [spots]);
    }
  });
}
And this:
function renderNearbySpots(event, spots) {
  var html = $.map(spots, function(spot) {
    return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>';
  });
  $('#spot_menu').html(html.join(''));
}

$(document).bind('nearby-spots-received', renderNearbySpots);
Or, if you prefer…
function getNearbySpotsFromServer(lat, lng) {
  $.ajax({
    url: '/spots',
    params: { lat: lat, lng: lng },
    success: function(spots) {
      renderNearbySpots(spots);
    }
  });
}

function renderNearbySpots(spots) {
  var html = $.map(spots, function(spot) {
    return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>';
  });
  $('#spot_menu').html(html.join(''));
}
Intra-module organization
(divide code up according to job)
A formal “contract”
Easier testing


function testNearbySpotsRendering() {
  renderNearbySpots(Fixtures.NEARBY_SPOTS);
  assertEqual($('#spot_menu > li').length, 3);
}
“What if it’s not enough?”
More complex web apps might need
    desktop-like architectures.
“Single-page apps” have
a few common characteristics:
maintaining data objects on
the client side, instead of expecting
   the server to do all the work;
creating views on the client side
and mapping them to data objects;
use of the URL hash for routing/permalinking
      (or HTML5 history management).
Is this MVC?
  Perhaps.
Backbone
http://documentcloud.github.com/backbone/
Models

                       define a model class     window.Todo = Backbone.Model.extend({
                                                 EMPTY: "new todo...",

property access wrapped in set/get methods      initialize: function() {
                                                   if (!this.get('content'))
                                                     this.set({ 'content': this.EMPTY });
                                                },

                                                toggle: function() {
                                                  this.set({ done: !this.get('done') });
                                                },

          triggered when the object is saved    validate: function(attributes) {
                                                   if (!attributes.content.test(/S/))
                                                     return "content can't be empty";
                                                },

                                                 // ...
                                               });
Views

                              define a view class    window.Todo.View = Backbone.View.extend({
                                                      tagName: 'li',

                bind events to pieces of the view    events: {
                                                       'dblclick div.todo-content' : 'edit',
                                                       'keypress .todo-input'      : 'updateOnEnter'
                                                     },

                                                     initialize: function() {
map to a model object; re-render when it changes       this.model.bind('change', this.render);
                                                     },

                          set the view’s contents    render: function() {
                                                       // ...
                                                     },

                                                      // ...
                                                    });
Synchronization

                                                 Backbone.sync = function(method, model, yes, no) {
determine the HTTP verb to use for this action     var type = methodMap[method];

                  serialize the object to JSON        var json = JSON.stringify(model.toJSON());

                   send the data to the server        $.ajax({
                                                        url: getUrl(model),
                                                        type: type,
                                                        data: json,
                                                        processData: false,
                                                        contentType: 'application/json',
                                                        dataType: 'json',
                                                        success: yes,
                                                        error: no
                                                      });
                                                 };
Other options:
      SproutCore
 (http://sproutcore.com/)


      Cappuccino
  (http://cappuccino.org/)


    JavaScriptMVC
(http://javascriptmvc.com/)
“Great. How do I start?”
Don’t do a
Grand Rewrite™
One strategy:
Write new code to conform to your architecture.
Improve old code little by little as you revisit it.
Maintainability
is not all-or-nothing.
Questions?



✍   PLEASE FILL OUT
    AN EVALUATION FORM
                                     Andrew Dupont
                            http://andrewdupont.net

More Related Content

Writing Maintainable JavaScript

  • 1. Writing Maintainable JavaScript Andrew Dupont http://andrewdupont.net
  • 2. I help maintain these. I write ugly JavaScript all the time.
  • 3. I work here. We write ugly JavaScript all the time.
  • 5. A JavaScript codebase gets uglier as it grows.
  • 7. Day 31 var trip = Gowalla.trip; $.each(trip.spots, function(i, spot) { var marker = new GMarker( new GLatLng(spot.lat, spot.lng), { icon: Gowalla.createLetterIcon(i), title: h(spot.name) } ); GEvent.addListener(marker, "click", function() { marker.openInfoWindowHtml('<div class="map-bubble"><img src="' + spot.image_url + '" width="50" height="50" /><b><a href="' + spot.url + '" style="color: #37451e;">' + h(spot.name) + '</a></b></div>'); return false; }); Gowalla.map.addOverlay(marker); }); Gowalla.zoomAndCenter(trip.spots);
  • 8. Day 90 options = options || {}; var params = this.getSearchParams(options); Paginator.currentPage = 1; Paginator.handler = Gowalla.displaySpots; Paginator.paginate('/spots', params); if (Gowalla.filterOptions["l"] || Gowalla.filterOptions["sw"] || Gowalla.filterOptions["lat"]) { $('#map-wrapper').show(); $('#spots_search_l').removeClass('off'); if (options.l) $('#spots_search_l').val(unescape(options.l)); } else { $('#map-wrapper').hide(); } if (Gowalla.mapVisible()) $('#map-placeholder').show(); $('#heading').hide(); $('#featured_spots').hide(); $('#new_spots').hide(); $.getJSON('/spots', this.getSearchParams(options), function(spots) { if (spots.length > 0) { $('.paging').show(); $('#filter').show(); $('#results').show(); $('#map-placeholder').hide(); if (Gowalla.mapVisible() && !Gowalla.map) { $('#map-placeholder').addClass("transparent"); Gowalla.createMap(); GEvent.addListener(Gowalla.map, "dragend", function() { var sw = this.getBounds().getSouthWest().toString(); var ne = this.getBounds().getNorthEast().toString(); Gowalla.searchSpots({sw:sw, ne:ne, limit:'150'}); }); } } Gowalla.displaySpots(spots); });
  • 9. Ugliness of Code over Time (Source: gut feeling)
  • 10. design patterns recipes ideas
  • 11. The solution: Use existing so ware principles to make your codebase more maintainable.
  • 13. WISH #1: Code that accomplishes a single task should all live together in one place.
  • 14. WISH #2: We should be able to rewrite a component without affecting things elsewhere.
  • 15. WISH #3: Troubleshooting should be somewhat easy even if you’re unfamiliar with the code.
  • 17. WISH: Code that accomplishes a single task should all live together in one place. THEREFORE: Divide your codebase into components, placing each in its own file.
  • 19. WISH: We should be able to rewrite a component without breaking things elsewhere. THEREFORE: A component should be whatever size is necessary to isolate its details from other code.
  • 20. A “component” is something you could rewrite from scratch without affecting other stuff.
  • 21. Law of Demeter: “Each unit should have only limited knowledge about other units.”
  • 22. The fewer “friends” a component has, the less it will be affected by changes elsewhere.
  • 23. Gowalla.Location handles all client-side geolocation. Gowalla.Location.getLocation(); //=> [30.26800, -97.74283] Gowalla.Location.getLocality(); //=> "Austin, TX"
  • 25. Gowalla.Flash handles the display of transient status messages. Gowalla.Flash.success("Your settings were updated.");
  • 27. Example: Gowalla.Map function addSpotsToMap(spots) { Gowalla.Map.clearSpots(); $.each(spots, function(i, spot) { Gowalla.Map.addSpot(spot); }); }
  • 28. Example: Gowalla.Map function addSpotsToMap(spots) { Gowalla.Map.clearSpots(); $.each(spots, function(i, spot) { Gowalla.Map.addSpot(spot, { infoWindow: true }); }); }
  • 29. WISH: We should be able to rewrite a component without breaking things elsewhere. THEREFORE: We should standardize the way components talk to one another.
  • 30. Have components communicate through a central message bus. (“custom events”)
  • 31. Publisher and subscriber don’t need to know about one another.
  • 32. Instead, they only know about a central event broker.
  • 33. WISH: Troubleshooting should be somewhat easy even if you’re unfamiliar with the code. THEREFORE: Embrace conventions.
  • 34. “Files are named according to their module names.”
  • 35. “Componets have a standard way of initializing.”
  • 38. jQuery $(document).bind('customevent', function(event, data) { // stuff }); $('#troz').trigger('customevent', [someAssociatedData]);
  • 39. Prototype $(document).observe('custom:event', function(event) { var customData = event.memo; // stuff }); $('troz').fire('custom:event', { foo: "bar" });
  • 40. Dojo (“pub-sub”) dojo.subscribe('some-event', function(data) { // stuff }); dojo.publish('some-event', someData);
  • 41. A custom event is an interface that publisher and subscriber adhere to.
  • 42. As long as the interface remains the same, either part can be safely rewritten.
  • 43. “So I should replace all my method calls with custom events? Fat chance.”
  • 44. A consistent public API is also an interface.
  • 45. It’s OK for a subscriber to call methods on a broadcaster, but not vice-versa.
  • 48. The auto-completer knows about the menu… var menu = new S2.UI.Menu(); menu.addChoice("Foo"); menu.addChoice("Bar"); someElement.insert(menu); menu.open();
  • 49. …but the menu doesn’t know about the auto-completer menu.observe('ui:menu:selected', function(event) { console.log('user clicked on:', event.memo.element); });
  • 50. “What does a rewrite look like?”
  • 51. Instead of: function showNearbySpotsInMenu() { $.ajax({ url: '/spots', params: { lat: someLat, lng: someLng }, success: function(spots) { var html = $.map(spots, function(spot) { return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>'; }); $('#spot_menu').html(html.join('')); } }); }
  • 52. Do this: function getNearbySpotsFromServer(lat, lng) { $.ajax({ url: '/spots', params: { lat: lat, lng: lng }, success: function(spots) { $(document).trigger('nearby-spots-received', [spots]); } }); }
  • 53. And this: function renderNearbySpots(event, spots) { var html = $.map(spots, function(spot) { return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>'; }); $('#spot_menu').html(html.join('')); } $(document).bind('nearby-spots-received', renderNearbySpots);
  • 54. Or, if you prefer… function getNearbySpotsFromServer(lat, lng) { $.ajax({ url: '/spots', params: { lat: lat, lng: lng }, success: function(spots) { renderNearbySpots(spots); } }); } function renderNearbySpots(spots) { var html = $.map(spots, function(spot) { return '<li id="spot-"' + spot.id + '>' + spot.name + '</li>'; }); $('#spot_menu').html(html.join('')); }
  • 57. Easier testing function testNearbySpotsRendering() { renderNearbySpots(Fixtures.NEARBY_SPOTS); assertEqual($('#spot_menu > li').length, 3); }
  • 58. “What if it’s not enough?”
  • 59. More complex web apps might need desktop-like architectures.
  • 60. “Single-page apps” have a few common characteristics:
  • 61. maintaining data objects on the client side, instead of expecting the server to do all the work;
  • 62. creating views on the client side and mapping them to data objects;
  • 63. use of the URL hash for routing/permalinking (or HTML5 history management).
  • 64. Is this MVC? Perhaps.
  • 66. Models define a model class window.Todo = Backbone.Model.extend({ EMPTY: "new todo...", property access wrapped in set/get methods initialize: function() { if (!this.get('content')) this.set({ 'content': this.EMPTY }); }, toggle: function() { this.set({ done: !this.get('done') }); }, triggered when the object is saved validate: function(attributes) { if (!attributes.content.test(/S/)) return "content can't be empty"; }, // ... });
  • 67. Views define a view class window.Todo.View = Backbone.View.extend({ tagName: 'li', bind events to pieces of the view events: { 'dblclick div.todo-content' : 'edit', 'keypress .todo-input' : 'updateOnEnter' }, initialize: function() { map to a model object; re-render when it changes this.model.bind('change', this.render); }, set the view’s contents render: function() { // ... }, // ... });
  • 68. Synchronization Backbone.sync = function(method, model, yes, no) { determine the HTTP verb to use for this action var type = methodMap[method]; serialize the object to JSON var json = JSON.stringify(model.toJSON()); send the data to the server $.ajax({ url: getUrl(model), type: type, data: json, processData: false, contentType: 'application/json', dataType: 'json', success: yes, error: no }); };
  • 69. Other options: SproutCore (http://sproutcore.com/) Cappuccino (http://cappuccino.org/) JavaScriptMVC (http://javascriptmvc.com/)
  • 70. “Great. How do I start?”
  • 71. Don’t do a Grand Rewrite™
  • 72. One strategy: Write new code to conform to your architecture. Improve old code little by little as you revisit it.
  • 74. Questions? ✍ PLEASE FILL OUT AN EVALUATION FORM Andrew Dupont http://andrewdupont.net