Stop Writing Rigid Code! Master the Decorator Pattern in Go for Ultimate Flexibility
Archit Agarwal
PMTS @ Oracle | Golang | Docker | Kubernetes| Typescript | Node.js | .NET | Angular | AWS Certified Cloud Practitioner | Educator | 2 Lac world wide rank on Leetcode
Imagine you’re a software developer building an ice cream vending machine for Disneyland. The menu was initially fixed?—?customers could only order predefined flavours with no customizations (boring, right? ??). You wrote simple code to calculate costs based on these fixed options.
But this is Disneyland! Nobody wants a plain old scoop. People want triple scoops, extra chocolate sauce, stacked wafer cones, sprinkles, and more. You realize you can’t hardcode every possible combination, or your codebase will become a nightmare! ??
So, how do you handle dynamic customizations without rewriting everything? Enter the Decorator Pattern, a lifesaver for scenarios where you need to dynamically extend an object’s functionality without modifying its structure.
Let’s first explore the problem with a naive approach and then refactor it using the Decorator Pattern for a flexible and scalable solution. ??
The Traditional Approach: Hardcoding Ice Cream?Variants
Imagine our first attempt at coding the vending machine. A simple function creates a Butterscotch Ice Cream:
func CreateButterscotchIcecream(scoops int) IceCream {
ingredients := []IceCreamIngredient{
BASE_PLAIN_CONE,
}
for i := 0; i < scoops; i++ {
ingredients = append(ingredients, FLAVOUR_BUTTERSCOTCH)
}
ingredients = append(ingredients, TOPPING_SPRINKLES)
return Create(CreateIceCreamRequest{Ingrediants: ingredients})
}
Now, we add Vanilla Ice Cream:
func CreateVanillaIcecream(scoops int) IceCream {
ingredients := []IceCreamIngredient{
BASE_PLAIN_CONE,
}
for i := 0; i < scoops; i++ {
ingredients = append(ingredients, FLAVOUR_VANILLA)
}
return Create(CreateIceCreamRequest{Ingrediants: ingredients})
}
This works fine for fixed flavours, but what if customers want to mix and match? What if someone wants vanilla with chocolate chips or a chocolate cone with butterscotch scoops? We can’t keep adding new functions for every combination. ????
The Problem????
Time to refactor using the Decorator Pattern!
Introducing the Decorator Pattern** ???
The Decorator Pattern allows us to dynamically extend an object’s functionality without modifying its structure. Instead of hardcoding different types of ice cream, we use composition over inheritance to wrap functionalities around each other.
Think of your manager adding their name to your presentation after changing the font size?—?that’s the Decorator Pattern at work. ??
Step 1: Define a Common Interface We start by creating an interface that all ice cream components (base, flavours, and toppings) will implement:
type IIceCreamIngredient interface {
GetPreparationSteps() []string
GetCost() int
}
Step 2: Implement the Base Ingredient (Chocolate Cone) Now, let’s implement the Chocolate Cone decorator, which wraps around another ingredient:
type BaseChocolateCone struct {
ExistingIngrediant IIceCreamIngrediant
}
func (ingrediant *BaseChocolateCone) GetPreperationSteps() []string {
step := "Chocolate Cone"
if ingrediant.ExistingIngrediant != nil {
return append(ingrediant.ExistingIngrediant.GetPreperationSteps(), step)
}
return []string{step}
}
func (ingrediant *BaseChocolateCone) GetCost() int {
oldCost := 0
if ingrediant.ExistingIngrediant != nil {
oldCost = ingrediant.ExistingIngrediant.GetCost()
}
return 12 + oldCost
}
? Breakdown:
Step 3: Implement More Decorators Let’s implement a Butterscotch Flavor decorator:
type FlavourButterscotch struct {
ExistingIngrediant IIceCreamIngrediant
}
func (ingrediant *FlavourButterscotch) GetPreperationSteps() []string {
step := "1 scoop Butterscotch"
if ingrediant.ExistingIngrediant != nil {
return append(ingrediant.ExistingIngrediant.GetPreperationSteps(), step)
}
return []string{step}
}
func (ingrediant *FlavourButterscotch) GetCost() int {
oldCost := 0
if ingrediant.ExistingIngrediant != nil {
oldCost = ingrediant.ExistingIngrediant.GetCost()
}
return 5 + oldCost
}
With this setup, we can now chain ingredients dynamically! ??
Step 4: Dynamically Creating Ice Cream Orders ??
Now, let’s take a user’s order and create their dream ice cream dynamically:
func main() {
iceCream := &BaseChocolateCone{
ExistingIngredient: &FlavourButterscotch{
ExistingIngredient: &FlavourVanilla{},
},
}
fmt.Println("Steps to prepare your ice cream:")
for index, step := range iceCream.GetPreparationSteps() {
fmt.Printf("Step %d: %s\n", index+1, step)
}
fmt.Printf("Total Cost: $%d\n", iceCream.GetCost())
}
Output:
Steps to prepare your ice cream:
Step 1: Chocolate Cone
Step 2: 1 scoop Butterscotch
Step 3: 1 scoop Vanilla
Total Cost: $20
? Now, any combination is possible dynamically!
The complete Code can be found on my GitHub https://github.com/architagr/design_patterns/tree/main/golang/structural/decorator
Real-World Use Cases of the Decorator Pattern
Wrapping Up???
The Decorator Pattern is a powerful technique in Go that allows you to dynamically compose behaviours while keeping your codebase clean and scalable. Next time you face a scenario where functionalities need to be added dynamically without modifying the base code think Decorators!
Have you used the Decorator Pattern in your projects? Share your experience in the comments! ??
Stay Connected!
?? Follow me here on LinkedIn for more insights on software development and architecture: https://www.dhirubhai.net/in/architagarwal984/
?? Subscribe to my YouTube channel for in-depth tutorials: https://lnkd.in/gauaRed7
?? Sign up for my newsletter, The Weekly Golang Journal, for exclusive content: https://lnkd.in/g8DzK7Ts
?? Follow me on Medium for detailed articles: https://lnkd.in/gXSMeXxm
???? Join the discussion on my subreddit, r/GolangJournal, and be part of the community!