El Abstract Factory es el primo hermano del Factory Method y muchas veces se les confunde. Este patrón también se le conoce cómo fabrica de fabricas ya que una de sus implementaciones se basa en que este objeto contiene otras factorías (Factory Method
).
El Abstract Factory
, que no el Factory Method
, nos proporciona la funcionalidad de poder crear una familia de objetos relacionados, o dependientes entre sí. Por ejemplo, dentro de la familia de héroes nos permitiría crear los personajes, sus armas, armaduras, etc.
Diferencia entre Abstract Factory y Factory Method
Por un lado el Factory Method
, sirve para crear objetos de un mismo tipo. Si hablamos de héroes o personajes podremos crear guerreros, arqueros, paladines, etc. Pero no podremos crear armas solo objetos del mismo tipo y con un padre en común.
Por el otro lado tenemos el Abstract Factory
que nos permite crear objetos de una misma familia. Dentro de la familia de personajes podremos crear heroes, armas y en general todo lo que esté relacionado.
Para hacer esto, la Factoría Abstracta
nos proporciona una interfaz que tendremos que implementar por cada familia que queramos crear. Si queremos crear objetos para guerreros tendremos un Abstract Factory
, si queremos crear objetos para arqueros tendremos otra. Aquí lo vemos en un diagrama:
Esto implica que por cada familia tenemos que implementar la interfaz y dentro de esta Factoría Abstracta
haremos el new o instanciaremos el prefab del objeto en concreto.
Si esto lo implementamos sobre Unity, personalmente lo veo un poco estático y que no explota todo el potencial de este editor, seguramente pase lo mismo con cualquier motor que permita serializar variables desde el editor. En lugar de hacer esta versión vamos a hacerle un par de modificaciones.
Implementación de un Abstract Factory con Unity
Para aprovechar todo el potencial de Unity lo que voy a hacer es crear una Factoría Abstracta
y en lugar de hacer el new de los objetos concretos le voy a inyectar varios Factory Method
para que pueda delegar esta funcionalidad a cada una de las fábricas.
Esto nos permitirá con una sola clase de Abstract Factory crear todos los objetos de la familia. Lo vemos en el diagrama:
Como estamos viendo en el diagrama, lo que tendremos es una factory de factorías por arriba, y está contendrá las factorías especificas para crear héroes, armas y todos los objetos de la familia. Vamos a ver como implementarlo.
Primero crearemos un Hero
como clase base y una configuración donde arrastraremos todos los prefabs de heroes que podemos crear.
public abstract class Hero : MonoBehaviour
{
[SerializeField] protected string id;
public string Id => id;
}
public class Warrior : Hero { ... }
public class Paladin : Hero { ... }
[CreateAssetMenu(menuName = "Custom/Heroes configuration")]
public class HeroesConfiguration: ScriptableObject{
[SerializeField] private Hero[] heroes;
private Dictionary<string, Hero> idToHero;
private void Awake()
{
idToHero = new Dictionary<string, Hero>();
foreach (var hero in heroes)
{
idToHero.Add(hero.Id, hero);
}
}
public Hero GetHeroPrefabById(string id)
{
if (!idToHero.TryGetValue(id, out var hero))
{
throw new Exception($"Hero with id {id} does not exit");
}
return hero;
}
}
Desde Unity tendremos que crear este ScriptableObject
y asignarle los prefabs:
Ahora vamos a crear la factoría (Factory Method) que consumirá esta configuración y se encargará de instanciar héroes.
public class HeroFactory
{
private readonly HeroesConfiguration heroesConfiguration;
public HeroFactory(HeroesConfiguration heroesConfiguration)
{
this.heroesConfiguration = heroesConfiguration;
}
public Hero Create(string id)
{
var hero = heroesConfiguration.GetHeroPrefabById(id);
return Object.Instantiate(hero);
}
}
Para las armas utilizaremos la misma estructura pero vamos a obviar el código para que el artículo no quede enorme, si quieres ver el código completo te lo puedes descargar desde aquí.
Teniendo estas fábricas el Abstract Factory
queda muy simple ya que solo tiene que delegar la creación a las fábricas que contiene, de ahí que también se le conozca como factoría de factorías, vemos el código:
private readonly HeroFactory heroFactory;
private readonly WeaponFactory weaponFactory;
public BattleFactory(HeroFactory heroFactory, WeaponFactory weaponFactory)
{
this.heroFactory = heroFactory;
this.weaponFactory = weaponFactory;
}
public Hero CreateHero(string heroId)
{
return heroFactory.Create(heroId);
}
public Weapon CreateWeapon(string weaponId)
{
return weaponFactory.Create(weaponId);
}
Ya solo nos queda el consumidor y el Installer
donde configurarlo todo. El consumidor lo vamos a mantener muy básico, simplemente para probar escucharemos el input y entonces crearemos los objetos a través de la Fabrica Abstracta
.
public class Consumer : MonoBehaviour
{
private BattleFactory currentBattleFactory;
public void Configure(BattleFactory battleFactory)
{
currentBattleFactory = battleFactory;
}
private void Update()
{
if (Input.GetKeyUp(KeyCode.F1))
{
currentBattleFactory.CreateHero("Warrior");
}
if (Input.GetKeyUp(KeyCode.F2))
{
currentBattleFactory.CreateHero("Paladin");
}
if (Input.GetKeyUp(KeyCode.F3))
{
currentBattleFactory.CreateWeapon("Sword");
}
if (Input.GetKeyUp(KeyCode.F4))
{
currentBattleFactory.CreateWeapon("Shield");
}
}
}
En el Installer
vamos a serializar las configuraciones de armas y héroes, crearemos las factorías y se la pasaremos al consumidor.
public class GameInstaller : MonoBehaviour
{
[SerializeField] private HeroesConfiguration heroesConfiguration;
[SerializeField] private WeaponsConfiguration weaponsConfiguration;
private Consumer consumer;
private BattleFactory battleFactory;
private void Start()
{
var heroFactory = new HeroFactory(Instantiate(heroesConfiguration));
var weaponFactory = new WeaponFactory(Instantiate(weaponsConfiguration));
var consumerGameObject = new GameObject();
consumer = consumerGameObject.AddComponent<Consumer>();
battleFactory = new BattleFactory(heroFactory, weaponFactory);
consumer.Configure(battleFactory);
}
}
Y desde Unity lo veremos así:
Hasta aquí todo normal, simplemente hemos encapsulado las distintas fabricas en otra más grande, pero imaginemos que ahora queremos hacer los mismos héroes y armas para halloween, navidad, otros festivos, skins, etc.
Aquí realmente es donde vamos a sacar todo el potencial de este patrón.
Mejorando el diseño
En el patrón original crearíamos una fábrica abstracta para cada evento pero con Unity lo que vamos a hacer es crear distintas configuraciones, asignarlas en el Installer y con estas configuraciones crear las distintas fabricas que consumirá el Abstract Factory
, vamos a verlo:
public class GameInstaller : MonoBehaviour
{
[SerializeField] private HeroesConfiguration heroesConfiguration;
[SerializeField] private WeaponsConfiguration weaponsConfiguration;
[SerializeField] private WeaponsConfiguration halloweenWeaponsConfiguration;
private Consumer consumer;
private BattleFactory battleFactory;
private BattleFactory halloweenBattleFactory;
private void Start()
{
var heroFactory = new HeroFactory(Instantiate(heroesConfiguration));
var weaponFactory = new WeaponFactory(Instantiate(weaponsConfiguration));
var halloweenWeaponFactory = new WeaponFactory(Instantiate(halloweenWeaponsConfiguration));
var consumerGameObject = new GameObject();
consumer = consumerGameObject.AddComponent<Consumer>();
battleFactory = new BattleFactory(heroFactory, weaponFactory);
halloweenBattleFactory = new BattleFactory(heroFactory, halloweenWeaponFactory);
consumer.Configure(battleFactory);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Q))
{
consumer.Configure(halloweenBattleFactory);
}
}
Lo que he hecho ha sido añadir una configuración extra para Halloween (lo podría hacer para todas), con esta configuración he creado una factoría especifica de armas de Halloween y una Abstract Factory
también de Halloween.
De primeras he configurado el consumidor con el Abstract Factory
«normal» pero cuando pulse la tecla «Q» cambiaré esta factoría por la de halloween. En un caso real no tendríamos un «pulsar Q», lo que tendríamos es un servidor, como podría ser PlayFab
, que al hacer login nos diría si debemos utilizar la factoría normal o por el contrario debemos utilizar alguna de las especiales.
De vuelta en Unity solo tendríamos que asignar la configuración en el Installer
y ya lo tendríamos funcionado.
Este es el verdadero potencial de este patrón, podemos definir un Abstract Factory
de base y configurarlo con los Factory Method que nos interesen, de esta forma los consumidores no se tienen que preocupar por la factoría que están utilizando, simplemente les inyectamos una y la configuramos previamente.
Si quieres descargar el código de ejemplo que hemos utilizado y probarlo tu mismo aquí te dejo el enlace.
Libros sobre patrones de diseño
- El libro de GANG OF FOUR, patrones de diseño (Erich Gamma)
- Game Programming Patterns (Robert Nystrom – Electronic Arts)
- Agile Principles, Patterns, and Practices in C# (Robert C. Martin)