บทที่ 11 · Part 3 — Organizing the Codebase
Package by Type vs Package by Feature
เปรียบเทียบ global technical folders กับ business slices ผ่าน cohesion, coupling และ locality of change
Package by Type vs Package by Feature
requirement “เพิ่ม reason ให้ Refund rejection” ดูเป็น change เล็ก แต่ใน codebase แบบ
handlers/services/repositories/models engineer ต้องเปิด 4 directory ค้นหาไฟล์ชื่อคล้ายกัน
และเสี่ยงแก้ generic mapper ที่หลาย feature ใช้ นี่คือ locality of change ต่ำ แม้ diagram
จะแบ่ง layer ถูกต้อง
จบบทนี้คุณจะ
เปรียบเทียบ package by type กับ package by feature ผ่าน cohesion, coupling และ change locality อธิบายตำแหน่งของ Vertical Slice อย่างถูกต้อง และ refactor change list ให้ capability owner เห็น code ที่ต้องเปลี่ยนอยู่ใกล้กัน
[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน
2 แกนการจัดโครงสร้าง
Package by type จัดของที่ทำหน้าที่เทคนิคเหมือนกันไว้ด้วยกัน:
internal/
handlers/
services/
repositories/
models/
Package by feature จัดของที่เปลี่ยนเพราะ business capability เดียวกันไว้ใกล้กัน:
internal/
refund/
approve.go
reject.go
http.go
mysql.go
charge/
customer/
ไม่มีแบบใดชนะทุกครั้ง code generator หรือ infrastructure library อาจเหมาะกับ technical package แต่ product code ที่เปลี่ยนตาม use case มักได้ locality จาก feature slice
ประเมิน Cohesion และ Coupling
Cohesion ถามว่าสิ่งใน package เปลี่ยนด้วยเหตุผลเดียวกันเพียงใด repositories
ที่มี Customer, Invoice, Charge และ Tax ไม่มี business reason ร่วมกันนอกจากใช้ SQL
Coupling ถามว่า module ต้องรู้รายละเอียดอีก module มากเพียงใด การ import package เดียว ไม่ได้แปล coupling ต่ำ หากต้องเรียก 10 method ตามลำดับหรือ share mutable model
Locality of change วัดด้วย history จริง:
- change หนึ่งแตะกี่ package/directory
- คน review ต้องโหลดกี่ concept พร้อมกัน
- merge conflicts เกิดที่ central files ใด
- feature สามารถ test และ delete โดยมี blast radius เท่าไร
ใช้ git log --name-only หรือ change records เป็น evidence อย่าตัดสินจาก tree screenshot
เพียงอย่างเดียว
Vertical Slice ไม่ใช่ Bounded Context
Jimmy Bogard เสนอ Vertical Slice เพื่อจัด code ตาม request/use case และลด coupling ระหว่าง use cases มันเป็น practitioner architecture/code organization pattern ไม่ใช่ DDD pattern ต้นฉบับ
ข้อแยกสำคัญ:
- Slice อาจเล็กระดับ
ApproveRefundภายใน Refund module - Bounded Context เป็น boundary ของ model/language ที่ใหญ่กว่าหรือคนละแกน
- Vertical Slice ไม่บังคับ CQRS, MediatR, handler class หรือ language ใด
- Slice ยังแชร์ domain model ได้เมื่อ invariant/language เดียวกัน
ถ้าสร้างทุก endpoint เป็น slice ที่ duplicate refund invariant การจัดไฟล์ดีขึ้นแต่ model แย่ลง ให้ share concept ภายใน owning business module ไม่ใช่ห้าม share ทุกอย่าง
Refactor ตามเหตุผลที่เปลี่ยน
ก่อน:
handlers/refund.go
services/refund.go
repositories/refund.go
models/refund.go
หลัง:
internal/refund/
approve.go
reject.go
domain.go
http.go
repository/mysql.go
อย่าย้ายไฟล์แบบ mechanical อย่างเดียว ขั้นตอนที่ปลอดภัยกว่า:
- เลือก use case ที่มี tests/observable contract
- ระบุ owner ของ business vocabulary
- ย้าย transport/application/persistence code มาใกล้โดยรักษาพฤติกรรม
- ตัด cross-feature imports ด้วย public contract
- รวม rule ที่ duplicate เฉพาะเมื่อเป็น invariant เดียวจริง
- เพิ่ม import/cycle check ก่อนย้าย slice ถัดไป
Shared platform code ควรมี capability ชัด เช่น clock, idgen, observability ไม่ใช่
common ซึ่งดึงทุก dependency มารวมกัน Domain value ที่ 2 module ใช้ไม่ควรถูกย้าย
กลางทันที อาจเป็น Published Language type หรือแปลที่ boundary
แบบฝึกปฏิบัติ: วัดการกระจายของ Change Set
ให้ change request 3 รายการ:
- เพิ่ม Refund rejection reason
- เพิ่ม Customer billing address validation
- map provider timeout เป็น unknown outcome
วาดไฟล์ที่ต้องเปลี่ยนใน type-based tree แล้วออกแบบ feature slices จากนั้นให้คะแนน:
Files touched:
Business concepts loaded:
Cross-slice imports introduced:
Shared files modified:
Tests proving behavior:
Owner who reviews the change:
เป้าหมายไม่ใช่ทำให้ files touched เป็นหนึ่งเสมอ แต่ทำให้การเปลี่ยน 1 business reason ไม่ต้องแก้ central switch/registry และ dependency ข้าม slice ผ่าน contract ที่ตั้งใจ
รายการตรวจสอบ
- Package มี cohesion จากเหตุผลการเปลี่ยนที่ชัด
- ประเมิน coupling จาก contract/runtime ไม่ใช่จำนวน imports เท่านั้น
- ใช้ history/change list เป็น evidence
- Vertical Slice ไม่ถูกเรียกว่า Bounded Context หรือ CQRS requirement
- Shared code มี purpose/owner และไม่เป็น dumping ground
- Refactor รักษา behavior ด้วย tests และทำทีละ slice
สรุปบทนี้
Type-based structure optimize การมอง technical roles ส่วน feature-based structure optimize business change locality สำหรับ product code ให้เริ่มจาก slice ภายนอกและ ใช้ role ภายในเมื่อมีประโยชน์ แต่ตัดสินด้วย cohesion/coupling จริง ไม่ใช่แฟชั่นของ tree
อ่านเพิ่มเติม
- Vertical Slice Architecture — original practitioner article
- Go module layout — official Go guidance
- Package names — Go package purpose และ naming
- A Philosophy of Software Design — information hiding และ complexity