Patrones de diseño – Observer

El objetivo principal del patrón Observer es avisarnos cuando se realiza algún cambio sobre el objeto que estamos observando. Este patrón es un patrón de comportamiento, aunque también nos ayuda a desacoplarnos.

Gracias a utilizar abstracciones podremos tener el Observador y el Observado (Sujeto) en capas distintas, lo cual va muy de la mano del principio de Inversión de Dependencias de SOLID.

Diagrama del patrón Observer

Subject: es la interfaz/clase abstracta del objeto que observaremos. Este objeto tiene métodos para suscribirnos a sus cambios y desuscribirnos, además de un método para notificar a todos los observadores.

Observer: es la interfaz del observador, solo tiene un método para avisarle de que se han producido cambios en el Subject (clase observada).

Algo importante a destacar es que el observador solo recibe un aviso de que hay un cambio, pero no recibe el cambio. El observador tiene que preguntarle activamente al Subject por su nuevo estado.

Como siempre, vamos con un ejemplo más concreto y su implementación.

Implementación del Observer pattern

Imaginemos que tenemos una clase Hero a la cual queremos observar para enterarnos de cuando modifica su vida y así actualizar el HUD.

pattern observer

Tendremos las interfaces Subject y Observer que son interfaces genéricas que ya hemos visto arriba.

También tendremos el Subject concreto Hero, y el Observer concreto HealthView. El HealthView está interesado en enterarse de los cambios producidos sobre el Hero.

Vamos a ver primero las interfaces:

public interface Subject
{
    void Subscribe(Observer observer);
    void Unsubscribe(Observer observer);
    void Notify();
}

public interface Observer
{
    void Updated(Subject subject);
}

Ahora vamos a ver sus implementaciones:

    public class Hero : Subject
    {
        public int Health { get; private set; }

        private readonly List<Observer> _observers;
        public Hero()
        {
            _observers = new List<Observer>();
            Health = 100;
            Notify();
        }
        public void Subscribe(Observer observer)
        {
            _observers.Add(observer);
        }

        public void Unsubscribe(Observer observer)
        {
            _observers.Remove(observer);
        }

        public void Notify()
        {
            foreach (var observer in _observers)
            {
                observer.Updated(this);
            }
        }

        public void ApplyDamage(int damage)
        {
            Health -= damage;
            Notify();
        }
    }

Cada vez que realizamos un cambio sobre Health, o cualquier variable, debemos llamar a Notify para avisar a todos los Observers registrados.

Una posible implementación del HealthView en Unity podría ser esta:

public class HealthView : MonoBehaviour, Observer
    {
        [SerializeField] private TextMeshProUGUI _health;

        public void Updated(Subject subject)
        {
            if (subject is Hero hero)
            {
                _health.SetText(hero.Health.ToString());
            }
        }
    }

Recibimos que hay una actualización pero es con la interfaz Subject y la tendremos que castear a Hero para obtener el dato.

¿Qué principal inconveniente presenta esta implementación?

Desde mi punto de vista, no me gusta nada tener que hacer un casting, o preguntar si el subject es el que estoy esperando. Esto lo podemos solucionar de una forma muy «rápida» que es tener una interfaz Subject y Observer por cada grupo de clases que queremos observar.

    public interface IHero
    {
        int Health { get; }
        void Subscribe(HeroObserver heroObserver);
        void Unsubscribe(HeroObserver heroObserver);
        void Notify();
    }

    public interface HeroObserver
    {
        void Updated(IHero subject);
    }

De esta forma el Observer recibirá un IHero al cual le podrá preguntar por su vida directamente:

    public class HealthView : MonoBehaviour, HeroObserver
    {
        [SerializeField] private TextMeshProUGUI _health;

        public void Updated(IHero hero)
        {
            _health.SetText(hero.Health.ToString());
        }
    }

Patrón Observer con Actions o Delegates

Otra forma de implementar este patrón es utilizando Actions o Delegates. En esta implementación no necesitamos la interfaz del observer ya que este se suscribe activamente al Action, ni los métodos de suscripción:

    public interface IHero
    {
        event Action<int> OnHealthUpdated;
    }

    public class Hero : IHero
    {
        private int Health { get; set; }
        public event Action<int> OnHealthUpdated;

        public Hero()
        {
            Health = 100;
            Notify();
        }
        private void Notify()
        {
            OnHealthUpdated?.Invoke(Health);
        }

        public void ApplyDamage(int damage)
        {
            Health -= damage;
            Notify();
        }
    }
    public class HealthView : MonoBehaviour
    {
        [SerializeField] private TextMeshProUGUI _health;

        public void Configure(IHero hero)
        {
            hero.OnHealthUpdated += Updated;
        }

        private void Updated(int health)
        {
            _health.SetText(health.ToString());
        }
    }

Como ves, la implementación es un poco más sencilla. Además podemos añadir parámetros a las Actions para enviar también el dato que ha cambiado.

A diferencia que la primera implementación, con esta podemos crear un Action para cada propiedad que queramos observar, así no tendremos que reaccionar siempre.

UniRx y el patrón Observer

La ultima implementación que te quería mostrar, es la implementación de las propiedades reactivas que incorpora UniRx, librería que te animo mucho a que conozcas.

Con esta implementación, la notificación a los Observers es totalmente automática, cada vez que realicemos un cambio se avisará a todos. Además, cuando los Observers se suscriban, recibirán el valor actual de la propiedad.

    public interface IHero
    {
        ReactiveProperty<int> Health { get; }
    }

    public class Hero : IHero
    {
        public ReactiveProperty<int> Health { get; }

        public Hero()
        {
            Health = new IntReactiveProperty(100);
        }

        public void ApplyDamage(int damage)
        {
            Health.Value -= damage;
        }
    }
    public class HealthView : MonoBehaviour
    {
        [SerializeField] private TextMeshProUGUI _health;

        public void Configure(IHero hero)
        {
            hero.Health.Subscribe(Updated);
        }

        private void Updated(int health)
        {
            _health.SetText(health.ToString());
        }
    }

UniRx además incorpora toda clase de Observers, podemos convertir en Input de Unity en un Observer y no estar preguntando en cada Update si ha habido un cambio.

Esta librería también incorpora las UniTask, que son Task más ligeras y optimas que las corrutinas y las Task de C#, desde luego es una librería para conocer.

Conclusiones

Si estás buscando separar tus datos de cómo se muestran en la vista, el patrón Observer es lo que necesitas. De está forma la vista solo tiene que suscribirse a los cambios y actualizarse según se produzcan.

Te recomiendo que una vez hayas practicado el patrón le des una oportunidad a UniRx, una vez lo pruebes no querrás volver a implementar un patrón Observer a mano.

Puedes descargar todos estos ejemplos en este enlace.

patrones de diseño

Otras entradas

Resumen
➤ Patrones de diseño - Observer
Nombre del artículo
➤ Patrones de diseño - Observer
Descripción
El Patrón OBSERVER nos avisará cuando se realiza algún cambio sobre el objeto que estamos observando 😍, vemos cómo implementarlo con 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 *