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.