Decorator Patterns In Go

Introduction

The decorator pattern is a software design pattern that lets you add more functionality on top of an existing logic. The first thing that comes to people's minds to tackle this is using inheritance - which completely makes sense. However, the nature of inheritance is 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 into separate classes that extends the base class. In these cases, your codebase quickly increases in size and, in my opinion, reduces its maintainability.

Decorator Pattern Alternative

The implementation of a decorator pattern requires the base logic that you are trying to extend to implement a base interface - one contract that defines what methods it has and what they produce. You can then create classes that implements the base interface while containing another internal object that also implements the interface. The logic inside the interface methods you're overriding can reuse the implementation of the internal object while adding additional logic to it - or even ignore the base logic completely, up to you. Moreover, it's entirely possible to even place any class that already has extended logic and in turn the decorator will add even more logic on top of them.

Consider the following case study:

package main

import "fmt"

type VanillaIceCream struct{}

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

Let's say there are two people who wants ice cream, Ben and Mike. Each wants custom 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"
}

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"
}

Now, consider more toppings you can think of and follow what we've done so far. You can already project how big the code is going to be if this continues. With decorator pattern, you simply state an interface, one that returns the actual flavors of the ice cream class implementation, and let your decorator classes follow them.

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())
}

Let's increase the complexity of the problem and consider a case where the existing flavors are needed to be stacked with a different order. Through inheritance, you would then double or maybe triple the amount of existing code. The decorator pattern unlocks possible solutions like accepting a stack of flavors and looping through each of them maybe with a recursive and applying the appropriate decorator with a switch case statement in every turn.

Cons of Decorator Patterns

You are enforced from the start to define the order of the decorators to apply - creating new ones are far easier than changing their order half-way or worse. It's very likely for you as well to notice even since half way of reading this article.

Conclusions

  • 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.

0
Subscribe to my newsletter

Read articles from Kresno Fatih Imani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kresno Fatih Imani
Kresno Fatih Imani