DEV Community

Cover image for Decorator Patterns in Go
Fatih
Fatih

Posted on

Decorator Patterns in Go

Introduction

The decorator pattern is a software design pattern that lets you add additional functionality on top of existing logic. The first thing that comes to people’s minds to tackle this is using inheritance — which makes complete sense. However, inheritance is inherently static. If you have multiple variations of additional functionality, or worse, various combinations of them, you would then have to create all the possible combinations as separate classes that extend the base class. In these cases, your codebase quickly grows in size and becomes less maintainable.

The Decorator Pattern

Implementing the decorator pattern requires the base logic you want to extend to implement a base interface — a single contract that defines what methods it provides and what they produce. You can then create classes that implement this interface while containing another internal object that also implements the same interface. The logic inside the interface methods you override can reuse the implementation of the internal object while adding additional behavior — or even ignore the base logic completely if desired. Moreover, it’s entirely possible to wrap any class that already has extended logic, allowing the decorator to add even more functionality on top of it.

package main

import "fmt"

type VanillaIceCream struct{}

func (v *VanillaIceCream) GetIceCream() string {
    return "Vanilla IceCream"
}
Enter fullscreen mode Exit fullscreen mode

Let’s say, Ben and Mike each wants acustom configuration of their ice cream, Ben prefers Vanilla ice cream with Chocolate Frostings on top of them while Mike prefers Vanilla ice cream with Chocolate Frostings & Caramel Sauce (not even sure if that’s a thing). The inheritance way of solving this would be to create an entirely new class that satisfies the combination required. Hence, the additional classes:

// ...
type VanillaIceCreamWithChocolateFrostings struct{
    VanillaIc VanillaIceCream
}

func (v *VanillaIceCreamWithChocolateFrostings) GetIceCream() string {
    return v.VanillaIc.GetIceCream() + " with Chocolate Frostings"
}

type VanillaIceCreamWithChocolateFrostingsWithCaramelSauce struct{
    VanillaIcWithChoco VanillaIceCreamWithChocolateFrostings 
}

func (v *VanillaIceCreamWithChocolateFrostingsWithCaramelSauce) GetIceCream() string {
    return v.VanillaIcWithChoco.GetIceCream() + " with Chocolate Frostings and Caramel Sauce"
}
// ...
Enter fullscreen mode Exit fullscreen mode

Then, another person, John, joins them and wants a Vanilla ice cream with only Caramel Sauce.

// ...
type VanillaIceCreamWithCaramelSauce struct{
    VanillaIc VanillaIceCream
}

func (v *VanillaIceCreamWithCaramelSauce) GetIceCream() string {
    return v.VanillaIcWithChoco.GetIceCream() + " with caramel sauce"
}
// ...
Enter fullscreen mode Exit fullscreen mode

Now, think about even more toppings you could add and follow the same approach we’ve used so far. You can already imagine how large the codebase will become if this continues. With the decorator pattern, you simply define an interface—one that returns the actual flavors of the ice cream class implementation—and let your decorator classes adhere to it.

package main

import "fmt"

type IIceCream interface {
    GetIceCream() string
}

type VanillaIceCream struct{}

func (v *VanillaIceCream) GetIceCream() string {
    return "Vanilla IceCream"
}

type ChocolateFrostingDecorator struct {
    IceCream IIceCream
}

func (c *ChocolateFrostingDecorator) GetIceCream() string {
    return c.IceCream.GetIceCream() + " with Chocolate Frosting"
}

type CaramelSauceDecorator struct {
    IceCream IIceCream
}

func (c *CaramelSauceDecorator) GetIceCream() string {
    return c.IceCream.GetIceCream() + " with Caramel Sauce"
}

func main() {
    iceCreamOne := &VanillaIceCream{}
    iceCreamOneWithChocolateFrosting := &ChocolateFrostingDecorator{IceCream: iceCreamOne}
    iceCreamOneWithChocolateFrostingWithCaramelSauce := &CaramelSauceDecorator{IceCream: iceCreamOneWithChocolateFrosting}
    fmt.Println("iceCream1: " + iceCreamOneWithChocolateFrostingWithCaramelSauce.GetIceCream())

    iceCreamTwo := &VanillaIceCream{}
    iceCreamTwoWithChocoFrosting := &ChocolateFrostingDecorator{IceCream: iceCreamTwo}
    iceCreamTwoWithChocoFrWithCaramelSauce := &CaramelSauceDecorator{IceCream: iceCreamTwoWithChocoFrosting}
    fmt.Println("iceCream2: " + iceCreamTwoWithChocoFrWithCaramelSauce.GetIceCream())

    iceCreamThree := &VanillaIceCream{}
    iceCreamThreeWithCaramelFrostingOnly := &CaramelSauceDecorator{IceCream: iceCreamThree}
    fmt.Println("iceCream3: " + iceCreamThreeWithCaramelFrostingOnly.GetIceCream())
}
Enter fullscreen mode Exit fullscreen mode

In the event that someone else wants the same customizations but on a banana ice cream instead, as long as the Banana IceCream class implements the base interface, the existing decorators will work on them just fine.

// ...

type BananaIceCream struct{}

func (b *BananaIceCream) GetIceCream() string {
    return "Banana IceCream"
}

// ...

func main() {
    // ...

    iceCreamFour := &BananaIceCream{}
    iceCreamFourWithCaramelSauceOnly := &CaramelSauceDecorator{IceCream: iceCreamFour}
    fmt.Println("iceCream4: " + iceCreamFourWithCaramelSauceOnly.GetIceCream())
}
Enter fullscreen mode Exit fullscreen mode

Let’s increase the complexity of the problem and consider a case where the existing flavors need to be stacked in a different order. With inheritance, you would end up doubling or even tripling the amount of existing code. The decorator pattern enables solutions such as accepting a stack of flavors and looping through each of them—perhaps recursively—and applying the appropriate decorator using a switch-case statement at each step.

Summary

  • Exploring design patterns can be a good thing to avoid common pitfalls of writing large scale projects.
  • Decorator patterns help you stack additional logic on top of existing code in a more maintainable way.

References

  1. Decorator Pattern by Refactoring Guru

Top comments (0)