blog นี้ Sameer Ajmani เขียนไว้ตั้งแต่เมื่อปี 2015 จนถึงวันนี้ก็ 9ปี แล้ว แต่ผมคิดว่าเรื่องนี้ไม่ได้เปลี่ยนไป และยังคงเป็นคำแนะนำที่ดีมากเช่นเดิม โดยตันฉบับอ่านได้ที่ https://go.dev/blog/package-names
เริ่มต้น Sameer เกริ่นก่อนว่าโค้ด Go จะถูกจัดโครงสร้างด้วยการใช้ package และเมื่อเราอ้างถึงของใดๆก็ตาม ที่ package นั้นเปิดให้เราเข้าถึงได้ เวลาที่เราเรียกใช้มันเช่น foo.Bar มันจะได้ความหมายของสิ่งนั้นจากชื่อของ package และชื่อของ ไม่ว่าจะฟังก์ชั่น หรือตัวแปรก็ตาม รวมกัน แล้วจะเพิ่มความเข้าใจให้กับโปรแกรมเมอร์เมื่ออ่านโค้ดนี้ได้เป็นอย่างดี
ชื่อ package ที่ดี จะทำให้โค้ดนั้นเป็นโค้ดที่ดียิ่งขึ้น มันควรจะบอกเราได้ว่าในนั้นมีบริบทของสิ่งใดอยู่ ซึ่งตรงนี้ผมอธิบายเพิ่มเติมว่า บางท่านอาจจะตีความไปในคนละทาง ผมแนะนำอีก blog ของคุณ Mina how-to-structure-our-code
เขาอธิบายเรื่องการวาโครงสร้างโค้ด java ว่ามี 2 แบบในมุมมองเขาคือ วางโครงสร้างแบบแยกชั้น Package by Layer และวางแบบแยกตามฟีเจอร์ Package by Feature ซึ่งมันตรงกับความคิดผม เวลาจะวางโครงสร้าง Go โดยผมแนะนำให้ใช้แบบ Package by Feature ใน Go ด้วยเช่นกันด้วยเหตุผลเดียวกัน
มาต่อกับที่บทความของ Sameer เขายกเอา Effective Go ซึ่งได้ให้แนวทางการตั้งชื่อ package, type, interface และเรื่องที่ชาว Go ควรได้อ่านไว้หลายเรื่อง ขึ้นมาเป็นแหล่งอ้างอิงของเขาด้วย
โดยจะทบทวนให้ว่า โดยหลักการแล้ว Go ควรตั้งชื่อ package ด้วยชื่อที่ สั้น กระชับ ได้ความหมาย และเป็นตัวอักษรพิมพ์เล็กทั้งหมด ไม่มี under_scores และไม่มี mixedCaps ใดๆเลย เช่น
- time
- list
- http
เป็นตัน ซึ่ง Sameer อธิบายต่อว่า การตั้งชื่อแบบนี้อาจจะเป็นแบบที่ควรเป็นใน Go และอาจไม่เหมาะถ้าจะเอาไปใช้กับภาษาอื่น และเขายกชื่อที่อาจจะเหมาะกว่าในภาษาอื่นเช่น
- computeServiceClient
- priority_queue
ซึ่งส่วนตัวผมก็เห็นคนเขียน Go ที่คุ้นเคยกับภาษาอื่นทำพลาดกันมาเยอะ เช่นตั้งชื่อฟังก์ชั่นแบบมี under_scores มาจ๋าๆเลย หรือแม้แต่ชื่อ interface ตั้งโดยเอา I
มาแปะไว้หน้าชื่อ นี่มันไม่ใช่ Go เลยแม่แต่นิด
ทีนี้การตั้งชื่อ package ให้สั้นแบบมีความหมาย มันจะทำให้ของที่ถูกเรียกใช้มันจะดูเข้าท่ามากขึ้นเช่นเวลาเราจะใช้ client ใน http เราก็จะได้ http.Client
ซึ่งจะทำให้ชื่อมันดูเข้าใจง่ายขึ้นโดยไม่ต้องใช้คำให้เว่นเว้อ
หรือการย่อชื่อ package ก็สามารถทำได้ แต่อยู่ในเงื่อนไขที่ว่า ต้องเป็นคำย่อที่ทุกคนเข้าใจอยู่แล้ว เช่นใน standard lib จะมี
- strconv
- syscall
- fmt
แต่ก็ต้องเตือนไว้ด้วยว่า ถ้าชื่อนั้นย่อแล้วมันเข้าใจยาก หรือทำคนสับสน ก็อย่าทำเลยจะดีกว่า และที่สำคัน Sameer บอกว่า
อย่าขโมยชื่อดีๆไปจาก user เช่น package ที่ชื่อ bufio มันไม่ชื่อ buf ก็เพราะคำว่า buf มันเป็นชื่อตัวแปรที่เหมาะมากๆ เมื่อเราคิดจะตั้งชื่อตัวแปรที่เป็น buffer แต่ถ้ามันถูกเอาไปตั้งเป็นชื่อ package ไปเสียแล้ว แล้วมันจะเหลืออะไรให้เราใช้กัน ไม่งั้นเราก็ต้องไปเดินเล่นรอบสวน จิบกาแฟ 2 ถ้วยก่อน เผื่อจะนึกชื่อดีๆออกใหม่
นี่ทำให้ผมนึกถึงชื่อ package แบบ service ขึ้นมา เพราะบางทีเวลาเราเขียนโค้ดแล้วเรารู้สึกว่า นี่แหล่ะ service ของฉัน แต่ถ้ามันถูกเอาไปตั้งชื่อ package เสียแล้ว งั้นชื่อตัวแปรที่เห็นบ่อยๆก็จะกลายเป็น accountService หรือ orderService ซึ่งมันก็ได้อยู่ แต่อาจจะเว่นเว้อนิดหน่อย
ผมเห็นบ่อยๆนะ โค้ดที่อ่านทีละบรรทัด โค้ดสวย แต่พอมันมารวมกันในฟังก์ชั่นแล้ว อ่านไม่รู้เรื่องเลย
ต่อมา Sameer อธิบายเรื่องเนื้อหาใน package ให้เข้าใจเพิ่มเติมต่อว่า เวลาเราตั้งชื่อของใน package ก็อย่าเอาชื่อ package ไปสร้างความซ้ำซ้อนต่อ เช่นใน http มันจะมี type ชื่อ Server ทำไมไม่ HTTPServer ก็เพราะตอนที่เรียกใช้น่ะ มันควรจะเป็น http.Server
ซึ่งสั้น กระชับ เข้าใจแล้ว ไม่จำเป็นต้อง http.HTTPServer
ไง
ส่วนต่อมาผมชอบมาก ก็คือเรื่องการ Simplify function names
ก็เรื่องเดียวกับประโยคก่อนนี้นั่นแหล่ะ ซึ่ง Sameer อธิบายเพิ่มเติมว่า ถ้ามีฟังก์ชั่นใน package นั้น คืนค่าที่เป็น type ของ package นั้นออกมาเลย ยกตัวอย่างเช่น ถ้ามีฟังก์ชั่นที่คือ type Time ออกมาจาก package time เวลาจะตั้งชื่อฟังก์ชั่นแบบนี้ มันจะออกมาหน้าตาประมาณนี้
start := time.Now() // start is a time.Time
t, err := time.Parse(time.Kitchen, "6:06PM") // t is a time.Time
ctx = context.WithTimeout(ctx, 10*time.Millisecond) // ctx is a context.Context
ip, ok := userip.FromContext(ctx) // ip is a net.IP
หรือการใช้ฟังก์ชั่นที่ชื่อ New ก็ใช้แบบเดียวกัน เพราะถ้า New แล้วได้ type ของ package นั้นออกมา มันก็สวยดี และใช้กันแทบจะเป็นมาตรฐานกันไปแล้วเช่น
q := list.New() // q is a *list.List
แต่ถ้าฟังก์ชั่นนั้นมี type อื่นๆ และเราต้องการตั้งชื่อฟังก์ชั่นที่จะคืน type อื่นใน package นั้นออกมา ก็ใส่ชื่อ type นั้นเข้าไปเหมือนเดิมเช่น
d, err := time.ParseDuration("10s") // d is a time.Duration
elapsed := time.Since(start) // elapsed is a time.Duration
ticker := time.NewTicker(d) // ticker is a *time.Ticker
timer := time.NewTimer(d) // timer is a *time.Timer
ทั้งหมดที่เล่ามานี้ มันจะนำไปสู่การที่เราจะสามารถตั้งชื่อเดียวกันใน package ที่ต่างกันได้ เช่นชื่อ Reader ซึ่งมันจะมีอยู่ในหลายๆ package เช่น jpeg.Reader, bufio.Reader, csv.Reader และอีกหลายๆที่ นั่นก็เพราะชื่อนี้มันเป็นชื่อที่ดีถ้ามันจะต้องมี Reader อยู่ในแต่ละ package แต่เราลองมานึกภาพว่า เรามี package ชื่อ reader ซึ่งมันเป็นวิธีคิดแบบเดียวกับที่เราตั้งชื่อ package ว่า service นั่นแหล่ะ
เราจะได้ของใน package reader ที่เป็นของจากทุกบริบทมารวมตัวกันกลายเป็น reader.BufIO, reader.Jpeg และ reader.CSV มันอาจจะดูง่ายในมุมหนึ่ง แต่มันไม่ดีในหลายๆมุม แนะนำให้อ่านบทความของ Mina ที่แปะไว้ด้านบนนะครับ
ผมขอข้ามเรื่อง Package paths
ของ Sameer ไปคุยเรื่อง Bad package names
แทนนะครับ
Sameer บอกว่าชื่อที่แย่ จะทำให้ยากต่อการดูแล เพราะเราจะต้องเสียเวลาไปหาว่าของที่เราต้องการเห็นมันอยู่ที่ไหนกันแน่ และที่เราเห็นอยู่ตรงหน้า มันหมายความว่าอะไร โดยมีคำแนะนำให้ว่า
หลีกเลี่ยงการใช้ชื่อที่ไร้ความหมาย เช่น util, common หรือ misc เป็นต้น เพราะมันไม่ได้สื่อว่าในนั้นมีอะไรอยู่ นี่จะสร้างความยากลำบากให้คนที่ต้องมาดูแลต่ออย่างมาก มันทำให้ต้องเสียเวลา และเวลาที่เสียไปเล็กน้อยรวมๆกัน มันมหาศาล การ compile ก็อาจจะช้าลง เพราะเราแยก package มากเกินไป และในบางครั้ง พอชื่อมันไม่สื่อ เราอาจจะเกิดการสร้างชื่อ package ซ้ำกันได้แล้วมันจะยิ่งทำให้สับสนเพิ่มขึ้น
แก้ชื่อที่ไม่สื่อความหมายซะ ยกตัวอย่างเช่น util เช่นถ้าในนั้นมีของแบบนี้
package util
func NewStringSet(...string) map[string]bool {...}
func SortStringSet(map[string]bool) []string {...}
เวลาใช้เราจะเห็นแบบนี้
set := util.NewStringSet("c", "a", "b")
fmt.Println(util.SortStringSet(set))
เมื่อเห็นแบบนี้ ให้เราดึงเอา 2 ฟังก์ชั่นนี้ออกมาส้ราง package ใหม่ให้มันซะ
package stringset
func New(...string) map[string]bool {...}
func Sort(map[string]bool) []string {...}
ทีนี้เวลาเรียกใช้ก็จะได้แบบนี้แทน
set := stringset.New("c", "a", "b")
fmt.Println(stringset.Sort(set))
Sameer บอกว่าชื่อ package นั้นสำคัญมาก ทางที่ดีให้กำจัดชื่อที่ไร้ประโยชน์ออกไปซะให้หมด
อย่าใช้ชื่อเดียวกันในทุกๆ API ของคุณ มีโปรแกรมเมอร์เก่งๆที่มีเจตนาที่ดีมากมาย พยายามตั้งชื่อของต่างๆให้เป็นมาตรฐานเดียวกัน เพราะเวลาหาของจะได้หาง่ายๆ เช่น package ชื่อ api, model, repository (ตัวอย่างอาจจะไม่เหมือน Sameer นะ) ใช่ครับ นี่มันจะทำให้เราหาของง่ายก็จริง แต่ Sameer บอกว่านี่มันน่าจะมาผิดทางละ เพราะการทำแบบนี้ก็ไม่ต่างอะไรกับการใช้ชื่ออย่าง util หรือ common เพราะถ้าเราทำแบบนี้ ต่อไปมันจะแพร่ขยายการใช้ออกไปโดยไม่ได้อธิบายมันให้ชัดเจน แต่ละคนจะตีความสิ่งที่เห็นออกไปไม่เหมือนกัน วางของมั่วซั่ว และพอมันเยอะขึ้นไปเรื่อยๆ ปัญหาอีกมากมายก็จะตามมา
คำแนะนำคือ ถ้าคิดว่าอะไรมันมีตัวตนชัดแล้วเช่น type นี้ใครๆก็ใช้ repo ไหนๆก็ต้องมีมัน งั้นก็แยกมันออกไปเป็น public package ให้ทุกคนได้เห็นเลยจะดีกว่า
หลีกเลี่ยงการใช้ชื่อซ้ำกัน เพราะบางทีพอเราแบ่งไดเร็คทอรี่ออกไปแล้ว เราอาจจะเผลอตั้งชื่อซ้ำกันได้ ซึ่งที่ถูกต้อง ถ้า 2 ชื่อที่ซ้ำกัน มีโอกาสถูกใช้พร้อมๆกันอยู่เรื่อยๆ มันควรต้องตั้งชื่อให้ต่างกันไปเลย และด้วยเหตุผลนี้ ก็ช่วยๆกันหลีกเลี่ยงการตั้งชื่อ package ให้อย่าไปซ้ำกับพวก package ยอดฮิตอย่าง io หรือ http ด้วยเช่นกัน
บทสรุปของ Sameer คือ ชื่อของ package ที่ดีเป็นจุดสำคัญที่จะทำให้โค้ด Go ของคุณเป็นระเบียบได้ ยอมเสียเวลากับมัน ตั้งชื่อดีๆ จัดระเบียบมันให้ดีแล้ว มันจะช่วยให้เพื่อนร่วมงาน และคนที่ต้องมาดูแลต่อเขาจะทำความเข้าใจมันได้ง่าย ดูแลต่อได้ดีขึ้น สร้างให้มันดีขึ้น ขยายงานได้อย่างไร้ความกังวล
บทสรุปของผมเอง ใช่ครับ ชื่อ package นั้นสำคัญ และสร้างความปวดตับให้คนที่ต้องมาดูแลต่อได้มหาศาลเช่นกัน ผมพยายามจะส่งเสียงว่า อย่าตั้งชื่อแบบแบ่งตาม Layer มาตลอด แต่เหมือนเสียงจะเบาไป และจากบทความนี้ Sameer เองก็คิดแบบเดียวกับผม มันจะมีชื่อมาตรฐานได้ยากมาก ถ้าคุณคิดแบบ Gopher แต่เพราะเรา fixed อยู่กับวิธีคิดแบบเดิมๆ มานาน การจะเปลี่ยนสิ่งนี้ก็ไม่ง่าย แต่เราควรช่วยกัน ไม่งั้นงานเราจะยากขึ้นทุกวันครับ ขอบคุณที่เข้ามาอ่านนะครับ
Top comments (1)
เห็นด้วยกับการอยากให้ทุกคนเห็นความสำคัญของการตั้งชื่อ ซึ่งมันส่งผลกับการทำความเข้าใจและ debug โค้ดจริง ๆ
แต่ส่วนตัวแล้วผมอาจจะไม่ได้เห็นด้วยซะทีเดียวกับการที่ยกวิธีแบบหนึ่งมาแล้วบอกว่ามันดีกว่าอีกแบบหนึ่ง ผมคิดว่าในบริบทหนึ่งมันอาจจะดีกว่า แต่ในอีกบริบทหนึ่ง แบบอื่นอาจจะเหมาะสมกว่าก็ได้
ผมคิดว่าเราไม่ควร fixed อยู่กับวิธีคิดแบบใดแบบหนึ่ง ไม่ว่าจะเป็นวิธีคิดแบบเดิม ๆหรือใหม่ ๆ ผมเชื่อว่าทุกวิธีคิดที่มันถูกคิดออกมาจากผู้คนมากมายที่มีความคิดที่แตกต่างกัน มันมีเหตุผลและมีความเหมาะสมกับบริบทหนึ่ง ๆ
สิ่งที่สำคัญคือการเข้าใจความคิดนั้น ๆแล้วเลือกนำไปใช้กับงานและบริบทที่เหมาะสมมากกว่า