Abstract Factory Pattern

El Patrón de diseño Abstract Factory es un patrón creacional que proporciona una interfaz para crear familias de objetos relacionados o dependientes sin especificar sus clases concretas. El Abstract Factory te proporciona una “fábrica de fábricas”, donde cada “fábrica” se encarga de producir una familia completa de objetos.

Abstract Factory vs. Factory Method

La principal diferencia con el patrón Factory Pattern es que, mientras que Factory Pattern te proporciona una interfaz para crear un objeto de un tipo determinado (ej. solo un tipo de Enemy), Abstract Factory ofrece interfaces para crear familias de objetos de múltiples tipos (ej. un Warrior y un Wizard)

Piensa en esto:

  • Factory Method: Tendrías una fábrica que crea solo un tipo de objeto, como una WarriorFactory. Podría tener un método createWarrior() que, dependiendo de la implementación de la fábrica, devuelve un Warrior2D o un Warrior3D. La elección es sobre la variante de un solo tipo de producto.

  • Abstract Factory: Tendrías una fábrica que crea múltiples tipos de objetos que pertenecen a la misma “familia” o “tema”, como una FabricaDeEstiloVisual. Esta fábrica tendría métodos como crearGuerrero() y crearMago(). Si usas la FabricaDeEstiloVisual2D, obtendrás un Guerrero2D y un Mago2D. Si usas la FabricaDeEstiloVisual3D, obtendrás un Guerrero3D y un Mago3D. La elección es sobre la familia completa de objetos que deseas usar.


Ejemplo en un Videojuego con Estilos Visuales

Siguiendo el ejemplo del videojuego, vamos a crear familias de enemigos por estilo visual: 2D y 3D. Esto significa que si el juego se renderiza en 2D, todos los enemigos (guerreros, magos, etc.) deben tener su versión 2D. Si se renderiza en 3D, todos deben tener su versión 3D, pero nunca se pueden mezclar.

// Interfaces de los productos abstractos (enemigos)
interface Enemy {
  attack(): string;
}
 
interface Warrior extends Enemy {
  shield(): string;
}
 
interface Wizard extends Enemy {
  fireball(): string
}
 
// Implementaciones concretas de los productos para el estilo 2D
class Warrior2D implements Warrior {
  constructor(protected name: string) {}
  shield(): string {
    return `${this.name} uses his 2D shield!`
  }
  attack() {
    return `${this.name} attacks with his 2D Weapon!`;
  }
}
 
class Wizard2D implements Wizard {
  constructor(protected name: string) {}
  attack() {
    return `${this.name} attacks with his 2D weapon`
  }
  fireball() {
    return `${this.name} throws a 2D fireball!`
  }
}
 
// Implementaciones concretas de los productos para el estilo 3D
class Warrior3D implements Warrior {
  constructor(protected name: string) {}
  shield(): string {
    return `${this.name} uses his 3D shield!`
  }
  attack() {
    return `${this.name} attacks with his 3D Weapon!`
  }
}
 
class Wizard3D implements Wizard {
  constructor(protected name: string) { }
  attack() {
    return `${this.name} attacks with his 3D weapon`
  }
  fireball() {
    return `${this.name} throws a 3D fireball!`
  }
}

Aquí hemos definido:

  • Abstracciones de producto: Enemy como base, de la que extienden Warrior y Wizard.

  • Implementaciones concretas: Cada tipo de enemigo (Warrior, Wizard) tiene ahora dos versiones concretas, una para cada estilo visual (familia): Warrior2D, Wizard2D para el estilo 2D, y Warrior3D, Wizard3D para el estilo 3D.


Creando las Fábricas Abstractas

Ahora, definiremos las interfaces para nuestras fábricas abstractas y sus implementaciones concretas.

// Interfaz del Abstract Factory
interface AbstractEnemyFactory {
  createWarrior(): Warrior;
  createWizard(): Wizard;
}
 
// Fábrica concreta para el estilo 2D
class EnemyFactory2D implements AbstractEnemyFactory {
  createWarrior() {
    return new Warrior2D('2D Warrior');
  }
  createWizard(): Wizard {
    return new Wizard2D('2D Wizard');
  }
}
 
// Fábrica concreta para el estilo 3D
class EnemyFactory3D implements AbstractEnemyFactory {
  createWarrior() {
    return new Warrior3D('3D Warrior');
  }
  createWizard(): Wizard {
    return new Wizard3D('3D Wizard');
  }
}

Hemos creado:

  • AbstractEnemyFactory: Esta es nuestra interfaz Abstract Factory. Define los métodos para crear múltiples tipos de objetos relacionados (createWarrior() y createWizard()). Garantiza que cualquier fábrica que implemente esta interfaz podrá producir tanto un guerrero como un mago, pero siempre dentro de la misma “familia” de estilo visual.

  • EnemyFactory2D y EnemyFactory3D: Estas son nuestras fábricas concretas. Cada una implementa AbstractEnemyFactory y es responsable de producir una familia coherente de productos. EnemyFactory2D siempre creará enemigos con estilo 2D, mientras que EnemyFactory3D siempre creará enemigos con estilo 3D.


Uso en el Bucle del Juego

Finalmente, veamos cómo se utiliza esto en la lógica principal del juego, que no necesita saber nada sobre el estilo visual concreto:

function gameLoop(assetFactory: AbstractGameAssetFactory) {
  const enemy1 = assetFactory.createWarrior();
  console.log(enemy1.attack());
  console.log(enemy1.shield());
 
  const enemy2 = assetFactory.createWizard();
  console.log(enemy2.fireball());
  console.log(enemy2.attack());
}
 
// Cambiamos el estilo visual del juego simplemente inyectando la fábrica correcta
console.log('--- Juego en estilo 2D ---');
gameLoop(new GameAssetFactory2D());
 
console.log('\n--- Juego en estilo 3D ---');
gameLoop(new GameAssetFactory3D());

En la función gameLoop, inyectamos la assetFactory (la fábrica concreta) que queremos utilizar. Esto significa que el gameLoop no tiene acoplamiento con las clases concretas (Warrior2D, Wizard3D, etc.). Simplemente le pide a la fábrica que le proporcione un guerrero y un mago, y la fábrica se encarga de crear los objetos correctos que corresponden al estilo visual (familia) elegido.

Al cambiar gameLoop(new GameAssetFactory2D()) por gameLoop(new GameAssetFactory3D()), puedes cambiar todo el conjunto de activos visuales del juego (de 2D a 3D o viceversa) sin modificar la lógica interna del gameLoop. Esto demuestra la flexibilidad y el bajo acoplamiento que proporciona el patrón Abstract Factory, permitiendo una fácil conmutación entre familias de objetos.