Upload
redradix
View
1.042
Download
4
Tags:
Embed Size (px)
DESCRIPTION
Documentación curso Javascript para profesionales por Redradix School. Curso avanzado de Javascript.
Citation preview
Javascript para Profesionales
made with love by Redradix (www.redradix.com)
Fundamentos
Objetos
! Conjunto de propiedades propias + heredadas de otro objeto (prototipos)
! ¡Qué no cunda el pánico!
Objetos
! Dinámicos
! Set de strings
var obj = {};obj.nuevaPropiedad = 1;delete obj.nuevaPropiedad;
var strset = { hola: true, adios: true};"hola" in strset;
Objetos
! Referencias
! Todo son objetos excepto: strings, números, booleans, null o undefined
! Strings, números y booleans se comportan como objetos inmutables
var p1 = {x: 1}, p2 = p1;
p1 === p2; // truep1.x = 5;p2.x; // 5
Objetos
! Ciudadanos de primer orden
! Contener valor primitivo u otros objetos. Incluyendo funciones.
(function (obj) { return {b: 2};})({a: 1});
var obj = { f: function() { console.log("hola"); }};obj.f();
Objetos
! Literales
- clase: Object
- sencillos y ligeros
! Construidos
- clase: prototype
- constructor
{ un: "objeto", literal: true};
new NombreDeUnaFuncion();
Clases
! Pueden entenderse como:− Tipo (jerárquico) de datos
− Aquí no
− Categoría de objetos con la misma estructura
− Al grano: objetos con el mismo prototipo.
Clases
Si esto es un “punto”
Y esto es otro “punto”
¿Qué es esto?
¿Y esto?
var point = {x: 0, y: 0};
var point2 = {x: 5, y: 5};
var what = {x: 10, y: 10};
var isit = {x: -10, y: -20};
Mensajes
• Teniendo:
• ¿Que significa esto?
var obj = { nombre: "Pepito", saludo: function () { return "Hola, Mundo!"; }};
obj.nombre;
Mensajes
• Teniendo:
• ¿Y esto?
var obj = { nombre: "Pepito", saludo: function () { return "Hola, Mundo!"; }};
obj.saludo;
Mensajes
• Teniendo:
• ¿Y esto otro? (¡cuidado!)
var obj = { nombre: "Pepito", saludo: function () { return "Hola, Mundo!"; }};
obj[“saludo”]();
Mensajes
• Teniendo:
• ¿Es lo mismo?
var obj = { nombre: "Pepito", saludo: function () { return "Hola, Mundo!"; }};
var fn = obj["saludo"];fn();
Mensajes
• Teniendo:
• ¡NO es no mismo!
var obj = { nombre: "Pepito", saludo: function () { return "Hola, Mundo!"; }};
var fn = obj["saludo"];fn();
Mensajes
• Una función se puede ejecutar de 4 maneras:
- Invocando directamente la función
- Enviando un mensaje a un objeto (método)
- Como constructor
- Indirectamente, a través de call(...) y apply(...)
(function() { alert("Hey!"); })();
objeto.metodo();
new MiConstructor();
fn.call({}, "param");
Mensajes
- Invocando directamente la función
- Enviando un mensaje a un objeto (método)
(function() { alert("Hey!"); })();
objeto.metodo();
Mensajes
Un mensaje se envía a un receptor
var obj = {};obj.toString(); // [object Object]
"hola don Pepito".toUpperCase();
Mensajes
Un mensaje se envía a un receptor
var obj = {};obj.toString(); // [object Object]
"hola don Pepito".toUpperCase();
Mensajes
La sintaxis es engañosa
var obj = { coleccion: ["uno", "dos", "tres"], metodo: function() { return "Hola, Mundo!"; }};
obj.coleccion[1]; obj.metodo();vs.
Mensajes
obj.metodo();var fn = obj.metodo;fn();
Mensajes
obj.metodo();var fn = obj.metodo;fn();
- Accede a la propiedad “metodo” de obj
- Supongo que es una función y la invoco
Mensajes
obj.metodo();var fn = obj.metodo;fn();
- Accede a la propiedad “metodo” de obj
- Supongo que es una función y la invoco
- Envía el mensaje “metodo” a obj
- Si existe, obj se encarga de ejecutar la función
Mensajes
obj.metodo();var fn = obj.metodo;fn();
- Accede a la propiedad “metodo” de obj
- Supongo que es una función y la invoco
- NO HAY RECEPTOR
- Envía el mensaje “metodo” a obj
- Si existe, obj se encarga de ejecutar la función
- obj ES EL RECEPTOR
Mensajes
Un error típico:
$("#elemento").click(objeto.clickHandler);
Mensajes
Un error típico:
• Lo que se intenta decir:
- “Al hacer click sobre #elemento, envía el mensaje clickHandler a objeto”
$("#elemento").click(objeto.clickHandler);
Mensajes
Un error típico:
• Lo que se intenta decir:
- “Al hacer click sobre #elemento, envía el mensaje clickHandler a objeto”
• Lo que se dice en realidad:
- “Accede al valor de la propiedad clickHandler de objeto y ejecútalo al hacer click sobre #elemento”
$("#elemento").click(objeto.clickHandler);
El receptor: ...
¿Por qué tanto lío con el receptor del mensaje?
El receptor: this
¿Por qué tanto lío con el receptor del mensaje?
- ¡El receptor es this!
- La metáfora mensaje/receptor aclara su (escurridizo) significado
El receptor: this
this = “el receptor de este mensaje”
var nombre = "Sonia";
var obj = { nombre: "Pepito", saludo: function() { alert("hola " + this.nombre); }}
obj.saludo();
El receptor: this
this• Su significado es dinámico
• Se decide en el momento (y según la manera) de ejecutar la función
• Se suele llamar “el contexto de la función”
• Cuando no hay receptor, apunta al objeto global
El receptor: this
Cuando no hay receptor, es el objeto global
var nombre = "Sonia"
var obj = { nombre: "Pepito", saludo: function() { alert("hola " + this.nombre) }}
var fn = obj["saludo"];fn();
El receptor: this
Su valor es dinámico
var obj = { nombre: "Pepito", saludo: function() { alert("hola " + this.nombre); }};
var maria = { nombre: "María"};
maria.saludo = obj.saludo;maria.saludo();
El receptor: this
Semánticamente, es como un parámetro oculto
que el receptor se encargara de proveer
function ([this]) { alert("hola " + this.nombre);}
obj.saludo(); => saludo([obj]);
El receptor: this
Semánticamente, es como un parámetro oculto
var nombre = "Sonia";
var obj = { nombre: "Pepito", saludo: function() { var saludo_fn = function() { alert("hola " + this.nombre); }; saludo_fn(); }};
obj.saludo();
El receptor: this
Semánticamente, es como un parámetro oculto
var nombre = "Sonia";
var obj = { nombre: "Pepito", saludo: function([this]) { var saludo_fn = function([this]) { alert("hola " + this.nombre); }; saludo_fn([objeto global]); }};
obj.saludo([obj]);
El receptor: this
Semánticamente, es como un parámetro oculto
var nombre = "Sonia";
var obj = { nombre: "Pepito", saludo: function([this]) { var saludo_fn = function([this]) { alert("hola " + this.nombre); }; saludo_fn([objeto global]); }};
obj.saludo([obj]);
El receptor: this
Es decir:
• Cada función tiene su propio this
• Una función anidada en otra NO comparte el receptor
• El valor de this depende de la invocación, NO de la definición (no se clausura)
El receptor: this
Otro error común:
var obj = { clicks: 0, init: function() { $("#element").click(function() { this.clicks += 1; }); }};
obj.init();
El receptor: this
Otro error común:
var obj = { clicks: 0, init: function([this]) { $("#element").click(function([this]) { this.clicks += 1; }); }};
obj.init([obj]);
El receptor: this
Una posible solución (aunque no la mejor):
var obj = { clicks: 0, init: function() { var that = this; $("#element").click(function() { that.clicks += 1; }); }};
obj.init();
Repaso: Mensajes
• Una función se puede ejecutar de 4 maneras:
- Invocando directamente la función
- Enviando un mensaje a un objeto (método)
- Como constructor
- Indirectamente, a través de call(...) y apply(...)
(function() { alert("Hey!"); })();
objeto.metodo();
new MiConstructor();
fn.call({}, "param");
Repaso: Mensajes
Una función se puede ejecutar de 4 maneras:
Invocando directamente la función
Enviando un mensaje a un objeto (método)
Como constructor
- Indirectamente, a través de call(...) y apply(...)
fn.call({}, "param");
El receptor: this
• Las funciones son objetos
• Se pueden manipular como cualquier otro objeto
- Asignar valores a propiedades
- Pasar como parámetros a otras funciones
- Ser el valor de retorno
- Guardarse en variables u otros objetos
• Tienen métodosvar fn = function() { alert("Hey!"); };
fn.toString();
El receptor: this
• Dos métodos permiten manipular el receptor (contexto):
- fn.call(context [, arg1 [, arg2 [...]]])
- fn.apply(context, arglist)
var a = [1,2,3];Array.prototype.slice.call(a, 1, 2); // [2]
var a = [1,2,3];Array.prototype.slice.apply(a, [1, 2]); // [2]
El receptor: this
var nombre = "Objeto Global";
function saluda() { alert("Hola! Soy " + this.nombre);}
var alicia = { nombre: "Alicia"};
saluda();
saluda.call(alicia);
arguments
• El otro parámetro oculto
• Contiene una lista de todos los argumentos
• NO es un Array
function echoArgs() { alert(arguments); // [object Arguments]}
echoArgs(1, 2, 3, 4);
arguments
• Se comporta (más o menos) como Array...
• ...pero NO del todo
function echoArgs() { alert(arguments[0]); // 1}
echoArgs(1, 2, 3, 4);
function echoArgs() { return arguments.slice(0, 1); // Error!}
echoArgs(1, 2, 3, 4);
arguments
• Un truco:
function echoArgs() { var slice = Array.prototype.slice; return slice.call(arguments, 0, 1);}
echoArgs(1, 2, 3, 4); // [1]
arguments
¡Cuidado, se comporta como parámetro oculto!
function exterior() { var interior = function() { alert(arguments.length); }; interior();}
exterior("a", "b", "c");
intermedio: this y arguments
¿Qué hace esta función?
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
Intermedio: this y arguments
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
var algo = misterio();
typeof algo; // ???
Intermedio: this y arguments
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
var algo = misterio();
algo(); // ???
Intermedio: this y arguments
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
var algo = misterio({}, function() { return this;});
typeof algo(); // ???
Intermedio: this y arguments
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
var obj = {};
var algo = misterio(obj, function() { return this;});
obj === algo(); // ???
Intermedio: this y arguments
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
var obj = {};
var algo = misterio({}, function() { return this;});
obj === algo(); // ???
Intermedio: this y arguments
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
var obj = { nombre: "Bárbara"};
var algo = misterio(obj, function() { return this.nombre;});
algo(); /// ???
Intermedio: this y arguments
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
var obj = { nombre: "Bárbara"};
var algo = misterio(obj, function (saludo) { return saludo + " " + this.nombre;});
algo("Hola, "); /// ???
Intermedio: this y arguments
function misterio(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
var barbara = { nombre: "Bárbara" };var carlos = { nombre: "Carlos" };
var algo = misterio(barbara, function (saludo) { return saludo + " " + this.nombre;});
algo.call(carlos, "Hola, "); /// ???
Intermedio: this y arguments
• bind: fija una función a un contexto
function bind(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
Intermedio: this y arguments
Volviendo al problema:
var obj = { clicks: 0, init: function() { $("#element").click(function() {
// MAL this.clicks += 1; }); }};
obj.init();
Intermedio: this y arguments
Apaño:
var obj = { clicks: 0, init: function() { var that = this; $("#element").click(function() { that.clicks += 1; }); }};
obj.init();
Intermedio: this y arguments
¿Qué pasa si el callback es un método?
var obj = { clicks: 0, incClicks: function() { this.clicks += 1; }, init: function() { $("#element").click( // ??? ); }};
obj.init();
Intermedio: this y arguments
Apaño cada vez más feo:
var obj = { clicks: 0, incClicks: function() { this.clicks += 1; }, init: function() { var that = this; $("#element").click(function() { that.incClicks(); }); }};
Intermedio: this y arguments
• bind al rescate
var obj = { clicks: 0, incClicks: function() { this.clicks += 1; }, init: function() { $("#element").click( bind(this, this.incClicks) ); }};
intermedio: this y arguments
¿Qué hace esta otra función?
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
intermedio: this y arguments
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
typeof enigma(); // ???
intermedio: this y arguments
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
var cosa = enigma();typeof cosa(); // ???
intermedio: this y arguments
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
var cosa = enigma(function() { return "Hola!";});
cosa(); // ???
intermedio: this y arguments
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
function saluda(nombre) { return "Hola, " + nombre + "!";}
var cosa = enigma(saluda);
cosa("Mundo"); // ???
intermedio: this y arguments
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
function saluda(nombre) { return "Hola, " + nombre + "!";}
var cosa = enigma(saluda, "Mundo");
cosa(); // ???
intermedio: this y arguments
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
function saluda(saludo, nombre) { return saludo + ", " + nombre + "!";}
var cosa = enigma(saluda, "Hola", "Mundo");
cosa(); // ???
intermedio: this y arguments
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
function saluda(saludo, nombre) { return saludo + ", " + nombre + "!";}
var cosa = enigma(saluda, "Hola");
cosa("Mundo"); // ???cosa("Don Pepito"); // ???
intermedio: this y arguments
function enigma(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
var dario = {nombre: "Darío"};var elena = {nombre: "Elena"};
function saluda(saludo) { return saludo + ", " + this.nombre + "!";}
var cosa = enigma(saluda, "Qué pasa");
cosa.call(dario); // ???cosa.call(elena); // ???
Intermedio: this y arguments
• curry: aplicación parcial de una función
function curry(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
Intermedio: rizar el rizo
var unObj = { nombre: "Manuel", edad: 32};
function getNombre() { return this.nombre; }function setNombre(nombre) { this.nombre = nombre; }function getEdad() { return this.edad; }function setEdad(edad) { this.edad = edad; }
var bindToUnObj = curry(bind, unObj), getUnObjNombre = bindToUnObj(getNombre), setUnObjNombre = bindToUnObj(setNombre);
setUnObjNombre("Pepito");getUnObjNombre(); // ???
Intermedio: rizar el rizo
function getter(prop) { return this[prop]; }function setter(prop, value) { this[prop] = value; }
var manuel = { nombre: "Manuel", edad: 32};
var edadDeManuel = bind(manuel, curry(getter, "edad"));edadDeManuel(); // ???
Prototipos
Prototipos
• ¿Por qué tan mala fama?
• ¡Es un mecanismo muy sencillo!
• Distinto a otros lenguajes
Prototipos
Un objeto obj:
qué pasa si hacemos:
var obj = {uno: 1, dos: 2};
obj.uno; // 1
Prototipos
var obj = {uno: 1, dos: 2};
obj.uno; // 1
uno 1
dos 2
uno 1
dos 2
obj
obj
Prototipos
Si hacemos:
var obj = {uno: 1, dos: 2};
obj.tres; // undefined
uno 1
dos 2
obj
objuno 1dos 2
Not found! undefined
Prototipos
¿De dónde sale?
var obj = {uno: 1, dos: 2};
obj.toString(); // '[object Object]'
uno 1
dos 2
obj
objuno 1dos 2
Not found! undefined
¿?
Prototipos
obj.toString(); // '[object Object]'
objuno 1
dos 2
prototype Object
toString functionvalueOf function
...Not found! undefined
Object
Prototipos
Teniendo:
auno 1dos 2
prototype b
toString functionvalueOf function
...Not found! undefine
d
Objectbtres 3
cuatro 4prototype Object
Prototipos
auno 1dos 2
prototype b
toString functionvalueOf function
...Not found! undefine
d
Objectbtres 3
cuatro 4prototype Object
a.uno; // 1
Prototipos
auno 1dos 2
prototype b
toString functionvalueOf function
...Not found! undefine
d
Objectbtres 3
cuatro 4prototype Object
a.cuatro; // 4
Prototipos
auno 1dos 2
prototype b
toString functionvalueOf function
...Not found! undefine
d
Objectbtres 3
cuatro 4prototype Object
a.toString; // [object Object]
Prototipos
auno 1dos 2
prototype b
toString functionvalueOf function
...Not found! undefine
d
Objectbtres 3
cuatro 4prototype Object
a.noExiste; // undefined
Prototipos
Pero... ¿Cómo establezco el prototipo de un objeto?
- No se puede hacer directamente
- No se puede modificar el prototipo de objetos literales
- Solo objetos generados (con new)
- Constructores!
Repaso: Mensajes
• Una función se puede ejecutar de 4 maneras:
- Invocando directamente la función
- Enviando un mensaje a un objeto (método)
- Como constructor
- Indirectamente, a través de call(...) y apply(...)
(function() { alert("Hey!"); })();
objeto.metodo();
new MiConstructor();
fn.call({}, "param");
Repaso: Mensajes
Una función se puede ejecutar de 4 maneras:
Invocando directamente la función
Enviando un mensaje a un objeto (método)
- Como constructor
new MiConstructor();
Constructores
• Funciones
• Invocación precedida por new
• Su contexto es un objeto recién generado
• return implícito
• La única manera de manipular prototipos
Constructores
function Constructor(param) { // this tiene otro significado! this.propiedad = "una propiedad!"; this.cena = param;}
var instancia = new Constructor("Pollo asado");instancia.propiedad; // una propiedad!instancia.cena; // "Pollo asado"
Constructores
function Constructor(param) { // this tiene otro significado! this.propiedad = "una propiedad!"; this.cena = param;}
var instancia = new Constructor("Pollo asado");instancia.propiedad; // una propiedad!instancia.cena; // "Pollo asado"
Constructores
3 pasos:
1. Crear un nuevo objeto
2. Prototipo del objeto = propiedadad prototype del constructor
3. El nuevo objeto es el contexto del constructor
Constructores
var b = { uno: 1, dos: 2};
function A() { this.tres = 3; this.cuatro = 4;}
A.prototype = b;
var instancia = new A();instancia.tres; // 3instancia.uno; // 1
Constructores
var b = { uno: 1, dos: 2};
function A() { this.tres = 3; this.cuatro = 4;}
A.prototype = b;
var instancia = new A();instancia.tres; // 3instancia.uno; // 1
instanciauno 1dos 2
proto b
btres 3
cuatro 4proto Object
Constructores
.hasOwnProperty(name)• Distinguir las propiedades heredadas de las propias
• true solo si la propiedad es del objeto
instancia.hasOwnProperty("tres"); // trueinstancia.hasOwnProperty("uno"); // false
Constructores
¿Qué pasa aquí?
var comun = { empresa: "ACME" };
function Empleado(nombre) { this.nombre = nombre;}Empleado.prototype = comun;
var pepe = new Empleado("Pepe");
pepe.nombre; // "Pepe"pepe.empresa; // ???
Constructores
¿Qué pasa aquí?
var comun = { empresa: "ACME" };
function Empleado(nombre) { this.nombre = nombre;}Empleado.prototype = comun;
var pepe = new Empleado("Pepe");
comun.empresa = "Googlezon";var antonio = new Empleado("Antonio");
antonio.empresa; // ???
Constructores
¿Qué pasa aquí?
var comun = { empresa: "ACME" };
function Empleado(nombre) { this.nombre = nombre;}Empleado.prototype = comun;
var pepe = new Empleado("Pepe");
comun.empresa = "Googlezon";var antonio = new Empleado("Antonio");
pepe.empresa; // ???
Constructores
pepenombre “Pepe”
proto comun
comunempresa “ACME”
proto Object
var pepe = new Empleado("Pepe");
Constructores
pepenombre “Pepe”
proto comun
comunempresa “Googlezone”
proto Object
comun.empresa = "Googlezon";
Constructores
pepenombre “Pepe”
proto comun
comunempresa “Googlezone”
proto Object
antonionombre “Antonio”
proto comun
var antonio = new Empleado("Antonio");
Constructores
pepenombre “Pepe”
proto comun
comunempresa “Googlezone”
proto Object
antonionombre “Antonio”
proto comun
pepe.empresa;
Prototipos
Es decir:
• Las propiedades de los prototipos se comparten!
• Se resuelven dinámicamente
• Modificar un prototipo afecta a todas las instancias anteriores (y futuras)!
Intermedio: constructores
¿Cómo hacer que C herede de B que hereda de A?
Intermedio: constructores
¿Cómo hacer que C herede de B que hereda de A?
Cuno 1
proto B
tres 3
proto Object
ABdos 2
proto A
var instancia = new C();instancia.tres; // 3
Intermedio: constructores
function C() { this.uno = 1;}
var instancia = new C();instancia.tres;
Intermedio: constructores
var B = {dos: 2};
function C() { this.uno = 1;}
C.prototype = B;
var instancia = new C();instancia.tres;
Intermedio: constructores
var A = {tres: 3};
function B() { this.dos = 2;}B.prototype = A;
function C() { this.uno = 1;}C.prototype = B;
var instancia = new C();instancia.tres;
Intermedio: constructores
var A = {tres: 3};
function B() { this.dos = 2;}B.prototype = A;
function C() { this.uno = 1;}C.prototype = B;
var instancia = new C();instancia.dos; // !!!
Intermedio: constructores
var A = {tres: 3};
function B() { this.dos = 2;}B.prototype = A;
function C() { this.uno = 1;}C.prototype = B;
typeof C.prototype; // ???C.prototype.dos; // ???
Intermedio: constructores
var A = {tres: 3};
function B() { this.dos = 2;}B.prototype = A;
function C() { this.uno = 1;}C.prototype = new B();
var instancia = new C();instancia.tres;
Intermedio: constructores
function A() { this.tres = 3;}
function B() { this.dos = 2;}B.prototype = new A();
function C() { this.uno = 1;}C.prototype = new B();
var instancia = new C();instancia.tres;
Cadena de prototipos
La herencia en varios niveles necesita:
• Encadenar prototipos
• El prototipo del “sub constructor” ha de ser siempre new Padre()
• Es la única manera de mantener el “padre del padre” en la cadena!
Mecanismos de herencia
Mecanismos de herencia
• Herencia clásica
• Herencia de prototipos
• Mixins (módulos)
• Extra: herencia funcional
Herencia clásica
¿Qué significa?
• Clases!
• El tipo de herencia más común en otros lenguajes
• Encapsulado y visibilidad
Herencia clásica
Vamos a empezar por un constructor:
function MiClase() { // ???}
var instancia = new MiClase();
Herencia clásica
Las propiedades (públicas)
function MiClase() { this.unaPropiedad = "valor"; this.otraPropiedad = "otro valor";}
var instancia = new MiClase();
Herencia clásica
¿Métodos?
Herencia clásica
¿Métodos?function MiClase() { this.unaPropiedad = "valor"; this.otraPropiedad = "otro valor"; this.unMetodo = function() { alert(this.unaPropiedad); }; this.otroMetodo = function() { alert(this.otraPropiedad); };}
var instancia = new MiClase();instancia.otroMetodo();
Herencia clásica
Mejor así:function MiClase() { this.unaPropiedad = "valor"; this.otraPropiedad = "otro valor";}
MiClase.prototype.unMetodo = function() { alert(this.unaPropiedad);};
MiClase.prototype.otroMetodo = function() { alert(this.otraPropiedad);};
var instancia = new MiClase();instancia.otroMetodo();
Herencia clásica
Solo falta... function Superclase() { /* ... */ }
function MiClase() { this.unaPropiedad = "valor"; this.otraPropiedad = "otro valor";}MiClase.prototype = new Superclase();
MiClase.prototype.unMetodo = function() { alert(this.unaPropiedad);};
MiClase.prototype.otroMetodo = function() { alert(this.otraPropiedad);};
Herencia clásica
¿Cómo se pueden crear métodos “de clase”?
MiClase.metodoEstatico("hola!");
Herencia clásica
¿Cómo se pueden crear métodos “de clase”?
¡Los constructores son objetos!
MiClase.metodoEstatico("hola!");
MiClase.metodoEstatico = function(cadena) { console.log("metodoEstatico:", cadena);}
Herencia clásica: Cuidado!
Este esquema tiene varios problemas:
Herencia clásica: Cuidado!
Este esquema tiene varios problemas:
• ¡No es fácil invocar al super constructor!
Herencia clásica: Cuidado!
function Animal(especie) { this.especie = especie;}
Animal.prototype.getEspecie = function() { return this.especie;}
function Perro(raza) { this.raza = raza;}Perro.prototype = new Animal();
Perro.prototype.describir = function() { return this.getEspecie() + ", de raza " + this.raza;}
function Animal(especie) { this.especie = especie;}
Animal.prototype.getEspecie = function() { return this.especie;}
function Perro(raza) { // ??? this.raza = raza;}Perro.prototype = new Animal();
Perro.prototype.describir = function() { return this.getEspecie() + ", de raza " + this.raza;}
Herencia clásica: Cuidado!
function Animal(especie) { this.especie = especie;}
Animal.prototype.getEspecie = function() { return this.especie;}
function Perro(raza) { Animal("perro"); this.raza = raza;}Perro.prototype = new Animal();
Perro.prototype.describir = function() { return this.getEspecie() + ", de raza " + this.raza;}
Herencia clásica: Cuidado!
function Animal(especie) { this.especie = especie;}
Animal.prototype.getEspecie = function() { return this.especie;}
function Perro(raza) { Animal("perro"); this.raza = raza;}Perro.prototype = new Animal();
Perro.prototype.describir = function() { return this.getEspecie() + ", de raza " + this.raza;}
Herencia clásica: Cuidado!
crea glob. especie!
function Animal(especie) { this.especie = especie;}
Animal.prototype.getEspecie = function() { return this.especie;}
function Perro(raza) { this = new Animal("perro"); this.raza = raza;}Perro.prototype = new Animal();
Perro.prototype.describir = function() { return this.getEspecie() + ", de raza " + this.raza;}
Herencia clásica: Cuidado!
function Animal(especie) { this.especie = especie;}
Animal.prototype.getEspecie = function() { return this.especie;}
function Perro(raza) { this = new Animal("perro"); this.raza = raza;}Perro.prototype = new Animal();
Perro.prototype.describir = function() { return this.getEspecie() + ", de raza " + this.raza;}
Herencia clásica: Cuidado!
ERROR!
function Animal(especie) { this.especie = especie;}
Animal.prototype.getEspecie = function() { return this.especie;}
function Perro(raza) { Animal.call(this, "perro"); this.raza = raza;}Perro.prototype = new Animal();
Perro.prototype.describir = function() { return this.getEspecie() + ", de raza " + this.raza;}
Herencia clásica: Cuidado!
Herencia clásica: Cuidado!
Este esquema tiene varios problemas:
• ¡No es fácil invocar al super constructor!
• No es fácil encapsular
Herencia clásica: Cuidado!
function MiClase() { this.propPublica = "pública!";}
MiClase.prototype.metPublico = function() { return "público!";}
var instancia = new MiClase();instancia.propPublica; // "pública!"instancia.metPublico();
Herencia clásica: Cuidado!
Este esquema tiene varios problemas:
• ¡No es fácil invocar al super constructor!
• No es fácil encapsular...
• ¡Se crea una instancia solo para mantener la cadena de prototipos!
Herencia clásica: Cuidado!
function Superclase() { operacionMuyCostosa(); alert(“Oh, no!”);}
function MiClase() { // ...}MiClase.prototype = new Superclase();
Herencia clásica?
¿Qué se puede hacer?
Herencia clásica?
Hay dos enfoques:
Herencia clásica?
Hay dos enfoques:
• El simple
El “simple”
“Hagamos una funcioncita!”
function inherits(subClass, superClass) { var F = function() {}; F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructor = subClass; // extra subClass.prototype.superclass = superClass;}
El “simple”
function Animal() { }Animal.prototype.mover = function() { console.log("El animal se mueve...");}; Animal.prototype.comer = function() { console.log("¡Ñam!");};
function Perro(raza) { this.superclass.call(this);}inherits(Perro, Animal);
Perro.prototype.comer = function() { console.log("El perro va a por su plato..."); this.superclass.prototype.comer.call(this);};
var p = new Perro("terrier");p.mover();p.comer();p instanceof Perro;
El “simple”
function Animal() { }Animal.prototype.mover = function() { console.log("El animal se mueve...");}; Animal.prototype.comer = function() { console.log("¡Ñam!");};
function Perro(raza) { this.superclass.call(this);}inherits(Perro, Animal);
Perro.prototype.comer = function() { console.log("El perro va a por su plato..."); this.superclass.prototype.comer.call(this);};
var p = new Perro("terrier");p.mover();p.comer();p instanceof Perro;
OMG!
El “simple”
• Ventajas
- Muy simple de implementar
- Muy ligero
- No añade demasiado ruido
• Inconvenientes
- No soluciona mucho...
- No se “heredan” los métodos/propiedades de clase
- Sigue sin ser cómodo de usar
El “simple”
Caso práctico: CoffeeScript
var __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; };
El “simple”
Caso práctico: CoffeeScript
var __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; };
El “simple”
var MiClase = (function(_super) {
__extends(MiClase, _super);
function MiClase() { MiClase.__super__.constructor.apply(this, arguments); this.miPropiedad = 1; }
MiClase.prototype.miMetodo = function() { return MiClase.__super__.miMetodo.call(this, "hola"); };
return MiClase;
})(Superclase);
Herencia clásica?
Hay dos enfoques:
• El simple
• El cómodo
El cómodo
Más complejo, pero merece la pena
var Persona = Class.extend({ init: function(nombre) { console.log("Bienvenido,", nombre); }});
var Ninja = Persona.extend({ init: function(){ this._super("ninja"); } esgrimirEspada: function(){ console.log("En guardia!"); }});
El cómodo
Más complejo, pero merece la pena
var Persona = Class.extend({ init: function(nombre) { console.log("Bienvenido,", nombre); }});
var Ninja = Persona.extend({ init: function(){ this._super("ninja"); } esgrimirEspada: function(){ console.log("En guardia!"); }});
Intermedio: klass.js
¡Rellena los huecos!
var Class = function(){};
Class.extend = function(prop) { var _super = this.prototype;
// ...
return Klass;};
Intermedio: klass.js
Está muy bien pero...
• No hay métodos de clase (y no se heredan!)
• Todo sigue siendo público
• ¡Es solo una primera versión!
El cómodo
¿Cuándo usar este método?
• ¡Siempre que sea posible!
Contras:
• La implementación es más compleja...
• Hay que incluir la librería externa
• No es el enfoque “tradicional”
Herencia de prototipos
Vamos a cambiar de marcha...
Herencia de prototipos
Vamos a cambiar de marcha...
¡Ahora, sin clases!
Herencia de prototipos
Vamos a cambiar de marcha...
¡Ahora, sin clases!
¿¿Cómo puede haber POO sin clases??
Herencia de prototipos
Herencia clásica: categorías
-Definir “Persona”
-crear instancias de persona según la definición
-Para hacer más concreto, ¡redefine!
Herencia de prototipos
Herencia clásica: categorías
-Definir “Persona”
-crear instancias de persona según la definición
-Para hacer más concreto, ¡redefine!
Herencia de prototipos: ejemplos
-“Como ese de ahí, pero más alto”
-Cualquier objeto concreto puede servir de ejemplo
-Para hacer más concreto, ¡cambia lo que quieras!
Herencia de prototipos
var benito = { nombre: "Benito", edad: 36, profesión: "jardinero", saludar: function() { alert("Buen día!"); }, envejecer: function() { this.edad += 1; }};
Herencia de prototipos
var benito = { nombre: "Benito", edad: 36, profesión: "jardinero", saludar: function() { alert("Buen día!"); }, envejecer: function() { this.edad += 1; }};
var gonzalo = clone(benito);gonzalo.nombre = "Gonzalo";gonzalo.profesion = "carpintero";gonzalo.saludar(); // Buen día!
Herencia de prototipos
var benito = { nombre: "Benito", edad: 36, profesión: "jardinero", saludar: function() { alert("Buen día!"); }, envejecer: function() { this.edad += 1; }};
var gonzalo = clone(benito);gonzalo.nombre = "Gonzalo";gonzalo.profesion = "carpintero";gonzalo.saludar(); // Buen día!
Herencia de prototipos
También se puede generalizarvar Animal = { vivo: true, comer: function() { console.log("Ñam, ñam"); }};
var Perro = clone(Animal);Perro.especie = "perro";
var Dogo = clone(Perro);Dogo.raza = "dogo";
var toby = clone(Dogo);toby.nombre = "Toby";
Herencia de prototipos
¡Así de simple!
function clone(obj) { function F(){} F.prototype = obj; return new F();}
Herencia de prototipos
Herencia clásica vs. de prototipos
• Clásica
✓ MUCHO más extendida y bien comprendida
✓ Mayor catálogo de mecanismos de abstracción
• Prototipos
✓ Uso mucho más eficiente de la memoria
✓ La “auténtica” herencia en JS
✓ Muy simple
Herencia de prototipos
Peeero...
• Clásica
๏ Solo se puede emular. Necesario entender los prototipos.
๏ A contrapelo
• Prototipos
๏ Bastante limitada
๏ Lenta con cadenas de prototipos largas!
Herencia de prototipos
¿Cuál uso?
Herencia de prototipos
¿Cuál uso?
¡Las dos!
Intermedio: prototipos
¿Qué significa?
objeto.propiedad;
Intermedio: prototipos
¿Qué significa?
“Accede a la propiedad propiedad del objeto objeto”
objeto.propiedad;
Intermedio: prototipos
¿Qué significa?
“Accede a la propiedad propiedad del objeto objeto. Si no la encuentras, sigue buscando por la cadena de prototipos.”
objeto.propiedad;
Intermedio: prototipos
¿Qué significa?
objeto.propiedad = 1;
Intermedio: prototipos
¿Qué significa?
“Guarda el valor 1 en la propiedad propiedad del objeto objeto.”
objeto.propiedad = 1;
Intermedio: prototipos
¿Qué significa?
“Guarda el valor 1 en la propiedad propiedad del objeto objeto. Si no la encuentras, créala!”
objeto.propiedad = 1;
Intermedio: prototipos
peso 1
proto Object
plumaplomoproto pluma
var pluma = { peso: 1};
var plomo = clone(pluma);
Intermedio: prototipos
peso 1
proto Object
plumaplomoproto pluma
plomo.peso; // 1
Intermedio: prototipos
peso 1
proto Object
plumaplomoproto pluma
peso 100
plomo.peso = 100;
Intermedio: prototipos
Es decir:
• Hay asimetría entre escritura y lectura!
• Lectura: busca en la cadena
• Escritura: crea una nueva propiedad
• Es el comportamiento natural
• Uso eficiente de la memoria: solo se crean los valores diferentes.
Intermedio: prototipos
¿Qué sucede?var Lista = { elementos: []};
var laCompra = clone(Lista);
laCompra.elementos.push("Leche");laCompra.elementos.push("Huevos");
var toDo = clone(Lista);toDo.elementos.push("Contestar emails");toDo.elementos.push("Subir a producción");
toDo.elementos;
prototipos
Object.create(proto)• Igual que clone
• Nativo en algunos navegadores
var pluma = { peso: 1, enStock: true,};
var plomo = Object.create(pluma);plomo.peso = 100;
¡Se acabaron los prototipos!
Clausuras
Sólo una idea importante más: ámbitos
Clausuras
• Una idea sencilla, pero difícil de explicar
• Están por todas partes
• Consecuencia natural del lenguaje
• ¡Muy útiles!
Clausuras
function clausurator() { var a = 1; return function() { return a; };}
Clausuras
function clausurator() { var a = 1; return function() { return a; };}
var fn = clausurator();typeof fn;
Clausuras
function clausurator() { var a = 1; return function() { return a; };}
var fn = clausurator();fn(); // ???
Clausuras
function clausurator() { var a = 1; return function() { return a; };}
var fn = clausurator();fn();
fn = function() { return a;}
Clausuras
function clausurator() { var a = 1; return function() { return a; };}
var fn = clausurator();fn();
fn = function() { return a;}
Clausuras
function clausurator() { var a = 1; return function() { return a; };}
var fn = clausurator();fn();
fn = function() { return a;}
Clausuras
function clausurator() { var a = 1; return function() { return a; };}
var fn = clausurator();fn();
fn = function() { return a;}
a = 1;
Clausuras
Otro caso:function makeContador() { var i = 0; return function() { return i++; }}
var contador1 = makeContador();contador1(); // ???contador1(); // ???
var contador2 = makeContador();contador2(); // ???
Clausuras
Otro caso:function makeContador() { var i = 0; return function() { return i++; }}
var contador1 = makeContador();contador1(); // 0contador1(); // 1
var contador2 = makeContador();contador2(); // 0
Clausuras
function makeContador() { var i = 0; return function() { return i++; }}
var contador1 = makeContador();contador1(); // 0contador1(); // 1
var contador2 = makeContador();contador2(); // 0
contador1 = function() { return i++;}
i = 0;
Clausuras
function makeContador() { var i = 0; return function() { return i++; }}
var contador1 = makeContador();contador1(); // 0contador1(); // 1
var contador2 = makeContador();contador2(); // 0
contador1 = function() { return i++;}
i = 1;
Clausuras
function makeContador() { var i = 0; return function() { return i++; }}
var contador1 = makeContador();contador1(); // 0contador1(); // 1
var contador2 = makeContador();contador2(); // 0
contador1 = function() { return i++;}
i = 1;
contador2 = function() { return i++;}
i = 0;
Clausuras
function interesante() { var algo = 0; return { get: function() { return algo; }, set: function(valor) { return algo = valor; } };}
var obj = interesante();obj.get(); // ???obj.set("hola!");obj.get(); // ???
Clausuras
• bind: fija una función a un contexto
function bind(ctx, fn) { return function() { return fn.apply(ctx, arguments); }}
Clausuras
• curry: aplicación parcial de una función
function curry(fn) { var slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { var newargs = slice.call(arguments); return fn.apply(this, args.concat(newargs)); };}
Clausuras
Es decir:
• Las clausuras son función + entorno
• Asocian datos a funciones
• No se puede acceder directamente a las variables clausuradas desde el exterior de la función
• Duración indefinida
• ¡Son muy útiles!
Intermedio: herencia funcional
function PseudoConstructor() { var self = {}; self.propiedad = "valor"; return self;}
var pseudoInstancia = PseudoConstructor();pseudoInstancia.propiedad; // "valor"
Intermedio: herencia funcional
function PseudoConstructor() { var self = {}; self.propiedad = "valor"; self.metodo = function(nombre) { return "No te duermas, " + nombre + "!"; }; return self;}
var pseudoInstancia = PseudoConstructor();pseudoInstancia.metodo("Abraham");
Intermedio: herencia funcional
function PseudoConstructor() { var self = {}; self.propiedad = "valor"; self.metodo = function(nombre) { return "No te duermas, " + nombre + "!"; }; return self;}
var pseudoInstancia = PseudoConstructor();pseudoInstancia.metodo("Abraham");
Intermedio: herencia funcional
function PseudoConstructor() { var self = {}, propiedad = "privada!"; var metodoPrivado = function(s) { return s.toUpperCase(); } self.metodo = function(nombre) { var mayus = metodoPrivado(nombre); return "No te duermas, " + mayus + "!"; }; return self;}
var pseudoInstancia = PseudoConstructor();pseudoInstancia.metodo("Abraham");
Intermedio: herencia funcional
1. Crear un pseudoconstructor
2. Definir una variable self con un objeto vacío
3. Añadir las propiedades/métodos públicos a self
4. Devolver self
Intermedio: herencia funcional
¿Y para heredar?
Intermedio: herencia funcional
¿Y para heredar?function A() { var self = {}; self.uno = 1; return self;}
function B() { var self = A(); self.dos = 2; return self;}
var b = B();b.uno; // 1
Intermedio: herencia funcional
¿Cómo llamar al supermétodo?
Intermedio: herencia funcional
function A() { var self = {}; self.metodo = function() { console.log("A"); } return self;}
function B() { var self = A(); var superMetodo = self.metodo; self.metodo = function() { superMetodo(); console.log("B"); } return self;}
Intermedio: herencia funcional
“Herencia funcional”:
✓ Explotar clausuras y objetos en linea
✓ Extremadamente simple e intuitivo
✓ Mejor encapsulado público/privado
✓ Poco ruido sintáctico
✓ No hacen falta helpers ni librerías
Intermedio: herencia funcional
“Herencia funcional”:
✓ Explotar clausuras y objetos en linea
✓ Extremadamente simple e intuitivo
✓ Mejor encapsulado público/privado
✓ Poco ruido sintáctico
✓ No hacen falta helpers ni librerías
๏ Un poco... ¿cutre?
๏ No es la manera más popular
๏ ¡Peor uso de la memoria!
Programación Funcional
made with love by Redradix (www.redradix.com)
¿Programación funcional?
La vamos a entender como
• Creación y manipulación de funciones
• Alteración de funciones
• Aplicación de funciones
• Asincronía
Funciones de orden superior
Funciones que devuelven funciones
• curry
• bind
• ¡Muchas otras!
Funciones de orden superior
Algunas de las más útiles:
• throttle
• debounce
• once
• after
• compose
• memoize
throttle
Controlar la frecuencia de invocación
• La función se invocará como máximo una vez
• Durante el periodo de tiempo especificado
throttle
var counter = 0, inc = function() { counter++; };
inc = throttle(inc, 10);
for (var i=100000; i--;) { inc();}
alert(counter); // ~6
throttle
function throttle(fn, time) { var last = 0; return function() { var now = new Date(); if ((now - last) > time) { last = now; return fn.apply(this, arguments); } }}
debounce
Ejecutar la función cuando se deje de llamar
• La llamada se pospone hasta que pasen x ms
• Desde la última invocación
debounce
var counter = 0, inc = function() { counter++; alert(counter); };
inc = debounce(inc, 1000);
for (var i=100000; i--;) { inc();}
debounce
function debounce(fn, time) { var timerId; return function() { var args = arguments; if (timerId) clearTimeout(timerId); timerId = setTimeout(bind(this, function() { fn.apply(this, args); }), time); }}
once
La función solo se puede invocar una vez
var counter = 0, inc = function() { counter++; };
inc = once(inc);
for (var i=100000; i--;) { inc();}
alert(counter);
once
function once(fn) { var executed = false; return function() { if (!executed) { executed = true; return fn.apply(this, arguments); } }}
after
La función se ejecuta solo tras haber sido invocada n veces
var counter = 0, inc = function() { counter++; };
inc = after(inc, 1000);
for (var i=100000; i--;) { inc();}
alert(counter);
after
function after(fn, n) { var times = 0; return function() { times++; if (times % n == 0) { return fn.apply(this, arguments); } }}
compose
Composición de funciones
function multiplier(x) { return function(y) { return x*y; }}
var randCien = compose(Math.floor, multiplier(100), Math.random);
alert(randCien());
compose
function compose() { var fns = [].slice.call(arguments); return function(x) { var currentResult = x, fn; for (var i=fns.length; i--;) { fn = fns[i]; currentResult = fn(currentResult); } return currentResult; }}
memoize
Nunca calcules el mismo resultado 2 veces!
• La primera invocación calcula el resultado
• Las siguientes devuelven el resultado almacenado
• Solo vale para funciones puras
memoize
function fact(x) { if (x == 1) { return 1; } else { return x * fact(x-1); }}
fact = memoize(fact);
var start = new Date();fact(100);console.log(new Date() - start);
start = new Date();fact(100);console.log(new Date() - start);
memoize
function memoize(fn) { var cache = {}; return function(p) { var key = JSON.stringify(p); if (!(key in cache)) { cache[key] = fn.apply(this, arguments); } return cache[key]; }}
Asincronía
JS es, por naturaleza, asíncrono
• Eventos
• AJAX
• Carga de recursos
Asincronía
¿Qué significa asíncrono?
function asincrona() { var random = Math.floor(Math.random() * 100); setTimeout(function() { return random; }, random);}
Asincronía
¿Cómo devuelvo el valor random desde dentro?
function asincrona() { var random = Math.floor(Math.random() * 100); setTimeout(function() { return random; }, random);}
Asincronía
function asincrona(callback) { var random = Math.floor(Math.random() * 1000); setTimeout(function() { callback(random); }, random);}
asincrona(function(valor) { alert(valor);});
Asincronía
function asincrona(callback) { var random = Math.floor(Math.random() * 1000); setTimeout(function() { callback(random); }, random);}
asincrona(function(valor) { alert(valor);});
Asincronía
function asincrona(callback) { var random = Math.floor(Math.random() * 1000); setTimeout(function() { callback(random); }, random);}
asincrona(function(valor) { alert(valor);});
Asincronía
Promesas
• Otra forma de escribir código asíncrono
• Más fácil de manipular
• Más fácil de combinar
Asincronía
Promesas
• Una idea muy sencilla:
- Un objeto que representa un estado futuro
• El estado futuro puede ser:
- La resolución de la promesa en un valor
- El rechazo de la promesa con un error
• Mucho, mucho más fácil de manejar que los callbacks
Promesas
function onSuccess(data) { /* ... */ }
function onFailure(e) { /* ... */}
var promesa = $.get('/mydata');promesa.then(onSuccess, onFailure);
Promesas
promise.then(onSuccess [, onFailure])• En caso de éxito, se invoca a onSuccess con el valor
• En caso de error, se invoca a onFailure
• Devuelve, a su vez, una promesa
Promesas
¿Para qué sirven?
• Dar un aspecto más coherente al código
• Hacer más explícito el flow
• Gestionar los errores en cascada
Promesas
Parse.User.logIn("user", "pass", { success: function(user) { query.find({ success: function(results) { results[0].save({ key: value }, { success: function(result) { // El objeto se guardó. }, error: function(result, error) { // Error. } }); }, error: function(error) { // Error. } }); }, error: function(user, error) { // Error. }});
Promesas
Parse.User.logIn("user", "pass").then(function(user) { return query.find();}).then(function(results) { return results[0].save({ key: value });}).then(function(result) { // El objeto se guardó.}, function(error) { // Error.});
Promesas
Parse.User.logIn("user", "pass").then(function(user) { return query.find();}).then(function(results) { return results[0].save({ key: value });}).then(function(result) { // El objeto se guardó.}, function(error) { // Error.});
Promesas
Casos: cuando onSuccess devuelve un valor
/* siendo promise una promesa... */
promise.then(function() { return 42;}).then(function(valor) { return "La respuesta es " + valor;}).then(function(mensaje) { console.log(mensaje);});
Promesas
Casos: cuando onSuccess devuelve un valor
/* siendo promise una promesa... */
promise.then(function() { return 42;}).then(function(valor) { return "La respuesta es " + valor;}).then(function(mensaje) { console.log(mensaje);});
Promesas
Casos: llamando varias a veces a .then
/* siendo promise una promesa... */
promise.then(function() { console.log("primer onSuccess!");});
promise.then(function() { console.log("segundo onSuccess!");});
Promesas
Casos: llamando varias a veces a .then
/* siendo promise una promesa... */
promise.then(function() { console.log("primer onSuccess!");}, function(e) { console.log("primer onFailure...");});
promise.then(function() { console.log("segundo onSuccess!");}, function(e) { console.log("segundo onFailure...");});
Promesas
Casos: capturar errores
/* siendo promise una promesa... */
promise.then(function() { throw new Error("Oops!");}).then(function() { console.log("Nunca llegamos aquí...");}, function(e) { console.log("Vaya por Dios!"); console.log(e);});
Promesas
Casos: capturar errores
/* siendo promise una promesa... */
promise.then(function() { throw new Error("Oops!");}).then(function() { console.log("Nunca llegamos aquí...");}, function(e) { console.log("Vaya por Dios!"); console.log(e);});
Promesas
Casos: cascada de errores
/* siendo promise una promesa... */
promise.then(function() { throw new Error("Oh no!");}).then(function() { console.log("Nunca se ejecuta.");}).then(function() { console.log("Esto tampoco.");}, function(e) { console.log("Vaya por Dios!"); console.log(e);});
Promesas
Casos: cascada de errores
/* siendo promise una promesa... */
promise.then(function() { throw new Error("Oh no!");}).then(function() { console.log("Nunca se ejecuta.");}).then(function() { console.log("Esto tampoco.");}, function(e) { console.log("Vaya por Dios!"); console.log(e);});
Promesas
Casos: errores localizados
/* siendo promise una promesa... */
promise.then(function() { throw new Error("Oh no!");}).then(function() { console.log("Nunca se ejecuta.");}, function(e) { console.log("Manejador del error");}).then(function() { /* ... */}, function(e) { /* este manejador no se ejecuta! */});
Promesas
Casos: errores localizados
/* siendo promise una promesa... */
promise.then(function() { throw new Error("Oh no!");}).then(function() { console.log("Nunca se ejecuta.");}, function(e) { console.log("Manejador del error");}).then(function() { /* ... */}, function(e) { /* este manejador no se ejecuta! */});
Promesas
¿Cómo creo una promesa?
Promesas
Deferreds o diferidos
• Objetos que nos permiten crear y controlar promesas de valores futuros
• Dos operaciones:
- resolve: resuelve la promesa como exitosa
- reject: rechaza la promesa como fracasada
Promesas
Promesa DiferidoRepresenta un valor futuro
Controla la generación del valor
onSuccess resolve(valor)
onFailure reject(error)
Promesas
function enDiezSegundos() { var diferido = new R.Deferred(); setTimeout(function() { diferido.resolve(new Date()); }, 10*1000); return diferido.promise();}
var promesa = enDiezSegundos();
promesa.then(function(elFuturo) { console.log("Ya han pasado diez segundos!"); console.log(elFuturo.getTime());});
Promesas
Deferred#resolve([arg1, arg2, ...])• Resuelve la promesa (ejecuta el callback onSuccess)
• Los parámetros con los que se llame a .resolve()
serán los que reciba el callback onSuccess
• Solo se debería llamar una vez
Promesas
Deferred#reject([arg1, arg2, ...])• Rechaza la promesa (ejecuta el callback onFailure)
• Los parámetros con los que se llame a .reject() serán los que reciba el callback onFailure
• Solo se debería llamar una vez
Promesas
Deferred#promise()• Devuelve la promesa asociada al diferido
Promesas
Deferred#then(onSuccess, onFailure)• Exactamente igual que hacer: deferred.promise().then(...);
Promesas
Vamos a crear una librería de promesas
• Una implementación sencilla
• Que satisfaga la especificación Promises/A+- http://promises-aplus.github.com/promises-spec/
• tema2/r-promise/index.html
Promesas
Por dónde empezar:
• Poder crear instancias de diferidos
• Poder poner un callback de éxito y uno de fracaso
• .then()- Por ahora, que no devuelva nada
- Solo se puede llamar a una vez por diferido
• .resolve([arg1, ...]) y .reject([arg1, ...])
- Invocan el callback adecuado
- Pasándole los parámetros adecuados
Promesas
Siguientes pasos:
• Poder invocar a .then() varias veces
- Es decir, tener varios callbacks para cada caso en un mismo diferido
• Que funcione el primer ejemplo del ejercicio
Lo último a abordar:
• Que las llamadas a .then() se puedan encadenar
• Es decir, que .then() devuelva a su vez una promesa
• Que funcione el segundo ejemplo
Promesas
when(pov1 [, pov2, ...])• Dos utilidades:
- Homogeneizar promesas y valores en el código
- Combinar varias promesas/valores
• Devuelve siempre una promesa
• La promesa devuelta:
- Se resolverá si todas las promesas se resuelven.
- Los parámetros del callback son los valores devueltos por cada una de las promesas.
- Se rechazará en caso contrario
Promesas
R.Deferred.when(1, 2, 3).then(function(a, b, c) { console.log(a, b, c); // 1 2 3});
Promesas
var p1 = new R.Deferred(), p2 = new R.Deferred(), p3 = new R.Deferred();
R.Deferred.when(p1, p2, p3).then(function(a, b, c) { console.log(a, b, c); // 1 2 3});
p1.resolve(1);p2.resolve(2);p3.resolve(3);
Promesas
/* Homogeneizar */
var promesaOValor = noSeQueDevuelve();
R.Deferred.when(promesaOValor).then(function(valor) { console.log(valor);});
Promesas
/* Homogeneizar */
var valor = 4, promesa = new R.Deferred();
R.Deferred.when(valor, promesa).then(function(a, b) { console.log(a, b); // 4, 5});
promesa.resolve(5);
Promesas
var valor = 4, promesa = new R.Deferred();
R.Deferred.when(valor, promesa).then(function(a, b) { console.log(a, b);}, function(e) { alert("Oh, no!");});
promesa.reject("No funciono");
Promesas
Implementa R.Deferred.when()• tema2/when/index.html
Patrones y principios de diseño
made with love by Redradix (www.redradix.com)
Principios de diseño
• SRP: Single Responsibility Principle
- El código de una elemento ha de tener solo una razón para cambiar.
- EL principio de diseño
- También el complementario: cada responsabilidad ha de tener un único lugar en el código (D.R.Y.)
SRP
Es común ver cosas como esta:
$.ajax({ ... }) .success(function() { cambioEnInterfaz(); mostrarModal(); if ($("#elemento").value() == "Ok") { /* ... */ } globalSeHaGuardado = true; }) .error(function() { // ... });
SRP
O como esta:
var Widget = Class.extend({ onClick: function() { ... }, guardar: function() { ... }, render: function() { ... }, mostrarError: function() { ... }});
SRP
var Widget = Model.extend({ guardar: function() { ... }});
var WidgetView = View.extend({ render: function() { ... }});
var WidgetController = Controller.extend({ onClick: function() { ... }});
var ErrorAlert = ModalWindow.extend({ mostrarError: function() { ... }});
SRP
Caso práctico: masonry.js
• https://github.com/desandro/masonry/blob/master/jquery.masonry.js
• en el método _create (línea 102)...
SRP
!!!!//!sets!up!widget!!!!_create!:!function(!options!)!{!!!!!!//![...]
!!!!!!//!get!original!styles!in!case!we!re3apply!them!in!.destroy()!!!!!!var!elemStyle!=!this.element[0].style;!!!!!!this.originalStyle!=!{!!!!!!!!//!get!height!!!!!!!!height:!elemStyle.height!||!''!!!!!!};!!!!!!//!get!other!styles!that!will!be!overwritten!!!!!!//![...]
3s.isFluid!=!this.options.columnWidth!&&!typeof!this.options.columnWidth!===!'function';
!!!!!!//!add!masonry!class!first!time!around!!!!!!var!instance!=!this;!!!!!!setTimeout(!function()!{!!!!!!!!instance.element.addClass('masonry');!!!!!!},!0!);!!!!!!!!!!!!//!bind!resize!method!!!!!!if!(!this.options.isResizable!)!{!!!!!!!!$(window).bind(!'smartresize.masonry',!function()!{!!!!!!!!!!!instance.resize();!!!!!!!!});!!!!!!}
SRP
!!!!//!sets!up!widget!!!!_create!:!function(!options!)!{!!!!!!//![...]
!!!!!!//!get!original!styles!in!case!we!re3apply!them!in!.destroy()!!!!!!var!elemStyle!=!this.element[0].style;!!!!!!this.originalStyle!=!{!!!!!!!!//!get!height!!!!!!!!height:!elemStyle.height!||!''!!!!!!};!!!!!!//!get!other!styles!that!will!be!overwritten!!!!!!//![...]
3s.isFluid!=!this.options.columnWidth!&&!typeof!this.options.columnWidth!===!'function';
!!!!!!//!add!masonry!class!first!time!around!!!!!!var!instance!=!this;!!!!!!setTimeout(!function()!{!!!!!!!!instance.element.addClass('masonry');!!!!!!},!0!);!!!!!!!!!!!!//!bind!resize!method!!!!!!if!(!this.options.isResizable!)!{!!!!!!!!$(window).bind(!'smartresize.masonry',!function()!{!!!!!!!!!!!instance.resize();!!!!!!!!});!!!!!!}
SRP
El resultado: caos!
• No hay un lugar claro para cada operación
• Es difícil entender qué hace cada línea
- El “qué” está enterrado en el “cómo”
• Muy complicado de testear
• Difícil de reutilizar
SRP
Caso práctico: BrowserQuest
• https://github.com/mozilla/BrowserQuest/blob/master/client/js/chest.js
SRP
!!!!var!Chest!=!Entity.extend({!!!!!!!!init:!function(id,!kind)!{!!!!! !!!!this._super(id,!Types.Entities.CHEST);!!!!!!!!},!!!!!!!!!!!!getSpriteName:!function()!{!!!!!!!!!!!!return!"chest";!!!!!!!!},!!!!!!!!!!!!isMoving:!function()!{!!!!!!!!!!!!return!false;!!!!!!!!},!!!!!!!!!!!!open:!function()!{!!!!!!!!!!!!if(this.open_callback)!{!!!!!!!!!!!!!!!!this.open_callback();!!!!!!!!!!!!}!!!!!!!!},!!!!!!!!!!!!onOpen:!function(callback)!{!!!!!!!!!!!!this.open_callback!=!callback;!!!!!!!!}!!!!});
SRP
Caso práctico: BrowserQuest
• Todo el proyecto está muy bien estructurado
- entity.js
- character.js
- animation.js
- ...
• A pesar de ser muy grande, cada responsabilidad tiene su sitio
SRP
Caso práctico: Backbone.js
• https://github.com/documentcloud/backbone/blob/master/backbone.js
• en Backbone.Model, línea 179...
SRP
• Por un lado..
๏ gestión de estado (set, get)
๏ validación
๏ formateo (toJSON, escape)
๏ servidor (fetch, save)
• Por otro...
✓ Delega los detalles a otros módulos (Sync, Event)
✓ Bajo acoplamiento (“interfaces”)
SRP
“Una responsabilidad”...
• Subjetivo
• “Una sola razón para cambiar”...
- “Para qué todo funcione bien”
- Muy dependiente del nivel de abstracción
- Y de cada módulo
• El exceso es tan malo como el defecto
Principios de diseño
• Tell, Don’t Ask
- “Dime lo que necesitas”
- Claridad y expresividad
- Encapsular las comprobaciones
Tell, Don’t Ask
Los síntomas:
!!!!!!!!!!!!!!!!!!!!if!(typeof(variables)!===!'object'!&&!!Array.isArray(variables))!{!!!!!!!!!!!!!!!!!!!!!!!!variables!=!Object.keys(variables).map(function!(k)!{!!!!!!!!!!!!!!!!!!!!!!!!!!!!var!value!=!variables[k];
!!!!!!!!!!!!!!!!!!!!!!!!!!!!if!(!!(value!instanceof!tree.Value))!{!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!if!(!!(value!instanceof!tree.Expression))!{!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!value!=!new(tree.Expression)([value]);!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!value!=!new(tree.Value)([value]);!!!!!!!!!!!!!!!!!!!!!!!!!!!!}!!!!!!!!!!!!!!!!!!!!!!!!!!!!return!new(tree.Rule)('@'!+!k,!value,!false,!0);!!!!!!!!!!!!!!!!!!!!!!!!});!!!!!!!!!!!!!!!!!!!!!!!!frames!=![new(tree.Ruleset)(null,!variables)];!!!!!!!!!!!!!!!!!!!!}
Tell, Don’t Ask
Los síntomas:
!!!!!!!!!!!!!!!!!!!!if!(typeof(variables)!===!'object'!&&!!Array.isArray(variables))!{!!!!!!!!!!!!!!!!!!!!!!!!variables!=!Object.keys(variables).map(function!(k)!{!!!!!!!!!!!!!!!!!!!!!!!!!!!!var!value!=!variables[k];
!!!!!!!!!!!!!!!!!!!!!!!!!!!!if!(!!(value!instanceof!tree.Value))!{!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!if!(!!(value!instanceof!tree.Expression))!{!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!value!=!new(tree.Expression)([value]);!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!}!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!value!=!new(tree.Value)([value]);!!!!!!!!!!!!!!!!!!!!!!!!!!!!}!!!!!!!!!!!!!!!!!!!!!!!!!!!!return!new(tree.Rule)('@'!+!k,!value,!false,!0);!!!!!!!!!!!!!!!!!!!!!!!!});!!!!!!!!!!!!!!!!!!!!!!!!frames!=![new(tree.Ruleset)(null,!variables)];!!!!!!!!!!!!!!!!!!!!}
Tell, Don’t Ask
Programación:
• Estructurada: adquiere info y toma decisiones
• OO: manda a los objetos hacer cosas
Tell, Don’t Ask
El error:
1. Preguntar a un objeto sobre su estado
2. Tomar una decisión
3. Decirle lo que tiene que hacer
Tell, Don’t Ask
El error:
1. Preguntar a un objeto sobre su estado
2. Tomar una decisión
3. Decirle lo que tiene que hacer
¡Probablemente ese código pertenece al objeto!
Tell, Don’t Ask
if (usuario.primerLogin) { usuario.mostrarMensajeBienvenida();} else { usuario.mostrarSaludo();}
Tell, Don’t Ask
var Usuario = Class.extend({ saludar: function() { if (this.primerLogin) { this.mostrarMensajeBienvenida(); } else { this.mostrarSaludo(); } }});
// y después...
usuario.saludar();
Tell, Don’t Ask
function comprobarTimeout(respuesta) { if ((Date.now() - respuesta.start) > 10000) { respuesta.notificarTimeout(); }}
Tell, Don’t Ask
var Respuesta = Class.extend({ comprobarTimeout: function() { if ((Date.now() - this.start) > 10000) { this.notificarTimeout(); } }});
// y después...
respuesta.comprobarTimeout();
Tell, Don’t Ask
var elementos = miColeccion.getItems();for (var i=0; i<elementos.length; i++) { var elemento = elementos[i]; console.log(elemento.nombre);}
Tell, Don’t Ask
miColeccion.forEach(function(e) { console.log(e.nombre);});
Tell, Don’t Ask
var elemento = new Elemento("hola", 12);var lista = miColeccion.getItems();lista.addElementAt(elemento.getOrder(), elemento);
Tell, Don’t Ask
var elemento = new Elemento("hola", 12);miColeccion.add(elemento);
Tell, Don’t Ask
Es decir:
• Los datos y las operaciones sobre esos datos deben estar en el mismo sitio (objeto)
• Encapsular, desacoplar
• “Command/Query Separation”
- Consulta información
- Da una orden y deja al objeto decidir
- Pero no las mezcles!
Tell, Don’t Ask
Ventajas:
✓ Más robusto (menor acoplamiento)
✓ Menor tendencia a repetir lógica
✓ Mejor estructurado
Inconvenientes:
๏ Miles de métodos de 2 o 3 líneas
๏ “Ruido” en las clases
Principios de diseño
• S.O.L.I.D.
- Single Responsibility
- Open-Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
S.O.L.I.D.
Open-Closed
• “Un elemento ha de estar abierto a la extensión pero cerrado a la modificación”
- Abierto a la extensión: poder ser adaptado a las (futuras) necesidades de la aplicación
- Cerrado a la modificación: que la adaptación no implique modificar su código
Open-Closed
var Lenguas = { Castellano: 0, Ingles: 1 };
var Persona = Class.extend({ init: function(lengua) { this.lengua = lengua; }, saludar: function(lengua) { if (this.lengua == Lenguas.Castellano) { alert("Hola!"); } else if (this.lengua == Lenguas.Ingles) { alert("Hello!"); } }});
new Persona(Lenguas.Castellano).saludar();
Open-Closed
var Persona = Class.extend({ saludar: function() { alert(this.saludo); }});
var Angloparlante = Persona.extend({ init: function() { this.saludo = "Hello!"; }});
var Hispanohablante = Persona.extend({ init: function() { this.saludo = "Hola!"; }});
new Hispanohablante().saludar();
Open-Closed
Pretende:
• Promover el uso de abstracciones
• Código modular y flexible ante el cambio
• Evitar un torrente de cambios en cascada!
Open-Closed
Pretende:
• Promover el uso de abstracciones
• Código modular y flexible ante el cambio
• Evitar un torrente de cambios en cascada!
Es decir:
• Especificar y respetar interfaces
Open-Closed
var Canvas = Class.extend({ render: function(figura) { if (figura instanceof Triangulo) { // ... } else if (figura instanceof Cuadrado) { // ... } }});
Open-Closed
var Canvas = Class.extend({ render: function(figura) { figura.draw(this); }});
var Triangulo = Figura.extend({ draw: function(canvas) { ...}});
var Cuadrado = Figura.extend({ draw: function(canvas) { ...}});
Open-Closed
Caso práctico: three.js
• https://github.com/mrdoob/three.js/blob/master/src/renderers/CanvasRenderer.js
• método render, línea 225
Open-Closed
if!(!element!instanceof!THREE.RenderableParticle!)!{
//!...
}!else!if!(!element!instanceof!THREE.RenderableLine!)!{
//!...
}!else!if!(!element!instanceof!THREE.RenderableFace3!)!{
//!...
}!else!if!(!element!instanceof!THREE.RenderableFace4!)!{
//!...}
Open-Closed
Caso práctico: jasmine.js
•https://github.com/pivotal/jasmine/blob/master/src/core/Reporter.js
Open-Closed
/**!No3op!base!class!for!Jasmine!reporters.!*!*!@constructor!*/jasmine.Reporter!=!function()!{};
//noinspection!JSUnusedLocalSymbolsjasmine.Reporter.prototype.reportRunnerStarting!=!function(runner)!{};
//noinspection!JSUnusedLocalSymbolsjasmine.Reporter.prototype.reportRunnerResults!=!function(runner)!{};
//...
S.O.L.I.D.
Sustitución de Liskov
• “Un objeto debe ser substituible por instancias de sus subclases”
- Si B es subclase de A- Y b es una instancia de B- Se debería poder usar b allí donde se espere un objeto de
clase A
Sustitución de Liskov
var Animal = Class.extend({ caminar: function() { /*...*/ }, comer: function() { /* .. */ }});
Sustitución de Liskov
var Animal = Class.extend({ caminar: function() { /*...*/ }, comer: function() { /* ... */ }});
var Serpiente = Animal.extend({ /* ... */});
var s = new Serpiente();s.caminar();
Sustitución de Liskov
Pretende:
• Promover la reutilización segura de código
• Mantener una semántica coherente
• Si “B es un A”, entonces “B ha de comportarse como A”
S.O.L.I.D.
Segregación de la Intefaz
• “muchas interfaces cliente específicas son mejores que una interfaz de propósito general”
- No obligues a un cliente a depender de interfaces que no necesita
- Polución de interfaz
Segregación de la intefaz
var Modal = Class.extend({ show: function() { ... }, hide: function() { ... }});
Segregación de la intefaz
var Modal = Class.extend({ show: function() { ... }, hide: function() { ... }});
var Timer = Class.extend({ setTimer: function(time) { ... }, startTimer: function() { ... }, onTimeout: function() { ... }});
Segregación de la intefaz
var Modal = Class.extend({ show: function() { ... }, hide: function() { ... }});
var Timer = Class.extend({ setTimer: function(time) { ... }, startTimer: function() { ... }, onTimeout: function() { ... }});
var TimedModal = Modal.extend({ init: function() { this.setTimer(100); this.onTimeout = this.hide; this.show(); this.startTimer(); }});
Segregación de la intefaz
var Timer = Class.extend({ setTimer: function(time) { ... }, startTimer: function() { ... }, onTimeout: function() { ... }});
var Modal = Timer.extend({ show: function() { ... }, hide: function() { ... }});
var TimedModal = Modal.extend({ init: function() { this.setTimer(100); this.onTimeout = this.hide; this.show(); this.startTimer(); }});
Segregación de la intefaz
var WidgetView = Class.extend({ init: function(cont) { var cbind = curry(bind, cont); $("#E1").click(cbind(cont.onE1Click)); }});
Segregación de la intefaz
var WidgetView = Class.extend({ init: function(cont) { var cbind = curry(bind, cont); $("#E1").click(cbind(cont.onE1Click)); $("#E2").click(cbind(cont.onE2Click)); $("#E3").click(cbind(cont.onE3Click)); $("#E4").click(cbind(cont.onE4Click)); }});
Segregación de la intefaz
var WidgetView = Class.extend({ init: function(cont) { var cbind = curry(bind, cont); $("#E1").click(cbind(cont.onE1Click)); $("#E2").click(cbind(cont.onE2Click)); $("#E3").click(cbind(cont.onE3Click)); $("#E4").click(cbind(cont.onE4Click)); $("#save").click(cbind(cont.save)); $("#reset").click(cbind(cont.reset)); $("#validate").click(cbind(cont.validate)); $("#next-page").click(cbind(cont.getNextPage)); // ... }});
Segregación de la intefaz
var WidgetView = Class.extend({ init: function(viewCont, cont, pagCont) { var vbind = curry(bind, cont), cbind = curry(bind, cont), pbind = curry(bind, pagCont); $("#E1").click(vbind(viewCont.onE1Click)); $("#E2").click(vbind(viewCont.onE2Click)); $("#E3").click(vbind(viewCont.onE3Click)); $("#E4").click(vbind(viewCont.onE4Click)); $("#save").click(cbind(cont.save)); $("#reset").click(cbind(cont.reset)); $("#validate").click(cbind(cont.validate)); $("#next-page").click(pbind(pagCont.getNextPage)); // ... }});
Segregación de la intefaz
var WidgetView = Class.extend({ init: function(viewCont, cont, pagCont) { var vbind = curry(bind, cont), cbind = curry(bind, cont), pbind = curry(bind, pagCont); $("#E1").click(vbind(viewCont.onE1Click)); $("#E2").click(vbind(viewCont.onE2Click)); $("#E3").click(vbind(viewCont.onE3Click)); $("#E4").click(vbind(viewCont.onE4Click)); $("#save").click(cbind(cont.save)); $("#reset").click(cbind(cont.reset)); $("#validate").click(cbind(cont.validate)); $("#next-page").click(pbind(pagCont.getNextPage)); // ... }});
S.O.L.I.D.
Dependency inversion
• “Depende de abstracciones. No dependas de cocreciones”
- Entidades de alto nivel no deben depender de entidades de bajo nivel. Ambos deben depender de abstracciones.
- Las abstracciones no deben depender de detalles. Los detalles deben depender de abstracciones.
Dependency Inversion
var Model = Class.extend({ save: function() { var tbind = curry(bind, this), stop = bind(this.icon, this.icon.stop); $.post(this.url, this.getData()) .success(tbind(this.saved)) .error(tbind(this.saveFailed)) .complete(stop); }});
Dependency Inversion
var Model = Class.extend({ save: function() { var tbind = curry(bind, this), stop = bind(this.icon, this.icon.stop); $.post(this.url, this.getData()) .success(tbind(this.saved)) .error(tbind(this.saveFailed)) .complete(stop); }});
Dependency Inversion
var Model = Class.extend({ init: function(store) { this.store = store; } save: function() { var tbind = curry(bind, this), stop = bind(this.icon, this.icon.stop); this.store.save( this.data, tbind(this.saved), tbind(this.saveFailed), stop ); }});
var Store = Class.extend({ save: function(data, success, error, complete) { }});
Dependency Inversion
var ServerStore = Store.extend({ save: function(data, success, error, complete) { $.post(this.url, data) .success(success) .error(error) .complete(complete); }});
var db = {};var MemStore = Store.extend({ save: function(data, success, error, complete) { db[this.url] = data; complete(); success(); }});
Dependency Inversion
Caso práctico: backbone.js
• https://github.com/documentcloud/backbone/blob/master/backbone.js
• Backbone.Model#fetch, línea 335
Dependency Inversion
!!!!fetch:!function(options)!{!!!!!!options!=!options!?!_.clone(options)!:!{};!!!!!!var!model!=!this;!!!!!!var!success!=!options.success;!!!!!!options.success!=!function(resp,!status,!xhr)!{!!!!!!!!if!(!model.set(model.parse(resp,!xhr),!options))!!!!!!!!!!return!false;!!!!!!!!if!(success)!success(model,!resp,!options);!!!!!!};!!!!!!return!this.sync('read',!this,!options);!!!!},
Patrones de organización
Patrones de organización
• Parámetros con nombre/por defecto
• Módulos y namespaces
• Control de acceso
• Mixins
Parámetros con nombre
function ajax(url, data, method, success, error, complete) { url || url = "/"; data || data = {}; method || method = "POST"; // ...}
ajax("/", {}, "GET", function(){ ... }, function() { ... }, function() { ... });
Parámetros con nombre
function ajax(options) { var url = options.url || "/", data = options.data || {}, method = options.method || "POST"; //...}
ajax({data: [1, 2], complete: function() { ... }});
Parámetros por defecto
function ajax(options) { var fn = function() {}, defaults = {url: "/", data: [], method: "POST", success: fn, error: fn, complete: fn}; options = merge(defaults, options); // ...}
ajax({data: [1, 2], complete: function() { ... }});
Parámetros por defecto
backbone.js:749
!!!!reset:!function(models,!options)!{!!!!!!for!(var!i!=!0,!l!=!this.models.length;!i!<!l;!i++)!{!!!!!!!!this._removeReference(this.models[i]);!!!!!!}!!!!!!this._reset();!!!!!!if!(models)!this.add(models,!_.extend({silent:!true},!options));!!!!!!if!(!options!||!!options.silent)!this.trigger('reset',!this,!options);!!!!!!return!this;!!!!},
Intermedio: merge
¿Cómo sería esa función merge?
Intermedio: merge
¿Cómo sería esa función merge?
function merge() { var slice = Array.prototype.slice, sources = slice.call(arguments), target = {}; sources.forEach(function(source) { for (var p in source) if (source.hasOwnProperty(p)) { target[p] = source[p]; } }); return target;}
Módulos y namespaces
• JavaScript no tiene concepto de namespace
• Todo tirado en objeto global
- Mucha polución
- Colisión de nombres
- Difícil de navegar
Módulos y namespaces
• JavaScript no tiene concepto de namespace
• Todo tirado en objeto global
- Mucha polución
- Colisión de nombres
- Difícil de navegar
•¡Pero tenemos funciones!
Módulos y namespaces
function miHelper() { // ...}
var miVariableTemporal = 0;var estadoLocal = {};
Módulos y namespaces
function sandbox() { function miHelper() { // ... }
var miVariableTemporal = 0; var estadoLocal = {}; }
Módulos y namespaces
(function sandbox() { function miHelper() { // ... }
var miVariableTemporal = 0; var estadoLocal = {}; }())
Módulos y namespaces
(function sandbox() { function miHelper() { // ... }
var miVariableTemporal = 0; var estadoLocal = {}; }())
Módulos y namespaces
function miFuncionUtil() { // ...}
function miGranMetodo() { // ...}
function miEstupendoHelper() { // ...}
Módulos y namespaces
function aux() { }var state = "off";
function miFuncionUtil() { // ...}
function miGranMetodo() { // ...}
function miEstupendoHelper() { // ...}
Módulos y namespaces
(function() { function aux() { } var state = "off";
function miFuncionUtil() { }
function miGranMetodo() { }
function miEstupendoHelper() { }}())
Módulos y namespaces
var Modulo = (function() { function aux() { } var state = "off";
function miFuncionUtil() { }
function miGranMetodo() { } return { miFuncionUtil: miFuncionUtil, miGranMetodo: miGranMetodo };}());
Módulos y namespaces
var Modulo = (function() { function aux() { } var state = "off";
function miFuncionUtil() { }
function miGranMetodo() { } return { miFuncionUtil: miFuncionUtil, miGranMetodo: miGranMetodo };}());
Módulos y namespaces
Modulo.miFuncionUtil();
Módulos y namespaces
var Modulo = {};
(function(Modulo) { function aux() { } var state = "off";
Modulo.miFuncionUtil = function() { }
Modulo.miGranMetodo = function() { }
}(Modulo));
Módulos y namespaces
var Modulo = {};
(function(Modulo) { function aux() { } var state = "off"; Modulo.miFuncionUtil = function() { }}(Modulo));
(function(Modulo) { Modulo.miGranMetodo = function() { }}(Modulo));
Módulos y namespaces
var Modulo = (function(Modulo) { function aux() { } var state = "off"; Modulo.miFuncionUtil = function() { } return Modulo;}(Modulo || {}));
var Modulo = (function(Modulo) { Modulo.miGranMetodo = function() { } return Modulo;}(Modulo || {}));
Módulos y namespaces
var Modulo = (function(Modulo) { function aux() { } var state = "off"; Modulo.miFuncionUtil = function() { } return Modulo;}(Modulo || {}));
var Modulo = (function(Modulo) { Modulo.miGranMetodo = function() { } return Modulo;}(Modulo || {}));
Módulos y namespaces
Una truco más sofisticado:
• Tenemos un módulo
• Al que añadimos propiedades en varios ficheros
• Queremos compartir cierta información entre ficheros
• Pero que no sea accesible una vez terminada la carga
Módulos y namespaces
var Modulo = (function(Modulo) { _private.password = "1234";}(Modulo || {}));
var Modulo = (function(Modulo) { Modulo.login = function(pass) { return pass == _private.password; };}(Modulo || {}));
Modulo._private; // undefined
Módulos y namespaces
var Modulo = (function (Modulo) { var _private = Modulo._private = (Modulo._private || {}), _seal = Modulo._seal = (Modulo._seal || function () { delete Modulo._private; delete Modulo._seal; });
// acceso permanente a _private y _seal return Modulo;}(Modulo || {}));
Módulos y namespaces
var Modulo = (function (Modulo) { var _private = Modulo._private; // acceso a _private
}(Modulo || {}));
Módulos y namespaces
Modulo._seal();
Intermedio: mejorar klass.js
Convierte klass.js en un módulo Class
Módulos y namespaces
Submódulos: muy fácil!
var MiLibreria = {};
MiLibreria.eventos = (function(eventos) { eventos.on = function() { }; eventos.off = function() { }; return eventos;}(MiLibreria.eventos));
Módulos y namespaces
Según crece la aplicación...
Módulos y namespaces
Según crece la aplicación...
var MiLibreria = MiLibreria || {};
Módulos y namespaces
Según crece la aplicación...
var MiLibreria = MiLibreria || {};MiLibreria.widgets = MiLibreria.widgets || {};
Módulos y namespaces
Según crece la aplicación...
var MiLibreria = MiLibreria || {};MiLibreria.widgets = MiLibreria.widgets || {};MiLibreria.widgets.buttons = MiLibreria.widgets.buttons || {};
MiLibreria.widgets.buttons.actionButtons = (function(buttons) { buttons.ok = new Widget({ ... }); buttons.cancel = new Widget({ ... });}(MiLibreria.widgets.buttons.actionButtons || {}));
Módulos y namespaces
Namespaces, pero más cómodos:
MiLib.namespace('widgets.buttons.actionButtons', function(my) { my.ok = new Widget({ ... }); my.cancel = new Widget({ ... });});
Intermedio: namespace
¿Como sería la función MiLib.namespace?
Intermedio: namespace
¿Como sería la función MiLib.namespace?
var MiLib = (function(my) { my.namespace = function(string, sandbox) { // ??? }; return my;}(MiLib || {}));
Intermedio: namespace
¿Como sería la función MiLib.namespace?
var MiLib = (function(my) { my.namespace = function(string, sandbox) { var spaces = string.split('.'), root = my, space; while (space = spaces.shift()) { root = root[space] || (root[space] = {}); } return sandbox(root); }; return my;}(MiLib || {}));
Mixins
• Otra forma de reutilizar código
• Sin las limitaciones de la herencia
• Para código de propósito general
• Algo similar a herencia múltiple
Mixins
var Mixin = function() {};Mixin.prototype = { inspect: function() { var output = []; for(key in this) { output.push(key + ': ' + this[key]); } return output.join(', '); }};
Mixins
var Persona = Class.extend({ init: function(nombre) { this.nombre = nombre; }});
augment(Persona, Mixin);
var pepito = new Persona("Pepito");pepito.inspect();
Mixins
function augment(target, source) { for (var prop in source.prototype) { if(!target.prototype[prop]) { target.prototype[prop] = source.prototype[prop]; } }}
Intermedio: Mejores Mixins
Lo podemos hacer mejor!
• Mejor integración con klass.js
• Callbacks de inclusión (estilo ruby)
Intermedio: Mejores Mixins
var StaticModule = { propiedadDeClase: "Soy una propiedad de clase", included: function(klass) { console.log("Includido!"); }};
var InstanceModule = { diHola: function() { alert("HOLA!"); }, mixed: function(klass) { console.log("Mezclado!"); }};
var MiClase = Class.extend({ init: function() { }});
MiClase.include(StaticModule);MiClase.mixin(InstanceModule);
Patrones de creación de objetos
Factoría
Delegar la creación de un objeto
• Elegir el constructor dinámicamente
• Procesos de construcción complejos
• Desacoplar detalles de implementación
Factoría
var locales = { es: {header: {title: "Mi Título"}}, en: {header: {title: "My Title"}}};
var I18n = Class.extend({ translate: function(path) { var position = locales[this.locale], path = path.split('.'),
currentPath; while (currentPath = path.shift()) { position = position[currentPath]; } return position; }});
var english = new I18n();english.locale = "en";english.translate('header.title'); // “My Title”
Factoría
var GlobalConfig = {locale: "es"};
Factoría
var GlobalConfig = {locale: "es"};
I18n.withCurrentLocale = function() { var instance = new this; instance.locale = GlobalConfig.locale; return instance;};
Factoría
var i18n = I18n.withCurrentLocale();alert(i18n.translate('header.title'));
Factoría
var Events = Class.extend({ on: function(event, cb) { }, off: function(event, cb) { }});
var IEEvents = Events.extend({ on: function(event, cb) { }, off: function(event, cb) { }});
Events.getInstance = function() { if (checkForIExplorer()) { return new IEEvents(); } else { return new Events(); }};
Factoría
Controlar las instanciasvar Enemigo = (function() { var enemigos = []; var Enemigo = Class.extend({ /*...*/ }); Enemigo.crear = function() { var instance; if (enemigos.length < 5) { instance = new Enemigo(); enemigos.push(instance); return instance; } else { throw new Error("No puede haber más!"); } } return Enemigo;}());
Factoría
var Recurso = (function() { var libres = []; var Recurso = Class.extend({ liberar: function() { libres.push(this); } }); Recurso.crear = function() { if (libres.length > 0) { return libres.pop(); } else { return new Recurso(); } } return Recurso;}());
Factoría
¿Cuándo usar factorías?
• La construcción de un objeto es compleja
• Seleccionar el constructor adecuado según entorno
• Necesitamos controlar el instanciado
Singleton
Clase con una única instancia
• Un tipo peculiar de Factoría
• Cuando no tiene sentido más de una instancia
Intermedio: I18n.js
Librería de internacionalización
var Config = {locale: 'en'};
I18n.addTranslation('en', {header: {title: "My Title"}});I18n.addTranslation('es', {header: {title: "Mi Título"}});
var i18n = I18n.withCurrentLocale();alert(i18n.translate('header.title'));
// Singleton!console.log(i18n === I18n.withCurrentLocale())
Patrones de abstracción
Patrones de abstracción
• Iteradores
• Decorador
• Fachada
• Estrategia
• Inyección de dependencias
• Proxy
Iteradores
Recorrer una colección
• Sin revelar detalles de implementación
• Mayor control sobre la iteración
Iteradores
var ListadoAlumnos = Class.extend({ init: function() { this.alumnos = []; }, add: function(nombre, ciudad) { this.alumnos.push({ nombre: nombre, ciudad: ciudad }); }});
Iteradores
var ListadoAlumnos = Class.extend({ init: function() { this.alumnos = []; }, add: function(nombre, ciudad) { this.alumnos.push({ nombre: nombre, ciudad: ciudad }); }});
var lista = new ListadoAlumnos();lista.add("Gonzalo", "Madrid");lista.add("Gerardo", "Madrid");lista.add("Guzman", "Valencia");
Iteradores
var lista = new ListadoAlumnos();lista.add("Gonzalo", "Madrid");lista.add("Gerardo", "Madrid");lista.add("Guzman", "Valencia");
var alumnos = lista.alumnos;for (var i=0; i<alumnos.length; i++) { if (alumnos[i].ciudad == "Madrid") { console.log(alumnos[i].nombre); }}
Iteradores
var lista = new ListadoAlumnos();lista.add("Gonzalo", "Madrid");lista.add("Gerardo", "Madrid");lista.add("Guzman", "Valencia");
var alumnos = lista.alumnos;for (var i=0; i<alumnos.length; i++) { if (alumnos[i].ciudad == "Madrid") { console.log(alumnos[i].nombre); }}
Iteradores
var ListadoAlumnos = Class.extend({ init: function() { this.alumnos = []; }, add: function(nombre, ciudad) { this.alumnos.push({ nombre: nombre, ciudad: ciudad }); }, forEachIn: function(ciudad, fn) { for (var i=0; i<this.alumnos.length; i++) if (this.alumnos[i].ciudad == ciudad) { fn(this.alumnos[i].nombre); } }});
Iteradores
var lista = new ListadoAlumnos();lista.add("Gonzalo", "Madrid");lista.add("Gerardo", "Madrid");lista.add("Guzman", "Valencia");
lista.forEachIn("Madrid", function(n) { console.log(n);});
Iteradores
var ListadoAlumnos = Class.extend({ init: function() { this.alumnos = []; }, add: function(nombre, ciudad) { this.alumnos.push({ nombre: nombre, ciudad: ciudad }); }, next: function() { this.index = this.index || 0; return this.alumnos[this.index++]; }});
Iteradores
var lista = new ListadoAlumnos();lista.add("Gonzalo", "Madrid");lista.add("Gerardo", "Madrid");lista.add("Guzman", "Valencia");
var alumno;while(alumno = lista.next()) { alert(alumno.nombre);}
Iteradores
var Iterator = Class.extend({ init: function(collection) { this.col = collection; this.pos = 0; }, next: function() { return this.col[this.pos++]; }});
var ListadoAlumnos = Class.extend({ init: function() { this.alumnos = []; }, add: function(nombre, ciudad) { this.alumnos.push({ nombre: nombre, ciudad: ciudad }); }, getIterator: function() { return new Iterator(this.alumnos); }});
Iteradores
var lista = new ListadoAlumnos();lista.add("Gonzalo", "Madrid");lista.add("Gerardo", "Madrid");lista.add("Guzman", "Valencia");
var alumno, iter = lista.getIterator();while(alumno = iter.next()) { if (alumno.ciudad == "Madrid") { console.log(alumno.nombre); }}
Iteradores
var Fibonacci = Class.extend({ init: function() { this.a = 0; this.b = 1; }, next: function() { var next = this.a; this.a = this.b; this.b = this.b + next; return next; }});
var iter = new Fibonacci();for (var i=10; i--;) { console.log(iter.next());}
Decorador
Añadir funcionalidad dinámicamente
• Evitar subclases innecesarias
• Modificar comportamientos “en vivo”
• Manteniendo la interfaz/contrato! (transparente)
• Muy apropiada para JS
Decorador
var Producto = Class.extend({ init: function(precio) { this.precio = precio; }, getPrecio: function() { return this.precio; }});
Decorador
var Producto = Class.extend({ init: function(precio) { this.precio = precio; }, getPrecio: function() { return this.precio; }});
var ProductoConIVA = Producto.extend({ init: function(precio) { this._super(precio); }, getPrecio: function() { return this._super() * 1.21; }});
Decorador
var ProductoConDescuento = Producto.extend({ init: function(precio) { this._super(precio); }, getPrecio: function() { return this._super() * 0.90; }});
var ProductoConIVAConDescuento = ProductoConIVA.extend({ init: function(precio) { this._super(precio); }, getPrecio: function() { return this._super() * 0.90; }});
Decorador
var ProductoTecnologico = Producto.extend({ });
var ProductoTecnologicoConDescuento = ProductoConDescuento.extend({});
var ProductoTecnologicoConIVA = ProductoConIVA.extend({});
var ProductoTecnologicoConIVAConDescuento = ProductoConIVAConDescuento.extend({});
Decorador
var Producto = Class.extend({ init: function(precio) { this.precio = precio; }, getPrecio: function() { return this.precio; }});
Decorador
var Producto = Class.extend({ init: function(precio) { this.precio = precio; }, getPrecio: function() { return this.precio; }});
var ProductoConIVA = Class.extend({ init: function(producto) { this.producto = producto; }, getPrecio: function() { return this.producto.getPrecio() * 1.21; }});
Decorador
var ProductoConDescuento = Class.extend({ init: function(producto) { this.producto = producto; }, getPrecio: function() { return this.producto.getPrecio() * 0.90; }});
var ProductoTecnologico = Class.extend({ init: function(producto) { this.producto = producto; }, getPrecio: function() { return this.producto.getPrecio() * 0.90; }});
Decorador
var producto = new Producto(20);producto = new ProductoConIVA(producto);producto = new ProductoConDescuento(producto);producto = new ProductoTecnologico(producto);producto.getPrecio(); // 19.602
Decorador
var producto = new Producto(20);producto = new ProductoConIVA(producto);producto = new ProductoConDescuento(producto);producto = new ProductoTecnologico(producto);producto.getPrecio(); // 19.602
ProductoTecnológico
ProductoConDescuentoProductoConIVA
ProductogetPrecio()
getPrecio()
getPrecio()
getPrecio()
Decorador
var producto = new Producto(20);producto = new ProductoConIVA(producto);producto = new ProductoConDescuento(producto);producto = new ProductoTecnologico(producto);producto.getPrecio(); // 19.602
ProductoTecnológico
ProductoConDescuentoProductoConIVA
Producto
(20 * 1.21) * 0.90
20 * 1.21
20
((20 * 1.21) * 0.90) * 0.90
19.602
Decorador
Cuando usar decoradores:
• Modificar el comportamiento manteniendo interfaz
• Añadir post- o preproceso a la salida/entrada de un obj
- Serialización
- Interfaz
• Dentro de factorías
• Intervenir el llamadas a métodos
Fachada
Simplificar el interfaz y desacoplar al cliente
• Patrón muy simple
• El mejor amigo del programador JavaScript
• Casi todas las librerías son fachadas
• Comodidad
Fachada
Ejemplo por excelencia: jQuery
• $(“selector”)• $(“selector”).click(...)• $.ajax• ...
Fachada
Nuestra librería klass.js
• Una capa sobre los mecanismos nativos
• Simplifica el “interfaz”
• Mucho más cómodo que hacerlo a mano
Fachada
var DOMNode = Class.extend({ init: function(tag) { this.el = document.createElement(tag); }, setClass: function(klass) { this.el.className += (" " + klass); }, setId: function(id) { this.el.id = id; }, getElement: function() { return this.el; }});
var node = new DOMNode('div');node.setClass("big");node.setClass("red");node.setId("my-button");document.body.appendChild(node.getElement());
Fachada
DOMNode.create = function(tag, classes, id) { var node = new this(tag); node.setClass(classes.join(" ")); node.setId(id); return node.getElement();}var el = DOMNode.create("div", ["big", "red"], "my-button");document.body.appendChild(el);
Fachada
¿Cuándo usar fachadas?
• Clases muy potentes y complejas
- Simplificar los usos más comunes
• Operaciones con mucha preparación
- XMLHttpRequest
• Código repetitivo
• Para cambios grandes del código
Estrategia
Seleccionar algoritmos dinámicamente
• Dada una familia de algoritmos
• Encapsulados bajo un mismo interfaz
• Elegir el más apropiado
Estrategia
var validate = (function() { var validators = { email: function(value) { return /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value); }, number: function(value) { return /^\d+$/.test(value); } }; return function(data) { var validation = validators[data.type]; return validation && validation(data.value); };}());
validate({type: "email", value: "[email protected]"});
Estrategia
var MoodConsole = Class.extend({ init: function() { this.state = "normal"; this.filters = { normal: new DummyFilter(), relajado: new RelaxedFilter(), cabreado: new AngryFilter() }; }, log: function(msg) { msg = this.filters[this.state].filter(msg); console.log(msg); }, molestar: function() { this.state = "cabreado"; }, masajear: function() { this.state = "relajado"; }, dejarEnPaz: function() { this.state = "normal"; }});
Estrategia
var DummyFilter = Class.extend({ filter: function(s) { return s; }});
var AngryFilter = DummyFilter.extend({ filter: function(s) { return s.toUpperCase() + "!!"; }});
var RelaxedFilter = DummyFilter.extend({ filter: function(s) { return "..." + s + "..."; }});
var moodConsole = new MoodConsole();moodConsole.log("hola!");moodConsole.molestar();moodConsole.log("grrr...");moodConsole.masajear();moodConsole.log("gracias");
Estrategia
¿Cuándo usar estrategias?
• Una misma acción puede tener varios significados
- Según un estado variable (selector de contexto)
- click, touch, una botón “Guardar”, etc..
• Validaciones
• En general: cuando hay que elegir un algoritmo diferente para cada caso
Inyección de dependencias
Selección dinámica de un componente
• Según el principio de Inversión de Dependencias
• El cliente consume siguiendo un interfaz
• La clase concreta se selecciona en tiempo de ejecución
Inyección de dependencias
var View = Class.extend({ init: function(renderer, model) { this.renderer = renderer; this.model = model; }, render: function() { this.renderer.render(this.template, this.model); }});
var HtmlRenderer = Class.extend({ render: function() { ... }});
var XMLRenderer = Class.extend({ render: function() { ... }});
Inyección de dependencias
var miVista = new View(new XMLRenderer('fast'), {});
Proxy
Un objeto hace de interfaz de otro objeto o recurso
• Entre el cliente y el servicio
• Controlar y optimizar el acceso
• El Proxy implementa exactamente el mismo interfaz
- No es un decorador (no añade funcionalidad)
- No es una fachada (no simplifica)
Proxy
Tres tipos de proxy:
• Virtual Proxy
- Sustituye al objeto real mientras carga
- Optimización
• Protection Proxy
- Controla el acceso
- Seguridad
• Remote Proxy
- Reflejo de un recurso remoto
- Abstracción
Virtual Proxy
var Recurso = Class.extend({ init: function() { operacionMuyCostosa(); }, read: function() { ... }, write: function() { ... }});
Virtual Proxy
var Recurso = Class.extend({ init: function() { operacionMuyCostosa(); }, read: function() { ... }, write: function() { ... }});
var RecursoProxy = Recurso.extend({ init: function() { this.super_init = this._super; }, read: function() { this.super_init(); this._super(); }, write: function() { this.super_init(); this._super(); }});
Virtual Proxy
var Recurso = Class.extend({ init: function() { operacionMuyCostosa(); if (this.onLoad) this.onLoad(); }, write: function() { ... }});
var RecursoProxy = Recurso.extend({ init: function() { this.pending = []; this.loaded = false; this._super(); }, write: function() { if (loaded) { this._super(); } else { pending.push({op: 'write', params: arguments}); } }});
Remote Proxy
var User = Class.extend({ init: function() { /*...*/ }, getName: function() { return this.name; }});
User.find = function(id, cb) { $.get('/user/' + id, function(data) { var user = new User(data); cb(user); });};
User.find(12, function(user) { console.log(user.getName());});
Proxy
¿Cuándo usar un Proxy?
• Inicialización perezosa
• “Sustituto” que anote mientras el sujeto real arranca
• Cuando el sujeto real es remoto
• Cache!
Patrones de interacción
Patrones de interacción
• Pub/Sub u Observador
• Mediator
• Comandos y Cadena de Responsabilidad
• “Hydra”
Observador
En vez de preguntar, escucha lo que sucede
• Muy común en JavaScript (eventos)
• Reaccionar ante cambios de estado o sucesos
• Dos entidades
- Publicador
- Suscriptor(es)
Observador
var Input = Class.extend({ getValue: function() { return this.value; }});
var InputView = Class.extend({ init: function(input) { this.input = input; this.value = this.input.getValue(); setInterval(bind(this, this.onTimer), 100); }, onTimer: function() { var newValue = this.input.getValue(); if (newValue !== this.value) { this.value = newValue; console.log("CHANGED: " + this.value); } }});
Observador
InputInputViewcambios?
no..
InputInputViewcambios?
no..
InputInputViewcambios?
no..
InputInputViewcambios?
no..
0ms
100ms
200ms
300ms
Observador
InputInputViewavísame!
InputInputViewcambios!
0ms
T ms
Observador
var Input = Class.extend({ init: function() { this.observers = []; }, getValue: function() { return this.value; }, setValue: function(v) { this.value = v; this.publish(v); }, onChange: function(cb) { this.observers.push(cb); }, publish: function(cb) { this.observers.forEach(function(o) { o(cb); }); }});
Observador
var Input = Class.extend({ init: function() { this.observers = []; }, getValue: function() { return this.value; }, setValue: function(v) { this.value = v; this.publish(v); }, onChange: function(cb) { this.observers.push(cb); }, publish: function(cb) { this.observers.forEach(function(o) { o(cb); }); }});
Observador
var InputView = Class.extend({ init: function(input) { this.input = input; this.input.onChange(bind(this, this.update)); }, update: function(newValue) { console.log("CHANGED: " + newValue); }});
Observador
var input = new Input();
var observer1 = new InputView(input);var observer2 = new InputView(input);var observer3 = new InputView(input);
input.setValue("No te duermas!");
Intermedio: Observable
¡Podemos hacerlo mejor!
Input.mixin(Observable);
var InputView = Class.extend({ init: function(input) { this.input.subscribe('change', bind(this, this.update)); this.input.subscribe('invalid', bind(this, this.invalid)); }, update: function(newValue) { console.log("CHANGED: " + newValue); }, invalid: function(value) { console.log("El valor " + value + " es inválido!" ); }});
Intermedio: Observable
var Observable = { mixed: function(klass) { // ??? }, subscribe: function(event, callback) { // ??? }, unsubscribe: function(event, callback) { // ??? }, publish: function(event) { // ??? }};
Observador
¿Cuándo utilizar Observador?
• Se utiliza muy a menudo!
• Propagar cambios
• Reaccionar a sucesos
• Comunicación uno-a-muchos
Mediator
Un punto central de control del sistema
• Desacoplamiento a gran escala
• Sencillo y muy importante
• Dividir la lógica en dos niveles
- Componente
- Aplicación
Mediator
Mediator
x
Mediator
x
Aceptar
Mediator
x
Aceptar
Mediator
componente
componente
componente
componente
componente
Mediator
componente
componente componente
componente componente
mediador
Mediator
componente
componente componente
componente componente
mediador
Mediator
var Mediator = Class.extend({ init: function(fn) { this.connections = {}; if (fn) fn(this); }, on: function(msg, cb) { this.connections[msg] = cb; }, send: function(msg) { var args = [].slice.call(arguments, 1), action = this.connections[msg]; if (action) action.apply({}, args); }, addComponent: function(component) { component.setMediator(this); }});
Intermedio: Mediator
• Mediador en acción:
- tema2/mediador-1/index.html
Intermedio: Mediator
// Inicialización
$(function() {
var mediator = new Mediator(), button1 = new Button('pulsador', '#button-1'); mediator.addComponent(button1); mediator.on('pulsador:clicked', function() { alert("hola?"); }); });
Intermedio: Mediator
// Inicialización
$(function() {
var mediator = new Mediator(function(mediator) { var button1 = new Button('pulsador', '#button-1'); mediator.addComponent(button1); mediator.on('pulsador:clicked', function() { alert("hola?"); }); }); });
Intermedio: Mediator
• Mediador en acción:
- tema2/mediador-2/index.html
Intermedio: Mediator
mediator.on('ignore:on', function() { enabled = false;});
mediator.on('ignore:off', function() { enabled = true;});
mediator.on('inc:clicked', function() { if (enabled) { count.increment(); } display.update(count.getCurrentValue());});
mediator.on('dec:clicked', function() { if (enabled) { count.decrement(); } display.update(count.getCurrentValue());});
Intermedio: Mediator
mediator.on('ignore:on', function() { enabled = false;});
mediator.on('ignore:off', function() { enabled = true;});
mediator.on('inc:clicked', function() { if (enabled) { count.increment(); } display.update(count.getCurrentValue());});
mediator.on('dec:clicked', function() { if (enabled) { count.decrement(); } display.update(count.getCurrentValue());});
Mediator
¿Cuándo usar un Mediador?
• Muchos componentes interactuando
• La interacción se puede separar del componente
• Es decir: coordinar diferentes widgets de la aplicación
Mediator
¿Cuándo usar un Mediador?
• Muchos componentes interactuando
• La interacción se puede separar del componente
• Es decir: coordinar diferentes widgets de la aplicación
¡Cuidado!
๏ El mediador tiende a convertirse en un cajón de sastre
๏ Una Gran Función que Todo lo Hace!
Comandos
Separar la preparación de una acción y su ejecución
• Flujos de ejecución no convencionales
• Tres actores: cliente, invocador, receptor
- cliente: prepara o configura una acción
- invocador: controla la ejecución
- receptor: lleva a cabo la acción
• Generalizar comportamientos
Comandos
tema2/comandos-1/index.html
Comandos
var Command = Class.extend({ init: function(msg) { this.msg = msg; }, execute: function() { alert(this.msg); }});
var ActionList = Class.extend({ init: function(selector) { this.ul = $(selector); }, append: function(name, command) { var li = $("<li>") .append($('<a/>', {html:name, href:'#'})) .click(bind(command, command.execute)) .appendTo(this.ul); }});
var list = new ActionList('#menu');list.append('saludo', new Command("Hola!"));list.append('achis!', new Command("Salud!"));
Comandos
var Command = Class.extend({ init: function(msg) { this.msg = msg; }, execute: function() { alert(this.msg); }});
var ActionList = Class.extend({ init: function(selector) { this.ul = $(selector); }, append: function(name, command) { var li = $("<li>") .append($('<a/>', {html:name, href:'#'})) .click(bind(command, command.execute)) .appendTo(this.ul); }});
var list = new ActionList('#menu');list.append('saludo', new Command("Hola!"));list.append('achis!', new Command("Salud!"));
Comandos
var AlertCommand = Class.extend({ init: function(msg) { this.msg = msg; }, execute: function() { alert(this.msg); }});
var LogCommand = Class.extend({ init: function(msg) { this.msg = msg; }, execute: function() { console.log(this.msg); }});
Comandos
Encapsular las acciones y controlar su ejecución
• Una idea muy potente
• Deshacer! (estados reversibles)
• Historial
• Control de cambios
• Sincronización de estados
Comandos
Un ejemplo sencillo de estado reversible:
• tema2/comandos-2/index.html
var Movement = Class.extend({ init: function(amount, axis) { this.amount = amount; this.axis = axis; }, execute: function(mosca) { mosca.move(this.amount, this.axis); }, undo: function(mosca) { mosca.move(-this.amount, this.axis); }});
Comandos
¿Cuándo usar Comandos?
• Controlar la ejecución de acciones
• Separar la definición de la acción y su ejecución
• Es decir:
- Intervienen actores que pueden fallar (servidor, usuario)
- Menús, selectores y otros “contenedores de acciones”
- Sincronizar cambios simultáneos (control de versiones)
Cadena de Responsabilidad
Cada eslabón decide actuar o delegar en el próximo
• Elegir dinámicamente quien responde a una petición
• Dividir una operación compleja en varios pasos
• Desacoplar la complejidad del cliente
Cadena de Responsabilidad
var AlmacenVehiculos = Class.extend({ init: function(tipo) { this.tipo = tipo; this.vehiculos = []; }, guardar: function(vehiculo) { if (vehiculo.getTipo() == this.tipo) { this.vehiculos.push(vehiculo); } }});
var hangar = new AlmacenVehiculos('avion'), parking = new AlmacenVehiculos('coche'), puerto = new AlmacenVehiculos('barco');
Cadena de Responsabilidad
var Vehiculo = Class.extend({ init: function(tipo) { this.tipo = tipo; }, getTipo: function() { return this.tipo; }, guardar: function() { hangar.guardar(this); puerto.guardar(this); parking.guardar(this); }});
Cadena de Responsabilidad
var AlmacenVehiculos = Class.extend({ init: function(tipo, siguiente) { this.tipo = tipo; this.vehiculos = []; this.siguiente = siguiente; }, guardar: function(vehiculo) { if (vehiculo.getTipo() == this.tipo) { this.vehiculos.push(vehiculo); } else if (siguiente.next) { this.siguiente.guardar(vehiculo); } }});
Cadena de Responsabilidad
var hangar = new AlmacenVehiculos('avion'), parking = new AlmacenVehiculos('coche', hangar), puerto = new AlmacenVehiculos('barco', parking), almacenes = new AlmacenVehiculos('', puerto);
var Vehiculo = Class.extend({ init: function(tipo) { this.tipo = tipo; }, getTipo: function() { return this.tipo; }, guardar: function() { almacenes.guardar(this); }});
Cadena de Responsabilidad
¿Cuándo usar Cadenas de Reponsabilidad?
• Jerarquías de objetos
- Los eventos del navegador
• Diferentes fuentes para responder a una petición
- Ej: buscar por nombre, por fecha, por ciudad, ...
• El input puede afectar a varias entidades
- Ej: Colisiones
Hydra
Árbol de comportamientos
• No existe
• Organización al más alto nivel
• Mediador + Comandos + Cadena de Responsabilidad
• Extremadamente escalable!
• Para aplicaciones muy grandes
Hydra
Librería DOM
Componente
Mediador Global
Componente Componente
Hydra
Librería DOM
Mediador Global
Mediador Local
Componente
Componente
Componente
Mediador Local
Componente
Componente
Componente
Mediador Local
Componente
Componente
Componente
Hydra
Librería DOM
Mediador Global
Mediador Local
Componente
Ajax + DOM
Coordinación general
Coordinación local
Lógica del componente
Hydra
Librería DOM
Mediador Global
Mediador Local
Componente
Ajax + DOM
Coordinación general
Coordinación local
Lógica del componente
click en “Guardar”
Hydra
Librería DOM
Mediador Global
Mediador Local
Componente
Ajax + DOM
Coordinación general
Coordinación local
Lógica del componente
click en “Guardar”
Dame los datosdel formulario
Componente
Hydra
Librería DOM
Mediador Global
Mediador Local
Componente
Ajax + DOM
Coordinación general
Coordinación local
Lógica del componente
click en “Guardar”
Guardar nuevosdatos de usuario
Hydra
Librería DOM
Mediador Global
Mediador Local
Componente
Ajax + DOM
Coordinación general
Coordinación local
Lógica del componente
click en “Guardar”
Guardar nuevosdatos de usuario
Actualiza lalista decontactos
Mediador Local
Hydra
Librería DOM
Mediador Global
Mediador Local
Componente
Ajax + DOM
Coordinación general
Coordinación local
Lógica del componente
click en “Guardar”
Guardar nuevosdatos de usuario
Haz un POSTa /users
Hydra
Separación por capas de significado
• Cada mediador gestiona lo que hay a su nivel
• El significado de las acciones va cobrando sentido
• La lógica de los componentes se limita a hablar con su mediador
• La coordinación es “recursiva”
Hydra
¿Cuándo utilizar Hydra?
• Aplicaciones grandes!
• Widgets complicados que se beneficien de mediación
• En general, en cuanto un mediador engorde demasiado
Hydra
¿Cuándo utilizar Hydra?
• Aplicaciones grandes!
• Widgets complicados que se beneficien de mediación
• En general, en cuanto un mediador engorde demasiado
¡Cuidado!
๏ El alto desacoplamiento hace complicado leer el código
๏ El significado TOTAL de cada acción está diseminado
MVC: Backbone.js
made with love by Redradix (www.redradix.com)
El problema: acoplamiento
Las aplicaciones JS tienden a ser un caos
• La lógica del interfaz se mezcla con
• La lógica de control y validación de datos y con
• La lógica de comunicación con el servidor y con
• La lógica de reacción a eventos!
El problema: acoplamiento
El peor tipo de código espagueti!
$.ajax({ url: 'events/since', data: { timestamp: old_id }, dataType: 'json', type: 'GET', global: false, cache: false, success: function(newEvents) { if (newEvents.events && newEvents.events.length != 0) { if (prepend_events) { if (page <= '1') { if ($('div.new_events').length == 0) { $('#events').prepend('<div class="note new_events"><strong class="new_event_count">'+newEvents.events.length+'</strong> New Events Are Available Click here To View Them.</div>');
MVC
En los 70 se propuso una solución:
• Separar el código en 3 componentes:
- Modelo: datos y lógica de negocio
- Vista: presentación de los datos
- Controlador: gestión de interacciones
• Limitar su comunicación
• Muy popular desde hace mucho tiempo!
- Aplicaciones de escritorio
- Aplicaciones móviles
- Una interpretación peculiar en los frameworks web
MVC
Se ha extendido hace “poco” por JS
• Backbone.js
• Spine.js
• Batman.js
• Ember.js
• ...
MVC
M
V C
Usuario
Ve Usa
ManipulaActualiza
MVC
Vamos a aplicar el patrón MVC
• Entender su filosofía
• Comprender los mecanismos fundamentales
• Apreciar sus ventajas
• Ver sus limitaciones
• Estudiar diferentes soluciones
MVC
JS con todas nuestras utilidades:
• tema4/lib/prelude.js
- Class con herencia de propiedades de clase
- bind, curry, clone, merge
- Namespaces
- Observable
- Algunas utilidades extra
Modelo
Modelo
El Modelo se encarga de manejar los datos
• Representa una entidad
• Mecanismos de lectura y modificación
• Operaciones con los datos
• Validación
• Serialización
Modelo
Representar una entidad
var Usuario = Backbone.Model.extend({ /* ... */})
Modelo
Un modelo, en esencia, un conjunto de atributos
var u = new Usuario();
/* Crear o modificar un atributo */u.set({nombre: "Pepito"});
/* Leer el valor de un atributo */var nombre = u.get("nombre");console.log(nombre);
Modelo
Valores por defecto para los atributos: defaults
var Usuario = Backbone.Model.extend({ defaults: { nombre: "Anónimo", nacionalidad: "Español" }});
var u = new Usuario();console.log(u.get("nombre"));
u.set({nombre: "Pepito"});console.log(u.get("nombre"));
Modelo
Un modelo es un objeto, ¡Pero ten cuidado!
var Usuario = Backbone.Model.extend({ defaults: { nombre: "Anónimo", nacionalidad: "Español" }, saludar: function() { return "Hola, soy " + this.nombre; }});
var u = new Usuario();u.set({nombre: "Pepito"});console.log(u.saludar()); // "Hola, soy undefined"
Modelo
Los atributos del modelo se consultan con get/set
var Usuario = Backbone.Model.extend({ defaults: { nombre: "Anónimo", nacionalidad: "Español" }, saludar: function() { return "Hola, soy " + this.get("nombre"); }});
var u = new Usuario();u.set({nombre: "Pepito"});console.log(u.saludar()); // "Hola, soy Pepito"
Modelo
Serializar los atributos: .toJSON()
var Usuario = Backbone.Model.extend({ defaults: { nombre: "Anónimo", nacionalidad: "Español" }, saludar: function() { return "Hola, soy " + this.get("nombre"); }});
var u = new Usuario();u.set({nombre: "Pepito"});
var attrs = u.toJSON();console.log(attrs);
Modelo
Validez: método .validate(attrs)• El método no viene definido por defecto
• Nosotros lo creamos para validar nuestro modelo
• Se llama automáticamente desde save
• Se puede forzar al hacer .set()
Modelo
Validez: método .validate(attrs)• Si los valores de attrs no son válidos:
- Devuelve una descripción del error
- Puede ser cualquier cosa...
• Si son válidos:
- No se devuelve nada
Modelo
var Usuario = Backbone.Model.extend({ defaults: { nombre: "Anónimo", nacionalidad: "Español" }, validate: function (attrs) { if (/^\s*$/.test(attrs["nombre"])) { return "El nombre no puede quedar en blanco!"; } }});
var u = new Usuario();
u.set({nombre: "Pepito"}, {validate: true});console.log(u.get("nombre"));
u.set({nombre: " "}, {validate: true});console.log(u.get("nombre")); // "Pepito"
Modelo
Backbone.js es una librería agnóstica en muchos sentidos
• No impone templates
• No impone estructura
• No impone almacenamiento
Modelo
Backbone.js es una librería agnóstica en muchos sentidos
• No impone templates
• No impone estructura
• No impone almacenamiento
Excepto...
• Impone su propio sistema de herencia y clases
• Muy simple
• Muy limitado
Modelo
var Usuario = Backbone.Model.extend({ initialize: function () { // Constructor }});
var Ninja = Usuario.extend({ initialize: function () { Usuario.prototype.initialize.apply(this, arguments); // Subclase }});
Modelo
Parecida a la nuestra, pero:
• No se pueden crear objetos que no hereden de una clase base de Backbone (Model, View, Collection o Router)
• La manera de llamar al súper método es incómoda y viola el principio DRY
Modelo
Por suerte...
• Javascript es tremendamente flexible
• El modelo de Backbone es muy simple
• klass.js la hemos escrito nosotros y entendemos cómo funciona
Modelo
Podemos integrar ambas librerías fácilmente!
• “Envolviendo” las clases base de Backbone en clases de klass.js
• Así tenemos lo mejor de las dos
klass.js <-> backbone.js
var ProJS = (function (my) { var wrapBackboneClass = function(className) { var backboneWrapped = Backbone[className], F = function() {}, K = function() {}; F.prototype = backboneWrapped.prototype; K.prototype = new F(); _.extend(K, ProJS.Class, {constructor: backboneWrapped}); _.extend(K.prototype, ProJS.Class.prototype); K.prototype.init = function() { return backboneWrapped.apply(this, arguments); }; return K; };
my.Model = wrapBackboneClass('Model'); my.View = wrapBackboneClass('View'); my.Collection = wrapBackboneClass('Collection');
return my;}(ProJS || {}));
klass.js <-> backbone.js
Ahora podemos hacer:
var Usuario = ProJS.Model.extend({ init: function() { // constructor this._super(); }, defaults: { // etc... }});
Modelo
Ejercicio (uno fácil para empezar):
tema4/model-1/index.html
• Escribe un modelo Producto
- Id (incremento automático)
- Nombre (obligatorio)
- Categoría (obligatorio)
- País (España, Portugal o Francia)
- Precio (obligatorio, sin IVA)
• Escribe un decorador para obtener precios con IVA
Modelo
Fíjate en...
• Lo fácil que resulta sobreescribir la funcionalidad de un método con klass.js
• Si no tuviéramos klass.js, ¿Cómo habríamos hecho el decorador? ¿De quién tendría que haber heredado?
• Puedes pasar varios atributos a la vez a .set()
• Puedes pasar los atributos directamente al constructor
• Es una base bastante sólida para controlar los datos de nuestra aplicación!
Modelo
Los modelos generan eventos
• Cuando alguno de sus atributos cambia: change
• Cuando un atributo en concreto cambia: change:[attr]
• Cuando las validaciones fallan: invalid
• Al ser destruidos: destroy
• Al ser sincronizado con el servidor: sync
• Si surge algún error al guardar: error
Modelo
var p1 = new Producto();
p1.on("change", function (model, options) { console.log("El producto", model.get("nombre"), "ha cambiado!");});
p1.set({ nombre: "Jamón", categoria: "Comida", pais: "España", precio: 65});
Modelo
Ejercicio:
• Modifica el ejercicio anterior de modo que:
• Cuando una validación falla, se informe al usuario del error por la consola
Modelo
Otras operaciones útiles:
.unset(): elimina un atributo
.clear(): elimina todos los atributos
.previous(attr): durante un evento change, devuelve el valor anterior de un atributo
.has(attr): ¿Tiene el modelo el atributo attr?
.escape(attr): como .get(), pero escapando el HTML
Modelo
Persistencia
• La “gracia” de Backbone es que sabe como hablar con el servidor
• Pedir y guardar modelos automáticamente por AJAX
• Muy flexible
• Soporta también localStorage
Modelo
Para hablar con el servidor, el modelo necesita:
• urlRoot: la base con la que construir su URL
• id: el identificador del recurso
• parse (opcional): una función que interprete la respuesta del servidor
Modelo
Las operaciones fundamentales de persistencia:
.fetch()1. dado un id, construye una url del tipo [baseURL]/[id]
2. GET al servidor
3. pasa la respuesta a .parse()4. con el resultado llama a .set() para establecer los atributos
del modelo
Modelo
Ejercicio
tema4/model-2/index.html
Con una api AJAX tal que:
GET /products -> lista de {nombre: “”, id: #}
GET /products/:id -> detalles del producto :id
PUT /products/:id -> guarda cambios del producto :id
Haz:
- Construye un array con un modelo por cada producto del listado
- Modifica algún producto y guarda los cambios
- Escucha los eventos “change”y “sync” e informa por consola
Colecciones
Colección: conjunto ordenado de modelos
• Se identifica con un listado de recursos
- GET a URLs de tipo “índice” (/users, /products, etc...)
• Los modelos de una colección son del mismo tipo
Colecciones
• Objetivo similar al del un modelo, pero con conjuntos
- Manejar colecciones de datos
- Ordenar, añadir, eliminar entidades a la colección
- Consultar y guardar la colección en el servidor
- Serializar el conjunto de datos
Colecciones
var ListadoProductos = ProJS.Collection.extend({ model: Producto});
var listado = new ListadoProductos();
Colecciones
Operaciones fundamentales:
- add(model, {at: i}): añadir un modelo a la colección
- remove(model|id|cid): eleminar un modelo
- get(id|cid): acceder a un objeto de la colección por id
- at(idx): acceder a un objeto de la colección por índice
- length: número de elementos en la colección
- where(attrs): query de atributos
- push/pop, shift/unshift
Colecciones
var listado = new ListadoProductos();
listado.add(new Producto({nombre: "Uno"}));listado.add(new Producto({nombre: "Dos"}));listado.add(new Producto({nombre: "Tres"}));
console.log(listado.at(2).toJSON());
Colecciones
Para identificar a los modelos dentro de una colección, podemos usar:
• id- Generalmente otorgado por el servidor
- Se utiliza para construir la URL del modelo
- Universal dentro de la app
- Se corresponde, habitualmente, con el ID de la tabla en BBDD
• cid- Generado automáticamente por Backbone
- Válido solo dentro de la página
- Modelos no guardados o que no tienen que ver con BBDD
Colecciones
console.log(new Producto().cid); // c1console.log(new Producto().cid); // c2console.log(new Producto().cid); // c3
Colecciones
var p = new Producto({nombre: "Zapatos"}), cid = p.cid;
console.log(listado.get(cid)); // undefined
listado.add(p);console.log(listado.get(cid)); // p
var resultado = listado.where({nombre: "Zapatos"});console.log(resultado.toJSON()); // p
Colecciones
Una colección sabe como interpretar datos “crudos” (instancia automáticamente un modelo)
listado.add({ nombre: "Corbata", categoría: "Caballero", precio: 40});
listado.at(0).constructor === Producto; // true
Colecciones
Cargar datos iniciales en una colección: reset
var listado = new ListadoProductos();
listado.reset([ {nombre: "Uno", categoria: "A", precio: 2}, {nombre: "Dos", categoria: "B", precio: 1}, {nombre: "Tres", categoria: "C", precio: 8},]);
Colecciones
Las 28 funciones de underscore para manipular listas se pueden aplicar a colecciones
mapreducefindfiltermax/minsortshuffleetc...
Colecciones
Persistencia:
url: dirección para pedir la colección
- Los modelos de la colección usarán esta URL como urlRoot para construir sus URLs individuales
fetch: pide la colección al servidor
- GET a url, esperando un array de hashes (atributos)
set: refresca los datos de la colección
- Aproximadamente igual que hacer un fetch y mergear
Colecciones
Ejercicio:
tema4/collection-1/index.html
Crea una colección de Productos que lea de /products
Y tenga los métodos:
listado(): todos los productos
ordenaPorNombre(): filtro
precioMenorQue(p): filtro
borrarProducto(id): lo elimina de la BBDD
nuevoProducto(attrs): añade el producto a la colección y lo guarda en BBDD
Colecciones
Las colecciones también emiten eventos:
add(model, col): se ha añadido un nuevo modelo
remove(model, col): se ha eliminado un modelo
sort(col): se ha reordenado
Vista
Es una representación del modelo
• Asociada a una instancia de un modelo
• Generalmente utiliza un template
• Actualización automática
Vista
Templates
• “Plantillas” que mezclan HTML y código JS
• Backbone funciona con cualquier librería de templates
• Trae una preinstalada: _.template()
Vista
_.template(texto, datos)• texto: el texto de nuestra plantilla
• datos: un objeto con las variables que queramos utilizar al evaluar el template
Vista
En el texto de la plantilla:
<%= expresión %>Se sustituye por el resultado de evaluar la expresión
<% código %>Ejecuta el código javascript
Vista
var plantilla = "<h1> Hola! </h1>";console.log( _.template(plantilla, {}));
Vista
var plantilla = " <h1> \ <% console.log('Hola!') %> \ </h1>";
console.log( _.template(plantilla, {}));
Vista
var plantilla = " <h1> \ <%= 10 + 10 %> \ </h1>";
console.log( _.template(plantilla, {}));
Vista
var plantilla = " <h1> \ Bienvenido, <%= nombre %> \ </h1>";
console.log( _.template(plantilla, {nombre: "Ulises"}));
Vista
Anatomía de una vista
• Un modelo del que se extraen los datos
• Un template que se rendea con los datos del modelo
• Un nodo del DOM donde se inserta el template rendeado
• Un método .render() que se encarga de ejecutar este proceso
Vista
el Vista Modeloescuchaactualiza
Vista
En Backbone:
.model: el modelo asociado a la vista
.tagName: la etiqueta para generar el nodo del DOM
.attributes: los atributos para generar el nodo
.el: el nodo, ya creado
.$el: el nodo, envuelto con jQuery (o Zepto)
.$: un atajo a jquery pero con el scope fijado en .el
.render: el método que se encarga de rendear el template y actualizar el contenido de .el
Vista
var MiVista = ProJS.View.extend({ init: function(options) { this._super(options); }, tagName: "div", attributes: {"class": "box large"}, template: "<h1> <%= nombre %> </h1>", render: function() { var data = this.model.toJSON(); this.$el.html( _.template(this.template, data) ); return this; }});
var producto = new Producto({nombre: "Vino"}), miVista = new MiVista({model: producto}).render();
$("body").append(miVista.el);
Vista
Ejercicio: una vista sencilla
tema4/view-1/index.html
Crea una vista que utilice el template #producto-template para mostrar una instancia de Producto
Vista
Ejercicio: vistas y colecciones
tema4/view-2/index.html
Crea una colección que se inicialice con los datos de la ruta /products
Para cada uno de los modelos:
Instancia una vista VistaListado
Muéstrala por pantalla
Una pista: ¡las colecciones emiten eventos!
Vista
La vista debería “escuchar” al modelo
Cambios automáticos cuando el modelo cambia
El patrón habitual:
var VistaListado = ProJS.View.extend({ init: function (options) { this._super(options); this.model.on("change", bind(this, this.render)); }, /* ... */});
Y ahora, ¿qué?
Tenemos Modelo y Vista
• Todo el mundo está de acuerdo en estos dos puntos
• Datos + presentación
• Todavía falta algo...
MV*
MV*
• El papel del Controlador no está tan claro
• Gestionar la interacción?
• Gestionar los eventos de la vista?
• Gestionar las rutas de la página?
• Gestionar al modelo?
• ...
MV*
La visión tradicional:
Modelo Controlador Vista
MV*
En JavaScript...
M Controlador V
MV*
En JavaScript...
M Controlador V
TemplateProxy ¡Todo lo demás!
Controlador
El modelo “estándar” de Backbone.js
• Nadie lo dice abiertamente, pero:
• No hay Backbone.Controller
• La vista propiamente dicha es el template
• Pero Backbone.View gestiona la interacción...
• Es decir, hace de controlador
Controlador
Si no te he convencido...
¿Quién reacciona al input del usuario?
Backbone.View
¿Quién se encarga de actualizar los datos del modelo según ese input?
Backbone.View
¿Dónde se programa la lógica del interfaz de usuario?
Backbone.View
Controlador
Escuchar eventos en la vista:
var VistaProducto = ProJS.View.extend({ events: { "click a": "marcarComoActivo" }, init: function(options) { this._super(options); this.template = $("#template-producto").html(); this.model.on("change", bind(this, this.render)); },
// Event handlers marcarComoActivo: function() { this.model.set({active: true}); }
/* ... */});
Controlador
Ejercicio: cogiendo el feeling del controlador
tema4/controller-1/index.html
Haz que las vistas VistaListado:
- Escuchen los cambios del modelo y se auto-rendeen
- Escuchen el evento click en el <a> dentro de this.el y lo asocien a marcarComoActivo
- marcarComoActivo pone a true la propiedad “activo” del modelo
- Si la propiedad “activo” del modelo es true, añade la clase CSS “active” a this.el (que debería ser un <li>)
Controlador
El ejemplo anterior tiene problemas:
¿Cómo podemos decirle a los demás elementos que se desactiven?
¿Cómo podemos avisar al resto de la aplicación que se ha seleccionado un nuevo Producto?
MV*
¡Un Mediador!
• La mejor solución para coordinar componentes de una página
• Muy bajo acoplamiento:
- Modificar los componentes sin problemas
- Añadir o eliminar funcionalidad en la página
• MV* + Mediador = un patrón muy común y muy flexible
MV*
Un ejemplo:
tema4/controller-2/index.html
Fíjate como VistaListado simplemente notifica al mediador
Y el mediador es quien se encarga de orquestar los demás elementos
Las ventajas son:
Tenemos todo el flujo “a vista de pájaro” de la página en un solo sitio: el mediador
Ningún componente está acoplado a ningún otro, solo notifica a su mediador
MV*
Ejercicio: un poco de todo!
Modifica el ejemplo anterior para que al hacer click en un elemento de la barra lateral se muestre ese modelo con una vista VistaProducto (la tabla de los primeros ejercicios)
Testing
made with love by Redradix (www.redradix.com)
¿Qué es?
Comprobación automática del código
• Organizada por casos
• Cada caso comprueba un aspecto
• Comparando el resultado obtenido con el esperado
¿Para qué sirve?
¡Para garantizar que todo funciona!
• Que el nuevo código es correcto
• Que no se ha roto nada de lo anterior
• Que una refactorización no ha introducido bugs
¿En JavaScript?
Una práctica que va penetrando poco a poco
• Aunque sigue sin estar muy extendida
• Necesaria para aplicaciones complejas
• En general, una garantía de calidad
Testing
Hay muchos tipos de tests:
• Unitarios: comprueban un componente o una parte específica del código
• Integración: comprueban la interacción de componentes
• Aceptación: comprueban los requisitos del proyecto
• Regresión: comprueban la corrección de cambios
• etc...
Tests Unitarios
La idea de test unitario es muy simple:
• Dado un componente del sistema
• Para cada caso posible
• Comprobar que se comporta de la manera adecuada
Test Unitarios
var Contador = ProJS.Class.extend({ init: function() { this.i = 0; }, get: function() { return this.i; }, inc: function() { this.i++; }, dec: function() { this.i--; }, reset: function() { this.i = 0; }});
Test Unitarios
¿Cómo podríamos comprobar, programáticamente, que Contador funciona bien?
Haciendo algo así:
➡ tema5/unitarios-1/index.html
Test Unitarios
Es bastante tedioso!
• Mucha repetición de código similar
• Se puede abstraer bastante
Test Unitarios
Segundo intento
➡ tema5/unitarios-2/index.html
Test Unitarios
var ContadorTests = Test.extend({ casos: { debe_empezar_a_cero: function(contador) { var i = contador.get(); this.assertEqual(i, 0, "Empieza a %1".format(i)); },
// ... }});
Jasmine
Estupenda librería de testing
• Al estilo rspec
• Sencilla
• Potente
• http://pivotal.github.com/jasmine/
Jasmine
¿Qué pinta tiene?
• tema5/jasmine-1/index.html
describe("Conjunto de tests", function() { it("debería ser un caso válido", function() { expect(true).toBe(true); }); it("debería ser un caso con error", function() { expect(true).toBe(false); });});
Jasmine
Test del contador con Jasmine
• tema5/jasmine-2/index.html
Jasmine
Test asíncronos
• ¿Cómo testearías que esta función llama al cb con true?
• tema5/jasmine-3/index.html
function asyncFn(cb) { setTimeout(function() { cb(true); }, 250);}
Jasmine
describe("Test asíncrono", function() { it("debería llamar al callback con true", function() { var result, callback = function(response) { result = response; }; runs(function() { asyncFn(callback); }); waitsFor(function() { return result == true; }, 300); runs(function() { expect(result).toBe(true); }); });});
Intermedio: Jasmine
¡Testea alguna de las funciones que hemos visto!
• La que te parezca más confusa
• Documentación y “matchers” de Jasmine en
➡ http://pivotal.github.com/jasmine/
Jasmine
Jasmine en la consola:
• Cambiar a ConsoleReporter
• Y un poco de magia funcional...
➡ tema5/consola-1/index.html
Jasmine
var lazyPrint = (function() { var buffer = "", print = function() { console.log(buffer); buffer = ""; }; print = debounce(print, 300); return function(msg) { buffer += msg; print(); };}());
Jasmine
¿Para qué sirve Jasmine en la consola?
• Dejar la página libre
• Poder cargar nuestro propio HTML
•¡Testear interacciones e interfaces!
Test de Integración (interfaz)
Comprobar que el UI funciona correctamente
• Simular la interacción del usuario disparando eventos DOM
• Observar el estado del programa inspeccionando el interfaz
• Asegurar la correcta integración de los componentes de la página
Jasmine
El resultado:
• tema5/integration/index.html
• Queremos testear que el intefaz funciona bien
• “Inc” incrementa el contador y el display
• “Dec” decrementa el contador y el display
• “Reset” lo pone a 0
• Salida por consola...
- Podríamos ver esta salida en algún emulador de DOM de node.js
- O hacer un reporter que se comunique con el servidor de integración continua
Spam Mode: ON
Al escribir test JS acaba surgiendo un problema:
• ¡Los datos!
• ¿De dónde saco datos válidos para testear?
• ¿Del servidor?
- No es fácil de conseguir modificar/resetear un set de datos cada vez que ejecuto un test
- Dependencia del backend
• Lo ideal sería:
- Factorías de datos (estilo FactoryGirl)
- Simular la interacción con el servidor de forma inocua
Solipsist.js
Solipsist.js es una librería auxiliar para testear
➡ https://github.com/WeRelax/solipsist-js
• Tests JS aislados
• Factorías
• Mocking de peticiones AJAX
• Otro uso: programar el frontend independiente del backend