Programación orientada a objetos

En el tema anterior hemos hablado de los objetos predefinidos, en este trataremos los objetos de usuario y cómo podemos programar en JavaScript con técnicas de orientación a objetos clásicas.

La programación orientada a objetos es un paradigma de programación que utiliza la abstracción para crear modelos basados en el mundo real.

Terminología básica de la programación orientada a objetos:

Nota: en JavaScript no existe el concepto de clase como sí tienen los lenguajes orientados a objetos como Java, C++ o python ES6 introduce el concepto de clase mediante la palabra reservada class

Creación de objetos

Los objetos se pueden crear de tres formas diferentes:

// Objeto de usuario
var libro= {
  titulo: 'Single Page Web Applications',
  autores: [
    'Michael S. Mikowski',
    'Josh C. Powell'
  ],
  year: 2014,
  editorial: 'Manning',
  isbn: '9781617290756',
  tags: ['SPA', 'JavaScript', 'Node.js', 'MongoDB']
}
// Objeto de usuario
var libro= new Object();

libro.titulo= 'Single Page Web Applications';
libro.autores= [
  'Michael S. Mikowski',
  'Josh C. Powell'
];
libro.year= 2014;
libro.editorial= 'Manning';
libro.isbn= '9781617290756';
libro.tags= ['SPA', 'JavaScript', 'Node.js', 'MongoDB'];
// Objeto de usuario
var libro= Object.create({});

libro.titulo= 'Single Page Web Applications';
libro.autores= [
  'Michael S. Mikowski',
  'Josh C. Powell'
];
libro.year= 2014;
libro.editorial= 'Manning';
libro.isbn= '9781617290756';
libro.tags= ['SPA', 'JavaScript', 'Node.js', 'MongoDB'];

Nota: Object es un objeto predefinido de JavaScript que se utiliza como base para crear todos los objetos (ya sean predefinos, de plataforma o de usuario)

Los objetos en JavaScript son mutables y se tratan por referencia, al contrario que los datos primitivos que son inmutables y se tratan por valor. Veamos que significa esto:

var dato_primitivo1= 'valor1',
    dato_primitivo2= 'valor2';

dato_primitivo2= dato_primitivo1;
console.log(dato_primitivo2);
dato_primitivo2= 'otro valor';
console.log(dato_primitivo1);

var objeto1= {una_propiedad: 'valor1'},
    objeto2= {una_propiedad: 'valor2'};

objeto2= objeto1;
console.log(objeto2.una_propiedad);
objeto2.una_propiedad= 'otro valor';
console.log(objeto1.una_propiedad);

repl.it

Recuperar y asignar valores a propiedades

Se accede a las propiedades de un objeto mediante el punto . y los corchetes []. Con la segunda opción podemos acceder a propiedades mediante expresiones.

Acceder a una propiedad que no existe devuelve undefined.

Para saber si un objeto tiene una propiedad se utiliza el operador in.

Para saber el tipo de objeto se utiliza el operador instanceof.

Crear y eliminar propiedades

Podemos distinguir entre propiedades propias y heredadas. Para saber si es una propiedad propia se utiliza el método hasOwnProperty().

En cualquier momento podemos crear nuevas propiedades y eliminar las propiedades propias, no las heredadas, mediante el operador delete.

var a= [];

console.log('Tiene propiedad length?', 'length' in a);
delete a.length;
console.log('Tiene propiedad length?', 'length' in a);

a.nueva_propiedad= 1000;
console.log('Tiene propiedad nueva_propiedad?', 'nueva_propiedad' in a);
delete a.nueva_propiedad;
console.log('Tiene propiedad nueva_propiedad?', 'nueva_propiedad' in a);

repl.it

Creación de clases

Supongamos que tenemos que trabajar con robots. Vamos a crear objetos con las características de un robot.

var R2D2= {
  nombre: 'R2D2',
  tipo: 'arreglatodo',
  estado: 0,
  modoEspera: function() {
    console.log(this.nombre + ': Iniciando modo espera...');
    this.estado= 0;
    console.log(this.nombre + ': En modo espera!');
  },
  activar: function() {
    console.log(this.nombre + ': Saliendo modo espera...');
    this.estado= 1;
    console.log(this.nombre + ': Activado!');
  },
  ayuda: function() {
    if (this.estado === 1) {
      console.log(this.nombre + ': Vengo inmediatamente!');
    } else console.log(this.nombre + ': Piiiip');
  },
  arreglar: function(item) {
    if (this.estado === 1) {
      if (item && item !== '') {
        console.log(this.nombre + ': Arreglando ' + item);
      } else console.log(this.nombre + ': Debes indicarme qué quieres que arregle');
    } else console.log(this.nombre + ': Piiiip');
  }
};

var C3PO= {
  nombre: 'C3PO',
  tipo: 'traductor',
  estado: 0,
  modoEspera: function() {
    console.log(this.nombre + ': Iniciando modo espera...');
    this.estado= 0;
    console.log(this.nombre + ': En modo espera!');
  },
  activar: function() {
    console.log(this.nombre + ': Saliendo modo espera...');
    this.estado= 1;
    console.log(this.nombre + ': Activado!');
  },
  ayuda: function() {
    if (this.estado === 1) {
      console.log(this.nombre + ': Ahora vengo. Aunque no es mi cometido intentaré ayudar');
    } else console.log(this.nombre + ': Piiiip');
  },
  traducir: function(texto) {
    if (this.estado === 1) {
      if (texto && texto !== '') {
        console.log(this.nombre + ': Traduciendo ' + texto);
      } else console.log(this.nombre + ': Debes indicarme qué quieres que traduzca');
    } else console.log(this.nombre + ': Piiiip');
  }
};

Nota: las funciones en javascript tienen propiedades, igual que los objetos (las funciones son objetos). Cuando se ejecuta una función se crea la propiedad this con el valor del objeto que ha invocado la función.
this siempre hace referencia a un objeto y no tiene asignado un valor hasta que un objeto invoca la función donde se define this.
Cuando se llama a un constructor con el operador new, this hace referencia al nuevo objeto creado.

repl.it

Pregunta: y si tenemos 100 robots?

Tenemos que crear 100 objetos robot con todas sus características? Qué pasa si hemos de modificar un método, lo hemos de cambiar 100 veces? NO, y aquí es donde introducimos el concepto de clase.

Nota: recordar que en JavaScript no existe la clase como un componente del lenguaje. En JavaScript se pueden definir clases mediante funciones (denominadas constructores)

Para definir clases mediante constructores antes debemos introducir el concepto de prototipo.

Prototipos

Esta es una de las características clave de JavaScript, y como tal debemos entenderla bien. De hecho JavaScript se define como un lenguaje orientado a prototipos y no orientado a objetos aunque, como veremos, se puede utilizar perfectamente dicho paradigma de programación.

En un lenguaje orientado a prototipos los objetos no son creados mediante la instanciación de clases sino mediante la clonación de otros objetos.

Los dos conceptos de prototype en JavaScript:

  1. Todas las funciones tienen una propiedad llamada prototype, un objeto al cual se pueden añadir propiedades y métodos para implementar la herencia

  2. Todos los objetos tienen un prototipo que hace referencia a la propiedad prototype de su padre, aquel objeto del cual han heredado sus propiedades. El acceso a propiedades de un objeto se realiza mediante la técnica denominada encadenamiento de prototipos (prototype chain)

Nota1: todos los objetos en JavaScript heredan propiedades y métodos de Object.prototype, que son: constructor, hasOwnProperty(), propertyIsEnumerable(), isPrototypeOf(), toLocaleString(), toString(), y valueOf()

Nota2: los objetos literales heredan propiedades y métodos de Object.prototype, los creados mediante new de la función constructor correspondiente

Función constructor

Es una función que se utiliza para inicializar nuevos objetos, se utiliza el operador new para invocar dicha función. Todos los objetos tienen una propiedad llamada constructor que apunta a esta función constructor.

Vamos a crear una clase Robot:

// Definición de la clase Robot
function Robot(nombre, tipo) {
  this.nombre= nombre || 'sin nombre',
  this.tipo= tipo || 'arreglatodo';
  this.estado= 0;
};

Robot.prototype= {
  modoEspera: function() {
    console.log(this.nombre + ': Iniciando modo espera...');
    this.estado= 0;
    console.log(this.nombre + ': En modo espera!');
  },

  activar: function() {
    console.log(this.nombre + ': Saliendo modo espera...');
    this.estado= 1;
    console.log(this.nombre + ': Activado!');
  },

  ayuda: function() {
    if (this.estado === 1) {
      console.log(this.nombre + ': Vengo inmediatamente!');
    } else console.log(this.nombre + ': Piiiip');
  },

  arreglar: function(item) {
    if (this.estado === 1) {
      if (item && item !== '') {
        console.log(this.nombre + ': Arreglando ' + item);
      } else console.log(this.nombre + ': Debes indicarme qué quieres que arregle');
    } else console.log(this.nombre + ': Piiiip');
  },

  traducir: function(texto) {
    if (this.estado === 1) {
      if (texto && texto !== '') {
        console.log(this.nombre + ': Traduciendo ' + texto);
      } else console.log(this.nombre + ': Debes indicarme qué quieres que traduzca');
    } else console.log(this.nombre + ': Piiiip');
  }
};
// FIN definición de la clase Robot

var R2D2= new Robot('R2D2', 'arreglatodo'),
    C3PO= new Robot('C3PO', 'traductor');

Buenas practicas: las funciones que actúan como constructor, por convención, comienzan con mayúsculas

repl.it

Hemos solucionado algunos problemas, ahora es mucho más fácil crear robots mediante la clase Robot. Pero aún no todos, veamos:

C3PO.arreglar('Propulsor Halcon Milenario');

Ya podemos coger una nave-taxi!!!

No tenemos robots especializados, todos nuestros robots tienen las mismas características. Podríamos eliminar características segun el tipo de robot, consultar el tipo de robot en los métodos específicos y actuar en consecuencia... todo soluciones inadecuadas.

La solución correcta es crear subclases de robots especializados. Para ello empleamos el concepto de herencia.

Herencia

Un objeto hereda las propiedades de otro objeto, su prototype.

Modificamos la clase Robot y creamos dos subclases RobotArreglaTodo y RobotTraductor:

// Definición de la clase Robot
function Robot(nombre) {
  this.nombre= nombre || 'sin nombre',
  this.estado= 0;
};

Robot.prototype= {
  modoEspera: function() {
    console.log(this.nombre + ': Iniciando modo espera...');
    this.estado= 0;
    console.log(this.nombre + ': En modo espera!');
  },

  activar: function() {
    console.log(this.nombre + ': Saliendo modo espera...');
    this.estado= 1;
    console.log(this.nombre + ': Activado!');
  },

  ayuda: function() {
    if (this.estado === 1) {
      console.log(this.nombre + ': Vengo inmediatamente!');
    } else console.log(this.nombre + ': Piiiip');
  }
};
// FIN definición de la clase Robot

// Definición de la subclase RobotArreglaTodo
function RobotArreglaTodo(nombre) {
  Robot.call(this, nombre); // Invocamos el constructor de Robot con this
  this.tipo= 'arreglatodo';
};

RobotArreglaTodo.prototype= new Robot(); // Herencia;

RobotArreglaTodo.prototype.constructor= RobotArreglaTodo;

RobotArreglaTodo.prototype.arreglar= function(item) {
  if (this.estado === 1) {
    if (item && item !== '') {
      console.log(this.nombre + ': Arreglando ' + item);
    } else console.log(this.nombre + ': Debes indicarme qué quieres que arregle');
  } else console.log(this.nombre + ': Piiiip');
};
// FIN definición de la subclase RobotArreglaTodo

// Definición de la subclase RobotTraductor
function RobotTraductor(nombre) {
  Robot.call(this, nombre); // Invocamos el constructor de Robot con this
  this.tipo= 'traductor';
};

RobotTraductor.prototype= new Robot(); // Herencia;

RobotTraductor.prototype.constructor= RobotTraductor;

RobotTraductor.prototype.traducir= function(texto) {
  if (this.estado === 1) {
    if (texto && texto !== '') {
      console.log(this.nombre + ': Traduciendo ' + texto);
    } else console.log(this.nombre + ': Debes indicarme qué quieres que traduzca');
  } else console.log(this.nombre + ': Piiiip');
};
// FIN definición de la subclase RobotTraductor


var R2D2= new RobotArreglaTodo('R2D2'),
    C3PO= new RobotTraductor('C3PO');

repl.it

Seguimos teniendo algunos problemillas:

C3PO.ayuda();

Todos los robots implementan la tarea de ayudar a quien solicita su ayuda, pero los traductores tienen su propio concepto de la diligencia en el momento de realizar esta función.

Polimorfismo

Comportamientos diferentes asociados a objetos diferentes pueden compartir el mismo nombre de método. Cuando se utiliza dicho método la tarea realizada dependerá del objeto en cuestión.

Si redefinimos el método ayuda() para los objetos RobotTraductor cambiaremos el comportamiento de estos objetos utilizando el mismo método.

Añadimos al código anterior:

RobotTraductor.prototype.ayuda= function() {
  if (this.estado === 1) {
    console.log(this.nombre + ': Ahora vengo. Aunque no es mi cometido intentaré ayudar');
  } else console.log(this.nombre + ': Piiiip');
};

repl.it

Ya sólo nos queda un detalle. Nada nos impide realizar la siguiente acción:

C3PO.tipo= 'arreglatodo';

El tipo de robot debería ser una propiedad privada del robot y la única acción que deberíamos poder realizar sobre ella es recuperar su valor.

Encapsulamiento

Cada tipo de objeto expone una interfaz (conjunto de propiedades y métodos) que especifica cómo se puede interactuar con los objetos de la clase. Si modificamos esa interfaz para, por una parte, ocultar la propiedad tipo y, por otra, añadir un método que devuelva su valor habremos resuelto el problema.

// Definición de la clase Robot
function Robot(nombre) {
  this.nombre= nombre || 'sin nombre',
  this.estado= 0;
};

Robot.prototype= {
  modoEspera: function() {
    console.log(this.nombre + ': Iniciando modo espera...');
    this.estado= 0;
    console.log(this.nombre + ': En modo espera!');
  },

  activar: function() {
    console.log(this.nombre + ': Saliendo modo espera...');
    this.estado= 1;
    console.log(this.nombre + ': Activado!');
  },

  ayuda: function() {
    if (this.estado === 1) {
      console.log(this.nombre + ': Vengo inmediatamente!');
    } else console.log(this.nombre + ': Piiiip');
  }
};
// FIN definición de la clase Robot

// Definición de la subclase RobotArreglaTodo
function RobotArreglaTodo(nombre) {
  var tipo= 'arreglatodo';
  Robot.call(this, nombre); // Invocamos el constructor de Robot con this
  this.getTipo= function() {
    return tipo;
  }
};

RobotArreglaTodo.prototype= new Robot(); // Herencia;

RobotArreglaTodo.prototype.constructor= RobotArreglaTodo;

RobotArreglaTodo.prototype.arreglar= function(item) {
  if (this.estado === 1) {
    if (item && item !== '') {
      console.log(this.nombre + ': Arreglando ' + item);
    } else console.log(this.nombre + ': Debes indicarme qué quieres que arregle');
  } else console.log(this.nombre + ': Piiiip');
};
// FIN definición de la subclase RobotArreglaTodo

// Definición de la subclase RobotTraductor
function RobotTraductor(nombre) {
  var tipo= 'traductor';
  Robot.call(this, nombre); // Invocamos el constructor de Robot con this
  this.getTipo= function() {
    return tipo;
  }
};

RobotTraductor.prototype= new Robot(); // Herencia;

RobotTraductor.prototype.constructor= RobotTraductor;

RobotTraductor.prototype.traducir= function(texto) {
  if (this.estado === 1) {
    if (texto && texto !== '') {
      console.log(this.nombre + ': Traduciendo ' + texto);
    } else console.log(this.nombre + ': Debes indicarme qué quieres que traduzca');
  } else console.log(this.nombre + ': Piiiip');
};

RobotTraductor.prototype.ayuda= function() {
  if (this.estado === 1) {
    console.log(this.nombre + ': Ahora vengo. Aunque no es mi cometido intentaré ayudar');
  } else console.log(this.nombre + ': Piiiip');
};
// FIN definición de la subclase RobotTraductor


var R2D2= new RobotArreglaTodo('R2D2'),
    C3PO= new RobotTraductor('C3PO');

repl.it

Ejercicio: necesitamos robots de combate. Definir una subclase RobotCombate con las siguientes características: deben atacar y defender activándose automáticamente; y estos robots no ayudan.
Respuesta

property-chain