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.

20bfe76b3d6105641f879fe45cfc9272?s=128

Brandon Keepers
PRO

May 16, 2012
Tweet

Transcript

  1. PINOCCHIO the plight of JavaScript's quest to become a real

    language
  2. once upon a time...

  3. one fine day...

  4. we built businesses...

  5. ...on this toy language

  6. go to school, study, always tell the truth, and be

    obedient.
  7. If we are going to use JavaScript to do real

    work, then we have to do the things that we do with real languages.
  8. github.com/bkeepers @bkeepers

  9. in real languages, we...

  10. in real languages, we... Use Test Driven Development

  11. in real languages, we... Use Test Driven Development Refactor

  12. in real languages, we... Use Test Driven Development Refactor Separate

    Concerns
  13. in real languages, we... Use Test Driven Development Refactor Separate

    Concerns Create Abstractions
  14. in real languages, we... Use Test Driven Development Apply Design

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

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

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

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

    Separate Concerns Use Encapsulation Create Abstractions DRY
  19. These principles and practices help us build flexible and maintainable

    systems.
  20. Litmus Test If your site or application breaks without JavaScript,

    then you should treat it like a real language.
  21. Use Test Driven Development Apply Design Patterns in real languages,

    we
  22. the simplest app ever... github.com/bkeepers/monologue

  23. jQuery(function($) { }); a typical JavaScript example... example lovingly stolen

    from @searls
  24. a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { return

    false; }); }); example lovingly stolen from @searls
  25. a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({

    url: '/statuses', type: 'POST', }); return false; }); }); example lovingly stolen from @searls
  26. 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
  27. 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
  28. 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
  29. Use Test Driven Development in real languages, we

  30. the evolution of testing...

  31. Developers initially view testing as a verification process.

  32. A good test suite quickly becomes an insurance policy.

  33. Test driven development is a design process.

  34. 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.”
  35. Test driven development in JavaScript is fast and easy.

  36. “ Testing JavaScript is not worth it.”

  37. “ Testing JavaScript is not worth it.” “It changes too

    frequently.”
  38. “ Testing JavaScript is not worth it.” “It changes too

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

    frequently.” “It’s just view code.” “The tests break all the time.”
  40. “ 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.”
  41. “ 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.”
  42. Testing is hard when we write bad code.

  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... example lovingly stolen from @searls
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. Now try to test that.

  51. Testing makes design problems painfully obvious.

  52. “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
  53. So how do we test this?

  54. Well...let’s talk about system testing.

  55. capybara js-test-driver buster.js

  56. 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"
  57. 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
  58. there are many advantages to system testing

  59. there are many advantages to system testing easy to write

  60. there are many advantages to system testing easy to write

    relatively easy to maintain
  61. there are many advantages to system testing easy to write

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

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

  64. there are many disadvantages to system testing run very slow

  65. there are many disadvantages to system testing test failures are

    difficult to decipher run very slow
  66. there are many disadvantages to system testing harder to simulate

    failure cases test failures are difficult to decipher run very slow
  67. 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
  68. System tests tell you your application is functioning, but tell

    you nothing about the internal state.
  69. Me Just Now™ “With system tests, write as few as

    possible, but no fewer.”
  70. Unit tests help ensure components are well designed and function

    properly.
  71. “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
  72. “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
  73. There are about 1,690,000 different unit testing frameworks in JavaScript.

  74. Just pick one that looks healthy.

  75. 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() {
  76. We can use unit tests to improve our design, but

    first…
  77. Apply Design Patterns in real languages, we

  78. 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.”
  79. Design patterns are the basic parts of speech that allow

    us to create coherent, well-structured systems.
  80. None
  81. 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”
  82. Advantages of Design Patterns

  83. Advantages of Design Patterns removes duplication

  84. Advantages of Design Patterns removes duplication shared vocabulary

  85. Advantages of Design Patterns removes duplication shared vocabulary faster communication

  86. Advantages of Design Patterns removes duplication shared vocabulary generic, reusable

    components faster communication
  87. Advantages of Design Patterns removes duplication shared vocabulary generic, reusable

    components faster communication proven paradigms
  88. Advantages of Design Patterns removes duplication shared vocabulary generic, reusable

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

    components faster communication proven paradigms easier to test supports system changes
  90. 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.”
  91. Model, View, Controller almost every web-related framework uses

  92. 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.”
  93. 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.”
  94. Model domain or structural objects representing the application’s state

  95. If your application has data or logic, then you need

    a model.
  96. 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...
  97. 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...
  98. 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' });
  99. 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>'); }); });
  100. 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
  101. Our models don’t have logic yet, so we’re skipping the

    unit test for now.
  102. 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
  103. Controller translates user input into operations on the model

  104. 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>'); }); });
  105. 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>'); }); });
  106. test driven refactoring... describe("PostStatus", function() { describe("submitting the form", function()

    { it("creates a status", function() { }); }); });
  107. test driven refactoring... describe("PostStatus", function() { describe("submitting the form", function()

    { it("creates a status", function() { $el.trigger('submit'); }); }); });
  108. 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!"}); }); }); });
  109. 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!"}); }); }); });
  110. 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
  111. 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!"}); }); }); });
  112. 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?
  113. 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!"}); }); }); });
  114. 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(
  115. 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
  116. a failing test...

  117. 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; } });
  118. all green

  119. 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>'); }); });
  120. 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
  121. View observes the model’s state and generates output for the

    user
  122. // 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...
  123. // 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...
  124. 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!'); }); });
  125. 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!'); }); });
  126. 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')); } });
  127. 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 }); });
  128. Let’s add a new feature.

  129. 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>'); } } }); });
  130. 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"
  131. 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
  132. 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!'); }); });
  133. 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')); } });
  134. 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) {
  135. 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 }); });
  136. MVC is just one design pattern. There are many others

    that can help us.
  137. None
  138. Epilogue before we depart, an

  139. 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.’ ”
  140. single responsibility principle Every object should have a single responsibility,

    and that responsibility should be entirely encapsulated by the object.
  141. “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
  142. 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({ // ... });
  143. References

  144. 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!”
  145. Thanks! @bkeepers http://bit.ly/pinocchio-slides Example App: github.com/bkeepers/monologue