Bake a Builder Pattern in iOS
Implementing Builder Pattern in Swift
Have you ever have a problem when designing a system that has a similar foundation and capabilities however the ingredients to form the object would vary? We may end up with an easy solution by providing different parameters for each subclass. However, multiple subclasses that need to override the constructor might also have to own those unnecessary properties into its interface. There is one design pattern that also considered as Creational Design Pattern which Android Design pattern by default having this pattern. It called as Builder Pattern.
Q: How do Builder work in iOS?
Of course, there are many ways to make it work on iOS. Let understanding the basic with what is Builder Pattern. Builder is a creational design pattern that lets you construct objects step by step per user requirement. The pattern allows you to produce different types and representations of an object using the same construction code. It refers to a declarative way to construct an object. Builder pattern can be included as Reactive Programming as well!
Let start with the common problem, “A Car Company”. Company “A” is a big brand company and it has multiple products to be made. This company will make a “CarA” until “CarE”. They do have 5 types of cars, start from a car with a single door, sports car, family car, SUV, and a truck. We can achieve this easily by extend a “Car” base class and has the subclasses to override the necessary function, but we will end up with tons of subclasses and, and new parameters and more and more. We can simplify it by having one Base Class that owns a huge constructor, however, those subclasses need to override the constructor with all of the parameters from possible combinations. The constructor with lots of parameters has its downside, not all the parameters are needed at all times.
SOLUTION: BUILDER PATTERN (THE CAR CONCEPT)
Yep, by using a builder pattern we eliminate that construction logic. The pattern will extract and define the behavior of construction into steps. We may have buildDoors, buildWheels, buildEngine, buildColor, buildExhaust. We don’t need to call every step if we don’t need it. If we do need to have another builder for another type of car with a different implementation of each step, we can make different builders and implement it in a different way!. For example, imagine a builder that builds a car from 4 sports wheel and 2 doors, a second one that builds everything with 4 normal wheel and 4 doors and a third one that uses 2 doors and a big wheel. By calling the same set of steps, you get a sports car from the first builder, a normal family car from the second, and a Sport SUV from the third. However, this would only work if the client code that calls the building steps is able to interact with builders using a common interface. We also able to escalate this by having another Manager class to manage the specific builder of the process of creating the car in the company
How do we achieve it in iOS? There are multiple ways, we can construct in the form of a block, or we can achieve it by setting it within the builder constructor, or even calling it in a declarative way. Let see how it differs from each other?
NOTE: All of the code below is not in full code and not executable because I cut some of the unnecessary object. I try to show the core of the definition base on the use case. You can generate, modify or enhance this code to be more powerful as long as it is still conform to Builder Pattern definition.
Declarative / by Function
1// This one is a SportCarBuilder for building a sport car
2// We just need two properties of wheel and color that can be added and modified
3// However for engine and door has been defined by SportCar class
4final class SportCarBuilder {
5
6 private var wheel: Wheel = Wheel(type: .racing(tyre20))
7 private var color: Color = Color(.red)
8
9 func withColor(color: UIColor) {
10 self.color = Color(color)
11 }
12
13 func withWheel(type: WheelType) {
14 self.wheel = Wheel(type: type)
15 }
16
17 func build() -> SportCar {
18 return SportCar(wheel: wheel, color: color)
19 }
20}
21
22// This one is a SportCar class that subclass of the Car base class
23// Car base class has 4 properties, this class will initialize through the base class with designated parameters that we define for SportCar
24final class SportCar: Car {
25 init(wheel: Wheel, color: Color) {
26 super.init(doors: Door(2), engine: Engine(V85000), wheel: wheel, color: color)
27 }
28}
29
30// How to call :
31let sportCar = SportCarBuilder()
32 .withColor(color: .red)
33 .withWheel(type: .normal)
34 .build()
35
36// sportCar.wheel === .normal --> Defined by user
37// sportCar.engine === Engine(V85000) --> Defined by SportCar implementation
In the above code, it is pretty clear that when we want to get the sportCar Object, we can easily call the builder and add the necessary requirement/modifier into the builder. This way we are defining the behavior of what we want to the builder (declarative) without intervening with the implementation. SportCar also hides the Engine and Door properties from the Car base class because it will automatically be adjusted in the SportCar class instead.
Block
1// We reuse the above SportCarBuilder, we just change some interface
2// Notice we instead are using the block for update the necessary properties
3class SportCar {
4 typealias Builder = (builder: SportCarBuilder) -> Void
5
6 private init(builder: SportCarBuilder) {
7 super.init(doors: Door(2), engine: Engine(V85000), wheel: builder.wheel, color: builder.color)
8 }
9
10 static func make(with builderBlock: Builder) -> SportCar {
11 let builder = SportCarBuilder()
12 builderBlock(builder)
13 return SportCar(builder: builder)
14 }
15}
16
17// This is the way we will use by defining the properties inside the block
18let sportCar = SportCar.make { builder in
19 builder.color = .red
20 builder.wheel = .racing
21}
In the above code it is pretty straight forward, we will define the behavior we want through the completion block when we call the make function inside SportCar class. In most cases, the second way is used by Objective-C developer.
Constructor Injection
1// This is the base Car class that has all of the foundation materials to make a Car
2class Car {
3 let doors: Door
4 let wheel: Wheel
5 let engine: Engine
6 let color: Color
7
8 func buildCar() -> Car
9}
10
11// This is the subclass of Car which is a SportCar that has a private initializer
12// Only bake the Car with the builder using a static function, however you can omit the static one and immediate using the constructor as well (your preference)
13final class SportCar: Car {
14 private init(builder: SportCarBuilder) {
15 let door = Door(2)
16 let engine = Engine(V85000)
17 let wheel = builder.wheel
18 let color = builder.color
19 super.init(doors: door, engine: engine, wheel: wheel, color: color)
20 }
21
22 static func make(with builder: SportCarBuilder) -> SportCar {
23 return SportCar(builder: builder)
24 }
25}
26
27// This is the SportCarBuilder that just need color and wheel because the engine and the door will be automatically adjusted on the class who will use this by default
28struct SportCarBuilder {
29 let color: Color
30 let wheel: Wheel
31}
32
33// This is the factory class for creating the necessary car on the company
34// User will just need this class to retrieve the final product they need
35final class CarFactory {
36
37 func sportCar(with builder: SportCarBuilder) -> SportCar {
38 return SportCar.make(builder: builder)
39 }
40
41 func truckCar(with builder: TruckCarBuilder) -> TruckCar {
42 return TruckCar.make(builder: builder)
43 }
44
45 func SUVCar(with builder: SUVCarBuilder) -> SUVCar {
46 return SUVCar.make(builder: builder)
47 }
48
49 func familyCar(with builder: FamilyCarBuilder) -> FamilyCar {
50 return FamilyCar.make(builder: builder)
51 }
52}
In the code above we can see that we do have CarFactory as a factory object that generates each car base on the Builder that the user wants to inject into the parameters. The user creates the necessary builder through their required constructor on the specific builder. SportCarBuilder doesn’t have engine properties because in the SportCar object that consumes the builder will automatically adjust the engine with the required engine only for a sport car. This way the specific requirement has been handled properly by the SportCar class.
Finally the conclusion, the goal of the builder pattern is to reduce the need to keep mutable state — resulting in objects that are simpler and generally more predictable. By enabling objects to become stateless, they are usually much easier to test and debug — since their logic consists only of “pure” input & output. There are other multiple pros to use the builder pattern, even though this is not iOS Common design pattern but we can achieve it. Remember, there is also some stuff to be considered before we go with the builder pattern. I will let you guys decide whether it is worth it or not for using this pattern on your project.