บทที่ 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