Patrones de diseño – Template Method

El cometido principal del patron Template Method, o plantilla, es definir un esqueleto común para un algoritmo y dejar los detalles de implementación para los hijos.

Esto lo que nos va a permitir es teniendo un esqueleto, podremos cambiar su comportamiento sobrescribiendo los detalles mediante herencia, pero solo los detalles, el comportamiento general estará definido en el padre.

Primer ejemplo del Template Method

Imaginemos que estamos definiendo el esqueleto, el cuerpo, de cómo van a ser nuestros juegos. Pongamos como ejemplo un juego por turnos.

Podríamos tener un método publico StartGame que desencadene toda la lógica y dentro de este llamar a InitializeGame y luego ir llamando a MakePlay mientras queden turnos que jugar.

Todos estos métodos serían abstractos y protected para que los puedan sobrescribir los hijos y así cada tipo de juego tener su propia lógica independiente.

template method c#

En este ejemplo tenemos la clase Game que es abstracta y los hijos que serían Chess y Checkers que sobrescribirian los métodos abstractos para definir sus propias reglas, pero el cuerpo principal del algoritmo lo tenemos en la clase padre.

Ejemplo de Hero con Template Method

Vamos con el segundo ejemplo y el que implementaremos. Vamos a definir el esqueleto de cómo queremos que se comporten los héroes en nuestro juego.

Como antes, creamos una clase base abstracta, el Hero, y en ella definimos unos métodos de entrada, Attack y ReceiveDamage, y una serie de métodos abstractos que tendrán que sobrescribir los hijos para implementar los detalles.

Template method pattern

En el método Attack queremos que antes de llamar a DoAttack preguntemos a CanAttack si podemos realizar el ataque, y estos métodos los queremos delegar en los hijos para que añadan sus condiciones.

public abstract class Hero : MonoBehaviour
{
        public void Attack()
        {
            if (CanAttack())
            {
                DoAttack();
            }
        }

        protected abstract bool CanAttack();
        protected abstract void DoAttack();
}

En el método ReceiveDamage lo que vamos a hacer es aplicar el daño en la clase Hero porque es común a todos los héroes, y entonces avisaremos a los hijos de que han recibido daño y llamaremos a un evento también para notificar a los colaboradores que estén interesados.

public abstract class Hero : MonoBehaviour
    {
        protected int CurrentHealth = 100;
        public event Action OnDamageReceived;

        public void ReceiveDamage(int damage)
        {
            var isDead = ApplyDamage(damage);
            DamageReceived(isDead);
            NotifyDamageReceived();
        }

        private bool ApplyDamage(int damage)
        {
            CurrentHealth -= damage;
            if (CurrentHealth > 0)
            {
                return false;
            }

            CurrentHealth = 0;
            return true;
        }

        protected abstract void DamageReceived(bool isDead);

        private void NotifyDamageReceived()
        {
            OnDamageReceived?.Invoke();
        }
}

Ahora solo nos queda sobrescribir los detalles en los hijos y que cada uno añada sus peculiaridades.

Implementando los detalles en los hijos

En nuestro Warrior hemos decidido que si la vida está por debajo de 30 no puede atacar, para esto solo tenemos que sobrescribir el método CanAttack.

Los métodos Attack y DamageReceived también los sobrescribiremos.

public class Warrior : Hero
    {
        protected override bool CanAttack()
        {
            if (CurrentHealth >= 30)
            {
                return true;
            }
            
            Debug.Log("Estoy muy débil, no puedo atacar");
            return false;
        }

        protected override void DoAttack()
        {
            Debug.Log("Ya veras el mazazo que te voy a dar...");
        }

        protected override void DamageReceived(bool isDead)
        {
            Debug.Log("Bah! Eso no es nada");
            if (isDead)
            {
                Debug.Log("O igual sí...");
            }
        }
}

Nuestro Archer será similar pero con sus peculiaridades:

public class Archer : Hero
    {
        protected override bool CanAttack()
        {
            return true;
        }

        protected override void DoAttack()
        {
            Debug.Log("Come flechas!!!");
        }

        protected override void DamageReceived(bool isDead)
        {
            if (isDead)
            {
                Debug.Log("Me muerooooo");
                return;
            }
            Debug.Log("Pues vale");
        }
 }

El consumidor del patrón Template Method

Para el consumidor va a ser totalmente transparente que tipo de héroe tenga, no le va a importar si es un Warrior o un Archer, lo cual facilita bastante el uso de estos héroes.

public class Consumer : MonoBehaviour
    {
        [SerializeField] private Hero hero1;
        [SerializeField] private Hero hero2;

        private void Awake()
        {
            Debug.Log("---------");
            Do(hero1, "Hero 1");
            Debug.Log("---------");
            Do(hero2, "Hero 2");
        }

        private static void Do(Hero hero, string heroName)
        {
            hero.OnDamageReceived += () => { Debug.Log($"{heroName} recibe daño"); };
            hero.ReceiveDamage(10);
            hero.Attack();
            hero.ReceiveDamage(70);
            hero.Attack();
            hero.ReceiveDamage(100);
        }
}

Si en hero1 asignamos un guerrero, y en hero2 el arquero, la salida que obtenemos es esta:

---------
Bah! Eso no es nada
Hero 1 recibe daño
Ya veras el mazazo que te voy a dar...
Bah! Eso no es nada
Hero 1 recibe daño
Estoy muy débil, no puedo atacar
Bah! Eso no es nada
O igual sí...
Hero 1 recibe daño
---------
Pues vale
Hero 2 recibe daño
Come flechas!!!
Pues vale
Hero 2 recibe daño
Come flechas!!!
Me muerooooo
Hero 2 recibe daño

Conclusiones

Hemos conseguido que todos los héroes se comporten igual pero que cada uno se encargue de sus detalles concretos.

Esto también le ha permitido al consumidor tratar al guerrero y al arquero como un héroe sin importar su especialización.

El gran punto a favor del Template Method es que definimos 1 vez cómo va a ser ese algoritmo, sistemas o lo que sea, hacemos el esqueleto, es el padre quien decide a qué se va a llamar y en qué orden, y después nos olvidamos, son los hijos los que implementan los detalles que al padre no le importan.

Como contrapartida de este patrón, al utilizar herencia nos estamos exponiendo a incumplir el principio solid de Liskov, y si no vamos con cuidado podríamos acabar con árboles de herencia muy grandes.

Puedes descargarte el código utilizado aquí desde este enlace.

Otras entradas

Resumen
➤ Patrones de diseño - Template Method
Nombre del artículo
➤ Patrones de diseño - Template Method
Descripción
➤ El TEMPLATE METHOD define un esqueleto común para un algoritmo y dejar los detalles de implementación para los hijos.
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 *