Principios SOLID en Javascript – Con ejemplos práticos – Franz Pereira



Principios SOLID en Javascript – Con ejemplos práticos – Franz Pereira

0 0


solid_presentation


On Github frnz / solid_presentation

Principios SOLID en Javascript

Con ejemplos práticos

Por @_frnz_ https://github.com/frnz

Franz Pereira

Technology Lead

Motivación

  • El software es una ingeniería diferente.
  • En software, el diseño es lo caro.
  • Como ingenieros de software podemos cambiar el diseño cuando queramos.
  • Al ser iterativos nos volvemos más flexibles.

¿Qué es SOLID?

Un conjunto de buenas prácticas de OO para preparar el código a cambiar. Mantiene el código los liviano posible, pero preparándolo para poder hacer cambios con el más mínimo esfuerzo.

SOLID

Single Responsibility Principle Open Closed Principle Liskov Substitution Principle Interface Segregation Principle Dependency Inversion Principle

Single Responsibility Principle

A class should have only one reason to change.

Una clase sólo debería tener una razón para cambiar.

Una razón para cambiar

No es una cantidad de métodos, más bien agrupaciones de métodos que cumplen un objetivo

Cart = function(items) {

  this.log = function(log) {
    console.log(log)
  },

  this.calculateTotal = function() {
    return items.sum("price") // sugar.js <3 <3 <3
  },


  this.displayInfo = function() {
    this.log("Total is " + this.calculateTotal())

    $("#total").html(total)
  }

}
          

Nuevos requerimientos:

No escribir en los logs todo el tiempo. Mostrar subtotal.
Cart = function(items, options) {

  this.log = function(log) {  
    if (options.log) {         // D: D: D:
      if (options.logActive) {
        console.log(log)
      }
    }
  },

  this.calculateSubtotal = function() {
    return items.sum("price")
  },
  this.calculateTotal = function() {...}

  this.displayInfo =  function() {
    this.log("Total is " + total)
    $("#subTotal").html(this.calculateSubtotal())
    $("#total").html(this.calculateTotal())
  }

}
          

Requerimientos:

No escribir en los logs todo el tiempo. Mostrar subtotal. El subtotal se muestra dependiendo en qué página esté.
Cart = function(items, options) {

  this.log = function(log) {  
    if (options.log) {         // D: D: D:
      if (options.logActive) {
        console.log(log)
      }
    }
  },

  this.calculateSubtotal = function() {
    return items.sum("price")
  },
  this.calculateTotal = function() {...}

  this.displayInfo =  function() {
    this.log("Total is " + total)
    if (options.showSubtotal) { // ლ(ಠ益ಠლ) 
      $("#subTotal").html(this.calculateSubtotal())
    }
    $("#total").html(this.calculateTotal())
  }

}
          

Complejidad Ciclomática

...functions and methods that have the highest complexity tend to also contain the most defects.

Cyclomatic Complexity:

6

Maintainability index:

98.986

CartCalculator

  CartCalculator = function(items) { 
    this.subTotal = function() {
      return this.items.sum("price")
    }

    this.total = function() {
      return this.subTotal() + this.subTotal()*0.13 
    } 
  }

Renderers

CartInfoRenderer = function(cartCalculator) {
  $("#total").html(cartCalculator.total()) 
}

CartPageInfoRenderer = function(cartCalculator) {
  $("#subtotal").html(cartCalculator.subTotal())
  $("#total").html(cartCalculator.total()) 
}
  

Logger


  Logger = function() {
    this.log = function(message) {
      if (DEBUG) {
        console.log(message)
      }
    }
  }
    

Strategy Pattern

In computer programming, the strategy pattern (also known as the policy pattern) is a particular software design pattern, whereby algorithms behaviour can be selected at runtime.

Controller

  CartController = function(options) {
    options ||= // default options

    this.render = function() {
      options.logger.log("Total is " + options.cart.total())
      options.renderer(options.cart)
    }
  }

  // Página de carrito:
  Cart({renderer: CartPageInfoRenderer}).render() 
  // Otras
  Cart({renderer: CartInfoRenderer}).render()

Ahí afuera: Backbone patterns:

Open Closed principle

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. Las entidades de software deberían estar abiertas a extensión, pero cerradas a modificación.

Open Closed Principle

Se debería extender el comportamiento de una clase sin modificarlo.

Node.js

  var http = require('http');
  http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
  }).listen(1337, '127.0.0.1');
  console.log('Server running at http://127.0.0.1:1337/');
          

Nuevos requerimientos:

Colocar una HTTP Auth.

Node.js

  var http = require('http');
  http.createServer(function (req, res) {
    var header=req.headers['authorization']||'',        
      token=header.split(/\s+/).pop()||'',            
      auth=new Buffer(token, 'base64').toString(),    
      parts=auth.split(/:/),                        
      username=parts[0],
      password=parts[1];

    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
  }).listen(1337, '127.0.0.1');
  console.log('Server running at http://127.0.0.1:1337/');
          

Nuevos requerimientos:

Colocar una HTTP Auth. Proteger contra CSRF. Loggear tiempos de respuesta.

Node.js

  var http = require('http');
  http.createServer(function (req, res) {
    var header=req.headers['authorization']||'',        
      token=header.split(/\s+/).pop()||'',            
      auth=new Buffer(token, 'base64').toString(),    
      parts=auth.split(/:/),                        
      username=parts[0],
      password=parts[1];

    // CSRF CODEZ @.@
    // Incio tiempo de respuesta

    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');

    // Log tiempo de respuesta
  }).listen(1337, '127.0.0.1');
  console.log('Server running at http://127.0.0.1:1337/');
          

Strategy Pattern?

Connect


  var app = connect()
    .use(connect.csrf())
    .use(connect.static('public'))
    .use(function(req, res){
      res.end('hello world\n');
    })
   .listen(3000);

          

Prototypal chainability

¿Encadenamiento prototípico?

Cómo hacer uno:

Hacer un constructor. Agregar métodos al prototypo del constructor. Devolver una instancia del constructor.
  (function ($) {
    $.iDareYou = $.fn.iDareYou = function () {
      return this;
    }
    $.iDoubleDareYou = $.fn.iDoubleDareYou = function () {
      return this;
    }
    $.motherFucker = $.fn.motherFucker = function () {
      return this;
    }
  })(jQuery);

  $(function() {
    // ZOMG!!! COMO SAMUEL L. JACKSON!!!
    $("#title").hide().iDareYou().iDoubleDareYou().motherFucker()
  });

          

Liskov Substitution Principle

Derived classes must be substitutable for their base classes.

Las clases derivadas deben poder sustituirse por sus clases base.

Rectangle

    class Rectangle
      setWidth: (width) ->
        @width = width

      setHeight: (height) ->
        @height = height

      area: ->
        @width * height

rect = new Rectangle
rect.setWidth(2)
rect.setHeight(1)  
rect.area() # 2 :D
            
  var Rectangle;

  Rectangle = (function() {
    function Rectangle() {}
    Rectangle.prototype.setWidth = function(width) {
      return this.width = width;
    };

    Rectangle.prototype.setHeight = function(height) {
      return this.height = height;
    };

    Rectangle.prototype.area = function() {
      return this.width * height;
    };

    return Rectangle;

  })();
            

Square

  class Square extends Rectangle
    setLength: (length) ->
      @width = length
      @height = length

  sq = new Square
  sq.setLength(2)
  sq.area() # 4 |m|
          
var Square, sq,
  __hasProp = {}.hasOwnProperty,
  __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };

Square = (function(_super) {

  __extends(Square, _super);

  function Square() {
    return Square.__super__.constructor.apply(this, arguments);
  }

  Square.prototype.setLength = function(length) {
    this.width = length;
    return this.height = length;
  };

  return Square;

})(Rectangle);

sq = new Square;

sq.setLength(2);

sq.area();
          

Square

  class Square extends Rectangle
    setLength: (length) ->
      @width = length
      @height = length

  sq = new Square
  sq.setWidth(2)
  sq.setHeight(1)  
  sq.area() # 2 D: D: D:
          

Square

  class Square extends Rectangle
    setLength: (length) ->
      @width = length
      @height = length

    setWidth: (length) ->
      setLength(length) # Ok...

    setHeight: (length) ->
      setLength(length) # Ok...

  sq = new Square
  sq.setWidth(2)
  sq.setHeight(1)  
  sq.area() # 1... 

          

Si no se mantiene LSP, la jeraquías de clases empezarían a integrar métodos inútiles, y eventualmente se convertirían en APIs difíciles de entender.

 

Sin LSP no se pueden crear pruebas unitarias que satisfagan toda la jerarquía de clases. Habría que estar rediseñando las pruebas.

¿Cómo preever?

¿Las clases derivadas usan menos funcionalidad (o menos métodos) que la clase base? Las clases base deberían tener la menor cantidad de métodos posibles. Definir un API y construir pruebas que compruebe que no se viola el principio. Particular para API abiertas.

No es sobre herencia, es sobre comportamiento.

Faye.js sobre extensiones

These methods should accept a message and a callback function, and should call the function with the message once they have made any modifications.

YAGNI

You ain't gonna need it

Interface Segregation Principle

Make fine grained interfaces that are client specific.

Ehhh... ¿interfaces?

¿Javascript tiene?

Interface Segregation Principle

Las entidades no deberían ser forzadas a depender de métodos que no usan.

SRP aplicado a librerías y extensiones.

Spine.js

 
  class Contact extend Spine.Model
    @configure "Contact", "name"
    @extend Spine.Model.Local
 

Observer pattern

  // Backbone
  var object = {};

  _.extend(object, Backbone.Events);

  object.on("alert", function(msg) {
    alert("Triggered " + msg);
  });

  object.trigger("alert", "an event");

  // Spine
  Tasks.bind "create", (foo, bar) -> alert(foo + bar)
  Tasks.trigger "create", "some", "data"

  // PJAX
  $('#main').pjax('a.pjax')
  .on('pjax:start', function() { $('#loading').show() })
  .on('pjax:end',   function() { $('#loading').hide() })
  

Signup wizard

El primer paso es para información básica. El segundo paso es para información de promociones.

Signup Wizard

  function showStepOne() {
    $("#step-1").show()
    // ...
  }

  function showStepTwo() {
    $("#step-1").hide()
    $("#step-2").show()
    // ...
  }

Signup wizard

El primer paso es para información básica. El segundo paso es para información secundaria. El tercer paso es para información de promociones.

Signup Wizard

  function showStepOne() {
    $("#step-1").show()
    // ...
  }

  function showStepTwo() {
    $("#step-1").hide()
    $("#step-2").show()
    // ...
  }

  function showStepThree() {
    $("#step-2").hide()
    $("#step-3").show()
    // ...
  }

Signup Wizard 2

  $mainInfo.on("enter", function() {
    // ...
    $mainInfo.show()
  })
  $mainInfo.on("leave", function() {
    $mainInfo.hide()
    // ...
  })

  // Al inicio
  $mainInfo.trigger("enter")

  // "Observador"
  $mainInfo.on("leave", function() {
    $profileInfo.trigger("enter")
  })

  coolWizardBuilder($mainInfo).then($profileInfo).then($promoInfo)

Dependency Inversion Principle

Depend on abstractions, not concretions.

Users

  // save user
  $.ajax({
    type: 'POST',
    url: "/users",
    data: user_data,
    success: success
  });

  // update user
  $.ajax({
    type: 'PUT',
    url: "/users/1",
    data: user_data,
    success: success
  });

Users

  // save user
  saveUser = function(user_data, success...) {
    $.ajax({
      type: 'POST',
      url: "/users",
      data: user_data,
      success: success
    });
  }

  // update user
  updateUser = function(user_data, success...) {
    $.ajax({
      type: 'PUT',
      url: "/users/1",
      data: user_data,
      success: success
    });
  }
  

App de registro de eventos de New Futuro

Selecciona un evento. Contiene la lista de miembros pre-registrados para dicho evento. Confirma quiénes se han registrado. Registra nuevos miembros.

App de registro de eventos de New Futuro

Es fácil de instalar. Drag and drop una carpeta y doble click en index.html. Online: Carga la lista de usuarios pre-registrados. Offline: Confirma quiénes se han registrado. Offline: Registra nuevos miembros. Offline: Exportar información en un CSV. Online: Envía información de miembros a NewFuturo.com.

Repository pattern

Factory method pattern

Método para delegar instanciamiento.

              factory.create(MyFactory, options)
              factory.create(MyOtherFactory, options)

              // Factories
              Storage.create()              // RestStorage
              Storage.create({local: true}) // LocalStorage

              // derbyjs
              store = derby.createStore(options)
            

¿Cómo empezar?

  • Estudiar un principio a la vez. Comenzar haciendo refactorings.
  • No abusar de los principios.
  • El error más común al inicio es crear complejidad innecesaria.
  • Test Driven Development.
  • Code Katas.

Working effectively with legacy code

Legacy code: Código sin pruebas.