What Is It?
A Decorator wraps an object, implements the same interface, and forwards calls to it while adding something of its own before or after. Because the wrapper looks exactly like the thing it wraps, you can wrap the wrapper too, building a chain of small, focused layers.
Weapon upgrades are the textbook game case. A BaseSword deals 10 damage. Wrap it in a FireDecorator and GetDamage() asks the sword, then adds burn. Wrap that in a CritDecorator and the whole result gets multiplied. The player’s attack code calls weapon.GetDamage() and has no idea how many layers are underneath.
This beats subclassing when combinations explode. You do not need FirePoisonCritSword; you need four small decorators and the ability to compose them in any order the loot system rolls.
When Is It Used?
Use it for stackable modifiers: weapon enchantments, buff and debuff layers, status effects that alter damage or speed, or logging and caching around a service.
It fits well when modifiers are picked at runtime from data (loot tables, perks, item sockets) rather than known at compile time.
Avoid it when the order of layers must be strictly controlled or when you need to inspect and remove a specific inner layer often. A flat list of modifiers processed by a single calculator is usually simpler for stat systems.
Interactive Demo
Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.
Code
using UnityEngine;
namespace Patterns.Structural.Decorator
{
/// <summary>
/// Builds a weapon by wrapping a base sword in decorators,
/// then attacks through the whole chain with a single call.
/// </summary>
public class WeaponTester : MonoBehaviour
{
private IWeapon weapon;
private void Start()
{
weapon = new BaseSword();
weapon = new FireDecorator(weapon); // +5 burn
weapon = new PoisonDecorator(weapon); // +3 poison
weapon = new CritDecorator(weapon); // x2 outer multiplier
Debug.Log(weapon.Describe()); // Sword + Fire + Poison + Crit
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
Debug.Log($"Hit for {weapon.GetDamage()}"); // (10 + 5 + 3) * 2 = 36
if (Input.GetKeyDown(KeyCode.F))
weapon = new IceDecorator(weapon); // stack another layer at runtime
}
}
}namespace Patterns.Structural.Decorator
{
/// <summary>Component interface shared by the base weapon and every decorator.</summary>
public interface IWeapon
{
int GetDamage();
string Describe();
}
/// <summary>Concrete component: the plain, undecorated weapon.</summary>
public class BaseSword : IWeapon
{
private readonly int baseDamage;
public BaseSword(int baseDamage = 10) => this.baseDamage = baseDamage;
public int GetDamage() => baseDamage;
public string Describe() => "Sword";
}
}namespace Patterns.Structural.Decorator
{
/// <summary>
/// Base decorator. Holds the wrapped weapon and forwards by default,
/// so concrete decorators only override what they change.
/// </summary>
public abstract class WeaponDecorator : IWeapon
{
protected readonly IWeapon inner;
protected WeaponDecorator(IWeapon inner) => this.inner = inner;
/// <summary>The weapon this layer wraps (useful for unwrapping).</summary>
public IWeapon Inner => inner;
public virtual int GetDamage() => inner.GetDamage();
public virtual string Describe() => inner.Describe();
}
}namespace Patterns.Structural.Decorator
{
/// <summary>Adds flat burn damage on top of whatever it wraps.</summary>
public class FireDecorator : WeaponDecorator
{
public FireDecorator(IWeapon inner) : base(inner) { }
public override int GetDamage() => inner.GetDamage() + 5;
public override string Describe() => inner.Describe() + " + Fire";
}
/// <summary>Adds a smaller amount of poison damage.</summary>
public class PoisonDecorator : WeaponDecorator
{
public PoisonDecorator(IWeapon inner) : base(inner) { }
public override int GetDamage() => inner.GetDamage() + 3;
public override string Describe() => inner.Describe() + " + Poison";
}
/// <summary>Adds frost damage (a real game would also apply a slow on hit).</summary>
public class IceDecorator : WeaponDecorator
{
public IceDecorator(IWeapon inner) : base(inner) { }
public override int GetDamage() => inner.GetDamage() + 4;
public override string Describe() => inner.Describe() + " + Ice";
}
/// <summary>Multiplies everything beneath it, so its position in the chain matters.</summary>
public class CritDecorator : WeaponDecorator
{
private readonly int multiplier;
public CritDecorator(IWeapon inner, int multiplier = 2) : base(inner)
=> this.multiplier = multiplier;
public override int GetDamage() => inner.GetDamage() * multiplier;
public override string Describe() => inner.Describe() + " + Crit";
}
}Advantages & Disadvantages
+ Advantages
- Behaviour is added without modifying the wrapped class or creating subclass explosions.
- Layers compose freely and can be added or peeled off at runtime.
- Each decorator does one thing, so it is easy to test in isolation.
− Disadvantages
- Order matters: a crit multiplier applied before or after flat fire damage gives different results, which can surprise designers.
- Removing a layer from the middle of the chain is awkward; you usually rebuild the chain.
- Many tiny wrapper objects make stack traces and debugging noisier.
Tips
- 01Put shared forwarding in an abstract
WeaponDecoratorbase so concrete decorators only override what they change. - 02Decide and document an ordering convention (flat bonuses first, multipliers last) and enforce it when building chains.
- 03Expose a
Describe()method that each layer appends to; it makes tooltips and debugging trivial. - 04Decorators are plain C# objects; keep them out of MonoBehaviours so they are cheap to create when an item is equipped.
- 05For heavy stat systems with dozens of sources, consider a modifier list instead, and keep decorators for behavioural changes like on-hit effects.