As you may have noticed, most of the decorator interfaces are the same and can be abstracted as (Burger) -> Burger functions. All the constructors take Burger as the first parameter and return a new instance of their class, which is BurgerDecorator, which in turn is a Burger.
It is possible to add a method to the toppings enum so that it can decorate a Burger:
extension Topping {
func decorate(burger: Burger) -> WithTopping {
return WithTopping(burger: burger, topping: self)
}
}
// Topping.ketchup.decorate(burger: burger)
It would technically be possible to gather all the functions that decorate a burger, and finally apply them to a burger:
var decorators = [(Burger) -> Burger]
decorators.append(Topping.ketchup.decorate)
decorators.append(WithCheese.init)
decorators.append(WithIncredibleBurgerPatty.init)
decorators.append(Topping.salad.decorate)
In order to build the burger, it is now possible to use a reducer on the array of decorators:
let reducedBurger = decorators.reduce(into: BaseBurger()) { burger, decorate in
burger = decorate(burger)
}
assert(burger.ingredients == reducedBurger.ingredients)
assert(burger.price == reducedBurger.price)
As we appended the decorators in the same order, the ingredients list is the same for both burgers, as is the price.
While this technique works well with the current code, it is separate to the decorator pattern. It is an extension that is possible because all the decorators we defined follow the same interface. Depending on your use case, you may want either to decorate the original objects as you go, or accumulate the function references in an array and resolve the final object by applying a reducer.