รับ interface แล้วคืนค่า struct

ในโลกของการเขียนโปรแกรมด้วย Go การออกแบบฟังก์ชันให้ยืดหยุ่นและดูแลง่ายคือกุญแจสำคัญของการสร้างแอปที่แข็งแรง หนึ่งในแพทเทิร์นที่ทรงพลังมากคือ
รับค่าเป็น interface แล้วคืนค่าเป็น struct
แนวคิดนี้ช่วยให้เทสง่ายขึ้น ลดการผูกติด (coupling) ระหว่างส่วนต่าง ๆ ในระบบ และทำให้โค้ดของเราปรับตัวตาม requirement ที่เปลี่ยนไปได้ดีขึ้นมาก
ทำไมควรใช้ Interface เป็น Input?
เวลาเราทำให้ฟังก์ชัน “รับ interface” เป็นพารามิเตอร์ หมายความว่าฟังก์ชันนั้นสามารถทำงานกับ type อะไรก็ได้ที่ “implements interface นั้น” อยู่
ลองนึกภาพเคสที่เราต้องประมวลผลเอกสารหลายประเภท ปกติอาจเผลอไปเขียนฟังก์ชันแยกเป็น ProcessPDF(), ProcessWord() ฯลฯ แต่จริง ๆ แล้วเราสามารถนิยาม interface กลางขึ้นมาชุดเดียวแล้วใช้ร่วมกันได้
package main
import "fmt"
// DocumentProcessor กำหนดสัญญา (contract) สำหรับตัวที่เอาไว้ประมวลผลเอกสาร
type DocumentProcessor interface {
Process() string
GetContent() string
}
// PDFDocument implements DocumentProcessor สำหรับไฟล์ PDF
type PDFDocument struct {
Content string
}
func (p PDFDocument) Process() string {
return fmt.Sprintf("Processing PDF document with content: %s", p.Content)
}
func (p PDFDocument) GetContent() string {
return p.Content
}
// WordDocument implements DocumentProcessor สำหรับไฟล์ Word
type WordDocument struct {
Text string
}
func (w WordDocument) Process() string {
return fmt.Sprintf("Processing Word document with text: %s", w.Text)
}
func (w WordDocument) GetContent() string {
return w.Text
}
// HandleDocument รับค่าอะไรก็ได้ที่ implements DocumentProcessor
func HandleDocument(processor DocumentProcessor) {
fmt.Println(processor.Process())
}
func main() {
pdf := PDFDocument{Content: "Go documentation"}
word := WordDocument{Text: "Software design patterns"}
HandleDocument(pdf)
HandleDocument(word)
}
ในตัวอย่างนี้ HandleDocument ไม่สนใจเลยว่ากำลังทำงานกับ PDFDocument หรือ WordDocument สิ่งเดียวที่มันสนใจคือ object ที่ส่งเข้ามา “ต้อง” implements DocumentProcessor ซึ่งการันตีว่าอย่างน้อยมีเมธอด Process() ให้เรียกแน่นอน
HandleDocument นำกลับมาใช้ซ้ำได้สูง และเทสง่ายมาก เพราะเราสามารถสร้าง mock ที่ implements DocumentProcessor ขึ้นมาทดแทนของจริงได้เลยทำไมควรใช้ Struct เป็น Output?
การคืนค่าเป็น struct ทำให้เราสื่อสาร “หลาย ๆ ข้อมูล” ออกจากฟังก์ชันได้อย่างมีความหมาย ชัดเจน และเป็นระบบมากกว่าแค่คืนค่าหลาย ๆ ตัวแบบกระจัดกระจาย เพราะ struct จะบอก semantic ของข้อมูลแต่ละฟิลด์ได้ชัดเจนกว่า และทำให้ signature ของฟังก์ชันอ่านง่าย ดูแลต่อได้ไม่ปวดหัว
ลองขยายตัวอย่างการประมวลผลเอกสารให้คืนค่าแบบมีโครงสร้างมากขึ้น
package main
import "fmt"
// DocumentProcessor กำหนดสัญญา (contract) สำหรับตัวที่เอาไว้ประมวลผลเอกสาร
type DocumentProcessor interface {
Process() (ProcessResult, error)
GetContent() string
}
// ProcessResult แทนผลลัพธ์แบบมีโครงสร้างของการประมวลผลเอกสาร
type ProcessResult struct {
DocumentType string
Status string
Message string
ProcessedBy string
}
// PDFDocument implements DocumentProcessor สำหรับไฟล์ PDF
type PDFDocument struct {
Content string
}
func (p PDFDocument) Process() (ProcessResult, error) {
// สมมติว่ามี logic การประมวลผลบางอย่าง
result := ProcessResult{
DocumentType: "PDF",
Status: "Success",
Message: "PDF processed successfully",
ProcessedBy: "PDFProcessorV1",
}
return result, nil
}
func (p PDFDocument) GetContent() string {
return p.Content
}
// WordDocument implements DocumentProcessor สำหรับไฟล์ Word
type WordDocument struct {
Text string
}
func (w WordDocument) Process() (ProcessResult, error) {
// สมมติว่ามี logic การประมวลผลบางอย่าง
result := ProcessResult{
DocumentType: "Word",
Status: "Success",
Message: "Word document processed successfully",
ProcessedBy: "WordProcessorV2",
}
return result, nil
}
func (w WordDocument) GetContent() string {
return w.Text
}
// HandleDocument ประมวลผลเอกสารแล้วคืนผลลัพธ์แบบมีโครงสร้าง
func HandleDocument(processor DocumentProcessor) (ProcessResult, error) {
return processor.Process()
}
func main() {
pdf := PDFDocument{Content: "Go documentation"}
word := WordDocument{Text: "Software design patterns"}
pdfResult, err := HandleDocument(pdf)
if err != nil {
fmt.Printf("Error processing PDF: %v\n", err)
} else {
fmt.Printf("PDF Processing Result: %+v\n", pdfResult)
}
wordResult, err := HandleDocument(word)
if err != nil {
fmt.Printf("Error processing Word: %v\n", err)
} else {
fmt.Printf("Word Processing Result: %+v\n", wordResult)
}
}
พอเราใช้ ProcessResult เป็น struct สำหรับผลลัพธ์ เราก็ห่อข้อมูลทุกอย่างที่เกี่ยวกับการประมวลผลใส่เข้าไปในที่เดียว
HandleDocument (ยังคงเป็น (ProcessResult, error) เหมือนเดิม)สรุป
มันช่วยให้โค้ดแยกส่วนกันดี (modular), เทสง่าย, และดูแลรักษาในระยะยาวได้สบายขึ้น พอเราโฟกัสที่ “สัญญา” (interface) ว่าฟังก์ชันต้องการอะไร และใช้ “ข้อมูลแบบมีโครงสร้าง” (struct) บอกสิ่งที่ฟังก์ชันส่งกลับ โค้ดที่ได้จะทั้งยืดหยุดและอ่านง่ายในเวลาเดียวกัน