บทที่ 12 · Part 4 — APIs and Boundaries
Interfaces, Generics and Reflection
เลือก abstraction จากชนิดของความแปรผัน แทนการใช้ interface หรือ generics กับทุกปัญหา
เมื่อทีมพบ duplication คำตอบที่มักได้ยินคือ “สร้าง interface” หรือ “ทำ generic” ทั้งที่ 2 เครื่องมือ แก้ความแปรผันคนละชนิด ส่วน reflection มักเข้ามาทีหลังเมื่อ abstraction แรกไม่พอ ผลลัพธ์คือ framework ภายในที่ type-safe ไม่เต็มที่และอ่านยากกว่าการทำซ้ำไม่กี่บรรทัด
จบบทนี้คุณจะ
- เลือก concrete code, interface, type parameter หรือ reflection จาก problem shape
- ออกแบบ constraint และ interface ที่เล็กจากฝั่ง consumer
- ประเมิน
samber/moโดยไม่ทำให้ Go กลายเป็นภาษาอื่น
แยกชนิดของความแปรผัน
ถ้า settlement.Service ต้องเก็บ batch ได้ผ่าน MySQL หรือ in-memory fake ความแปรผันคือ behavior
จึงใช้อินเทอร์เฟซ BatchStore ถ้าต้องหา item จาก slice ของ type ใด ๆ ด้วย key function algorithm
เหมือนกันและ type ต่างกัน จึงใช้ generics ถ้าแค่มี code 2 ชุดที่บังเอิญยาวเท่ากันแต่ business rule
ต่างกัน ให้คง concrete code จน pattern ชัด
Interface คือ Behavior Contract
interface ที่ดีเล็กและตั้งอยู่ใน package ที่ใช้มัน ไม่ต้องสร้างเพราะมี implementation 1 ตัว และ
ไม่ต้องสร้าง IService คู่กับทุก struct:
type Clock interface {
Now() time.Time
}
type BatchFinder interface {
ByID(ctx context.Context, id string) (Batch, error)
}
อย่าเพิ่ม method “เผื่ออนาคต” เพราะทุก method ทำให้ fake/mock และ alternate implementation ยากขึ้น
การ compose small interfaces ทำได้เมื่อ consumer ต้องการชุดใหญ่จริง และ compile-time assertion
var _ BatchFinder = (*mysql.BatchStore)(nil) มีประโยชน์เมื่อ package ตั้งใจประกาศ relationship นั้น
embedding เป็น composition พร้อม method promotion ไม่ใช่ inheritance ถ้า outer type ไม่ต้อง expose API ทั้งหมดของ inner type ให้ใช้ named field และ delegate เฉพาะ behavior ที่เป็น contract ของตัวเอง
Generics สำหรับ Algorithm ไม่ใช่ Architecture
ตัวอย่างต่อไปนี้ใช้ type parameter เพราะ implementation เหมือนกันข้าม type:
func IndexBy[T any, K comparable](items []T, key func(T) K) map[K]T {
result := make(map[K]T, len(items))
for _, item := range items {
result[key(item)] = item
}
return result
}
constraint ควรเล็กเท่าที่ operation ต้องการ comparable เหมาะกับ map key แต่การสร้าง constraint
ชื่อใหญ่ที่รวม method หลายตัวอาจเป็น interface design ที่ซ่อนอยู่ หาก generic function เพียงเรียก
method เดียวกับ type ให้พิจารณา interface ปกติก่อน
อย่าใช้ generics แทน domain name เช่น Repository[T] ที่มี CRUD ทุกชนิดทำให้ Order, Batch และ
Ledger ถูกบังคับให้มี lifecycle เหมือนกันและ error vocabulary เดียวกัน Generic repository มักลด
boilerplate แต่เพิ่ม coupling ที่ business boundary
Reflection ต้องมี Boundary แคบ
reflection เหมาะเมื่อ type shape รู้เฉพาะ runtime เช่น serializer, validator, DI container หรือ framework แต่ควรถูกซ่อนหลัง typed API, cache metadata ที่คำนวณซ้ำ, รับมือ invalid/unexported values และมี fuzz test เพราะ panic surface สูง อย่าใช้ reflection เพื่อหลีกเลี่ยงเขียน adapter ที่มี field ไม่กี่ตัว
ถ้าความต้องการคือ decode external payload เรามักเลือก generated code หรือ explicit DTO เพราะ compiler ช่วยตรวจ contract ได้มากกว่า reflection-based map
samber/mo เป็นทางเลือกเฉพาะทาง
samber/mo มี Option, Result, Either, Future และ monadic
combinators ที่ type-safe เหมาะกับทีมที่มี vocabulary functional ชัด หรือ pipeline ที่ composition
ได้ประโยชน์จริง แต่ไม่ใช่ default ของคอร์ส เพราะ Go มี multiple returns, explicit error และ control
flow ที่ ecosystem เข้าใจร่วมกันอยู่แล้ว การห่อทุก result อาจทำให้ API ต่างจาก stdlib และ debugging
ยากสำหรับคนใหม่
Production Toolbox
Default คือ concrete code จนเห็นความแปรผัน, consumer interface สำหรับ behavior, generics สำหรับ
algorithm และ reflection เป็นทางเลือกสุดท้ายที่กัก boundary samber/mo ใช้เมื่อ vocabulary ของทีม
และ composition benefit ชัด ไม่ใช้เพื่อทำให้ error check หายจากสายตา
Checklist ก่อนสร้าง Abstraction
- ระบุได้ว่า behavior, type หรือ runtime shape อะไรเปลี่ยน
- มี client อย่างน้อย 1 รายที่ได้ประโยชน์ ไม่ใช่ abstraction เผื่ออนาคต
- interface อยู่กับ consumer และเล็กพอให้ implement โดยไม่รู้เรื่องเกินจำเป็น
- generic constraint สะท้อน operation จริง ไม่ใช่ framework taxonomy
- reflection ถูกซ่อนหลัง typed API และมี panic/error tests
- abstraction ลด cost รวมทั้ง debug และ onboarding ไม่ใช่ลดเพียงจำนวนบรรทัด
อ่านเพิ่ม: When To Use Generics, An Introduction to Generics, Generic interfaces และ Go Code Review Comments — interfaces