Menu
Coddy logo textTech

Decorator Pattern

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 93 of 107.

The Decorator pattern allows you to add new behavior to objects dynamically by wrapping them in other objects. While Adapter changes an interface to match another, Decorator keeps the same interface but enhances functionality by layering wrappers around the original object.

In Go, we implement this by having both the base type and decorators implement the same interface:

type Notifier interface {
    Send(message string) string
}

type BasicNotifier struct{}

func (b BasicNotifier) Send(message string) string {
    return "Sending: " + message
}

Decorators wrap another Notifier and add behavior before or after delegating to it:

type TimestampDecorator struct {
    wrapped Notifier
}

func (t TimestampDecorator) Send(message string) string {
    timestamped := "[2024-01-15] " + message
    return t.wrapped.Send(timestamped)
}

type UppercaseDecorator struct {
    wrapped Notifier
}

func (u UppercaseDecorator) Send(message string) string {
    return strings.ToUpper(u.wrapped.Send(message))
}

The power of Decorator lies in stacking multiple wrappers to combine behaviors:

notifier := BasicNotifier{}
withTimestamp := TimestampDecorator{wrapped: notifier}
withBoth := UppercaseDecorator{wrapped: withTimestamp}

fmt.Println(notifier.Send("Hello"))
// Sending: Hello

fmt.Println(withBoth.Send("Hello"))
// SENDING: [2024-01-15] HELLO

Decorator is ideal when you need to add responsibilities to objects without modifying their code, especially when different combinations of features are needed. It's commonly used for logging, caching, authentication, and compression layers.

challenge icon

Challenge

Easy

Let's build a coffee shop ordering system using the Decorator pattern! You'll create a base coffee drink and decorators that add extras like milk, sugar, and whipped cream. Each decorator wraps the previous one, building up both the description and the price—a perfect real-world example of how decorators layer functionality.

You'll organize your code across three files:

  • beverage.go: Define your core interface and base coffee type.

    Create a Beverage interface with two methods:

    • Description() string — returns what the drink is
    • Cost() float64 — returns the price

    Implement a Coffee struct as your base beverage. Its description should be Coffee and its cost should be 2.00.

  • decorators.go: Create your decorator types that add extras to any beverage.

    Each decorator wraps a Beverage and implements the same interface. Build three decorators:

    • MilkDecorator — adds , Milk to the description and 0.50 to the cost
    • SugarDecorator — adds , Sugar to the description and 0.25 to the cost
    • WhipDecorator — adds , Whip to the description and 0.75 to the cost

    Each decorator should call the wrapped beverage's methods and enhance the result.

  • main.go: Build customized drinks by stacking decorators.

    Read a count of extras to add. Then for each extra, read its type (milk, sugar, or whip) and wrap your current beverage with the appropriate decorator.

    After applying all extras, print the final drink's description and cost on separate lines. Format the cost as $X.XX with exactly two decimal places.

The following inputs will be provided:

  • Line 1: Number of extras to add
  • Following lines: One extra type per line (milk, sugar, or whip)

For example, given:

2
milk
sugar

Your output should be:

Coffee, Milk, Sugar
$2.75

And given:

3
whip
milk
milk

Your output should be:

Coffee, Whip, Milk, Milk
$3.75

And given:

0

Your output should be:

Coffee
$2.00

Notice how each decorator wraps the previous beverage, building up both the description and cost. You can add the same extra multiple times (like double milk), and the order of decorators determines the order in the description. The base coffee doesn't know anything about the extras—each decorator simply enhances whatever it wraps!

Cheat sheet

The Decorator pattern adds new behavior to objects dynamically by wrapping them in other objects. It keeps the same interface but enhances functionality through layering.

Both the base type and decorators implement the same interface:

type Notifier interface {
    Send(message string) string
}

type BasicNotifier struct{}

func (b BasicNotifier) Send(message string) string {
    return "Sending: " + message
}

Decorators wrap another object and add behavior before or after delegating:

type TimestampDecorator struct {
    wrapped Notifier
}

func (t TimestampDecorator) Send(message string) string {
    timestamped := "[2024-01-15] " + message
    return t.wrapped.Send(timestamped)
}

type UppercaseDecorator struct {
    wrapped Notifier
}

func (u UppercaseDecorator) Send(message string) string {
    return strings.ToUpper(u.wrapped.Send(message))
}

Stack multiple decorators to combine behaviors:

notifier := BasicNotifier{}
withTimestamp := TimestampDecorator{wrapped: notifier}
withBoth := UppercaseDecorator{wrapped: withTimestamp}

fmt.Println(notifier.Send("Hello"))
// Sending: Hello

fmt.Println(withBoth.Send("Hello"))
// SENDING: [2024-01-15] HELLO

Use Decorator to add responsibilities without modifying object code, especially for different feature combinations. Common uses: logging, caching, authentication, compression.

Try it yourself

package main

import (
	"fmt"
)

func main() {
	// Read the number of extras
	var count int
	fmt.Scanln(&count)

	// Start with a base coffee
	var drink Beverage = &Coffee{}

	// Read each extra and wrap the drink with the appropriate decorator
	for i := 0; i < count; i++ {
		var extra string
		fmt.Scanln(&extra)

		// TODO: Based on the extra type, wrap drink with the appropriate decorator
		// Use MilkDecorator for "milk"
		// Use SugarDecorator for "sugar"
		// Use WhipDecorator for "whip"
	}

	// TODO: Print the final description and cost
	// Format cost as $X.XX with exactly two decimal places
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming