บทที่ 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 อย่างเดียว ขั้นตอนที่ปลอดภัยกว่า:

  1. เลือก use case ที่มี tests/observable contract
  2. ระบุ owner ของ business vocabulary
  3. ย้าย transport/application/persistence code มาใกล้โดยรักษาพฤติกรรม
  4. ตัด cross-feature imports ด้วย public contract
  5. รวม rule ที่ duplicate เฉพาะเมื่อเป็น invariant เดียวจริง
  6. เพิ่ม 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 รายการ:

  1. เพิ่ม Refund rejection reason
  2. เพิ่ม Customer billing address validation
  3. 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

อ่านเพิ่มเติม