๐ Introduction
The Builder Design Pattern is a creational design pattern that allows you to construct complex objects step by step.
The idea behind the builder pattern is that the process of setting up an object is handled by a dedicated Builder
type, rather than by the object itself.
This way it separates the construction of an object from its representation, giving you more control and flexibility during the building process.
The pattern is especially useful when working with objects that have many configurable parameters.
๐จ Diagram
๐ท Key Components
Product
: is the complex object that will be created.Builder
: is the object that takes inputs and creates theProduct
.Director
: provides the builder with inputs and a step by step guide and requests the builder to create the product once everything has been provided.
๐จ๐ผโ๐ป Implementation
Let's consider an example where we have a Car
object that needs to be constructed and in this case this will be our Product
.
The Car
object has various properties such as brand
, model
, color
, engineType
, and numberOfDoors
.
We can showcase this through the Car
object:
struct Car {
let brand: String
let model: String
let color: String
let engineType: String
let numberOfDoors: Int
}
Then we create the Builder
that will be responsible for creating the Product
following the provided instructions.
class CarBuilder {
enum BuilderError: Error {
case missingPart
}
private var brand: String?
private var model: String?
private var color: String?
private var engineType: String?
private var numberOfDoors: Int?
func setBrand(_ brand: String) -> CarBuilder {
self.brand = brand
return self
}
func setModel(_ model: String) -> CarBuilder {
self.model = model
return self
}
func setColor(_ color: String) -> CarBuilder {
self.color = color
return self
}
func setEngineType(_ engineType: String) -> CarBuilder {
self.engineType = engineType
return self
}
func setNumberOfDoors(_ numberOfDoors: Int) -> CarBuilder {
self.numberOfDoors = numberOfDoors
return self
}
func build() throws -> Car {
guard let brand = brand,
let model = model,
let color = color,
let engineType = engineType,
let numberOfDoors = numberOfDoors else {
throw BuilderError.missingPart
}
return Car(brand: brand, model: model,
color: color, engineType: engineType,
numberOfDoors: numberOfDoors)
}
}
In the example above, the CarBuilder
class is responsible for constructing the Car
object.
It provides methods to set each property of the Car
object and returns a reference to the builder itself, enabling method chaining.
The build()
method throws an error if any required data is missing, this will ensure that the Builder
is provided with the necessary data to create the Product
.
After we have the first two components than we can create the Director
that will be responsible for using the Builder
to create the Product
that we need.
class CarFactory {
func createSportsCar() throws -> Car {
let builder = CarBuilder()
return try builder
.setBrand("BMW")
.setModel("M4")
.setColor("Black")
.setEngineType("V10")
.setNumberOfDoors(2)
.build()
}
func createFamilyCar() throws -> Car {
let builder = CarBuilder()
return try builder
.setBrand("Volvo")
.setModel("XC60")
.setColor("White")
// .setEngineType("")
.setNumberOfDoors(4)
.build()
}
}
In this example we create a CarFactory
that will create different types of Car
products using the CarBuilder
.
So we can create an instance of the CarFactory
and see that in action:
let carFactory = CarFactory()
if let spotCar = try? carFactory.createSportsCar() {
print("The Sport-Car is created successfully!")
}
// The Sport-Car is created successfully!
if let familyCar = try? carFactory.createFamilyCar() {
print("The Family-Car is created successfully!")
} else {
print("Sorry, there was a missing part in the process of creating this car. Please check again.")
}
// Sorry, there was a missing part in the process of creating this car. Please check again.
โ Positive aspects
1. Encourages a clear separation between object construction and representation, promoting code maintainability.
2. Provides a flexible and expressive way to construct objects, allowing for different configurations.
3. Supports testability by enabling the creation and testing of various object variations.
โ Negative aspects
1. Introduces additional code for the builder, potentially increasing the overall complexity, especially for simpler objects.
2. Requires careful handling of optional properties and error management during object construction.
๐ Conclusions
The Builder Design Pattern, when implemented with attention to best practices, offers a powerful and flexible approach to constructing complex objects.
It separates the construction process from the object's representation, facilitating code maintenance and testability.
However, it should be applied judiciously, considering the complexity and requirements of the project.
Remember, the implementation of design patterns should align with the specific needs and constraints of your application, and the Builder
pattern is particularly useful when dealing with complex object creation that requires multiple steps and variations.
If you want to be notified of the upcoming articles you can subscribe to the Newsletter and support The iOS Mentor blog using Buy Me a Coffee.
You also have the option to support this blog as a sponsor, contributing to the growth of our iOS Development community.
I am also available on LinkedIn and GitHub so let's connect!
Thanks for reading everyone and enjoy the rest of your day! ๐