Patrones de dise̱o РAbstract Factory

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:

Abstract Factory c#

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:

abstract factory Unity

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í:

abstract factory patter unity

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.

curso patrones de software

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

Otras entradas

Resumen
➤ Patrones de diseño - Abstract Factory
Nombre del artículo
➤ Patrones de diseño - Abstract Factory
Descripción
El patrón ABSTRACT FACTORY 🏭 nos permite crear varios objetos de una misma familia de objetos que estén relacionados. Veamos un ejemplo ⤵️.
Autor
Publisher Name
The Power Ups - Learning
Publisher Logo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *