In the previous post, we converted an incompatible interface ino another that fits nicely with our existing design thanks to the adapter pattern. We were able to design a weapon system that allows composing different damage types to different weapons, and also able to incorporate a third party library to add a specific type of weapon damage. While the exising solution works, it’s not very clean due to a few reasons. Firstly, the decorator is exposed, and it takes 3 parameters to call such decorators as NewElementalDamage
. Note that the second parameter is actually an enum (we use string
for simplicity), which means client code will need to check which enum values are available here. Secondly, decorating elemental damage is different from decorating dark souls damage. In fact, when we add more decorators, our client code needs to know the different ways to invoke, and this is a problem of consistency.
To make the interface of the weapon system clearer to client code and friendlier to use, we can provide a better abstraction by hiding the unnecessary details, and deliver a set of more intuitive and consistent functions/methods/types to use. The act of simplifying and unifying a complex interface can be done with a facade.
Facade Pattern
We can hide all these complications of enhancing weapon with different damage types behind an Enhancer
type.
type Enhancer struct {
baseDamager Damager
}
func NewEnhancer(baseDamager Damager) *Enhancer {
return &Enhancer{
baseDamager: baseDamager,
}
}
We define a custom type EnhanceFunc
to illustrate the processes of weapon enhancement, and add a method to Enhancer
to apply these processes.
type EnhanceFunc func(d Damager) Damager
func (e *Enhancer) Enhance(fns ...EnhanceFunc) Damager {
for _, fn := range fns {
e.baseDamager = fn(e.baseDamager)
}
return e.baseDamager
}
Then we define different enhancements via these functions.
func WithFire(dmg int) EnhanceFunc {
return func(d Damager) Damager {
return NewElementalDamage(d, "fire", dmg)
}
}
func WithIce(dmg int) EnhanceFunc {
return func(d Damager) Damager {
return NewElementalDamage(d, "ice", dmg)
}
}
func WithEarth(dmg int) EnhanceFunc {
return func(d Damager) Damager {
return NewElementalDamage(d, "earth", dmg)
}
}
func WithThunder(dmg int) EnhanceFunc {
return func(d Damager) Damager {
return NewElementalDamage(d, "thunder", dmg)
}
}
func WithSlash(dmg int) EnhanceFunc {
return func(d Damager) Damager {
adapter := NewDarkSoulsAdapter(d)
adapter.AddSlash(dmg)
return adapter
}
}
func WithPierce(dmg int) EnhanceFunc {
return func(d Damager) Damager {
adapter := NewDarkSoulsAdapter(d)
adapter.AddPierce(dmg)
return adapter
}
}
func WithStrike(dmg int) EnhanceFunc {
return func(d Damager) Damager {
adapter := NewDarkSoulsAdapter(d)
adapter.AddStrike(dmg)
return adapter
}
}
In our client code, it looks much simpler and consistent to enhance weapon via the enhancer instead of the decorators and adapters that we did in our previous posts.
smallSword := Sword{
Dmg: 10,
}
enh := NewEnhancer(&smallSword)
enhancedSmallSword := enh.Enhance(
WithEarth(5), // inserts an earth gemstone
WithFire(5), // inserts a fire gemstone
WithSlash(10), // puts a skill point in slash mastery
WithStrike(10), // puts a skill point in strike mastery
)
All the implementation details of decorator and adapter patterns are encapsulated by these enhancement functions thanks to the Enhancer
acting as a facade. Our weapon public interface looks much nicer, and more intuitive to use since the enhancement functions all follow the same pattern: a keyword With
followed by the damage type, and an integer as the damage value.
Decorator vs. Adapter vs. Facade
It’s easy to realize that the decorator pattern, adapter pattern and facade pattern are similar to each other as they all wrap an existing type. One may ask why we need to separate them into 3 different concepts while what they do are essentially the same. The truth is that they wrap types for different purposes.
- A decorator pattern wraps a type to give it extra responsibility and behavior. In our example, each damage type is a new responsibility, which is supported by a separate decorator
- An adapter wraps a type to provide an interface that is expected by the client. In our example, the dark souls interface is incompatible with our decorators; therefore, we wrap it with an interface that work well with our existing decorators.
- A facade simplifies/unifies a complex interface. In our example, we provide a weapon enhancer as a facade to hide all the details that may distract the client, resulting in a more consistent interface for client code to work with.
Conclusion
Decorator, adapter and facade design patterns are very powerful and can help us solve different problems in a more elegant way. Throughout this 3 part series, we learned how to design a robust and flexible weapon system of a video game, evolving from naive solutions to more sophisticated ones. Nevertheless, it must be emphasized that design patterns are not silver bullets. There are trade offs to be made when applying a design pattern, and there will be situations where a design pattern does not make sense. However useful these design patterns are, just as with any other tools in the box, we need to know when to use them, and more importantly learn to be not afraid of replacing them when the job requirement changes.