Let us discuss how Builder design pattern in this article:
The intent of the Builder design pattern is to separate the construction of a complex object from its representation. This Pattern provides a series of methods that provides transparency to the consumer of class to better understand what is happening under the hood.
The builder pattern keeps the logic and default configuration values required to create an object into a builder class. This allows consumer to create objects with minimal configuration data and without needing to know the default values that will be used to create the object.
Consumer can create objects by providing just the data values for which there are no default values.
This pattern can be combined with the factory method or abstract factory patterns
The builder pattern solves the problem by introducing an intermediary called the builder between a component and the object it needs to work with.
In the first operation, the consumer provides the builder with an item of data that replaces one of the default values [if present] used to create an object. The operation is repeated each time the consumer obtains a new value from the process it is following.
In the second operation, the consumer asks the builder to create an object. This signals to the builder that there will be no new data and that an object should be created using the data values it has received so far, along with default values for the data items that were not specified by the consumer.
In the third operation, the builder creates an object and returns it to the consumer with an object.
It might look complicated but it’s simple. We can compare it to LEGO, To build a square, we need four elements of same size. We can’t say to build until these 4 elements are provided.
To understand Builder Pattern better, I tried out many approaches, You can adopt anyone which looks better to you.
I have a class with a lot of optional values. While creating instance of particular type, it just require few values. In swift it could be easily achieved with enum with associated values, but sometimes it complicate things and a lot of duplication if there is common field present.
1st Iteration:
class OptionDetailData: Equatable {
var isExpired: Bool = false
var expiryDate: String?
var cardNumber: String?
var formattedNumber: String?
var phoneNumber: String?
static func == (lhs: OptionDetailData, rhs: OptionDetailData) -> Bool {
return true
}
class Builder {
private var detailData: OptionDetailData = OptionDetailData()
func set(isExpired: Bool) -> Builder {
detailData.isExpired = isExpired
return self
}
func set(expiryDate: String) -> Builder {
detailData.expiryDate = expiryDate
return self
}
func set(cardNumber: String) -> Builder {
detailData.cardNumber = cardNumber
return self
}
func set(formattedNumber: String) -> Builder {
detailData.formattedNumber = formattedNumber
return self
}
func set(phoneNumber: String) -> Builder {
detailData.phoneNumber = phoneNumber
return self
}
func build() -> OptionDetailData {
return detailData
}
}
}
let data = OptionDetailData.Builder()
.set(isExpired: false)
.set(expiryDate: "Today")
.set(formattedNumber: "****1234****")
.set(phoneNumber: "9876543210")
.set(cardNumber: "441122224411")
.build()
print(data)
let walletMetadata = OptionDetailData.Builder()
.set(phoneNumber: "9876543210")
.build()
print(walletMetadata.phoneNumber)
As I have lot of optionals in this class which is not required by all options. I just set required field values using builder. This one is first iteration but frankly I didn’t like the approach. As I need to create class to create model.
2nd Iteration:
struct OptionDetailDataStruct: Equatable {
let isExpired: Bool
let expiryDate: String?
let cardNumber: String?
let formattedNumber: String?
let phoneNumber: String?
}
class OptionDetailDataBuilderStruct {
private var isExpired: Bool = false
private var expiryDate: String?
private var cardNumber: String?
private var formattedNumber: String?
private var phoneNumber: String?
func set(isExpired: Bool) -> OptionDetailDataBuilderStruct {
self.isExpired = isExpired
return self
}
func set(expiryDate: String) -> OptionDetailDataBuilderStruct {
self.expiryDate = expiryDate
return self
}
func set(cardNumber: String) -> OptionDetailDataBuilderStruct {
self.cardNumber = cardNumber
return self
}
func set(formattedNumber: String) -> OptionDetailDataBuilderStruct {
self.formattedNumber = formattedNumber
return self
}
func set(phoneNumber: String) -> OptionDetailDataBuilderStruct {
self.phoneNumber = phoneNumber
return self
}
func build() -> OptionDetailDataStruct {
return OptionDetailDataStruct(isExpired: isExpired, expiryDate: expiryDate, cardNumber: cardNumber, formattedNumber: formattedNumber, phoneNumber: phoneNumber)
}
}
let data1 = OptionDetailDataBuilderStruct()
.set(isExpired: false)
.set(expiryDate: "Today")
.set(formattedNumber: "****1234****")
.set(phoneNumber: "9876543210")
.set(cardNumber: "441122224411")
.build()
let walletMetadata1 = OptionDetailDataBuilderStruct()
.set(phoneNumber: "9876543210")
.build()
print(walletMetadata.phoneNumber)
In this I have created model using struct and builder using class so there is no mutability further on struct.
Builder pattern is also useful when you have complex logic for object creation.
protocol ScopeFunc {}
extension ScopeFunc {
@inline(__always) func apply(block: (Self) -> ()) -> Self {
block(self)
return self
}
}
class URLBuilder: ScopeFunc {
private var components = URLComponents()
func set(scheme: String) -> URLBuilder {
self.components.scheme = scheme
return self
}
func set(host: String) -> URLBuilder {
apply { $0.components.host = host }
}
func set(port: Int) -> URLBuilder {
apply { $0.components.port = port }
}
func set(path: String) -> URLBuilder {
var path = path
if !path.hasPrefix("/") {
path = "/" + path
}
self.components.path = path
return self
}
func addQueryItem(name: String, value: String) -> URLBuilder {
if self.components.queryItems == nil {
self.components.queryItems = []
}
self.components.queryItems?.append(URLQueryItem(name: name, value: value))
return self
}
func build() -> URL? {
return self.components.url
}
}
let url = URLBuilder()
.set(scheme: "https")
.set(host: "localhost")
.set(path: "api/v1")
.addQueryItem(name: "sort", value: "name")
.addQueryItem(name: "order", value: "asc")
.build()
print(url)
You would have noticed in builder pattern, each set(#) function returns Builder itself which is duplicated line of code in each function. I was recently reading about Kotlin apply and also function which looks good candidate for functional programming. It’s difficult to create kotlin apply function in swift as it’s not supported by language. also is possible which I implemented having name apply as also doesn’t make sense here.
Other examples of builder pattern are PizzaBuilder, BurgerBuilder, URLComponents and DateComponents.
A more swifty approach can be the use of blocks instead of builder classes to configure objects.
extension UILabel {
static func build(block: ((UILabel) -> Void)) -> UILabel {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
block(label)
return label
}
}
var label: UILabel {
return UILabel.build { label in
label.text = "Hello wold!"
label.font = UIFont.systemFont(ofSize: 12)
}
}
Ofcourse, you can create generic implementation of this build function if you are really incorporating to use it for all ui views and subclasses.
Here goes UIView builder for generic implementation:
extension UIView {
@inline(__always) static func build<T>(applyAttributes: ((T) -> Void)? = nil) -> T where T: UIView {
let uiComponent = T(frame: .zero)
uiComponent.translatesAutoresizingMaskIntoConstraints = false
applyAttributes?(uiComponent)
return uiComponent
}
}
let label: UILabel = {
return UILabel.build { label in
label.text = "Hello wold!"
label.font = UIFont.systemFont(ofSize: 12)
}
}
let view: UIView = {
return UIView.build()
}
let button: UIButton = {
return UIButton.build {
$0.setTitle("Button", for: .normal)
}
}
Please note that the builder implementation can vary on the specific use case. Sometimes a builder is combined with factories. As far as I can see almost everyone interpreted it in a different way, but I don’t think that’s a problem.
This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.