Factory Pattern
El Patrón Factory (también conocido como Método de Fábrica) es un Patrón de diseño creacional que te permite crear objetos sin especificar la clase exacta del objeto que se va a crear. En esencia, proporciona una interfaz para crear objetos, pero permite que las subclases alteren el tipo de objetos que se crearán.
¿Por qué usar el Patrón Factory?
El problema principal que resuelve este patrón es cuando tu código necesita crear diferentes tipos de objetos, pero la lógica para decidir qué objeto crear se mezcla con el código que utiliza esos objetos. Esto lleva a:
-
Alto acoplamiento: Los cambios en la lógica de creación (por ejemplo, añadir un nuevo tipo de objeto) requieren modificar el código que los utiliza, lo que aumenta el riesgo de introducir errores.
-
Falta de flexibilidad: Es difícil cambiar el comportamiento de creación de objetos sin tocar directamente las partes del código que los instancian.
El Patrón Factory soluciona esto encapsulando la lógica de creación de objetos en una clase separada (la “fábrica” o “factoría”).
Imagina que estás desarrollando un videojuego y necesitas crear diferentes tipos de enemigos.
Primero, defines una interfaz común para todos los enemigos, lo que te permite aplicar polimorfismo. Esto significa que, sin importar el tipo de enemigo, siempre podrás interactuar con ellos de la misma manera (por ejemplo, llamando a su método attack()).
interface Enemy {
attack(): string;
}
class Goblin implements Enemy {
attack(): string {
return "Goblin attacks with a club!";
}
}
class SandWorm implements Enemy {
attack(): string {
return "Sand Worm emerges and bites!";
}
}
class Dragon implements Enemy {
attack(): string {
return "Dragon breathes fire!";
}
}Inicialmente, podrías tener la lógica de creación de enemigos directamente en tu bucle principal del juego (gameLoop), decidiendo qué enemigo crear de forma aleatoria:
function gameLoop() {
const randomNum = Math.random();
let enemy;
if (randomNum > 0.66) {
enemy = new Goblin();
} else if (randomNum > 0.33) {
enemy = new SandWorm();
} else {
enemy = new Dragon()
}
}El problema aquí es que si decides añadir un nuevo tipo de enemigo o cambiar la forma en que se crean, tendrías que modificar directamente gameLoop. Esto puede ser problemático en proyectos grandes, ya que corres el riesgo de introducir errores en otras partes del código.
Encapsulando la lógica de creación
Para evitar modificar el gameLoop cada vez que cambias la lógica de creación de enemigos, puedes encapsular esta lógica en una clase “factoría”:
class EnemyFactory {
public createEnemy(): Enemy {
const randomNum = Math.random();
let enemy;
if (randomNum > 0.66) {
enemy = new Goblin();
} else if (randomNum > 0.33) {
enemy = new SandWorm();
} else {
enemy = new Dragon()
}
return enemy;
}
}
function gameLoop() {
console.log('Starting game...')
const spawner = new EnemySpawner();
const enemy = spawner.createEnemy()
console.log(enemy.attack());
}
//'Starting game...'
//'Goblin attacks with a club!'Ahora, la lógica de creación de enemigos está aislada en EnemyFactory. Si quieres añadir un nuevo enemigo o cambiar la forma en que se seleccionan, solo necesitas modificar EnemyFactory, sin tocar gameLoop.
Estrategias de creación flexibles
El Patrón Factory va un paso más allá para permitir aún más flexibilidad. ¿Qué pasa si quieres tener diferentes estrategias de creación de enemigos (por ejemplo, una que siempre genere dragones para un nivel difícil, y otra que genere enemigos aleatoriamente)?
Aquí es donde se aplica el polimorfismo a los propios “creadores” de objetos. Defines una interfaz para las factorías:
interface EnemySpawner<T> {
createEnemy(): <T>
}
//Random Spawns
class RandomEnemySpawner implements EnemySpawner<Enemy> {
public createEnemy(): Enemy {
const randomNum = Math.random();
let enemy: Enemy;;
if (randomNum < 0.33) {
enemy = new Goblin();
} else if (randomNum < 0.66) {
enemy = new SandWorm();
} else {
enemy = new Dragon();
}
return enemy;
}
}
//Always spawns dragons
class DifficultEnemySpawner implements EnemySpawner<Enemy> {
createEnemy(): Enemy {
return new Dragon()
}
}Ahora tienes dos clases que implementan la interfaz EnemySpawner, cada una con su propia lógica para crear enemigos. Lo más importante es que, al compartir la misma interfaz, puedes utilizarlas de manera intercambiable en tu gameLoop:
///gameLoop no sabe qué enemySpawner le inyectamos y le da igual Lo único que sabe es que tiene un método createEnemy.
function gameLoop(enemySpawner: EnemySpawner<Enemy>) {
console.log('Starting game...')
const enemy = enemySpawner.createEnemy()
console.log(enemy.attack());
}De esta forma, puedes “inyectar” la estrategia de creación de enemigos que desees en tu gameLoop sin modificar el gameLoop en sí:
gameLoop(new DifficultEnemySpawner()); //Crea siempre Dragones
gameLoop(new RandomEnemySpawner()); //Enemigos aleatoriosEn resumen
El Patrón Factory te permite:
- Desacoplar la lógica de creación de objetos del código que los utiliza. Esto hace que tu código sea más modular y fácil de mantener.
- Introducir nuevos tipos de objetos sin modificar el código cliente. Simplemente creas una nueva implementación de la interfaz de la factoría.
- Cambiar las estrategias de creación de objetos en tiempo de ejecución. Puedes decidir qué factoría usar según las necesidades de tu aplicación.
Este patrón es fundamental para construir sistemas flexibles y escalables, especialmente cuando trabajas con jerarquías de clases y necesitas un control centralizado sobre cómo se instancian los objetos.