Hoy os traigo el patrón de diseño Decorator, este patrón forma parte de los patrones estructurales y muchas veces se le confunde con el patrón Adapter ya que su diagrama es bastante parecido, pero el problema que intenta resolver es otro.
Este patrón de diseño de software nos proporciona una forma fácil de añadir responsabilidades adicionales a un objeto de forma dinámica y sin tener que modificar este objeto. Además proporciona una alternativa a la herencia para extender su funcionalidad.
Vamos a ver su diagrama:
Tenemos una interfaz de los componentes que queremos decorar y extender su funcionalidad. Nuestro Decorator base implementará esta interfaz para que el consumidor no sepa si está tratando con un decorator o con el componente, ya que utilizará la interfaz directamente.
El Decorador tendría esta forma:
public abstract class Decorator : IComponente {
private IComponente componente;
public Decorator(IComponente componente) { this.componente = componente; }
public virtual void Operacion() { componente.Operacion(); }
}
Simplemente llama al componente para que haga su operación, como si no existiera. Y el DecoratorConcretoA tendría esta otra forma:
public class DecoratorConcretoA : Decorator {
public override void Operacion() {
base.Operacion();
ComportamientoConcreto();
}
private void ComportamientoConcreto() { ... }
}
De esta forma estamos extendiendo la funcionalidad de Componente sin que nadie se entere de esto, y no tengamos que modificar su código, ya que utilizaremos la interfaz para comunicarnos.
Esto nos permitiría también concatenar Decoradores, podemos hacer que un Decorador herede de otro y añada funcionalidad extra, o que esté compuesto por otro Decorador.
Vamos a verlo con un ejemplo concreto para videojuegos:
Extendiendo la funcionalidad con el patrón Decorator
Imaginemos que tenemos un sistema de combate ya hecho con ataques básicos pero ahora nos gustaría añadir ataques elementales. Estos ataques elementales añadirán un daño extra, queremos que se puedan combinar y que se muestre en la UI de daño de un color diferente.
Podríamos heredar del ataque básico y crear una clase por cada ataque y combinación posible, o podríamos utilizar el patrón Decorator, respetar el principio Open-Close de SOLID y extender el sistema sin tener que cambiar absolutamente nada del sistema de combate.
Vamos a ver una versión reducida de lo que podría ser esto en un diagrama:
En naranja tenemos el sistema de combate que ya teníamos, y en azul los nuevos componentes que hemos añadido para extender el sistema y añadir estos ataques elementales. Como veis, gracias al Decorator no hemos tenido que cambiar nada del sistema existente.
Vamos a verlo en código:
Sistema de combate sin Decorator
public interface IDamageReceiver
{
void ReceiveDamage(int damage, Color color);
}
public interface IAttacker
{
void Attack(IDamageReceiver damageReceiver);
}
public class RegularAttacker : IAttacker
{
private readonly int _damage;
public RegularAttacker(int damage) { _damage = damage; }
public void Attack(IDamageReceiver damageReceiver)
{
damageReceiver.ReceiveDamage(_damage, Color.white);
}
}
Un sistema muy básico pero funcional. Vamos a ver ahora como ampliamos esto con ataques elementales.
Patrón Decorator en acción
Lo primero es definir la clase base de los decoradores, esta clase tiene que implementar la interfaz del atacante para que el sistema actual no se vea afectado, y contener un atacante al que vamos a añadir funcionalidad (lo vamos a decorar).
public abstract class AttackerDecorator : IAttacker
{
private readonly IAttacker _attacker;
public AttackerDecorator(IAttacker attacker)
{
_attacker = attacker;
}
public virtual void Attack(IDamageReceiver damageReceiver)
{
_attacker.Attack(damageReceiver);
}
}
Con esto ya podemos crear todos los decoradores específicos que queramos, vamos a ver dos de ellos, ataques de fuego y de tierra:
public class FireAttackerDecorator : AttackerDecorator
{
private readonly int _fireDamage;
public FireAttackerDecorator(IAttacker attacker, int fireDamage) : base(attacker)
{
_fireDamage = fireDamage;
}
public override void Attack(IDamageReceiver damageReceiver)
{
base.Attack(damageReceiver);
damageReceiver.ReceiveDamage(_fireDamage, Color.red);
}
}
public class WoodAttackerDecorator : AttackerDecorator
{
private readonly int _woodDamage;
public WoodAttackerDecorator(IAttacker attacker, int woodDamage) : base(attacker)
{
_woodDamage = woodDamage;
}
public override void Attack(IDamageReceiver damageReceiver)
{
base.Attack(damageReceiver);
damageReceiver.ReceiveDamage(_woodDamage, Color.green);
}
}
Estos ataques llaman a la base para que haga el comportamiento normal, y además añaden su comportamiento extra. Veamos como se consumiria:
public class Consumer : MonoBehaviour
{
[SerializeField] private DamageReceiver _damageReceiver;
private RegularAttacker _regularAttacker;
private AttackerDecorator _fireAttacker;
private AttackerDecorator _woodAttacker;
private AttackerDecorator _fireAndWoodAttacker;
private void Awake()
{
const int damage = 100;
const int fireDamage = 10;
const int woodDamage = 13;
_regularAttacker = new RegularAttacker(damage);
_fireAttacker = new FireAttackerDecorator(_regularAttacker, fireDamage);
_woodAttacker = new WoodAttackerDecorator(_regularAttacker, woodDamage);
_fireAndWoodAttacker = new FireAttackerDecorator(_woodAttacker, fireDamage);
}
private void Update()
{
if (Input.GetKeyUp(KeyCode.F1))
{
_regularAttacker.Attack(_damageReceiver);
}
else if (Input.GetKeyUp(KeyCode.F2))
{
_fireAttacker.Attack(_damageReceiver);
}
else if (Input.GetKeyUp(KeyCode.F3))
{
_woodAttacker.Attack(_damageReceiver);
}
else if (Input.GetKeyUp(KeyCode.F4))
{
_fireAndWoodAttacker.Attack(_damageReceiver);
}
}
}
Como veis con _fireAndWoodAttacker
aquí estamos combinando dos tipos de ataques elementales sin tener que crear una lógica especifica, simplemente un decorador contiene al otro. Las posibilidades de extensión con este patrón de diseño son muchísimas.
Conclusiones
Hemos visto como decorando nuestras clases podemos extender la funcionalidad sin necesidad de cambiar nada del código existente. Además aquí hemos visto solo un pequeñísimo ejemplo pero podríamos hacer cosas como:
Decorar nuestras llamadas al servidor para añadirles un log cuando se envía y se recibe la respuesta. Decorar los botones de la UI para que envíen una analítica a nuestro servidor y poder entender el comportamiento del usuario.
En un documento de texto, las palabras en negrita o cursiva, o ambas, se podrían hacer mediante decoradores que modifiquen el pintado de esta palabra y añadan su funcionalidad.
Como veis, le podemos dar muchos usos distintos, la clave es que nos ayuda a respetar Open-Close y no tenemos que modificar todo el sistema para extenderlo.
Como siempre, os podéis descargar el proyecto de ejemplo desde este enlace.
Fuentes
- 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)