#49 Builder pattern
The Builder Pattern
In issue #41 in which we have built an app that uses a photo camera to capture one’s loayalty cards we used a pattern that we named Builder
to configure properties of objects. How does the code look like when we use Builder
pattern?
let tableView = UITableView(frame: .zero, style: .plain).with {
$0.backgroundColor = .red
$0.separatorColor = .green
$0.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
$0.allowsMultipleSelection = true
}
We simply initialize an object we want to configure and call the with(:)
function on the newly initilized object. The with(:)
function takes only one parameter - a closure in which one has possibility to set up properties of the object. How does it look like in the code?
We created a protocol named Builder
and in extension we created a default implementation of the with(:)
function:
protocol Builder {}
extension Builder {
func with(configure: (Self) -> Void) -> Self {
configure(self)
return self
}
}
The with(:)
function is very simple - it takes configure
closure as a parameter. The closure is immediately called with self
which allows “configuring” self
- e.g. setting properties on it. We also conformed NSObject
to this protocol so that every subclass can use .with{}
syntax:
extension NSObject: Builder {}
let tableView = UITableView(frame: .zero, style: .plain).with {
$0.backgroundColor = .red
$0.separatorColor = .green
$0.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
$0.allowsMultipleSelection = true
}
If we create a class that doesn’t inherit from NSObject
we need to conform to the protocol manually:
class FooBar: Builder {
var id: Int = 0
}
We can check if our pattern works by using assertions from XCTest
framework:
var foobar = FooBar().with {
XCTAssertTrue($0.id == 0) //checks default value
$0.id = 1
}
XCTAssertTrue(foobar.id == 1) //checks value set in `configure` closure
There is one problem with the presented approach. It doesn’t work for value types (i.e. struct
).
struct Foo: Builder {
var id: Int = 0
}
var foo = Foo().with {
XCTAssertTrue($0.id == 0)
$0.id = 1 //🚨 Cannot assign to property: '$0' is immutable 💥
}
XCTAssertTrue(foo.id == 1)
A struct given as a parameter to the configure
closure is immutable by default. In order to make it mutable we need to use inout
keyword and pass a reference to a type we want to configure. We want the syntax to work with class
and struct
types and we want to be able to assign returned type to a variable (as previously). So let’s create a BetterBuilder
!
protocol BetterBuilder {}
extension BetterBuilder {
public func with(configure: (inout Self) -> Void) -> Self {
var this = self
configure(&this)
return this
}
}
Our BetterBuilder
now works for structs!
struct Bar: BetterBuilder {
var id: Int = 0
}
var bar = Bar().with {
XCTAssertTrue($0.id == 0)
$0.id = 1
}
XCTAssertTrue(bar.id == 1)
And it still works for classes!
class Car: BetterBuilder {
var id: Int = 0
}
var car = Car().with {
XCTAssertTrue($0.id == 0)
$0.id = 1
}
XCTAssertTrue(car.id == 1)