visit
One way to become very good at something is to practice repeatedly. Like Depeche Mode said in one of their songs: “I like to practice what I (p)reach”. Each time I encounter the same or a similar problem, I find a better, more elegant solution. If you have read my previous articles, you know I am fond of Go. This time, I'd like to start a series of articles on design patterns in Go and will begin with the one I use most frequently - the Builder pattern.
type Response struct {
User User
Account Account
Address Address
}
type User struct {
Email string
}
type Account struct {
Balance float64
}
type Address struct {
City string
}
Imagine I have obtained all the data from a database or some other sources and now I need to map it to a Response struct using the Builder pattern. To do this, I will use a new struct named ResponseBuilder
type ResponseBuilder struct {
Response Response
}
func NewBuilder() *ResponseBuilder {
return &ResponseBuilder{
Response: Response{},
}
}
The NewBuilder
function is a convenient method that returns a pointer to a ResponseBuilder
with all the expected zero-value data structures initialized. With this in place, I can start adding helper methods to the builder for constructing my response.
func (rb *ResponseBuilder) SetEmail(email string) {
// can perform email validation here if needed
rb.Response.User.Email = email
}
func (rb *ResponseBuilder) SetBalance(bal float64) {
rb.Response.Account.Balance = bal
}
func (rb *ResponseBuilder) SetCity(city string) {
rb.Response.Address.City = city
}
func (rb *ResponseBuilder) Build() Response {
return rb.Response
}
func main() {
rb := NewBuilder()
rb.SetEmail("[email protected]")
rb.SetBalance(100.54)
rb.SetCity("London")
fmt.Printf("response %+v\n", rb.Build())
}
Can it get any better than this? It depends. I could apply the Fluent Interface Design Pattern to allow method chaining and further reduce the amount of code in the main
function. The implementation would look like this:
package main
type ResponseBuilder struct {
Response Response
}
func NewBuilder() *ResponseBuilder {
return &ResponseBuilder{
Response: Response{},
}
}
type Response struct {
User User
Account Account
Address Address
}
type User struct {
Email string
}
type Account struct {
Balance float64
}
type Address struct {
City string
}
func (rb *ResponseBuilder) SetEmail(email string) *ResponseBuilder {
// can perform email validation here if needed
rb.Response.User.Email = email
return rb
}
func (rb *ResponseBuilder) SetBalance(bal float64) *ResponseBuilder {
rb.Response.Account.Balance = bal
return rb
}
func (rb *ResponseBuilder) SetCity(city string) *ResponseBuilder {
rb.Response.Address.City = city
return rb
}
func (rb *ResponseBuilder) Build() Response {
return rb.Response
}
func main() {
rb := NewBuilder().
SetEmail("[email protected]").
SetBalance(100.54).
SetCity("London")
fmt.Printf("response %+v\n", rb.Build())
}
Arguably, the above example is a bit better. Each ResponseBuilder
method now assigns a value, as it did previously, and returns a pointer to itself, which allows me to chain method calls.
package main
type Response struct {
User User
Account Account
Address Address
}
type User struct {
Email string
}
type Account struct {
Balance float64
}
type Address struct {
City string
}
type ResponseBuilder struct {
Response Response
}
func NewBuilder() *ResponseBuilder {
return &ResponseBuilder{
Response: Response{},
}
}
func (rb *ResponseBuilder) User() *UserBuilder {
return &UserBuilder{*rb}
}
func (rb *ResponseBuilder) Account() *AccountBuilder {
return &AccountBuilder{*rb}
}
func (rb *ResponseBuilder) Address() *AddressBuilder {
return &AddressBuilder{*rb}
}
func (rb *ResponseBuilder) Build() Response {
return rb.Response
}
type UserBuilder struct {
ResponseBuilder
}
func (ub *UserBuilder) Email(email string) *UserBuilder {
ub.Response.User.Email = email
return ub
}
type AccountBuilder struct {
ResponseBuilder
}
func (ab *AccountBuilder) Balance(bal float64) *AccountBuilder {
ab.Response.Account.Balance = bal
return ab
}
type AddressBuilder struct {
ResponseBuilder
}
func (addrb *AddressBuilder) City(city string) *AddressBuilder {
addrb.Response.Address.City = city
return addrb
}
func main() {
rb := NewBuilder().
User().
Email("[email protected]")
Account().
Balance(100.54).
Address().
City("London")
fmt.Printf("response %+v\n", rb.Build())
}
In the above example, I've broken down a single builder into multiple builders, each dealing with its own subset of data, but all utilizing the same ResponseBuilder
and having access to Response
struct.