Menu
Coddy logo textTech

Decorator Pattern

Part of the Object Oriented Programming section of Coddy's Swift journey. Lesson 47 of 57.

The decorator pattern adds behaviour to an object by wrapping it in another object with the same protocol. The wrapper forwards the call to the wrapped object and adds its own part before or after:

protocol Coffee {
    func cost() -> Int
    func label() -> String
}

struct Espresso: Coffee {
    func cost() -> Int { 200 }
    func label() -> String { "espresso" }
}

struct WithMilk: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 50 }
    func label() -> String { base.label() + " + milk" }
}

let drink = WithMilk(base: Espresso())
print(drink.label())
print(drink.cost())

Output:

espresso + milk
250

Because a decorator is itself a Coffee, decorators can wrap each other in any combination and order, without a subclass for every combination:

protocol Coffee { func cost() -> Int; func label() -> String }
struct Espresso: Coffee { func cost() -> Int { 200 }; func label() -> String { "espresso" } }
struct WithMilk: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 50 }
    func label() -> String { base.label() + " + milk" }
}
struct WithSyrup: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 30 }
    func label() -> String { base.label() + " + syrup" }
}

let fancy = WithSyrup(base: WithMilk(base: WithMilk(base: Espresso())))
print(fancy.label())
print(fancy.cost())

Output:

espresso + milk + milk + syrup
330

Decorators fit cross-cutting additions such as logging, caching or validation around an existing component, which stays unchanged:

protocol DataSource { func fetch(_ id: Int) -> String }
struct Database: DataSource { func fetch(_ id: Int) -> String { "row \(id)" } }

struct Logged: DataSource {
    let inner: any DataSource
    func fetch(_ id: Int) -> String {
        print("fetching \(id)")
        return inner.fetch(id)
    }
}

let source: any DataSource = Logged(inner: Database())
print(source.fetch(7))

Output:

fetching 7
row 7

The code that uses the object sees only the protocol, so it cannot tell a decorated object from a plain one. That is what makes the pattern safe to add later.

func printReceipt(_ c: any Coffee) {      // works for Espresso and for any wrapped version
    print("\(c.label()): \(c.cost())")
}
challenge icon

Challenge

Easy

Pizzas are built with decorators. The protocol Pizza has price() and toppings() (an array of names). Base is 800 with no toppings. Implement the decorators Cheese (+150), Olives (+100) and Double (doubles the price of everything inside it and repeats its toppings twice). Complete build(_:), which starts from Base and wraps it with one decorator per name in order.

The supplied code reads lines of comma-separated topping names (or an empty line), builds each pizza and prints 1050: cheese, olives or 800: plain.

Your code goes in Pizza.swift, Base.swift, Cheese.swift, Olives.swift, Double.swift and Functions.swift. main.swift holds the supplied input/output code and cannot be edited.

Try it yourself

// Supplied input/output code: keep it as it is
var input: [String] = []
while let line = readLine() { input.append(line) }
for line in input {
    let names = line.isEmpty ? [] : line.split(separator: ",").map(String.init)
    let p = build(names)
    let tops = p.toppings()
    print("\(p.price()): " + (tops.isEmpty ? "plain" : tops.joined(separator: ", ")))
}
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

Practice on your own: Swift playground