Design Patterns in Swift (Abstract Factory)
Hello everyone, let’s start with the first design pattern from the Creational category => Abstract Factory.
If you’ve been immersed in iOS development for about a year, you might have unknowingly danced with this pattern without giving it a proper name.
Let me introduce our problem.
I created my Pizza shop 🍕, here is the code:
struct Dough {
var description: String = "traditional dough"
}
struct Sauce {
var description: String = "traditional sauce"
}
struct Pizza {
// MARK: - Public properties
var dough: Dough
var sauce: Sauce
// MARK: - Initializer
init(dough: Dough, sauce: Sauce) {
self.dough = dough
self.sauce = sauce
}
// MARK: - Public method
func describe() {
print("Pizza with \(dough.description) and \(sauce.description)")
}
}
struct PizzaOven {
// MARK: - Pivate properties
private let dough = Dough()
private let sauce = Sauce()
// MARK: - Public method
func cookPizza() -> Pizza {
return Pizza(dough: dough,
sauce: sauce)
}
}
// MARK: - Sample code
let pizzaOven = PizzaOven()
let pizza = pizzaOven.cookPizza()
pizza.describe()
It’s a hit, but my customers lament, “Only one pizza? How sad!”
So I decided to create a new one, just copy paste the actual code to be quicker.
TAADAA ! With a small refactoring on the name, here is my new code with the neapolitan pizza.
// MARK: - Traditional
struct TraditionalDough {
var description: String = "traditional dough"
}
struct TraditionalSauce {
var description: String = "traditional sauce"
}
struct TraditionalPizza {
// MARK: - Public properties
var dough: TraditionalDough
var sauce: TraditionalSauce
// MARK: - Initializer
init(dough: TraditionalDough, sauce: TraditionalSauce) {
self.dough = dough
self.sauce = sauce
}
func describe() {
print("Pizza with \(dough.description) and \(sauce.description)")
}
}
struct TraditionalPizzaOven {
// MARK: - Pivate properties
private let dough = TraditionalDough()
private let sauce = TraditionalSauce()
// MARK: - Public methods
func cookPizza() -> TraditionalPizza {
return TraditionalPizza(dough: dough,
sauce: sauce)
}
}
// MARK: - Neapolitan
struct NeapolitanDough {
var description: String = "neapolitan dough"
}
struct NeapolitanSauce {
var description: String = "neapolitan sauce"
}
struct NeapolitanPizza {
// MARK: - Public properties
var dough: NeapolitanDough
var sauce: NeapolitanSauce
// MARK: - Initializer
init(dough: NeapolitanDough, sauce: NeapolitanSauce) {
self.dough = dough
self.sauce = sauce
}
func describe() {
print("Pizza with \(dough.description) and \(sauce.description)")
}
}
struct NeapolitanPizzaOven {
// MARK: - Pivate properties
private let dough = NeapolitanDough()
private let sauce = NeapolitanSauce()
// MARK: - Public methods
func cookPizza() -> NeapolitanPizza {
return NeapolitanPizza(dough: dough,
sauce: sauce)
}
}
// MARK: - Sample
let traditionalPizzaOven = TraditionalPizzaOven()
let traditionalPizza = traditionalPizzaOven.cookPizza()
traditionalPizza.describe()
let neapolitanPizzaOven = NeapolitanPizzaOven()
let neapolitanPizza = neapolitanPizzaOven.cookPizza()
neapolitanPizza.describe()
But now, I have a lot of ideas and combinations to do with all my ingredient to create new pizzas. But I can see all the problems I’ll have with my “copy paste” trick.
- A lot of duplication (multiple oven)
- Create new structs for each new pizzas
- Maintenance challenges
- Lack of scalability
- Impossible to test
Enter the savior — Abstract Factory. It’s the solution that enables you to conjure families of related or dependent objects without locking them into concrete classes.
Here is a quick schema:
Explanation:
- Abstract Factory: Declares the interface for creating abstract products.
- Concrete Factory: Implements the Abstract Factory interface to produce families of related or dependent concrete products.
- Abstract Product: Declares the interface for a type of product
- Concrete Product: Implements the Abstract Product interface and represents a specific type of product
- Client: Uses the Abstract Factory and Abstract Product interfaces to interact with products. The client is decoupled from the specific classes of products, relying on abstract interfaces.
- Client Code: The specific code that interacts with concrete factories and products. It’s adaptable to different families of products without knowing their concrete classes.
First up, let’s streamline our ingredients with interfaces for Dough and Sauce — both abstract and concrete products.
// MARK: - Abstract products
protocol Dough {
var description: String { get }
}
protocol Sauce {
var description: String { get }
}
// MARK: - Concrete products
struct TraditionalDough: Dough {
var description: String = "traditional dough"
}
struct TraditionalSauce: Sauce {
var description: String = "traditional sauce"
}
struct NeapolitanDough: Dough {
var description: String = "neapolitan dough"
}
struct NeapolitanSauce: Sauce {
var description: String = "neapolitan sauce"
}
Now, optimize the pizza objects. No need for an interface (not yet), as our pizzas share a structural identity.
// MARK: - Client
struct Pizza {
// MARK: - Public properties
let dough: Dough
let sauce: Sauce
// MARK: - Public method
func describe() {
print("pizza with \(dough.description) and \(sauce.description)")
}
}
class Oven {
func cook(pizza: Pizza) {
print("Cook => ", terminator: "")
pizza.describe()
}
}
Our overall code is now sleek, robust, and easily updatable!
The complete code:
// MARK: - Abstract products
protocol Dough {
var description: String { get }
}
protocol Sauce {
var description: String { get }
}
// MARK: - Concrete products
struct TraditionalDough: Dough {
var description: String = "traditional dough"
}
struct TraditionalSauce: Sauce {
var description: String = "traditional sauce"
}
struct NeapolitanDough: Dough {
var description: String = "neapolitan dough"
}
struct NeapolitanSauce: Sauce {
var description: String = "neapolitan sauce"
}
// MARK: - Client
struct Pizza {
// MARK: - Public properties
let dough: Dough
let sauce: Sauce
// MARK: - Public method
func describe() {
print("pizza with \(dough.description) and \(sauce.description)")
}
}
class Oven {
func cook(pizza: Pizza) {
print("Cook => ", terminator: "")
pizza.describe()
}
}
// MARK: - Client code
let traditionalPizza = Pizza(dough: TraditionalDough(),
sauce: TraditionalSauce())
let neapolitanPizza = Pizza(dough: NeapolitanDough(),
sauce: NeapolitanSauce())
let oven = Oven()
oven.cook(pizza: traditionalPizza)
oven.cook(pizza: neapolitanPizza)
The only thing to do is to create new doughs and sauces, and combine them to create new pizzas !
We’re close to the end! Our code is pretty but doesn’t represent the Abstract Factory design pattern, so we’ve cleaned up before implementing it.
Let’s create our Abstract Factory “PizzaFactory”.
// MARK: - Abstract factory
protocol PizzaFactory {
func createDough() -> Dough
func createSauce() -> Sauce
}
// MARK: - Concrete factories
struct TraditionalPizzaFactory: PizzaFactory {
func createDough() -> Dough {
return TraditionalDough()
}
func createSauce() -> Sauce {
return TraditionalSauce()
}
}
struct NeapolitanPizzaFactory: PizzaFactory {
func createDough() -> Dough {
return NeapolitanDough()
}
func createSauce() -> Sauce {
return NeapolitanSauce()
}
}
These factories will churn out the ingredients required for various pizzas.
Now, let’s tie it all together with a pizza store — the client to the Abstract Factory. It abstracts away the nitty-gritty details of pizza creation, providing flexibility to order different types of pizzas.
class PizzaStore {
// MARK: - Public properties
let pizzaFactory: PizzaFactory
let oven = Oven()
// MARK: - Initializer
init(factory: PizzaFactory) {
self.pizzaFactory = factory
}
// MARK: - Public method
func orderPizza() -> Pizza {
let dough = pizzaFactory.createDough()
let sauce = pizzaFactory.createSauce()
let pizza = Pizza(dough: dough,
sauce: sauce)
oven.cook(pizza: pizza)
return pizza
}
}
There you have it! A stellar demonstration of the Abstract Factory design pattern in Swift — with a pizza twist. Flexibility, scalability, and maintainable code are now at your fingertips. The power to create an infinite array of pizza types is now yours.
Here is the complete code:
// MARK: - Abstract products
protocol Dough {
var description: String { get }
}
protocol Sauce {
var description: String { get }
}
// MARK: - Concrete products
struct TraditionalDough: Dough {
var description: String = "traditional dough"
}
struct TraditionalSauce: Sauce {
var description: String = "traditional sauce"
}
struct NeapolitanDough: Dough {
var description: String = "neapolitan dough"
}
struct NeapolitanSauce: Sauce {
var description: String = "neapolitan sauce"
}
// MARK: - Abstract factory
protocol PizzaFactory {
func createDough() -> Dough
func createSauce() -> Sauce
}
// MARK: - Concrete factories
struct TraditionalPizzaFactory: PizzaFactory {
func createDough() -> Dough {
return TraditionalDough()
}
func createSauce() -> Sauce {
return TraditionalSauce()
}
}
struct NeapolitanPizzaFactory: PizzaFactory {
func createDough() -> Dough {
return NeapolitanDough()
}
func createSauce() -> Sauce {
return NeapolitanSauce()
}
}
// MARK: - Client
struct Pizza {
let dough: Dough
let sauce: Sauce
func describe() {
print("Pizza with \(dough.description) and \(sauce.description)")
}
}
class Oven {
func cook(pizza: Pizza) {
print("Cook => ", terminator: "")
pizza.describe()
}
}
class PizzaStore {
// MARK: - Public properties
let pizzaFactory: PizzaFactory
let oven = Oven()
// MARK: - Initializer
init(factory: PizzaFactory) {
self.pizzaFactory = factory
}
// MARK: - Public method
func orderPizza() -> Pizza {
let dough = pizzaFactory.createDough()
let sauce = pizzaFactory.createSauce()
let pizza = Pizza(dough: dough,
sauce: sauce)
oven.cook(pizza: pizza)
return pizza
}
}
// MARK: - Client code
let traditionalPizzaFactory = TraditionalPizzaFactory()
let neapolitanPizzaFactory = NeapolitanPizzaFactory()
let traditionalPizzaStore = PizzaStore(factory: traditionalPizzaFactory)
let traditionalPizza = traditionalPizzaStore.orderPizza()
let neapolitanPizzaStore = PizzaStore(factory: neapolitanPizzaFactory)
let neapolitanPizza = neapolitanPizzaStore.orderPizza()
And that concludes our exploration of the Abstract Factory pattern in Swift. Your code has transformed into a versatile tool for crafting diverse pizzas. If you found this journey insightful, stay tuned for more coding insights. Feel free to share your thoughts in the comments, and happy coding! 🚀
Diagram: