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

The Plight of Pinocchio

The Plight of Pinocchio

JavaScript's quest to become a real language

JavaScript is no longer a toy language. Many of our applications can't function without it. If we are going to use JavaScript to do real things, we need to treat it like a real language, adopting the same practices we use with real languages.

This framework agnostic talk takes a serious look at how we develop JavaScript applications. Despite its prototypical nature, good object-oriented programming principles are still relevant. The design patterns that we've grown to know and love work just as well in JavaScript as they do any other language. Test driven development forces us to write modular, decoupled code.

Brandon Keepers

May 16, 2012
Tweet

More Decks by Brandon Keepers

Other Decks in Programming

Transcript

  1. If we are going to use JavaScript to do real

    work, then we have to do the things that we do with real languages.
  2. in real languages, we... Use Test Driven Development Apply Design

    Patterns Refactor Separate Concerns Create Abstractions
  3. in real languages, we... Use Test Driven Development Apply Design

    Patterns Decouple Refactor Separate Concerns Create Abstractions
  4. in real languages, we... Use Test Driven Development Apply Design

    Patterns Decouple Refactor Separate Concerns Use Encapsulation Create Abstractions
  5. in real languages, we... Use Test Driven Development Apply Design

    Patterns Decouple Refactor Separate Concerns Use Encapsulation Create Abstractions DRY
  6. why? Use Test Driven Development Apply Design Patterns Decouple Refactor

    Separate Concerns Use Encapsulation Create Abstractions DRY
  7. Litmus Test If your site or application breaks without JavaScript,

    then you should treat it like a real language.
  8. a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({

    url: '/statuses', type: 'POST', }); return false; }); }); example lovingly stolen from @searls
  9. a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({

    url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, }); return false; }); }); example lovingly stolen from @searls
  10. a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({

    url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { } }); return false; }); }); example lovingly stolen from @searls
  11. a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({

    url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append( '<li>' + data.text + '</li>'); } }); return false; }); }); example lovingly stolen from @searls
  12. Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce

    “Starting with a test means that we have to describe what we want to achieve before we consider how.”
  13. “ Testing JavaScript is not worth it.” “It changes too

    frequently.” “The tests break all the time.”
  14. “ Testing JavaScript is not worth it.” “It changes too

    frequently.” “It’s just view code.” “The tests break all the time.”
  15. “ Testing JavaScript is not worth it.” “It changes too

    frequently.” “Browser implementations are too different.” “It’s just view code.” “The tests break all the time.”
  16. “ Testing JavaScript is not worth it.” “It changes too

    frequently.” “Browser implementations are too different.” “It’s just view code.” “The tests break all the time.” “It’s too hard.”
  17. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... example lovingly stolen from @searls
  18. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event example lovingly stolen from @searls
  19. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event example lovingly stolen from @searls
  20. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event 3. network IO example lovingly stolen from @searls
  21. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event 3. network IO 4. user input example lovingly stolen from @searls
  22. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event 3. network IO 4. user input 5. network event example lovingly stolen from @searls
  23. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event 3. network IO 4. user input 5. network event 6. HTML templating example lovingly stolen from @searls
  24. “The same structure that makes the code difficult to test

    now will make it difficult to change in the future.” Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce
  25. system tests with cucumber... Feature: Telling the world about my

    every breath Scenario: Successfully posting a status update When I am on the home page And I fill in "Status" with "Integration tests are easy" And I press "Post Update" Then I should see "Integration tests are easy"
  26. system tests with cucumber... $ cucumber Feature: Posting Statuses Scenario:

    Successfully posting a status When I am on the home page And I fill in "Status" with "Integration tests are easy" And I press "Post Update" Then I should see "Integration tests are easy" 1 scenario (1 passed) 4 steps (4 passed) 0m0.988s
  27. there are many advantages to system testing easy to write

    run in real browser environment relatively easy to maintain
  28. there are many advantages to system testing easy to write

    run in real browser environment verifies system actually works relatively easy to maintain
  29. there are many disadvantages to system testing harder to simulate

    failure cases test failures are difficult to decipher run very slow
  30. there are many disadvantages to system testing harder to simulate

    failure cases test failures are difficult to decipher run very slow does not encourage good design
  31. “To construct an object for a unit test, we have

    to pass its dependencies to it, which means that we have to know what they are.” Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce
  32. “To keep unit tests understandable (and, so, maintainable), we have

    to limit their scope.” Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce
  33. a unit test in jasmine require('/js/views/status_list.js'); describe("View.StatusList", function() { beforeEach(function()

    { this.view = new View.StatusList(); }); it("fetches records from the server", function() { expect(this.view.collection.fetch).toHaveBeenCalled(); }); it("renders when collection is reset", function() { this.view.collection.reset([{text: 'Unit testing is fun'}]); expect(this.view.$el.find('li').text()).toEqual('Unit testing is fun'); }); it("appends newly added items", function() {
  34. Architect Christopher Alexander “Each pattern describes a problem that occurs

    over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.”
  35. Design patterns are the basic parts of speech that allow

    us to create coherent, well-structured systems.
  36. Creational Abstract Factory Builder Factory Method Prototype Singleton Structural Adapter

    Bridge Composite Decorator Flyweight Proxy Behavioral Chain of responsibility Command Interpreter Iterator Mediator Memento Observer State Strategy Template method Visitor “Classical Design Patterns”
  37. Advantages of Design Patterns removes duplication shared vocabulary generic, reusable

    components faster communication proven paradigms easier to test
  38. Advantages of Design Patterns removes duplication shared vocabulary generic, reusable

    components faster communication proven paradigms easier to test supports system changes
  39. Addy Osmani Learning JavaScript Design Patterns “Patterns don’t solve all

    design problems, nor do they replace good software designers, however, they do support them.”
  40. Martin Fowler http://www.martinfowler.com/ “The idea behind [MVC] is to make

    a clear division between domain objects that model our perception of the real world, and presentation objects that are the GUI elements we see on the screen.”
  41. Martin Fowler http://www.martinfowler.com/ “...Domain objects should be completely self contained

    and work without reference to the presentation, they should also be able to support multiple presentations, possibly simultaneously.”
  42. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append( '<li>' + data.text + '</li>'); } }); return false; }); }); back to our example...
  43. jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',

    dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append( '<li>' + data.text + '</li>'); } }); return false; }); }); back to our example...
  44. refactoring to use models... // models/status.js Model.Status = Backbone.Model.extend({ });

    // collections/statuses.js Collection.Statuses = Backbone.Collection.extend({ model: Model.Status, url: '/statuses' });
  45. and the result... // app.js jQuery(function($) { var statuses =

    new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({ text: $(this).find('textarea').val() }); return false; }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); });
  46. and the result... // app.js jQuery(function($) { var statuses =

    new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({ text: $(this).find('textarea').val() }); return false; }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); Observer Pattern
  47. but our system test still passes... $ cucumber Feature: Posting

    Statuses Scenario: Succesfully posting a status When I am on the home page And I fill in "Status" with "Integration tests are easy" And I press "Post Update" Then I should see "Integration tests are easy" 1 scenario (1 passed) 4 steps (4 passed) 0m0.988s
  48. back to our example... // app.js jQuery(function($) { var statuses

    = new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({text: $(this).find('textarea').val()}); return false; }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); });
  49. back to our example... // app.js jQuery(function($) { var statuses

    = new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({text: $(this).find('textarea').val()}); return false; }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); });
  50. test driven refactoring... describe("PostStatus", function() { describe("submitting the form", function()

    { it("creates a status", function() { $el.trigger('submit'); }); }); });
  51. test driven refactoring... describe("PostStatus", function() { describe("submitting the form", function()

    { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); });
  52. test driven refactoring... describe("PostStatus", function() { var $el; beforeEach(function() {

    $el = $("<form><textarea>It's easy!</textarea></form>"); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); });
  53. test driven refactoring... describe("PostStatus", function() { var $el; beforeEach(function() {

    $el = $("<form><textarea>It's easy!</textarea></form>"); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); }); unattached from DOM
  54. test driven refactoring... describe("PostStatus", function() { var $el, collection; beforeEach(function()

    { $el = $("<form><textarea>It's easy!</textarea></form>"); collection = new Collection.Statuses(); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); });
  55. test driven refactoring... describe("PostStatus", function() { var $el, collection; beforeEach(function()

    { $el = $("<form><textarea>It's easy!</textarea></form>"); collection = new Collection.Statuses(); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); }); do we need this test dependency?
  56. test driven refactoring... describe("PostStatus", function() { var $el, collection; beforeEach(function()

    { $el = $("<form><textarea>It's easy!</textarea></form>"); collection = {create: jasmine.createSpy('create')}; }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); });
  57. test driven refactoring... describe("PostStatus", function() { var $el, collection, view;

    beforeEach(function() { $el = $("<form><textarea>It's easy!</textarea></form>"); collection = {create: jasmine.createSpy('create')}; view = new View.PostStatus({ el: $el, collection: collection }); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith(
  58. test driven refactoring... describe("PostStatus", function() { var $el, collection, view;

    beforeEach(function() { $el = $("<form><textarea>It's easy!</textarea></form>"); collection = {create: jasmine.createSpy('create')}; view = new View.PostStatus({ el: $el, collection: collection }); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( dependency injection
  59. refactoring to use controllers... // views/post_status.js View.PostStatus = Backbone.View.extend({ events:

    { "submit": "submit" }, submit: function(e) { var $input = this.$el.find('textarea'); this.collection.create({text: $input.val()}); return false; } });
  60. and the result... // app.js jQuery(function($) { var statuses =

    new Collection.Statuses(); new View.PostStatus({ el: $('#new-status'), collection: statuses }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); });
  61. and our system test still passes... $ cucumber Feature: Posting

    Statuses Scenario: Succesfully posting a status When I am on the home page And I fill in "Status" with "Integration tests are easy" And I press "Post Update" Then I should see "Integration tests are easy" 1 scenario (1 passed) 4 steps (4 passed) 0m0.988s
  62. // app.js jQuery(function($) { var statuses = new Collection.Statuses(); new

    View.PostStatus({ el: $('#new-status'), collection: statuses }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); back to our example...
  63. // app.js jQuery(function($) { var statuses = new Collection.Statuses(); new

    View.PostStatus({ el: $('#new-status'), collection: statuses }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); back to our example...
  64. test driven refactoring... describe("View.StatusList", function() { it("appends newly added items",

    function() { collection.add({text: 'this is fun!'}); expect($el.find('li').length).toBe(1); expect($el.find('li').text()).toEqual('this is fun!'); }); });
  65. test driven refactoring... describe("View.StatusList", function() { beforeEach(function() { $el =

    $('<ul></ul>'); collection = new Backbone.Collection(); view = new View.StatusList({ el: $el, collection: collection }); }); it("appends newly added items", function() { collection.add({text: 'this is fun!'}); expect($el.find('li').length).toBe(1); expect($el.find('li').text()).toEqual('this is fun!'); }); });
  66. refactoring to use views... // views/status_list.js View.StatusList = Backbone.View.extend({ initialize:

    function() { _.bindAll(this, 'add'); this.collection.on('add', this.add); }, add: function(model) { this.$el.append(this.template(model)) }, template: function(model) { return this.make('li', {className:'status'}, model.get('text')); } });
  67. and the result... // app.js jQuery(function($) { var statuses =

    new Collection.Statuses(); new View.PostStatus({ el: $('#new-status'), collection: statuses }); new View.StatusList({ el: $('#statuses'), collection: statuses }); });
  68. adding new features, the cowboy way jQuery(function($) { $.ajax({ url:

    '/statuses', dataType: 'json', success: function(data) { var $statuses = $('#statuses'); for(var i = 0; data.length > i; i++) { $('#statuses').append( '<li>' + data[i].text + '</li>'); } } }); });
  69. but we’re better than that now... Feature: Telling the world

    about my every breath # ... Scenario: Viewing previous status updates Given the following statuses exist: | This is outside-in development | | It’s not so bad | When I am on the home page Then I should see "This is outside-in development" And I should see "It’s not so bad"
  70. failing system test... $ cucumber features/statuses.feature:9 Feature: Posting Statuses Scenario:

    Viewing previous status updates Given the following statuses exist: | This is outside-in development | | It's not so bad | When I am on the home page Then I should see "This is outside-in development" expected there to be content "This is outside-in development" in "Post Update" (RSpec::Expectations::ExpectationNotMetError) And I should see "It's not so bad" Failing Scenarios: cucumber features/statuses.feature:9 1 scenario (1 failed) 4 steps (1 failed, 1 skipped, 3 passed) 0m2.619s
  71. then a unit test... describe("View.StatusList", function() { // … it("fetches

    records from the server", function() { expect(collection.fetch).toHaveBeenCalled(); }); it("renders when collection is reset", function() { collection.reset([{text: 'This is fun!'}]); expect($el.find('li').length).toBe(1); expect($el.find('li').text()).toEqual('This is fun!'); }); });
  72. and finally, the implementation... // views/status_list.js View.StatusList = Backbone.View.extend({ initialize:

    function() { this.collection.on('add', this.add); }, add: function(model) { this.$el.append(this.template(model)) }, template: function(model) { return this.make('li', {className:'status'}, model.get('text')); } });
  73. and finally, the implementation... // views/status_list.js View.StatusList = Backbone.View.extend({ initialize:

    function() { this.collection.on('add', this.add); this.collection.on('reset', this.render); this.collection.fetch(); }, render: function() { this.collection.each(this.add); }, add: function(model) { this.$el.append(this.template(model)) }, // ... template: function(model) {
  74. our top level code didn’t change... // app.js jQuery(function($) {

    var statuses = new Collection.Statuses(); new View.PostStatus({ el: $('#new-status'), collection: statuses }); new View.StatusList({ el: $('#statuses'), collection: statuses }); });
  75. Robert C. Martin Clean Code “Writing clean code requires the

    disciplined use of a myriad of little techniques applied through a painstakingly acquired sense of ‘cleanliness.’ ”
  76. single responsibility principle Every object should have a single responsibility,

    and that responsibility should be entirely encapsulated by the object.
  77. “Our heuristic is that we should be able to describe

    what an object does without using any conjunctions (‘and’, ‘or’).” Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce
  78. responsibility & dependencies // Responsibility: // Create a status from

    user input // // Dependencies: // * el - A form element with a textarea // * collection - the collection that the status will be // created in View.PostStatus = Backbone.View.extend({ // ... });
  79. Carlo Collodi Adventures of Pinocchio “One day I was about

    to become a boy, a real boy, but on account of my laziness and my hatred of books, and because I listened to bad companions…I awoke to find myself changed into a donkey– long ears, gray coat, even a tail!”