Join us
@vansika_gupta ă» Dec 13,2022 ă» 5 min read ă» 708 views
Peek into how we can make use of design patterns using Go as a language with the help of our very own Strategy pattern.
The reason why thereâs so much popularity around design patterns is that it lets you leverage the wisdom of other brilliant developers that have come up with tried and tested ways to resolve the same design problems.
Instead of code reuse, with patterns you get experience reuse.
Let me also tell you that this is going to be my first blog on medium and start of a series where Iâll be sharing my journey of learning some popular design patterns with the Go language.
Design patterns, at core, are an Object-Oriented discipline. Although, Go is not truly and strictly object-oriented, patterns are still useful.
Thereâs no concept of Inheritance in Go
Encapsulation is rather weak, no classes but we have structs
No IS-A, but there is HAS-A
We need to understand that Design patterns are not like libraries or framework which we can plug-in our application. They teach us the way to structure our application for easier and better understandability and maintainability.
So, we need to truly understand them first in order to leverage their benefits.
Without further ado, letâs dive into our first pattern, the Strategy pattern.
Strategy Pattern
Iâll give the definition later, letâs first build some understanding. You can find all this code here, but please go there after reading this through. : )
In our example, we have two groups in a war-game: Zombies and Vigilantes.
Vigilantes job is to kill Zombies and to do so, we are providing them swords with which they can slay the Zombies.
Hereâs some code for it:
type Vigilante struct {
Name string
}func (v Vigilante) KillWithSword(){
//use sword to slay
fmt.Printf("%s slayed with a sword", v.Name)
}func (v Vigilante) Introduction(){
//introduces the Vigilante character
fmt.Printf("Hey there!! %s is here to save you from Zombies", v.Name)
}
So we have the Vigilante struct with some characteristics like name and defined actions like introducing itself and killing the zombies.
Later we decide to empower our Vigilantes with more weapons like Guns that can fire and Grenades to be thrown. Now they have multiple weapons that are designed to kill but in different ways.
So in programming lingo, we can say that the implementation differs but is designed for the same behavior, that is, to kill.
We can do this by adding more methods similar to KillWithSword() like KillWithGun() and KillWithGrenade(). But later there can be more weapons added and few might be removed. We are not yet sure. With each change, we will need to alter our working package for Vigilantes again and again. Some mistake might break a feature that was just fine so far. Compromises maintainability.
The second concern is that we plan to provide the same weapons to the Zombies at the higher levels of the war-game. With the existing structure, we will need to write the same code for KillWithSword(), KillWithGun() and KillWithGrenade() in the Zombies package as well.
That is poor code re-use.
What we are going to adopt now is an important design principle:
Segregation of what will vary from what is not going to change
After looking closely at the code, we realize that for our use-case, the state like name and the action like Introduction is not going to change in the near future, but the act of killing with a weapon will keep changing with the addition of more and more weapons.
So we are going to take the varying weapons out of the Vigilante package and define an interface Weapon{} that will have a method useWeapon(). Vigilantes only need to be aware of this new method and not the specifics like whether they are slaying, firing or throwing the weapon. They are just using it. Thatâs all.
The code would now look like this:
//package vigilante
type Vigilante struct {
Name string
Weapon Weapon
}func (v Vigilante) Introduction(){
//Introduces the Vigilante character
fmt.Printf("Hey there!! %s is here to save you from Zombies", v.Name)
}//package weapon
type Weapon interface {
UseWeapon()
}
We have a reference to the Weapon interface in our Vigilante struct.
Letâs create some weapons that will implement the Weapon interface
//package weapon
type SwordWeapon struct {
Length int
}func (s SwordWeapon) UseWeapon() {
//define slaying action
fmt.Println("Slay with a sword")
}type GunWeapon struct {
RangeMM int
}func (gn GunWeapon) UseWeapon() {
//define fire action
fmt.Println("Fire with a gun")
}type GrenadeWeapon struct {
ImpactRadius int
}func (gr GrenadeWeapon) UseWeapon() {
//define throw action
fmt.Println("Throw a grenade")
}
The implementation details of how to use a particular kind of weapon are now not the headache of the Vigilante. It doesnât mind which weapon it is.
To see this in action, we will create our first Vigilante character AlexZ đ„
//package main
func main() {
weapon := weapon.GunWeapon{10}
alexZ := vigilante.Vigilante{Name: "AlexZ", Weapon: weapon}
alexZ.Weapon.UseWeapon()
}//Output:
Fire with a gun
Since the weapons are now in their own package, they can be used by the Zombies as well as any other group we decide to add to our war game later.
In addition to this reusability, we are also getting a lot of flexibility in using the weapon of our choice. With this design, we can very easily interchange the weapons at run-time.
Letâs quickly see how:
//package main
func main() {
weapon := weapon.GunWeapon{10}
alexZ := vigilante.Vigilante{Name: "AlexZ", Weapon: weapon}
alexZ.Weapon.UseWeapon() swordWeapon := weapon.SwordWeapon{10}
alexZ.Weapon = swordWeapon
alexZ.Weapon.UseWeapon() grenadeWeapon := weapon.GrenadeWeapon{10}
alexZ.Weapon = grenadeWeapon
alexZ.Weapon.UseWeapon()
}//Output:
Fire with a gun
Slay with a sword
Throw a grenade
My dear friend, that is run-time polymorphism for you :)
Polymorphism allows us to perform a single action in different ways.
Note that we do alexZ.Weapon.UseWeapon() in order to use the weapon, no matter what weapon it is. As I said earlier, this design provides us with the flexibility to switch between the various implementations for a single behavior.
This was the Strategy Pattern in go.
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Another example where we can adopt the Strategy pattern:
Behavior: evict entries from cache
Family of algos: LRU, FIFO, LFU
If you have read so far, please take a moment to show some support.
Put down some thoughts or if you like this post, please do click on that đ icon.
Thanks for reading!!
Join other developers and claim your FAUN account now!
Influence
Total Hits
Posts
Only registered users can post comments. Please, login or signup.