The decorator pattern is extremely powerful when assembling a series of objects together. We'll use a burger as an example, from the point of view of the cashier who processes and bills it.
First, let's define a basic protocol, which will encapsulate the price and the different ingredients in the current burger. In the decorator pattern, this protocol will represent the object that will be decorated over and over:
public protocol Burger {
var price: Double { get }
var ingredients: [String] { get }
}
Then we define a decorator: BurgerDecorator. In other languages, it may be implemented as an abstract class.
The decorator is implemented with the following protocol. It also conforms to the type that defines the base object being decorated, in the same way as a proxy:
public protocol BurgerDecorator: Burger {
var burger: Burger { get }
}
Here is the corresponding extension, which forwards the calls to the burger object if nothing is implemented in BurgerDecorator:
extension BurgerDecorator {
public var price: Double {
return burger.price
}
public var ingredients: [String] {
return burger.ingredients
}
}
Now, everything is ready for the final implementation. A burger is composed of many things: a bun, patties, cheese, salad, sauces, and more. Customers may be eligible for discounts. Finally, we'll need to apply tax to the bill.
BaseBurger will be the basic element—the bun, on which we'll build the rest of our burger:
public struct BaseBurger: Burger {
public var price = 1.0
public var ingredients = ["buns"]
}
Now, here is the first decorator, WithCheese. Note that adding cheese increases the price by 0.5:
public struct WithCheese: BurgerDecorator {
public let burger: Burger
public var price: Double { return burger.price + 0.5 }
public var ingredients: [String] {
return burger.ingredients + ["cheese"]
}
}
Next, we can add the Incredible Burger Patty, which will give a great taste (not to mention increase the price by 2.0):
public struct WithIncredibleBurgerPatty: BurgerDecorator {
public let burger: Burger
public var price: Double { return burger.price + 2.0 }
public var ingredients: [String] {
return burger.ingredients + ["incredible patty"]
}
}
Now, we have a burger with the bun, cheese, and a patty. We now need to add the toppings, which are free. Let's do that through an enum, as follows:
enum Topping: String {
case ketchup
case mayonnaise
case salad
case tomato
}
struct WithTopping: BurgerDecorator {
let burger: Burger
let topping: Topping
var ingredients: [String] {
return burger.ingredients + [topping.rawValue]
}
}
We're done with the implementation now. I'll leave it for you as an exercise to implement decorators for both the taxes and the discounts.
The cashier can now implement any kind of burger requested, using the following:
var burger: Burger = BaseBurger() // it's just a simple burger
burger = WithTopping(burger: burger, topping: .ketchup) // put the mayo first
burger = WithCheese(burger: burger) // Add some cheese
burger = WithIncredibleBurgerPatty(burger: burger) // Add the patty
burger = WithTopping(burger: burger, topping: .salad)
assert(burger.ingredients == ["buns", "ketchup", "cheese", "incredible patty", "salad"])
assert(burger.price == 3.5)
We successfully leveraged the decorator pattern in order to build an object over time. With this pattern, it is possible to add new decorators in the future, without changing the original implementation, and therefore propose more options to the consumers of your program very easily.
Let's now see how it is possible to go further with this design pattern.