In our previous post, we leveraged the decorator design pattern to implement a weapon system for our next video game. Thanks to the decorator pattern, we were able to provide additional damage types to our weapons without changing the weapons’ implementation. Adding a new weapon therefore is very easy, and so is adding a new damage type. In this post, we’ll cover a different kind of problem that can be solved by the adapter design pattern. The sample codes are written in Golang, but it should be easy enough to port them to your programming language of choice.

Hypothetical problem

Our game is doing well, and we acquire a new feature request: the weapon system needs to support 3 new damage types Slash, Strike and Pierce (we’re Dark Souls now!). Luckily we don’t need to implement these new damage types as there is an open source library for it. However, the library public API is not compatible with our decorators.

// Open source package that implements the logic to add 
// Slash, Strike and Pierce damage to generic weapons.
package darksouls

type Weapon struct {
    ID string
    Slash int
    Pierce int
    Strike int
}

func NewWeapon(ID string) *Weapon {
    return &Weapon{ID: ID}
}

func (w *Weapon) AddSlash(damage int) {
    w.Slash += damage
}

func (w *Weapon) AddPierce(damage int) {
    w.Pierce += damage
}

func (w *Weapon) AddStrike(damage int) {
    w.Strike += damage
}

func (w *Weapon) Hit() int {
    return w.Slash + w.Strike + w.Pierce
}

The library does not support concrete weapons like Sword, Bow or Staff. Instead it supports a generic Weapon type, which has methods to add slash, pierce and strike damage to itself. To make matters worse, this weapon type does not implement the Damager interface that our decorators expect: to get the total damage, we have to call the weapon’s Hit() instead.

Initial solution

Because the Weapon type does not support the Damager interface, we may have to add an instance of Weapon to our implementation of Sword, Bow and Staff like this:

type Sword struct {
    Dmg int
    DsWeapon *darksouls.Weapon
}

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

This solution works, but it suffers from the same problem as the initial solution in the previous post. We need to add *darksouls.Weapon as a struct property to all of our existing weapons, and modify their Damage() method. This does not only introduce duplications, but also results in more dependencies within our code base. For instance, if we want to change the implementation of Dark Souls damage types, like making it only 80% effective against certain enemies, then we need to modify all Damage() method implementation of our weapons. Furthermore, there are now 2 ways to enhance our weapons: one with the decorators that we cover in the previous post, and the other through a struct property. Needless to say, this increases cognitive load for code readers, and one can make a mistake in enhancing the wrong damage type.

Adapter pattern

Instead of adding *darksouls.Weapon as a struct property of our weapons, we can create an adapter to wrap *darksouls.Weapon, keeping the existing capabilities of this type (adding Slash, Pierce and Strike damage) while also implement an interface that can work well with our existing decorators.

type DarkSoulsAdapter struct {
	*darksouls.Weapon
	baseDamager Damager
}

func NewDarkSoulsAdapter(baseDamager Damager) *DarkSoulsAdapter {
	return &DarkSoulsAdapter{Weapon: darksouls.NewWeapon("ds-weapon"), baseDamager: baseDamager}
}

func (a *DarkSoulsAdapter) Damage() int {
	return a.Hit() + a.baseDamager.Damage()
}

Because the adapter also implements the Damager interface, we can easily combine it with our other decorators to implement another layer of damage enhancer.

func NewDarkSoulsDamage(baseDamager Damager, dmgType string, dmg int) Damager {
	adapter := NewDarkSoulsAdapter(baseDamager)

	switch dmgType {
	case "slash":
		adapter.AddSlash(dmg)
	case "pierce":
		adapter.AddPierce(dmg)
	case "strike":
		adapter.AddStrike(dmg)
	}

	return adapter
}

In our client’s code, this new decorator work along side other decorators very well to enhance our weapons with the existing elemental damages, and dark souls damages.

s := Sword{Dmg: 10}

// adds slash damage
enhancedSword := NewDarkSoulsDamage(&s, "slash", 10)

// adds pierce
enhancedSword = NewDarkSoulsDamage(enhancedSword, "pierce", 25)

// inserts a fire gemstone
enhancedSword = NewElementalDamage(enhancedSword, "fire", 5)

// adds strike damage
enhancedSword = NewDarkSoulsDamage(enhancedSword, "strike", 5)

// inserts an ice gemstone
enhancedSword = NewElementalDamage(enhancedSword, "ice", 5)

What’s next

In this post we learned about the adapter pattern: we were able to wrap an incompatible interface with an adapter, and this adapter implements an interface that is expected by our system. In the next post, we’ll get to know of another pattern that’s closely related to decorator and adapter.