รับมือกับปัญหา Import Cycles ใน Golang ด้วยการใช้ Interface
สืบเนื่องมาจากว่า มีเพื่อนผมท่านหนึ่งติดปัญหาเกี่ยวกับการ Import ข้าม Package ทำให้ผมมีโอกาสได้หาข้อมูลวิธีการแก้ปัญหาดังกล่าวช่วยเพื่อน และบทความนี้เป็นผลพลอยได้ของการหาข้อมูลนั้นครับ 🎉
ในฐานะนักพัฒนา Golang เราคุ้นเคยกับการจัดการโค้ดด้วยการแบ่งเป็น Package เพื่อความเป็นระเบียบเรียบร้อย แต่บางครั้งการแยก Package อาจนำไปสู่ปัญหาที่เรียกว่า Import Cycles
Import Cycles
ลองนึกภาพว่าเรามีสอง Package คือ a
และ b
กรณีที่ Package a
จำเป็นต้องใช้ฟังก์ชันหรือตัวแปรภายใน Package b
ดังนั้น a
จึง import Package b
ในขณะเดียวกัน Package b
ก็จำเป็นต้องใช้ฟังก์ชันหรือตัวแปรภายใน Package a
ด้วยเช่นกัน ทำให้ b
import Package a
กลับคืนด้วย
สถานการณ์นี้ก่อให้เกิด Import Cycles ขึ้น ซึ่งเป็นสิ่งที่ภาษา Go ไม่อนุญาต
Import Cycles ไม่ได้เกิดขึ้นแค่กรณีง่ายๆ แบบนี้เสมอไป ตัวอย่างเช่น Package b
อาจจะไม่ได้ Import Package a
โดยตรง แต่ Package b
Import Package c
และ Package c
กลับไป Import Package a
สุดท้ายก็ยังคงเป็น Import Cycles อยู่ดี
มาดูตัวอย่างโค้ดที่ทำห้เกิด Import Cycles กันดีกว่า
Package a:
package a
import (
"import-cycle-example/b"
)
func A() {
b.B()
}
Package b:
package b
import (
"import-cycle-example/a"
)
func B() {
a.A()
}
Import Cycles Are Bad Design
ภาษา Go มุ่งเน้นไปที่การ Compile ที่รวดเร็ว ซึ่งเร็วมากกว่าความเร็วในการ Execute โปรแกรมด้วยซ้ำ แม้ว่าจะต้องสละประสิทธิภาพรันไทม์บางส่วนก็ตาม
ดังนั้น Go Complier จึงไม่เสียเวลากับการสร้างพยายามสร้าง Machine Code ให้ประสิทธิภาพสูงสุดเท่าที่จะเป็นไปได้ แต่หันมาให้ความสำคัญกับการ Compile โค้ดจำนวนมากให้ได้อย่างรวดเร็วแทน
การอนุญาตให้มี Cyclic/Circular ระหว่าง Package ใน Go จะส่งผลเสียให้เวลา Compile นานขึ้น เพราะทุกครั้งที่มีการเปลี่ยนแปลงใน Package ใดๆ ภายในวงจร Package อื่นๆ ที่เกี่ยวข้องทั้งหมดจะต้องถูก Compile ใหม่ด้วย และอาจเกิด Recursion ไม่สิ้นสุดได้ในบางกรณี และอาจทำให้เกิดการเรียกตัวเองซ้ำไม่รู้จบ
เท่านั้นยังไม่พอ ยังส่งผลให้เกิด Memory Leaks ได้อีกด้วย เนื่องจาก Object ภายใน Package ต่างๆ ที่อยู่ในวงจรต่างก็อ้างอิงซึ่งกันและกัน ส่งผลให้จำนวนการอ้างอิงของ Object เหล่านั้นไม่เคยลดลงเป็น 0 เลย และทำให้ Garbage Collector ไม่สามารถเก็บกวาดข้อมูลเหล่านั้นออกไปได้
การแก้ไขปัญหา Import Cycles
เมื่อเราข้อผิดพลาดเกี่ยวกับ Import Cycles สิ่งที่ควรทำคือ ให้ลองหยุดคิดทบทวนเกี่ยวกับการจัดโครงสร้างโปรเจกต์ของเราสักพัก
มีวิธีแก้ไขปัญหานี้หลายวิธี ซึ่งวิธีที่ง่ายและพบมากที่สุดคือการใช้ Interface
ใช้ Interface
- Package
a
มีการใช้functions/variables
จาก Packageb
โดยการ Import Packageb
- Package
b
มีการใช้functions/variables
จาก Packagea
โดยไม่ต้อง Import Packagea
การกระทำทั้งหมดจะต้องทำผ่าน Instance ของ Packagea
ซึ่งใช้ Interface ที่กำหนดในb
Instance พวกนั้นจะถูกมองว่าเป็น Object ของ Packageb
Package b:
package b
import (
"fmt"
)
type pa interface {
HelloFromA()
}
type PB struct {
PA pa
}
func New(pa pa) *PB {
return &PB{
PA: pa,
}
}
func (p *PB) HelloFromB() {
fmt.Println("Hello from package b")
}
func (p *PB) HelloFromASide() {
p.PA.HelloFromA()
}
Package a:
package a
import (
"fmt"
"import-cycle-example/b"
)
type PA struct{}
func New() *PA {
return &PA{}
}
func (p *PA) HelloFromA() {
fmt.Println("Hello from package a")
}
func (p *PA) HelloFromBSide() {
pb := b.New(p)
pb.HelloFromB()
}
Package main:
package main
import (
"import-cycle-example/a"
)
func main() {
pa := a.PA{}
pa.HelloFromBSide()
}
ผมหวังว่าจะแชร์ความเข้าใจนี้ให้ผู้อ่านทุกท่านได้ไม่มากก็น้อย และขอให้มีความสุขกับการโค้ดดิ้งครับ 🎉