ศูนย์รวมความรู้วิศวกรรมซอฟต์แวร์

แบ่งปันประสบการณ์การออกแบบสถาปัตยกรรมระบบ, การเขียนโค้ดด้วย Laravel และการจัดการ Server ระดับ Production เพื่อยกระดับทักษะของนักพัฒนาทุกคน

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

  1. Ubiquitous Language (ภาษาภาพรวม): ทีม Dev, QA (Tester), SA, PM และฝั่ง Business ต้องใช้ "คำศัพท์เดียวกัน" ทั้งในเอกสารไปจนถึงชื่อตัวแปรในโค้ด เช่น ถ้าธุรกิจเรียกว่า "การเติมก๊าซ" (Refill) ในโค้ดก็ควรเป็น RefillCylinderAction ไม่ใช่ UpdateStock

  2. 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