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
250Because 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
330Decorators 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 7The 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
EasyPizzas 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: ", ")))
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Classes and Objects
Working With FilesDefining ClassesMethods and selfInitializersClasses Are ReferencesRecap - Library Card4Encapsulation
Access ControlRead-Only From OutsideGuarding StateFailable InitializersRecap - Bank Account7Polymorphism and Generics
PolymorphismType CastingGeneric FunctionsGeneric TypesRecap - Shape Calculator5Inheritance
SubclassesOverriding MethodsInitializers and superfinal and HierarchiesRecap - Employee Hierarchy11Project: Library Management
Books and MembersBorrowing BooksPractice on your own: Swift playground