Make it for me: Factory pattern and Swift

Ario Liyan
13 min readFeb 4, 2023

--

In this article, we are going to take a voyage deep into the factory pattern, we talk about the factory method pattern, the abstract factory, and the simple factory. I wrote this article after reading three books(Design patterns: elements of reusable object-oriented software, Head First Design Pattern, and Dive Into Design Patterns), watching a couple of Youtube videos, and reading some other articles. This article is written with Swift language in mind. This post is intended to be used as a complete reference. Good read. (This article is divided into two parts. In first, we will discuss the Factory Method and Simple Factory patterns. In the second, we will explore the Abstract Factory pattern.)

From Dive into design patterns by Alexander Shevts

Table of contents

  • What is the factory pattern?
  • Factory Method — Simple factory

What is the factory pattern?

The factory design pattern is a creational design pattern in software engineering that is used to create objects in a structured way. The factory design pattern is based on the concept of an abstract factory, which defines a method for creating objects without specifying the exact class of object that will be created. Instead, the factory pattern uses a set of classes that implement the abstract factory interface to create objects. This allows for a level of flexibility and extensibility in the design of the application since the implementation of the actual classes is not known at design time. In addition, the factory pattern can be used to create objects of different classes depending on the input parameters or environment variables, thus allowing for greater flexibility in the design.

Factory Method — Simple factory

  • Why do we need the factory method pattern?
  • Simple Factory
  • Advantages of using the factory method pattern
  • UML
  • Let’s see some code
  • Use cases
  • Conclusions

Why do we need the factory method pattern?

In object-oriented programming, we often use “wishful thinking” when programming. We imagine the system and state how we want our classes to behave and what attributes our objects should have. Based on this, we define our classes. But there are times, we don’t know exactly what objects we want to create or how to create them. Factories are for those times, they take care of the details of object creation and are responsible for creating the objects.

As an example:

Logistics management application

Imagine we’re creating a logistics management application. The first version only handles transportation by truck, so the bulk of the code lives inside the Truck class.

After a while, the app becomes popular and we receive dozens of requests from sea transportation companies to incorporate sea logistics.

That’s great news, but how about the code? Most of it is coupled to the Truck class, so adding Ships would require changes to the entire codebase. If later we decide to add another type of transportation, we'd have to make all these changes again.

This would lead to nasty code, full of conditionals that switch the app’s behavior depending on the class of transportation objects.

A Logistic app that supports all kinds of transportation…

The Factory Method pattern suggests replacing direct object construction calls with calls to a special factory method. The objects are still created via the with traditional form, but it’s being called from within the factory method. Objects returned by a factory method are often referred to as “products”.

Simple Factory

Note that we can handle the creation process within a class or a function. Instead of the factory class discussed in this article, we can use a static method to handle the creation process (known as a simple factory). Static factories are somewhat prevalent, but we generally avoid defining and using them, as subclassing and changing the factory’s behavior is easier with classes.

Advantages of Using Factory Method Pattern

The advantages of using factories include:

  • Encapsulating the creation of an object gives us control over when and how we create it.
  • If we need to change the object, we only need to modify the code in one place.
  • Factories can handle the complexity and heavy loading associated with object creation in one place.
  • The instantiation may need some computation and dependencies that we are not aware of, in our classes and those dependencies will be specified on run time, the factory can handle all of them in one place.
  • Factories can be swapped at runtime for instances of another factory, allowing for polymorphism.
  • Single Responsibility Principle. You can move the product creation code into one place in the program, making the code easier to support.
  • You avoid tight coupling between the creator and the concrete products.
  • Open/Closed Principle. You can introduce new types of products into the program without breaking existing client code.

For example, if we have three different classes of the same type or conform to the same protocol, we may not know which one to instantiate. The factory can determine which one to create based on runtime data and take care of its creation.

A disadvantage for using the factory pattern would be “the code may become more complicated since you need to introduce a lot of new subclasses to implement the pattern. The best case scenario is when you’re introducing the pattern into an existing hierarchy of creator classes”.

An example of polymorphism and Factory pattern:

National park simulation

In the national park simulation app, we want to have three different animals: Lion, Reindeer, and Elephant.

For the first scenario in our app, it doesn’t matter which animal we create first, but we want to maintain a ratio of 1:3 for lions and other animals. That is, for every lion, there should be three elephants and three reindeer.

At our factory, we randomly create animals for the first time. On subsequent attempts, we check which animals we have. If we have a Lion, we cannot create another until we have three Reindeer objects and three Elephant objects. So the second attempt in that case we randomly create and animal between the Elephan and Reindeer….

In another scenario, imagine that we need to have the same number of animals. In this case, for each creation, we count the number of animals present and then randomly generate the remaining desired animals. For example, if we have one Lion, we randomly create either a Reindeer or an Elephant.

For each of these scenarios, we can have a factory class to take responsibility for the creation process.

Arcade Game Space shooter — Galaxy attack

In the Space shooter — Galaxy attack we have spaceships that voyage through space in each level different kinds of enemies are spawned in different sizes and at different speeds.

So for the creation of enemies, we have a factory that each time it wants to spawn a new enemy, the algorithm firstly checks our level, then the number of the current enemies on screen and their types, and finally our gun power and some other criteria that it should check, after all, it creates the enemy based on these values.

UML

Gangs of four book

In this UML, taken from the Gangs of Four book, the Product is the type that we want to create in our Factory. The Creator is also the Factory, and both Product and Creator can be protocols or superclasses. ConcreteProduct and ConcreteCreator can subclass to conform to Product and Creator.

Product is the type that we need to create and it has all the attributes and functions that do not concern us.

Concrete product is the class from which we want to create objects.

The creator is the Factory type that compels each Factory to implement the FactoryMethod function, which creates an object of type Product and returns it.

Shape example UML for the factory pattern

Let’s see some code

Scenario

We have a national park simulation app, with three wild animals: elephants, lions, and Reindeer. According to the business decision, we want to be able to add new animals through three different flows:

  • Adding a random animal
  • Adding a specific animal
  • Adding new animals to ensure an equal number of each species

Our code has an Animal protocol that represents an animal without considering its species. Each animal class added to the park should conform to this protocol.

We maintain an array of animals that we take care in our park. We also have a Factory protocol, so any factory that wants to add animals to our collection should conform to it.

The code given below is not following the OOP standards and it is brought just to showcase an example with the factory pattern.

//Factory protocol - Part 1
@objc protocol AnimalFactory {
@objc optional func create(name: String)
}

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
//Product protocol - Part 2
protocol Animal {
func eat()
var name: String {get set}
var specie: String {get}
}

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
//MARK: - Business Logic
//The business body - Part 3A
var nationalParkAnimals = [Animal]()
var numberOfLion = 0
var numberOfElephant = 0
var numberOfReeindeer = 0

func countAnimals() {
numberOfLion = 0
numberOfElephant = 0
numberOfReeindeer = 0

for animal in nationalParkAnimals {
switch animal.specie {
case "Lion":
numberOFLions += 1
case "Reindeer":
numberOFElephants += 1
case "Elephant":
numberOFReindeers += 1
default:
print("LOG: - animal Specie is not specified")
}
}
}


enum Animals:CaseIterable {
case lion
case elephant
case reindeer

var count: Int {
switch self {
case .lion:
return numberOfLion
case .elephant:
return numberOfElephant
case .reindeer:
return numberOfReeindeer
}
}
var name: String {
switch self {
case .lion:
return "Lion"
case .elephant:
return "Elephant"
case .reindeer:
return "Reindeer"
}
}

static var minNumber: Animals {
let minNumber = min(numberOfLion,
numberOfElephant,
numberOfReeindeer)

return Animals.allCases.first(where: {$0.count == minNumber})!
}
static var maxNumber: Animals {
let maxNumber = max(numberOfLion,
numberOfElephant,
numberOfReeindeer)

return Animals.allCases.first(where: {$0.count == maxNumber})!
}
}

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
//Part 3-B - Product classes
class Lion: Animal {

var specie: String = "Lion"

func eat() {
print("Meat")
}

var name: String

init(name: String) {
self.name = name
numberOfLion += 1
}
}

class Reindeer: Animal {

var specie: String = "Reindeer"

func eat() {
print("Grass")
}

var name: String

init(name: String) {
self.name = name
numberOfReeindeer += 1
}
}

class Elephant: Animal {
var specie: String = "Elephant"

func eat() {
print("Leaves")
}

var name: String

init(name: String) {
self.name = name
numberOfElephant += 1
}
}

//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
//Part 3-C Factory classes
class NationalParkRandomFactory: AnimalFactory {

static let instance = NationalParkRandomFactory()

func create(name: String) {
let randomInteger = Int.random(in: 1...3)

switch randomInteger {
case 1:
NationalParkDirectFactory.instance.create(name: name,
Specie: .lion)
case 2:
NationalParkDirectFactory.instance.create(name: name,
Specie: .elephant)

case 3:
NationalParkDirectFactory.instance.create(name: name,
Specie: .reindeer)
default:
print("Random Number out of range")
}
}
private init() {}

}

class NationalParkEqualFactory: AnimalFactory {

static let instance = NationalParkEqualFactory()

func create(name: String) {
countAnimals()
if Animals.minNumber == Animals.maxNumber {
NationalParkRandomFactory.instance.create(name: name)
} else {
let minNumberAnimal = Animals.minNumber
NationalParkDirectFactory.instance.create(name: name , Specie: minNumberAnimal)
}
}

private init() {}
}

class NationalParkDirectFactory: AnimalFactory {

static let instance = NationalParkDirectFactory()

private init() {}
func create(name: String, Specie: Animals) {
switch Specie{
case .lion:
let lion = Lion(name: name)
nationalParkAnimals.append(lion)
numberOfLion += 1
case .elephant:
let elephant = Elephant(name: name)
nationalParkAnimals.append(elephant)
numberOfElephant += 1
case .reindeer:
let reindeer = Reindeer(name: name)
nationalParkAnimals.append(reindeer)
numberOfReeindeer += 1
}
}
}

Part 1 — Factory protocol

@objc protocol AnimalFactory {
@objc optional func create(name: String)
}

In the factory protocol, we only have the “create” function, which takes a name for an animal and creates it. We define this function as optional, so if one of the factories wants to create animals in any other way, they can do so without much difficulty.

Part 2 — Product protocol

protocol Animal {
func eat()
var name: String {get set}
var specie: String {get}
}

The “Animal” protocol represents an animal in our system with regard to what it eats, its name, and its specie. Each new Animal type that we want to add to our system should conform to this protocol.

Part 3-A — The business body

var nationalParkAnimals = [Animal]()
var numberOfLion = 0
var numberOfElephant = 0
var numberOfReeindeer = 0

In our code we have an array of animals that contains all animals in our park, each factory works directly with this array and adds new animals to this array. The array is our only source of truth and we consider this array as a representation of our park. We also have three variables each for the relative type of animal that they hold the number of each animal.

func countAnimals() {
numberOfLion = 0
numberOfElephant = 0
numberOfReeindeer = 0

for animal in nationalParkAnimals {
switch animal.specie {
case "Lion":
numberOFLions += 1
case "Reindeer":
numberOFElephants += 1
case "Elephant":
numberOFReindeers += 1
default:
print("LOG: - animal Specie is not specified")
}
}
}

We also have a function called “countAnimals” which count each specie and update the number of animals when called.


enum Animals:CaseIterable {
case lion
case elephant
case reindeer

var count: Int {
switch self {
case .lion:
return numberOfLion
case .elephant:
return numberOfElephant
case .reindeer:
return numberOfReeindeer
}
}
var name: String {
switch self {
case .lion:
return "Lion"
case .elephant:
return "Elephant"
case .reindeer:
return "Reindeer"
}
}

static var minNumber: Animals {
let minNumber = min(numberOfLion,
numberOfElephant,
numberOfReeindeer)

return Animals.allCases.first(where: {$0.count == minNumber})!
}
static var maxNumber: Animals {
let maxNumber = max(numberOfLion,
numberOfElephant,
numberOfReeindeer)

return Animals.allCases.first(where: {$0.count == maxNumber})!
}
}

We have defined an Animals enum as a helper data structure to simplify our task. It provides information about the animal species, including the maximum and minimum number. The count property returns the number of each species.

Part 3-B — Product classes

class Lion: Animal {

var specie: String = "Lion"

func eat() {
print("Meat")
}

var name: String

init(name: String) {
self.name = name
}
}

class Reindeer: Animal {

var specie: String = "Reindeer"

func eat() {
print("Grass")
}

var name: String

init(name: String) {
self.name = name
}
}

class Elephant: Animal {
var specie: String = "Elephant"

func eat() {
print("Leaves")
}

var name: String

init(name: String) {
self.name = name
}
}

As it was stated before, according to our business we only want to have three species of animals: Lion, Reindeer, and Elephant. So we only have three concrete product classes(Lion, Reindeer, Elephant). Each class conforms to the Animal protocol.

Part 3-C Factory classes

Adding a random animal to our park:

class NationalParkRandomFactory: AnimalFactory {

static let instance = NationalParkRandomFactory()

func create(name: String) {
let randomInteger = Int.random(in: 1...3)

switch randomInteger {
case 1:
let lion = Lion(name: name)
nationalParkAnimals.append(lion)
numberOfLion += 1
case 2:
let elephant = Elephant(name: name)
nationalParkAnimals.append(elephant)
numberOfElephant += 1

case 3:
let reindeer = Reindeer(name: name)
nationalParkAnimals.append(reindeer)
numberOfReeindeer += 1
default:
print("Random Number out of range")
}
}
private init() {}

}

The National Park Random Factory facilitates the addition of animals to the park. As there are only three types of animals, it generates a random number between 1 and 3 and creates the corresponding animal. It then updates the number of each type of animal (e.g. lions, elephants) and adds it to the animal array.

Adding a specific animal to our park:

class NationalParkDirectFactory: AnimalFactory {

static let instance = NationalParkDirectFactory()

private init() {}
func create(name: String, Specie: Animals) {
switch Specie{
case .lion:
let lion = Lion(name: name)
nationalParkAnimals.append(lion)
numberOfLion += 1
case .elephant:
let elephant = Elephant(name: name)
nationalParkAnimals.append(elephant)
numberOfElephant += 1
case .reindeer:
let reindeer = Reindeer(name: name)
nationalParkAnimals.append(reindeer)
numberOfReeindeer += 1
}
}
}

This factory only creates animals of a certain specie and adds them to the array.

Adding Animals Equally:

class NationalParkEqualFactory: AnimalFactory {

static let instance = NationalParkEqualFactory()

func create(name: String) {
countAnimals()
if Animals.minNumber == Animals.maxNumber {
NationalParkRandomFactory.instance.create(name: name)
} else {
let minNumberAnimal = Animals.minNumber
NationalParkDirectFactory.instance.create(name: name , Specie: minNumberAnimal)
}
}

private init() {}
}

The equal factory search for the minimum number of existing animals of a specific specie and adds the new animal to that specie.

Use cases

  • implementing a universal app for different platforms(iOS, macOS)
  • Creating and presenting a UI Component that needs some logic for the creation

Implementing a universal app for different platforms(iOS, macOS)

When creating a single app for multiple platforms with a single source code, we need to consider how to display the same data differently on each platform, or how to configure the same UI elements differently for each platform. The Factory pattern could be the answer to this issue. The UIFactory can decide how data should be displayed on each device, and populate the UI elements based on the platform and OS. For example, it shows a tab view on iPhone and a sidebar on Mac.

Creating and presenting a UI Component that needs some logic for the creation

Imagine an alert component factory that, based on alert type, decides which kind of alert to present on the device, such as banners or pop-ups, etc.

Conclusions:

  • Use the Factory Method when you don’t know beforehand the exact types and dependencies of the objects your code should work with.
  • Use the Factory Method when you want to provide users of your library or framework with a way to extend its internal components.
  • Use the Factory Method when you want to save system resources by reusing existing objects instead of rebuilding them each time.

Abstract factory pattern

--

--

Ario Liyan

As an iOS developer with a passion for programming concepts. I love sharing my latest discoveries with others and sparking conversations about technology.