Problem
It's considered "best practice" when creating model or state structs to make all members immutable 'let' variables and construct them whole using initializers.
struct Address {
let address1: String
let address2: String?
let city: String
let state: String
let zipcode: String
let country: String
}
func example1() {
let address = Address(
address1: "10 E West St",
address2: nil,
city: "New York",
state: "NY",
zipcode: "10101",
country: "US"
)
}
Unfortunately, however, there usually comes a time when we need to update the state or model with new information.
Currently in Swift, that either means we change all of our properties to 'var' with all of the tradeoffs those entail, or we need to create brand new structs by hand, copying all of the old values along with the new values.
func example2() {
let newAddress = Address(
address1: "20 W East Ave ",
address2: "Apt 12",
city: address.city,
state: address.state,
zipcode: address.zipcode,
country: address.country
)
}
This is somewhat tedious to write and more than a little error prone, and the potential for error increases each and every time you need to update a different value.
The Builder Pattern
One solution to making our structs and models easier to use is implementing a Builder pattern which makes updating individual members much easier.
func example3() {
let newAddress = address
.address1("20 W East Ave ")
.address2("Apt 12")
}
Here the result of each of the above address functions is a new constant struct with the address updated. Simple, and much easier to use than reinitializing the struct as shown in example2.
That said, actually implementing the Builder pattern in Swift is incredibly mind-numbing and involves a ton of boilerplate code:
extension Address {
func address1(address1: String) -> Address {
return Address(
address1: address1,
address2: address2,
city: city,
state: state,
zipcode: zipcode,
country: country
)
}
func address2(address2: String) -> Address {
return Address(
address1: address1,
address2: address2,
city: city,
state: state,
zipcode: zipcode,
country: country
)
}
func city(city: String) -> Address {
return Address(
address1: address1,
address2: address2,
city: city,
state: state,
zipcode: zipcode,
country: country
)
}
func zipcode(address1: String) -> Address {
return Address(
address1: address1,
address2: address2,
city: city,
state: state,
zipcode: zipcode,
country: country
)
}
func country(country: String) -> Address {
return Address(
address1: address1,
address2: address2,
city: city,
state: state,
zipcode: zipcode,
country: country
)
}
}
As shown, implementing the Builder pattern for complex structs often becomes a huge copy and paste jobs and can become a breeding ground for bugs. (Like forgetting to change the parameter name in the zipcode function above.)
And heaven help you if at a later date your parameter order changes or you need to add new values.
Solution? Buildable
So what if we let the computer do what it does best and manage all of the above boilerplate for us?
Image the following:
struct Address: Buildable {
let address1: String
let address2: String?
let city: String
let state: String
let zipcode: String
let country: String
}
func example4() {
let newAddress = address
.address1("20 W East Ave ")
.address2("Apt 12")
}
This is akin to what Codable does, when it writes the boring code for us.
Implementation
Behind the scenes the actual implementation could write all of the above code for us, macro-style, or if we wanted to be more efficient the compiler could simply do a copy-on-write and update the appropriate value for us, eliminating the need to perform each initialization steps and all of the attendant code.
The compiler could even notice that we're stringing builders together and optimize things such that we only do one copy with the various updates.
Wrap Up
So that's the Buildable pitch. Clearer code. Smaller code. No boilerplate, No copy and paste. And less potential for error.
What do you think?