[ Design Patterns ] - Builder pattern with Golang
Ha Noi, on Monday 18/09/2023
At the moment, a little speech to give to the present...
Hà Nội mùa này vắng những cơn mưa
Cái rét đầu đông khăn em hiu hiu gió lạnh
Hoa sữa thôi rơi ta bên nhau một chiều tan lớp
Đường Cổ Ngư xưa chầm chậm bước ta về...😥😥😥
Ha Noi is approaching winter, when there will be no rain. I'm sitting in my room and writing this post. Today's temperature in Ha Noi is 25°C, cloudy, and starting to get colder than previous days. The weather is appropriate for lonely people 😉😉😉
The news about layoffs in big tech is not good for everyone around the world, and it was beginning to affect employees in Vietnam. I hope that by 2023, everything will be back to normal. Best wishes to everyone! 😍😍😍
I. Builder pattern
The Builder pattern is used when the desired product is complex and requires multiple steps to complete. In this case, several construction methods would be simpler than a single monstrous constructor. The potential problem with the multistage building process is that a partially built and unstable product may be exposed to the client. The Builder pattern keeps the product private until it’s fully built.
Reference : Builder pattern
II. Components of the builder pattern
-
Product : These are the resultant objects. Products will have different results. It will be determined by how the object is constructed.
-
Concreate Builder : Provides implementation for Builder. It is an object able to construct other objects. Constructs and assembles parts to build the objects.
-
Builder : Abstract interface for creating objects (product) and getting concreate builder.
-
Director : The class will call order-by-step to build objects.
III. Implement
1. The problem
My team must build one application for selling offline fruits to one restaurant. The customer will select fruits and add them to their cart after going to the counter and requiring payment. The employee of the restaurant will scan the QR code of the fruit, and your application will display information about the fruit's price, amount, and discount.
The application must calculate the amount of money that the customer must pay for the restaurant according to the rules below.
Example rules :
Name | Amount | Price | Discount |
---|---|---|---|
Orange | More than 10 | Price = Price - $5 | |
eg : each fruit's price = $49.99 => Price = $49.99 - | Discount = Discount + 2% | ||
eg : each fruit's discount = 10% => Discount = 10 + 2 = 12% | |||
Orange | Less than 10 | fruit's price | fruit's discount |
Apple | More than 10 | Price = Price - $10 | |
eg : each fruit's price = $49.99 => Price = $49.99 - | Discount = Discount + 5% | ||
eg : each fruit's discount = 10% => Discount = 10 + 5 = 15% | |||
Apple | Less than 10 | fruit's price | fruit's discount |
Note : *The restaurant will have far more fruit, and the rules will be far more complicated. *
2. Problem solving
I think you will code as in the example below. We will use if-else to check the fruit's name and process logic for it in each case.
type Output struct {
Money float64
}
func GetMoney(name string, amount int64, price, discount float64) Output {
money := float64(0)
if name == "orange" {
if amount > 10 {
price -= 5
discount += 2
money = price * (discount - discount/100) * float64(amount)
}
// do anything logical
} else if name == "apple" {
if amount > 10 {
price -= 10
discount += 5
money = price * (discount - discount/100) * float64(amount)
}
// do anything logical
}
return Output{Money: money}
}
Sure, it will still work, but when restaurants need to calculate the price, discount, and scale the sale of fruit, our codebase becomes too large, and updating the codebase becomes difficult.
When the restaurant adds one new fruit, updates, or adds one new rule, we must hold a meeting to confirm and update the codebase in a unified manner.
So, to resolve the problem, we will use the builder design pattern.
3. Implement builder design pattern
3.1. ifruit_builder.go : Builder interface
package builder
import (
"design-pattern-golang-example/builder-pattern/concreate_builder"
"design-pattern-golang-example/builder-pattern/constant"
"design-pattern-golang-example/builder-pattern/product"
)
type IFruitBuilder interface {
SetPrice(in *product.InfoInput)
SetDiscount(in *product.InfoInput)
SetMoneyPayment(in *product.InfoInput)
ToOutputMoney() *product.InfoOutput
}
func GetFruitBuilder(fruitName string) IFruitBuilder {
switch fruitName {
case constant.Orange:
return concreate_builder.NewOrangeBuilder()
case constant.Apple:
return concreate_builder.NewAppleBuilder()
}
return nil
}
3.2. orange_builder.go: Concreate builder for orange
package concreate_builder
import "design-pattern-golang-example/builder-pattern/product"
type OrangeBuilder struct {
Price float64
Discount float64
MoneyPayment float64
}
func NewOrangeBuilder() *OrangeBuilder {
return &OrangeBuilder{}
}
func (o *OrangeBuilder) SetPrice(in *product.InfoInput) {
if in.Amount > 10 {
o.Price = in.Price - 5
}
}
func (o *OrangeBuilder) SetDiscount(in *product.InfoInput) {
if in.Amount > 10 {
o.Discount = in.Discount + 2
}
}
func (o *OrangeBuilder) SetMoneyPayment(in *product.InfoInput) {
o.MoneyPayment = o.Price * (100 - o.Discount/100) * float64(in.Amount)
}
func (o *OrangeBuilder) ToOutputMoney() *product.InfoOutput {
return &product.InfoOutput{
MoneyPayment: o.MoneyPayment,
}
}
3.3. apple_builder.go: Concreate builder for apple
package concreate_builder
import "design-pattern-golang-example/builder-pattern/product"
type AppleBuilder struct {
Price float64
Discount float64
MoneyPayment float64
}
func NewAppleBuilder() *AppleBuilder {
return &AppleBuilder{}
}
func (o *AppleBuilder) SetPrice(in *product.InfoInput) {
if in.Amount > 10 {
o.Price = in.Price - 10
}
}
func (o *AppleBuilder) SetDiscount(in *product.InfoInput) {
if in.Amount > 10 {
o.Discount = in.Discount + 5
}
}
func (o *AppleBuilder) SetMoneyPayment(in *product.InfoInput) {
o.MoneyPayment = o.Price * (100 - o.Discount/100) * float64(in.Amount)
}
func (o *AppleBuilder) ToOutputMoney() *product.InfoOutput {
return &product.InfoOutput{
MoneyPayment: o.MoneyPayment,
}
}
3.4. fruit.go : Product
package product
type InfoInput struct {
Price float64
Discount float64
Amount int64
}
type InfoOutput struct {
MoneyPayment float64
}
3.5. director.go: Director
package director
import (
"design-pattern-golang-example/builder-pattern/builder"
"design-pattern-golang-example/builder-pattern/product"
)
type Director struct {
FruitBuilder builder.IFruitBuilder
}
func NewDirector(f builder.IFruitBuilder) *Director {
return &Director{
FruitBuilder: f,
}
}
func (d *Director) BuildOutput(in *product.InfoInput) *product.InfoOutput {
d.FruitBuilder.SetPrice(in)
d.FruitBuilder.SetDiscount(in)
d.FruitBuilder.SetMoneyPayment(in)
return d.FruitBuilder.ToOutputMoney()
}
3.6. main.go
package main
import (
"fmt"
"design-pattern-golang-example/builder-pattern/builder"
"design-pattern-golang-example/builder-pattern/constant"
"design-pattern-golang-example/builder-pattern/director"
"design-pattern-golang-example/builder-pattern/product"
)
func main() {
in := &product.InfoInput{
Price: 100,
Discount: 10,
Amount: 15,
}
builderApple := builder.GetFruitBuilder(constant.Apple)
directorApple := director.NewDirector(builderApple)
outApple := directorApple.BuildOutput(in)
builderOrange := builder.GetFruitBuilder(constant.Orange)
directorOrange := director.NewDirector(builderOrange)
outOrange := directorOrange.BuildOutput(in)
fmt.Printf("Pay for orange : $%v\n", outOrange.MoneyPayment)
fmt.Printf("Pay for apple : $%v\n", outApple.MoneyPayment)
}
Ouput :
Pay for orange : $142329
Pay for apple : $134797.5
Process finished with the exit code 0
IV. Source code
- Source code builder pattern : Example with Golang
- Source almost popular design patterns : Design patterns
- Reference my blog: https://tuannguyenhust.hashnode.dev/design-patterns-builder-pattern-with-golang
All rights reserved