Builder [Design Patterns with Swift (Part 2)]

Fatih Öztürk
adessoTurkey
Published in
5 min readOct 10, 2023

The Builder design pattern is one of the creational design patterns.

The Builder design pattern is created to ease the object creation process. Sometimes, several processes are handled to obtain an object, or many arguments are required. The Builder design pattern suggests a step-by-step object creation structure to address this issue.

Use Case

Here we have a TabBarController and its inializeTabs method. In the initializeTabs methods, the view controllers are initialized and their tab bar items are configured.

class ViewControllerA: UIViewController {}
class ViewControllerB: UIViewController {}
class ViewControllerC: UIViewController {}
class ViewControllerD: UIViewController {}
class ViewControllerE: UIViewController {}

class TabBarController: UITabBarController {
func initializeTabs() {
let viewControllerA = ViewControllerA()
viewControllerA.tabBarItem.title = "ViewControllerA"
viewControllerA.tabBarItem.image = UIImage(systemName: "pencil")

let viewControllerB = ViewControllerB()
viewControllerB.tabBarItem.title = "ViewControllerB"
viewControllerB.tabBarItem.image = UIImage(systemName: "eraser")

let viewControllerC = ViewControllerC()
viewControllerC.tabBarItem.title = "ViewControllerC"
viewControllerC.tabBarItem.image = UIImage(systemName: "highlighter")

let viewControllerD = ViewControllerD()
viewControllerD.tabBarItem.title = "ViewControllerD"
viewControllerD.tabBarItem.image = UIImage(systemName: "keyboard")

viewControllers = [viewControllerA,
viewControllerB,
viewControllerC,
viewControllerD]
}

override var description: String {
var text = ""
guard let viewControllers else { return text }
for vc in viewControllers {
text += String(describing: type(of: vc))
text += "\n"
}
return text
}
}

let tabBarController = TabbarController()
tabBarController.initializeTabs()
print(tabBarController.description)
// Prints
// ViewControllerA
// ViewControllerB
// ViewControllerC
// ViewControllerD

As you see in the initializeTabs method, it has several operations and is already massive. Also, the readability of the code is low.

Builder

When it is preferred to implement the initialization of the tab bar controller by using the Builder pattern, a TabBarBuilder class comes out.

class TabBarBuilder: CustomStringConvertible {
private var viewControllers: [UIViewController] = []
var tabBarController: UITabBarController

init(tabBarController: UITabBarController) {
self.tabBarController = tabBarController
}

func addTab(viewController: UIViewController, title: String, image: UIImage) {
viewController.tabBarItem.title = title
viewController.tabBarItem.image = image
viewControllers.append(viewController)
tabBarController.viewControllers = viewControllers
}

var description: String {
var text = ""
for vc in viewControllers {
text += String(describing: type(of: vc))
text += "\n"
}
return text
}
}

let viewControllerA = ViewControllerA()
let viewControllerB = ViewControllerB()
let viewControllerC = ViewControllerC()
let viewControllerD = ViewControllerD()

let tabBarController = TabbarController()
let tabBarBuilder = TabBarBuilder(tabBarController: tabBarController)

tabBarBuilder.addTab(viewController: viewControllerA,
title: "viewControllerA",
image: UIImage(systemName: "pencil")!)
tabBarBuilder.addTab(viewController: viewControllerB,
title: "viewControllerB",
image: UIImage(systemName: "eraser")!)
tabBarBuilder.addTab(viewController: viewControllerC,
title: "viewControllerC",
image: UIImage(systemName: "highlighter")!)
tabBarBuilder.addTab(viewController: viewControllerD,
title: "viewControllerD",
image: UIImage(systemName: "keyboard")!)

print(tabBarBuilder.description)
// Prints
// ViewControllerA
// ViewControllerB
// ViewControllerC
// ViewControllerD

viewControllers and tabBarControllers are properties of the TabBarBuilder. The tabBarController property to be built is set from outside. The relevant properties for each tab bar item and the view controller are passed as arguments to the addTab method, which adds the view controller to the tabBarController’s viewControllers and builds the tabBarController. In other words, the TabBarBuilder presents a piecewise construction, which makes the ceremony simpler and more readable.

Fluent Builder

The TabBarBuilder can become Fluent Builder by making small changes to the addTab method.

class TabBarBuilder: CustomStringConvertible {
...

func addTab(viewController: UIViewController,
title: String,
image: UIImage) -> TabBarBuilder {
viewController.tabBarItem.title = title
viewController.tabBarItem.image = image
viewControllers.append(viewController)
tabBarController.viewControllers = viewControllers
return self
}

...
}

tabBarBuilder.addTab(viewController: viewControllerA,
title: "viewControllerA",
image: UIImage(systemName: "pencil")!)
.addTab(viewController: viewControllerB,
title: "viewControllerB",
image: UIImage(systemName: "eraser")!)
.addTab(viewController: viewControllerC,
title: "viewControllerC",
image: UIImage(systemName: "highlighter")!)
.addTab(viewController: viewControllerD,
title: "viewControllerD",
image: UIImage(systemName: "keyboard")!)

print(tabBarBuilder.description)
// Prints
// ViewControllerA
// ViewControllerB
// ViewControllerC
// ViewControllerD

As you can see, the addTab method now returns the reference of the builder. Because the addTab returns the builder itself, consecutive addTab methods can be invoked. There is one builder reference and multiple addTab methods, so that is a Fluent Builder.

[BONUS] Faceted Builder

Faceted Builder is a builder type that allows building an object with multiple properties. In the classic builder example above, TabBarBuilder was just building a UITabBarController object with the addTab() method. In addition to addTab, if different features are built with methods such as addX() and addY(), a kind of Faceted Builder is obtained. In the example below, there is a class named Car. This class has brand, color, and engine properties.

class Car: CustomStringConvertible {
// Brand
var brand = ""
var model = ""

// Color
var color = ""

// Engine
var volume = 0.0
var horsepower = 0
var cyclinder = 0
var fuel = ""

var description: String {
return "\(color) \(brand) \(model) with \(volume) L, \(horsepower) hp, and V\(cyclinder) engine that uses \(fuel)"
}
}

Methods such as color() can be added to CarBuilder to add each feature, but since the brand and engine have more than one feature in themselves, they can have a separate builder structure. So, it’s a kind of nested Builder.

The CarBrandBuilder and CarEngineBuilder are below:

class CarBrandBuilder: CarBuilder {
init(_ car: Car) {
super.init()
self.car = car
}

func brand(_ brand: String) -> CarBrandBuilder {
car.brand = brand
return self
}

func model(_ model: String) -> CarBrandBuilder {
car.model = model
return self
}
}

class CarEngineBuilder: CarBuilder {
init(_ car: Car) {
super.init()
self.car = car
}

func engine(_ volume: Double) -> CarEngineBuilder {
car.volume = volume
return self
}

func horsepower(_ horsepower: Int) -> CarEngineBuilder {
car.horsepower = horsepower
return self
}

func cyclinder(_ cyclinder: Int) -> CarEngineBuilder {
car.cyclinder = cyclinder
return self
}

func uses(_ fuel: String) -> CarEngineBuilder {
car.fuel = fuel
return self
}
}

To access the CarBrandBuilder and the CarEngineBuilder, they need to be added as properties to CarBuilder. Finally, a comprehensive builder is obtained where different kinds of features can be added. The usage of the CarBuilder is below:

class CarBuilder {
var car = Car()
var runsWith : CarEngineBuilder {
return CarEngineBuilder(car)
}
var its : CarBrandBuilder {
return CarBrandBuilder(car)
}

func color(_ color: String) -> CarBuilder {
car.color = color
return self
}

func build() -> Car {
return car
}
}

let carBuilder = CarBuilder()
let car = carBuilder
.its
.brand("Mercedes")
.model("E 200")
.runsWith
.engine(2.0)
.horsepower(204)
.cyclinder(4)
.uses("Petrol")
.color("Black")
.build()
print(car)
// Prints
// Black Mercedes E 200 with 2.0 L, 204 hp, and V4 engine that uses Petrol

Pros and Cons

Pros

  • Easier object creation and Readability: You can create objects step-by-step by preferring needed properties from whole properties, due to piecewise construction. It increases code readability and simplifies maintenance.
  • Abstraction of Object Creation and Single Responsibility: The Builder design pattern isolates the process of object creation. This means that client code does not need to be familiar with the object creation process, and the object creation logic is centralized in one place.
  • Flexibility: The Builder pattern can be used to build several versions of objects, enabling the construction of various types of objects using the same basic structure.

Cons

  • Additional Complexity: The Builder design pattern increases the code complexity since many new classes must be created in order to implement the pattern.

--

--

Fatih Öztürk
adessoTurkey

Computer Engineer - iOS Developer @ adesso Turkey