Domain Driven Development in Laravel
Domain Driven Development in Laravel
Domain-Driven Design (DDD) คือแนวคิดในการออกแบบและพัฒนาซอฟต์แวร์ที่ "ยึดเอา Business Domain (บริบทของธุรกิจ) เป็นศูนย์กลาง" แทนที่จะยึดติดกับโครงสร้างของเทคโนโลยีหรือ Framework
ในโปรเจกต์ Laravel ทั่วไป เรามักจะคุ้นเคยกับ MVC (Model-View-Controller) ซึ่งเป็นการจัดกลุ่มไฟล์ตาม "หน้าที่ทางเทคนิค" (เช่น เอา Controller ทุกระบบไปรวมกัน เอา Model ทุกตัวไปกองรวมกัน) แต่เมื่อโปรเจกต์สเกลใหญ่ขึ้น มีความซับซ้อนระดับ Enterprise การทำแบบเดิมจะทำให้เกิดปัญหา "Fat Controller" หรือ "Fat Model" โค้ดพันกันจนแก้ระบบหนึ่งแล้วไปพังอีกระบบหนึ่ง
DDD เข้ามาแก้ปัญหานี้โดยการจัดโครงสร้างโฟลเดอร์และโค้ดใหม่ ให้สะท้อนถึง "การทำงานจริงของธุรกิจ"
หัวใจสำคัญของ DDD
- Ubiquitous Language (ภาษาภาพรวม): ทีม Dev, QA (Tester), SA, PM และฝั่ง Business ต้องใช้ "คำศัพท์เดียวกัน" ทั้งในเอกสารไปจนถึงชื่อตัวแปรในโค้ด เช่น ถ้าธุรกิจเรียกว่า "การเติมก๊าซ" (Refill) ในโค้ดก็ควรเป็น RefillCylinderAction ไม่ใช่ UpdateStock
- Bounded Context (ขอบเขตบริบท): การแบ่งระบบใหญ่ๆ ออกเป็นระบบย่อยๆ ที่มีขอบเขตชัดเจน เช่น แยก ระบบคลังสินค้า (Inventory) ออกจาก ระบบขายหน้าร้าน (Point of Sale - POS) อย่างเด็ดขาด
แนวทางในการพัฒนา DDD ใน Laravel
เพื่อตอบโจทย์การคิดงานเชิงโครงสร้าง (System Thinker) และหลีกเลี่ยงความซับซ้อนของ Design Pattern เก่าๆ อย่าง Service-Repository การนำ DDD มาประยุกต์ใช้ร่วมกับสถาปัตยกรรมแบบ Action-DTO-Query จะช่วยให้โค้ดคลีน แยกส่วนประกอบจาก Database ไปจนถึง UI ได้อย่างชัดเจน
1. การจัดโครงสร้างโฟลเดอร์ (Directory Structure)
เราจะย้าย Core Logic ออกจาก app/Http/Controllers และ app/Models โดยสร้างโฟลเดอร์ app/Domains/ ขึ้นมาเพื่อจัดกลุ่มตาม Bounded Context:
Plaintext
app/ ├── Domains/ │ ├── Inventory/ # ขอบเขต: ระบบจัดการคลังและสินค้า │ │ ├── Actions/ # เขียนข้อมูล (Write/Command) เช่น RefillCylinderAction │ │ ├── DTOs/ # แพ็กเกจข้อมูล เช่น CylinderData, RestockData │ │ ├── Queries/ # อ่านข้อมูล (Read) เช่น GetLowStockCylindersQuery │ │ ├── Models/ # Eloquent Models เช่น Cylinder, StockHistory │ │ └── Enums/ # เช่น CylinderStatusEnum │ │ │ └── PointOfSale/ # ขอบเขต: ระบบขายหน้าร้าน POS │ ├── Actions/ # เช่น ProcessCheckoutAction │ ├── DTOs/ # เช่น CheckoutOrderData │ ├── Queries/ # เช่น GetDailyReceiptsQuery │ └── Models/ # เช่น Order, Receipt
2. บทบาทของแต่ละ Layer (การวาง Action-DTO-Query ใน DDD)
- UI / Controller (ทางผ่านข้อมูล): Controller จะทำหน้าที่แค่รับ Request (HTTP/Livewire) ตรวจสอบความถูกต้อง (Validation) แพ็กเป็น DTO แล้วโยนไปให้ Action (สำหรับการบันทึก/แก้ไข) หรือ Query (สำหรับการดึงข้อมูล) จากนั้นนำผลลัพธ์ส่งกลับไปที่ UI
- DTO (Data Transfer Object): คือ Class ที่รับส่งข้อมูลระหว่าง Layer แทนที่จะส่งเป็น array ที่เดา Type ไม่ได้ DTO จะช่วยบังคับ Type เสมอ ทำให้รู้ว่าข้อมูลที่ส่งเข้าไปทำงานมีโครงสร้างอย่างไร
- Action (ทำหน้าที่เดียว - Single Responsibility): จุดรวม Business Logic ที่แท้จริง แต่ละคลาสจะทำหน้าที่แค่อย่างเดียวเท่านั้น (เช่น คลาส CreateReceiptAction) ทำให้เทส (QA Automation) และ Debug ได้ง่ายมาก
- Query (ดึงข้อมูลซับซ้อน): แยกการดึงข้อมูลออกจาก Controller หรือ Model ถ้าระบบต้องการ Report ซับซ้อน หรือ Join หลายตาราง จะถูกรวมไว้ในคลาส Query (เช่น GetMonthlySalesQuery)
3. ตัวอย่างการทำงานร่วมกัน (Flow Example)
ตัวอย่าง: การขายสินค้าหน้าร้าน (Point of Sale Domain)
PHP
// 1. Controller: รับ Request และสั่งการ
class CheckoutController extends Controller
{
public function __invoke(CheckoutRequest $request, ProcessCheckoutAction $action)
{
// สร้าง DTO จาก Request
$orderData = new CheckoutOrderData(
customerId: $request->customer_id,
items: $request->items,
totalAmount: $request->total_amount
);
// โยน DTO เข้า Action ทำงาน Business Logic
$receipt = $action->execute($orderData);
return response()->json(['receipt' => $receipt]);
}
}
PHP
// 2. Action: รวม Business Logic ของ Domain (ไม่ต้องพึ่ง Service หรือ Repository)
class ProcessCheckoutAction
{
public function execute(CheckoutOrderData $data): Receipt
{
return DB::transaction(function () use ($data) {
// 1. ตัดสต๊อก (อาจจะเรียกใช้อีก Action ของฝั่ง Inventory)
// 2. คำนวณยอดเงิน
// 3. สร้างใบเสร็จ (Receipt Model)
return $receipt;
});
}
}
สรุปข้อดีของการใช้ DDD + Action-DTO-Query: โครงสร้างนี้จะแยก Data Flow อย่างชัดเจน ทำให้ฝั่ง UI (เช่น React, Vue หรือ Livewire Volt) ไม่ต้องผูกติดกับ Database เมื่อสเกลโปรเจกต์ หรือมีการแบ่งทีมพัฒนา (SA, PM, Dev, QA Tester) ทุกคนจะมองเห็นภาพกระบวนการทำงานของระบบเป็นชิ้นๆ (Modular) ที่ชัดเจนและนำไปเขียน Test Script ด้วย Playwright/Postman ได้ง่ายขึ้นมาก เพราะรู้ว่า Endpoint หรือ Action ไหนมี Input (DTO) และ Output เป็นอะไร
laravel