Stop Writing Rigid Code! Master the Decorator Pattern in Go for Ultimate Flexibility

Stop Writing Rigid Code! Master the Decorator Pattern in Go for Ultimate Flexibility

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????

  1. Hardcoding every variation is not scalable.
  2. The code becomes repetitive and difficult to maintain.
  3. There is no flexibility to create custom orders dynamically.

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:

  1. If this ingredient is added on top of something else, it appends the steps to the existing preparation list.
  2. If it’s the first ingredient, it starts the list.
  3. Cost calculation is handled dynamically by summing up all wrapped ingredients.

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!

  • No hardcoding different combinations
  • Easily scalable for new flavours & toppings
  • Follows the Open-Closed Principle (extend behavior without modifying existing code)

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

  1. Logging Middleware (Wrap API requests to log data)
  2. Authentication Middleware (Add security checks dynamically)
  3. Rate Limiting (Restrict API calls per user)
  4. Caching Mechanism (Cache responses dynamically)
  5. Monitoring & Metrics Collection (Track execution time)
  6. Encryption & Compression (Wrap data processing functions)
  7. Feature Flags (Enable/disable features at runtime)
  8. Payment Processing (Add discounts/taxes dynamically)
  9. Data Validation (Validate user input before processing)
  10. Web Scraping Pipelines (Apply transformation layers dynamically)

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!


要查看或添加评论,请登录

Archit Agarwal的更多文章

社区洞察