The decorator design pattern lets us provide additional behavior to a type. This design pattern is extremely powerful when we want to provide extra functionalities without requiring changes to an existing type, helping us achieve the Open Closed principle. In this post we’ll examine how the decorator design pattern handles a hypothetical problem. The code will be written in Golang, but the idea should be simple enough to port to other programming languages.

Hypothetical Problem

We work in a video game studio, and our next feature is the weapon system for our next big hit - an action RGP (something like Diablo). For the initial development phase, we’ll have to support 3 weapont types: Sword, Bow and Staff.

type Sword struct {
    Dmg int
}

func (s *Sword) Damage() int {
    return s.Dmg
}

type Staff struct {
    Dmg int
}

func (s *Staff) Damage() int {
    return s.Dmg
}

type Bow struct {
    Dmg int
}

func (b *Bow) Damage() int {
    return b.Dmg
}

Each weapon will have its own base damage value, and can be further enhanced via different ways. Our character can increase weapon damage by inserting gemstone to a weapon, or putting skill points in different weapon masteries. For example, a warrior can insert a thunder gemstone into his sword, which increases its damage by the gemstone damage value; a rogue can put a skill point in bow mastery, which increases bow damage by 10 points.

Initial Solution

The most straight forward and naive solution is to add some properties to our structs to reflect the new damage categories.

// sample code to add ThunderDmg and MasteryDmg to Sword
// we need to duplicate these fields to Bow and Staff as well
type Sword struct {
    Dmg int
    ThunderDmg int
    MasteryDmg int
}

func (s *Sword) Damage() int {
    return s.Dmg + s.ThunderDmg + s.MasteryDmg 
}

There are a few issues here. Firstly, some codes are duplicated in Sword, Bow and Staff. While it’s generally ok to have these duplications at the initial stage of the project, it could pose a potential risk for maintenance and readability when the code base grows larger. Secondly, adding a new damage type means modifying all weapon types and their damage calculation. For instance, if we want fire damage, we will need to add FireDmg to all of the 3 weapon structs, and modify their Damage() implementation.

Decorator pattern

One solution is to leverage the decorator pattern. It is possible to split up the implementation of adding extra damage, and the implementation of our weapons. We can start with two decorators, one for elemental damage types like fire and lightning, the other for weapon mastery damage

// all our weapons and decorators will need to satisfy Damager interface
type Damager interface {
    Damage() int
}

type elementalDamage struct {
    baseDamager Damager
    element string
    dmg int
}

// this wraps an existing Damager with some elemental damage
func NewElementalDamage(baseDamager Damager, element string, dmg int) Damager {
    return &elementalDamage{baseDamager: baseDamager, element: element, dmg: dmg}
}

func (d *elementalDamage) Damage() int {
    return d.baseDamager.Damage() + d.dmg
}

type weaponMasteryDamage struct {
    baseDamager Damager
}

// this wraps an existing Damager with some damage thanks to adding skill points to weapon mastery
func NewWeaponMasteryDamage(baseDamager Damager) Damager {
    return &weaponMasteryDamage{baseDamager: baseDamager}
}

func (d *weaponMasteryDamage) Damage() int {
    return d.baseDamager.Damage() + 10
}

We introduce the Damager interface, which is the center point of the implemetation. As long as a type implements this interface, it can be decorated with NewElementalDamage and NewWeaponMasteryDamage to add extra damage without modifying the base damager. An example of how the client can use this code can be seen as below

// we start with a basic short sword
shortSword := &Sword{Dmg: 5}

// we insert a thunder gemstone into the sword
thunderSword := NewElementalDamage(shortSword, "thunder", 10)

// we put a skill point into sword mastery upon level up
masteredThunderSword := NewWeaponMasteryDamage(thunderSword)

// we insert a fire gemstone into the sword
masteredElementalSword := NewElementalDamage(masteredThunderSword, "fire", 20)

// we can calculate the total damage of our weapons by calling the following method
// masteredElementalSword.Damage()

With this implementation, we can compose different weapons, giving them extra capabilities without modifying their implementation. In addition, adding a new elemental damage type like fire only requires changes in elementalDamage while Sword, Bow and Staff are unaffected. Similarly, adding a new weapon type, Gun for example, will not affect the damage calculation of elemental damage, and does not require us to put a number of extra fields to Gun struct.

Caveats

The decorator pattern is not a silver bullet. It’s very easy to get lost within multiple layers of decorators especially when debugging. Having too many mini decorators results in more cognitive load for the readers. In our example, it may be very tempting to have a separate decorator for fire damage and thunder damage instead of having a single one. To use the decorator pattern effectively, it’s highly recommended to think more about the design upfront, and make the decision whether or not to have multiple layers of similar decorators.

What’s next

In this post, we examine a programming problem that can be solved by the decorator design pattern. In our next post, we’ll examine a close cousin of the decorator pattern and see how it helps us deal with a different type of problem.