Patrones de diseño – Strategy

Hoy vamos a ver uno de los patrones de diseño que más utilizamos sin saber qué lo estamos utilizando, os hablo del patrón Strategy, que los modernos decidimos renombrarlo a inyección de dependencias, pero es exactamente lo mismo.

Este patrón es un patrón de comportamiento, nos ayuda a definir comportamientos, algoritmos y cómo comunicarlos.

En concreto el Strategy pattern nos permite definir una familia de algoritmos, o clases, de forma que sean intercambiables entre sí, y que el consumidor de estos no sepa si está utilizando el algoritmo 1, el 2, o el que sea, y esto lo conseguimos gracias a abstracciones, a interfaces 🤩.

Diagrama del patrón Strategy

Strategy pattern

Strategy es la interfaz común a todas las estrategias intercambiables que tendremos, esta define los puntos de entrada a nuestro sistema/algoritmo.

ConcreteStrategyA y B son las estrategias concretas que vamos a definir, y que serán intercambiables entre sí.

Aquí la magia realmente está en cómo hacemos para que Consumer reciba la estrategia concreta sin percatarse de cuál está utilizando. Si estamos utilizando C# puro es tan fácil como pasarle la interfaz en el constructor, otra opción sería tener un método para asignar la instancia.

En Unity, y en los MonoBehaviour, no podemos utilizar el constructor, por lo que estamos obligados a utilizar un método de configuración.

A lo largo de los posts y vídeos sobre patrones, me habréis visto utilizar más de una vez una clase Installer que no hace otra cosa que resolver las dependencias e inyectar las estrategias.

Esta es una opción, otra opción sería utilizar frameworks como Zenject, que se encargan de hacer esto de forma casi automática, pero siempre va bien saber hacerlo a mano para conocer qué hay por debajo.

Strategy pattern con Unity

Vamos a ver ahora otro ejemplo, y el que implementaré en código:

En este diagrama tenemos una clase Hero, el Consumer, que recibirá una Weapon y no sabrá si es una Sword o Bow. Esta es la magia del patrón Strategy, que el consumidor no necesita conocer los detalles de la implementación.

Para que esto sea transparente al Hero, necesitaremos de otro colaborador que haga la inyección, el Installer:

public class Installer : MonoBehaviour
    {
        [SerializeField] private Hero _heroPrefab;
        [SerializeField] private Sword _swordPrefab;
        [SerializeField] private Bow _bowPrefab;

        [SerializeField] private bool _useSword;

        private void Awake()
        {
            var hero = Instantiate(_heroPrefab);
            var sword = GetWeapon(hero.transform);
            hero.SetWeapon(sword);
        }

        private Weapon GetWeapon(Transform parent)
        {
            if (_useSword)
            {
                return Instantiate(_swordPrefab, parent);
            }

            return Instantiate(_bowPrefab, parent);
        }
    }

En este caso decidimos con un bool que arma utilizar, pero aquí podríamos añadir una configuración, o hacer que cuando el usuario cambie de arma desde el inventario, esto llame a SetWeapon del Hero.

La familia de estrategias

    public interface Weapon
    {
        void Attack();
    }
    public class Sword : MonoBehaviour, Weapon
    {
        [SerializeField] private Transform _damageZoneCenter;
        [SerializeField] private float _damageZoneRadius;
        private readonly Collider2D[] _hitColliders = new Collider2D[10];
        
        public void Attack()
        {
            var size = Physics2D.OverlapCircleNonAlloc(_damageZoneCenter.position, _damageZoneRadius, _hitColliders);

            for (var i = 0; i < size; i++)
            {
                var hitCollider = _hitColliders[i];
                var hero = hitCollider.GetComponent<Damageable>();
                hero?.DoDamage(10);
            }
        }
    }

En el caso de la espada, he decidido hacer un overlap y comprobar si hay algún Damageable al que aplicarle daño, obviamente esto solo es un ejemplo sencillo, lo importante es que aquí tenemos una estrategia para hacer daño con esta arma.

En el caso del arco, lo que haré será instanciar una flecha y esta al colisionar aplicará daño.

    public class Bow : MonoBehaviour, Weapon
    {
        [SerializeField] private GameObject _arrowPrefab;
        [SerializeField] private Transform _spawnReference;
        
        public void Attack()
        {
            var arrow = Instantiate(_arrowPrefab, _spawnReference.position, _spawnReference.rotation);
            
        }
    }

    [RequireComponent(typeof(BoxCollider2D))]
    [RequireComponent(typeof(Rigidbody2D))]
    public class Arrow : MonoBehaviour
    {
        [SerializeField] private float _speed;

        private void Awake()
        {
            gameObject.GetComponent<Rigidbody2D>().velocity = transform.right * _speed;
        }

        private void OnTriggerEnter2D(Collider2D other)
        {
            var hero = other.GetComponent<Damageable>();
            hero?.DoDamage(10);

            Destroy(gameObject);
        }
    }

Con esto ya tenemos la familia lista, solo nos queda implementar el Hero y hacer que todo esto le sea indiferente.

Aplicando el patrón Strategy al Hero

    public interface Damageable
    {
        void DoDamage(int damage);
    }

    public class Hero : MonoBehaviour, Damageable
    {
        private Weapon _weapon;

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                _weapon.Attack();
            }
        }

        public void DoDamage(int damage)
        {
            Debug.Log("Damage received");
        }

        public void SetWeapon(Weapon weapon)
        {
            _weapon = weapon;
        }
    }

Aquí el Hero recibe una Weapon, la interfaz, y no le importa si es una Sword o un Bow, por lo que podemos intercambiarlos.

Está claro que tendríamos que añadir las animaciones y los tiempos de cooldown, pero esto sería común a la familia de armas, a las estrategias.

Conclusión

Este patrón me parece crucial para tener un proyecto mantenible y que soporte cambios sin mucho esfuerzo. Además es un fiel aliado del principio de Inversión de Dependencias (DIP) de SOLID.

El Strategy nos permitirá cambiar entre clases, estrategias, que sigan una misma interfaz sin que el consumidor se percate del cambio, o de cuál está utilizando.

Esto lo podemos aplicar por ejemplo a la conexión con el servidor, podemos tener una estrategia para utilizar una API y con otra estrategia utilizar otra distinta. Al consumidor le dará igual porque estará utilizando la interfaz.

Puedes descargar el ejemplo que hemos utilizado en este enlace.

patrones de diseño

Otras entradas

Resumen
➤ Patrones de diseño - Strategy
Nombre del artículo
➤ Patrones de diseño - Strategy
Descripción
🚀 Patrón STRATEGY nos permite intercambiar una familia de algoritmos sin que el consumidor se percate del cambio, lo implementamos en Unity.
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 *