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étodocreateWarrior()que, dependiendo de la implementación de la fábrica, devuelve unWarrior2Do unWarrior3D. 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 comocrearGuerrero()ycrearMago(). Si usas laFabricaDeEstiloVisual2D, obtendrás unGuerrero2Dy unMago2D. Si usas laFabricaDeEstiloVisual3D, obtendrás unGuerrero3Dy unMago3D. 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:
Enemycomo base, de la que extiendenWarrioryWizard. -
Implementaciones concretas: Cada tipo de enemigo (
Warrior,Wizard) tiene ahora dos versiones concretas, una para cada estilo visual (familia):Warrior2D,Wizard2Dpara el estilo 2D, yWarrior3D,Wizard3Dpara 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()ycreateWizard()). 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. -
EnemyFactory2DyEnemyFactory3D: Estas son nuestras fábricas concretas. Cada una implementaAbstractEnemyFactoryy es responsable de producir una familia coherente de productos.EnemyFactory2Dsiempre creará enemigos con estilo 2D, mientras queEnemyFactory3Dsiempre 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.