Design Patterns in Swift (Abstract Factory)

Jeremy Bailly
7 min readOct 9, 2023

--

photo by Alan on Unsplash

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:

--

--