บทที่ 10 · Part 4 — APIs and Boundaries
Packages as APIs
ออกแบบ package จาก client code, รักษา dependency direction และสร้าง interface ฝั่ง consumer
เมื่อ service โตขึ้น ทีมมักแก้ความรกด้วยการเพิ่มโฟลเดอร์ models, services, repositories,
utils และ interfaces ผลคือการแก้ feature หนึ่งต้องเดินข้ามทุกโฟลเดอร์ และ import cycle เริ่ม
บอกว่า ownership ไม่ชัด Package ที่ดีไม่ใช่กล่องเก็บชนิดเดียวกัน แต่เป็นหน่วยของ capability และ API
จบบทนี้คุณจะ
- ออกแบบ package จาก client code และสิ่งที่มันรับผิดชอบ
- ใช้
internalและ dependency direction เพื่อป้องกัน coupling - สร้าง interface ฝั่ง consumer เมื่อมี use case จริง
เริ่มจาก Client ที่อยากเห็น
ก่อนสร้าง package ให้เขียน call site ของ use case หลัก:
store := sqlite.New(db)
service := todo.NewService(store, clock)
task, err := service.Add(ctx, "ซื้อของเข้าบ้าน")
if err != nil {
return err
}
fmt.Println(task.Title)
ตัวอย่างนี้เป็น application จัดการรายการสิ่งที่ต้องทำ ผู้ใช้เพิ่ม task แล้ว service บันทึกลง SQLite
จาก call site สั้น ๆ เราเห็นว่า todo เป็นเจ้าของ use case และคำว่า Task ส่วน sqlite เป็น adapter
ที่ประกอบจาก connection ซึ่ง application root เปิดไว้แล้ว ไม่ควรมี common.NewService() หรือให้
business package เปิดไฟล์ database เอง เพราะ dependency และ failure timing จะถูกซ่อน
ลูกศรจริงควรชี้จาก adapter เข้าหา contract ของ application ไม่ให้ core use case รู้ว่า request
เข้ามาทาง HTTP หรือเก็บด้วย SQLite การใช้ internal/ บังคับจาก compiler ว่า package ภายนอก parent
tree import ไม่ได้ เหมาะกับ implementation ที่ยังไม่ต้องเป็น public commitment
Package ต้อง Cohesive ก่อนจะเล็ก
ไม่ต้องมี package ต่อ 1 struct และไม่ต้องมี pkg/ เป็นพิธีกรรม เริ่มจากโครงเรียบง่ายแล้วแตก
เมื่อ code มีเหตุผลเปลี่ยนแปลงต่างกัน เช่น HTTP transport กับกฎของ task ไม่ควรอยู่ไฟล์เดียวกัน
เพราะ test และ dependency ต่างกัน แต่ Task, Priority และกฎการปิดงานที่เปลี่ยนพร้อมกันอาจอยู่ package
เดียวได้แม้มีหลายไฟล์
สัญญาณว่า package กว้างเกินไปคือชื่ออธิบายไม่ได้ในประโยคเดียว, import จำนวนมากจากหลาย domain, exported surface โตโดยไม่มี client ชัด หรือทุก feature ต้องแก้ package นั้น ส่วน package ที่เล็กเกินไป จะมี type forwarding, constructor ผ่านหลายชั้น และ import เยอะกว่าพฤติกรรมจริง
Import cycle ไม่ใช่ศัตรูที่ต้องแก้ด้วย package interfaces กลางทันที มันเป็น feedback ว่า ownership
หรือ dependency direction ผิด ให้ถามว่า behavior ใดเป็นของ consumer, data type ใดควรย้าย และ
orchestration ควรอยู่ชั้นไหนก่อนสร้าง abstraction ใหม่
Interfaces Belong to Consumers
ให้ producer ส่ง concrete type และ consumer ประกาศเฉพาะ behavior ที่ต้องใช้:
// package todo
type TaskStore interface {
Create(ctx context.Context, task Task) error
ByID(ctx context.Context, id string) (Task, error)
}
type Service struct {
store TaskStore
}
package sqlite ไม่ต้องประกาศ Repository ขนาด 10 method เผื่อ consumer ในอนาคต มันเพียง export
concrete type *Store ที่บังเอิญ satisfy interface ของ todo วิธีนี้ทำให้ interface เป็นเอกสารของ use case,
mock เล็ก และเปลี่ยน implementation โดยไม่ให้ adapter เป็นผู้กำหนดความต้องการของ core
คำว่า “accept interfaces, return structs” เป็น default ไม่ใช่กฎห้าม return interface ทั้งหมด standard library บาง package ตั้งใจซ่อน family ของ implementation หลัง abstraction ที่ stable จริง แต่ constructor ทั่วไปควร return concrete type เพื่อให้ caller เห็น capability ครบและไม่ต้องสร้าง interface ฝั่ง producerก่อนมีเหตุผล
Public Surface คือ Compatibility Budget
ทุก exported name, method, error และ behavior เป็นสิ่งที่ consumer อาจพึ่งพา การ export เผื่อไว้
แก้กลับยากกว่าการเปิดภายหลัง ให้ unexport อย่างตั้งใจ, ใช้ internal, เขียน doc comment ที่บอก
concurrency/ownership และมี example test สำหรับ API สำคัญ
Production Toolbox
Default คือ standard Go module layout, capability-oriented packages และ manual dependency
direction ใช้ go list -deps, go mod graph และ architecture tests เมื่อ codebase โต อย่าเพิ่ม
framework เพื่อแก้ import cycle ก่อนเข้าใจ ownership; หาก domain architecture ซับซ้อนต่อ ให้เรียน
DDD & Code Architecture
Checklist ของ Package Boundary
- อธิบาย responsibility ของ package ได้ใน 1 ประโยค
- call site อ่านง่ายและไม่มีชื่อซ้ำกับ package
- core ไม่ import transport, database driver หรือ framework
- interface อยู่กับ consumer และมีเฉพาะ method ที่ใช้
- constructor return concrete type เว้นแต่ตั้งใจซ่อน implementation family
- exported surface มี owner, documentation และ compatibility reason
- import cycle ถูกแก้ที่ ownership ไม่ใช่ย้ายทุก interface ไป package กลาง
อ่านเพิ่ม: Organizing a Go module, Developing modules และ Go Code Review Comments — interfaces