Builder Pattern in Swift
Greetings again. Our topic today is not about learning how to do something, but about doing something better. As you know, design patterns do not allow us to do anything, but to write more sustainable projects and make the project manageable. So let’s start the review. 👀
First of all, let’s look at what the builder pattern is and what it does for us. Creating simple objects is easy. For example, let’s consider a user object with only one property. 📖
// Defination
final class User: Identifiable {
var username: UUID
init(username: UUID) {
self.username = username
}
}
// Example Usage
User(username: UUID())
As you can see, we can create and use it quite simply. What if this class was a car object and branched out to its subclasses?
// Definations
final class Car {
var engine: Engine
var wheels: [Wheel]
var body: Body
init(engine: Engine, wheels: [Wheel], body: Body) {
self.engine = engine
self.wheels = wheels
self.body = body
}
}
final class Engine {
var horsepower: Int
var cylinderCount: Int
init(horsepower: Int, cylinderCount: Int) {
self.horsepower = horsepower
self.cylinderCount = cylinderCount
}
}
final class Wheel {
var size: Int
var type: String
init(size: Int, type: String) {
self.size = size
self.type = type
}
}
final class Body {
var color: String
var type: String
init(color: String, type: String) {
self.color = color
self.type = type
}
}
// Example Usage
Car(engine: .init(horsepower: 100, cylinderCount: 4),
wheels: [.init(size: 20, type: "Low Profile"),
.init(size: 20, type: "Low Profile"),
.init(size: 20, type: "Low Profile"),
.init(size: 20, type: "Low Profile")],
body: .init(color: "red", type: "Sport")
)
You can see that things are getting more and more complicated. And this is a situation that unfortunately reduces legibility and reduces maintainability after a while. And also remember, you have to reflect these properties in the constructor anyway. What about the Builder Pattern? What if we used the Builder Pattern. What would we gain?💪🏼
- By separating the object creation steps, it makes the object creation process more understandable and readable.
- It facilitates the creation of objects with different properties. Builder classes provide customized build methods for a particular object type, so you can easily build objects with different properties.
- It makes the object creation process more modular. The Builder interface separates the object creation steps into smaller and more manageable parts. In this way, you can customize the object creation process as you wish and have the option to add or remove different steps.
- Provides flexibility in the object creation process. With the Builder design pattern, you can make the object creation process more flexible.
- Using different Builder classes, you can create objects with different properties and customize the object creation process the way you want.
- Increases code reusability. The Builder design pattern uses the same interface for building different objects. In this way, it increases the reusability of the code and prevents unnecessary code duplication.
So what are we waiting for? Let’s do this. When we look at the object, our vehicle is waiting for the engine, wheels and a body from us. With the Builder pattern, we can provide them flexibly.
protocol CarBuilderProtocol {
func setEngine(horsepower: Int, cylinderCount: Int) -> CarBuilderProtocol
func setWheel(size: Int, type: String) -> CarBuilderProtocol
func setBody(color: String, type: String) -> CarBuilderProtocol
func build() -> Car
}
final class CarBuilder: CarBuilderProtocol {
var engine: Engine?
var wheels: [Wheel] = []
var body: Body?
@discardableResult
func setEngine(horsepower: Int, cylinderCount: Int) -> CarBuilderProtocol {
engine = Engine(horsepower: horsepower, cylinderCount: cylinderCount)
return self
}
@discardableResult
func setWheel(size: Int, type: String) -> CarBuilderProtocol {
wheels.append(Wheel(size: size, type: type))
return self
}
@discardableResult
func setBody(color: String, type: String) -> CarBuilderProtocol {
body = Body(color: color, type: type)
return self
}
func build() -> Car {
return Car(engine: engine ?? Engine(horsepower: 0, cylinderCount: 0), wheels: wheels, body: body ?? Body(color: "Undefined", type: "Undefined"))
}
}
No protocol is required here, but since Swift is a P.O.P language, I chose to write it this way.
What has changed now? Now we are generating car objects through the builder object. This gives us tremendous flexibility for every feature. Like what, let’s see.
// The car object can now be created without any attributes.
CarBuilder()
.build()
// Can only be created by giving the engine.
CarBuilder()
.setEngine(horsepower: 400, cylinderCount: 8)
.build()
// It can also be created by just giving the body.
CarBuilder()
.setBody(color: "Black", type: "SUV")
.build()
// Or you can set all the features.
CarBuilder()
.setEngine(horsepower: 300, cylinderCount: 6)
.setBody(color: "Black", type: "SUV")
.setWheel(size: 22, type: "Off-Road")
.setWheel(size: 22, type: "Off-Road")
.setWheel(size: 22, type: "Off-Road")
.setWheel(size: 22, type: "Off-Road")
.build()
So how much can we improve? We have ready-made cars and do we want to use them? How about a director class, for example.🌞
final class CarDirector {
var builder: CarBuilderProtocol
init(builder: CarBuilderProtocol) {
self.builder = builder
}
func constructSportsCar() -> Car {
return builder.setEngine(horsepower: 400, cylinderCount: 8)
.setWheel(size: 19, type: "Low Profile")
.setWheel(size: 19, type: "Low Profile")
.setWheel(size: 19, type: "Low Profile")
.setWheel(size: 19, type: "Low Profile")
.setBody(color: "Red", type: "Sports")
.build()
}
func constructSUV() -> Car {
return builder.setEngine(horsepower: 300, cylinderCount: 6)
.setWheel(size: 22, type: "Off-Road")
.setWheel(size: 22, type: "Off-Road")
.setWheel(size: 22, type: "Off-Road")
.setWheel(size: 22, type: "Off-Road")
.setBody(color: "Black", type: "SUV")
.build()
}
}
The Director class is fed with an imported builder class. Thus, any builder with the appropriate protocol can be used. For example, our builder is constantly adding wheels. Instead, we can create a controlled builder and update the setWhell function by adding the wheel if the number of wheels is 4 or less. And that works perfectly in our director class 🤩
Our job becomes easier when we also prepare our ready-made templates using Director. For example, you can connect it to a service and easily create objects that you use all the time. 😎
// Returns a suv car for us.
CarDirector(builder: CarBuilder())
.constructSUV()
// Returns a sports car for us.
CarDirector(builder: CarBuilder())
.constructSportsCar()
Result
The Builder pattern, like any busy but beautiful thing, may not be desirable in the first place and may not make a spectacular change instantly. However, it provides great flexibility and strong manageability in a long-term project.
Happy coding everyone. With Swift of course. 🧡
Resources
- Builder Pattern By Swiftbysundell
Also
Thanks also to Volkan Sönmez, who informed and supported me about the subject.