Golang — Functional Options Pattern

Pattern สำหรับกำหนดค่าให้ Options
Example of Functional Options

เนื่องจากในภาษา Go ไม่มี Optional Parameter เวลาเขียน function เราจึงต้องรับค่าที่ต้องการมาทั้งหมด

func QueryUser(limit, offset int, status string) ([]*User, error) {}

เวลาเรียกใช้ function เลยต้องกำหนดค่าให้มันทั้งหมด ค่าไหนที่ไม่ต้องการก็ใส่เป็นค่าเริ่มต้น

QueryUser(30, 0, "")

ปัญหาที่ 1 init value มีค่า!!

ปัญหาของ Code ข้างบนนี้คือ ถ้า status ที่เป็น string เปล่า ๆ (“”) ถือเป็นค่า ๆ นึงด้วยหล่ะ จะทำยังไง ?

เราก็ให้มันเป็น pointer ซะสิ จะได้ใส่ nil ได้

func QueryUser(limit, offset int, status *string) ([]*User, error) {}

คราวนี้เราก็สามารถใส่ nil ใน status ได้แล้ว

QueryUser(30, 0, nil)

แล้วถ้าจะ query status หล่ะ ???

status := "active"
QueryUser(30, 0, &status)

หนักกว่าเก่าอีก เพราะเราต้องกำหนดค่า status ใส่ในตัวแปรก่อน แล้วค่อยใส่ pointer ของตัวแปรนั้นลงไป…


ปัญหาที่ 2 ถ้า parameter เยอะมาก

จาก Code ที่อยู่ข้างบน เราอาจจะยังไม่เห็นปัญหานี้ แต่ถ้า Option มันเยอะมากหล่ะ เช่น

func QueryUser(limit, offset int, status *string, createdFrom, createdTo *time.Time) ([]*User, error)

เวลาจะ Query ทีนึง ก็ต้องกำหนดค่าทั้งหมดเลย ทั้ง ๆ ที่บางตัวอาจจะไม่ได้ใช้

เราสามารถแก้ปัญหานี้ได้ด้วยเอา struct มาช่วย

type QueryUserOptions struct {
Limit int
Offset int
Status *string
CreatedFrom *time.Time
CreatedTo *time.Time
}
func QueryUser(opts *QueryUserOptions) ([]*User, error) {}

เวลา query ถ้าต้องการแค่บาง field ก็กำหนดค่าให้มันแค่ field ที่ต้องการ

status := "active"
QueryUser(&QueryUserOptions{
Limit: 30,
Status: &status,
})

เราก็จะเจอปัญหาเดียวกับปัญหาที่ 1 แล้ว คือต้องสร้างตัวแปรก่อน ถึงจะกำหนดค่าได้


ปัญหานี้จะหมดไปเมื่อเราใช้ Functional Options :D

โดยการสร้าง options struct เหมือนกับวิธีแก้ปัญหาที่ 2

type QueryUserOptions struct {
Limit int
Offset int
Status *string
CreatedFrom *time.Time
CreatedTo *time.Time
}

แล้วก็สร้าง function ที่รับ options เข้ามา set ค่า

type QueryUserOption func(*QueryUserOptions)

แล้วก็สร้าง function ที่รับค่าที่จะ set แล้ว return เป็น function ที่ รับ options :D

func QueryUserLimit(limit int) QueryUserOption {
return func(args *QueryUserOptions) {
args.Limit = limit
}
}
func QueryUserStatus(status string) QueryUserOption {
return func(args *QueryUserOptions) {
args.status = &status
}
}
// ...

ใน function จากที่ต้องรับ options ก็ให้รับ function ที่รับ options แทน

func QueryUser(setters ...QueryUserOption) ([]*User, error) {
// สร้าง default options
opt := &QueryUserOptions{
Limit: 10, // ถ้าไม่กำหนด Limit จะ default เป็น 10
}
// set ค่าให้ options
for _, setter := range setters {
setter(opt)
}
  // ...

วิธีใช้ก็ง่าย ๆ

QueryUser(
QueryUserLimit(30),
QueryUserStatus("active"),
)

Functional Options เป็น Pattern ที่ใช้บ่อยมาก อีกวิธีที่สามารถใช้ได้ก็คือ Builder Pattern ครับ โดยการสร้าง Builder เพื่อ build Options ถ้าดูในรูป Cover ของบทความนี้จะเห็นว่า datastore.NewQuery ก็สร้าง query ที่เป็น builder ออกมา

ถ้า Options ของเราซับซ้อนมาก ๆ มี condition เยอะ ก็ควรใช้ Builder แต่ถ้าไม่ซับซ้อน เขียนจบได้ใน parameter ก็ใช้ Functional Options ได้ครับ xD

Like what you read? Give acoshift a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.