La herencia es una de las ventajas que nos aporta la orientación a objetos, pero hay una aún mayor: la composición. Si en la herencia conseguíamos que un objeto delegara en su prototipo para conseguir un resultado, en la composición lo conseguimos a base de componer un objeto a partir de varios. Para ello, en lugar de definir entidades intentaremos definir comportamientos, es decir, no lo que son sino lo que hacen.

Lo has descrito muy bonito pero me suena a chino. Un ejemplo siempre ayuda asi que para ello haremos uso de nuevo del reino animal.

Supongamos que queremos definir varios animales en función de lo que hacen, por tanto crearemos objetos que definan comportamientos (qué pueden hacer).

const eater  = { eat(){ console.log('Is eating') } };  
const runner = { run(){ console.log('Is running!') } };  
const flying = { fly(){ console.log('Is flying!') } };  

Queremos definir dos nuevos objetos dog y bird. Usaremos por tanto la composición para crear un nuevo objeto. Para ello nos ayudaremos de la función Object.assign introducida en ES6. Esta función añade, de forma dinámica, a un objeto las propiedades (métodos y atributos) de otros. Podemos pasarle tantos objetos como queramos pero tenemos que tener en cuenta que muta el estado, no devuelve un nuevo objeto sino que modifica el primero. Por tanto como no queremos modificar nuestros objetos ya definidos, le pasaremos un nuevo objeto como primer parámetro.

const dog  = Object.assign({}, eater, runner);  
const bird = Object.assign({}, eater, flying);

dog.run();     // 'Is running!'  
dog.eat();     // 'Is eating'  
bird.fly();    // 'Is flying!'  
bird.eat();    // 'Is eating'  

Ya que dog y bird son demasiado genericos añadamos algo mas para definir a nuestras mascotas.

const max = Object.assign({ name: 'Max' }, eater, runner);  
const pit = Object.assign({ name: 'Pit' }, eater, flying);

max.name;  // 'Max'  
max.run(); // 'Is running!'  
pit.name;  // 'Pit'  
pit.fly(); // 'Is flying!'  

Necesitamos tambien crear a nuestra mascota tom, un perro pastor, por tanto necesitamos definir antes que es capaz de hacer un pastor.

const shepherd = { sheperding() { console.log('Is sheperding!') } };  

Sin embargo empieza a resultar repetitiva la tarea de componer una y otra vez. ¿Alguien ha dicho factory? ¿Por qué no usar una función que lo haga por nosotros?

function dog(options) {  
  const name = options.name || 'Dog';
  if (options.isShepherd) {
    return Object.assign({ name: name }, eater, runner, shepherd);
  } else {
    return Object.assign({ name: name }, eater, runner);
  }
}

const max = dog({ name: 'Max' });  
const tom = dog({ name: 'Tom', isShepherd: true });

tom.sheperding(); // 'Is shepherding!'  

Queremos además definir el color y que tipo de pelo tienen. Sabemos que ambos son del mismo color, así que ¿por qué no usar un valor por defecto?

function dog(options) {  
  const name = options.name || 'Dog';
  const color = options.color || 'Brown';
  const hair = options.hair || 'short';

  if (options.isShepherd) {
    return Object.assign({
      name,
      color,
      hair
    }, eater, runner, shepherd);

  } else {
    return Object.assign({
      name,
      color,
      hair
    }, eater, runner);
  }
}

La cosa empieza a complicarse y a ser de nuevo repetitiva. Los valores por defecto son los mismos en ambos casos. ¿Cómo evitarlo? Haremos de nuevo uso de nuestra nueva función amiga Object.assign para simplificarlo.

function dog(options) {  
  const defaults = { name: 'Dog', hair: 'short', color: 'brown' };
  if (options.isShepherd) {
    return Object.assign(defaults, eater, runner, shepherd, options);
  } else {
    return Object.assign(defaults, eater, runner, options);
  }
}

defaults puede ser usado como nuevo objeto que asigne valores por defecto cuando no estén presentes en el objeto options que recibimos como parámetro.

Pero antes dijiste que Object.assign modifica el primer objeto, ¡estás sobreescribiendo defaults! ¿Seguro? defaults es un objeto que se define de forma dinámica dentro de la función cada vez que se ejecuta, por tanto podemos ahorrarnos pasar un objeto vacío.

Como vemos el uso de funciones factory y de composición en lugar de herencia nos ofrece una manera mucho mas flexible de definir objetos. Sin embargo tenemos que tener en cuenta que los objetos que creamos no tienen relación entre si por prototipos sino que son completamente independientes al ser creados de forma dinámica.

En el próximo capítulo de esta serie veremos que ventajas tiene una sobre la otra y cuando deben usarse o como combinarlas. Pero antes veamos otra forma de componer objetos.

Modularidad

A parte de la composición por asignación dinámica, podemos emplear el concepto de modularidad: el divide y vencerás. Podemos tener objetos complejos compuestos a partir de muchos otros, ú objetos simples que contengan otros objetos simples. Pongamos por ejemplo que tenemos un objeto car que necesita poder moverse y tener luz durante la noche. Definamos entonces el objeto con los métodos speedUp, speedDown y toggleLight; y los atributos speed y headLight.

const car = {  
  speed: 0,
  headLight: false,
  speedUp() {
    this.speed += 1;
  },
  speedDown() {
    this.speed -= 1;
  },
  toggleLight() {
    this.headLight = !this.headLight;
  }
};

Todo funciona. Sin embargo necesitamos que el coche tenga turbo para acelerar mas rápido y que también encienda la luz larga.

const car = {  
  speed: 0,
  headLight: false,
  highLight: false,
  isTurboOn: false,
  toggleTurbo() {
    this.isTurboOn = !this.isTurboOn;
  },
  speedUp() {
    this.isTurboOn ? this.speed += 2 : this.speed += 1;
  },
  speedDown() {
    this.speed -= 1;
  },
  toggleLight() {
    this.headLight = !this.headLight;
  },
  toggleHighLight() {
    this.highLight = !this.highLight;
  }
};

Un solo coche no es suficiente, queremos tener varios que aceleren de forma distinta y que puedan encender todas las luces a la vez. Esto nos obliga a tener que crear y modificar objetos car según lo que necesitemos, pero realmente podemos simplificarlo todo haciendo que un coche tenga un objeto engine y un objeto lights.

const engine = {  
  speed: 0,
  isTurboOn: false,
  toggleTurbo() {
    this.isTurboOn = !this.isTurboOn;
  },
  speedUp() {
    this.isTurboOn ? this.speed += 2 : this.speed += 1;
  },
  speedDown() {
    this.speed -= 1;
  }
};

const lights = {  
  headLight: false,
  highLight: false,
  toggleLight() {
    this.headLight = !this.headLight;
  },
  toggleHighLight() {
    this.highLight = !this.highLight;
  },
  toggleAll() {
    this.toggleLight();
    this.toggleHighLight();
  }
};

const car = { engine, lights };  
car.engine.speedUp()  
car.lights.toggleAll();  
car.engine.speed // 1  

Si necesitamos que el coche tenga un motor de más potencia podemos crear un nuevo objeto powerEngine y cambiarlo.

const powerEngine = Object.assign({}, engine, {  
  speedUp() {
    this.speed += 4
  }
});

car.engine = powerEngine;  
car.engine.speedUp() // 4  

Podríamos crear más objetos similares a car con distintos engine y lights y que funcionaran de la misma manera aunque el resultado sea distinto.

Conclusión

Ambas formas de composición son válidas, usar una u otra dependerá de la situación y lo que nos resulte más cómodo en cada caso. Se puede usar la composición dinámica mientras no se creen objetos demasiado complejos en los que resulte más fácil hacerlo de manera modular.

"Creo que este es el principio de una hermosa amistad" - Casablanca (1942)