ทำความรู้จักกับ Domain-Driven Design (DDD) ในโลกของ Laravel
เวลาเราเริ่มเขียนโปรแกรมโปรเจกต์ใหญ่ขึ้นเรื่อยๆ โค้ดของเรามักจะกลายเป็นก้อนยุ่งเหยิงที่เรียกว่า Spaghetti Code (โค้ดที่พันกันจนแกะไม่ออก) Domain-Driven Design หรือที่เรียกสั้นๆ ว่า DDD คือแนวคิดการออกแบบซอฟต์แวร์ที่เน้นการแบ่งโค้ดตาม "ขอบเขตของธุรกิจ" เพื่อให้เราจัดการความซับซ้อนได้ง่ายขึ้น แทนที่จะโยนทุกอย่างลงไปในโฟลเดอร์เดียวแบบสะเปะสะปะ
ในการพัฒนาเว็บด้วย Laravel (เฟรมเวิร์กยอดนิยมสำหรับภาษา PHP) เรามักจะชินกับการใส่ตรรกะทุกอย่างไว้ใน Controller (ตัวควบคุมการทำงาน) หรือ Model (ตัวแทนข้อมูลในฐานข้อมูล) ซึ่งพอโปรเจกต์โตขึ้นมันจะเริ่มคุมยาก DDD จะเข้ามาช่วยให้เราแยกส่วน "หัวใจของธุรกิจ" ออกมาให้ชัดเจน เพื่อให้โค้ดอ่านง่ายและดูแลรักษาได้ในระยะยาวโดยไม่ต้องทิ้งเครื่องมือดีๆ อย่าง Eloquent (เครื่องมือจัดการฐานข้อมูลของ Laravel) ไป
เป้าหมายของเราไม่ใช่การเปลี่ยน Laravel ให้กลายเป็นระบบที่ซับซ้อนเกินจำเป็น แต่เป็นการใช้ Laravel Boost (เครื่องมือช่วยให้ AI เข้าใจโครงสร้างโปรเจกต์) เพื่อเป็นไกด์นำทางให้เราจัดระเบียบโค้ดได้ดีขึ้น เราจะมาดูกันว่าเราจะประยุกต์ใช้แนวคิดนี้โดยยังคงความเรียบง่ายแบบ Laravel ไว้ได้อย่างไร เพื่อให้คนเขียนโค้ดมือใหม่หรือรุ่นพี่ในทีมทำงานร่วมกันได้โดยไม่หลงทาง
โครงสร้างโปรเจกต์แบบแบ่งเขตแดน
หัวใจสำคัญของ DDD คือการแบ่งโฟลเดอร์ตาม "ความรับผิดชอบ" ไม่ใช่แค่ชื่อไฟล์ทั่วไป เราจะเริ่มสร้างโครงสร้างใหม่ในโปรเจกต์ Laravel โดยแยกออกเป็น 3 ส่วนหลัก ได้แก่ App\Domain สำหรับกฎเกณฑ์ธุรกิจ, App\Application สำหรับขั้นตอนการทำงาน, และ App\Infrastructure สำหรับการเชื่อมต่อภายนอก
การแบ่งแบบนี้ช่วยให้เราไม่ต้องงมหาโค้ดที่กระจัดกระจาย สมมติว่าเราทำระบบสั่งซื้อของ กฎว่า "ออเดอร์ยกเลิกได้ตอนไหน" จะอยู่ในส่วนของ Domain ส่วนการทำงานที่ต้องไปดึงข้อมูลจากฐานข้อมูลหรือส่งอีเมลจะถูกจัดการโดย Application ทำให้เราแยกส่วนที่เปลี่ยนแปลงบ่อยออกจากส่วนที่เป็นแกนหลักของระบบได้ชัดเจนยิ่งขึ้น
เราสามารถใช้ Laravel Boost เพื่อสอนให้ AI รู้จักโครงสร้างนี้ได้ทันที เมื่อเราพิมพ์คำสั่งสร้างฟีเจอร์ใหม่ AI จะรู้ว่าควรวางไฟล์ไว้ที่ไหนตามกฎที่เราตั้งไว้ นี่คือตัวอย่างโครงสร้างโฟลเดอร์พื้นฐานที่เราจะเริ่มนำมาใช้ในโปรเจกต์เพื่อให้งานเป็นระบบระเบียบมากขึ้น
// โครงสร้างโฟลเดอร์ที่แนะนำ
app/
Domain/ // เก็บกฎธุรกิจ (เช่น Order.php)
Application/ // เก็บขั้นตอนทำงาน (เช่น CancelOrder.php)
Infrastructure/ // เก็บการเชื่อมต่อ (เช่น PaymentGateway.php)
Http/ // เก็บ Controller ตามปกติของ Laravel
อธิบายโค้ด: โครงสร้างนี้แยกโฟลเดอร์ตามหน้าที่ชัดเจน Domain เก็บกฎที่สำคัญที่สุด, Application เป็นตัวประสานงาน, และ Infrastructure ดูแลเรื่องภายนอก ส่วน Http ยังคงเก็บ Controller ตามมาตรฐานของ Laravel เพื่อให้คนในทีมยังคุ้นเคยกับเฟรมเวิร์กเดิมได้
ผลลัพธ์ที่ควรเห็น: เมื่อเราเริ่มสร้างไฟล์ใหม่ เราจะไม่งงว่าต้องไปวางไว้ไหน แต่ละไฟล์จะมีที่อยู่เป็นหลักแหล่งและสื่อความหมายว่ามันมีหน้าที่อะไรในโปรเจกต์ ทำให้การไล่โค้ดในอนาคตทำได้รวดเร็วขึ้นกว่าเดิมมาก
กฎธุรกิจต้องอยู่ในที่ของมัน
บ่อยครั้งที่เราเขียนโค้ดเช็คเงื่อนไขต่างๆ ไว้ใน Controller จนทำให้มันบวมและอ่านยาก DDD สอนให้เราย้ายกฎเหล่านั้นไปไว้ใน Model หรือ Domain Object (วัตถุที่เก็บข้อมูลธุรกิจ) เพื่อให้กฎเหล่านั้นติดตัวไปกับข้อมูลเสมอและนำกลับมาใช้ใหม่ได้ง่าย
ลองนึกภาพการยกเลิกออเดอร์ แทนที่จะให้ Controller ตัดสินใจว่าออเดอร์ยกเลิกได้ไหม เราควรสร้างฟังก์ชันใน Model เพื่อตัดสินใจเรื่องนี้เอง วิธีนี้จะช่วยให้เรามั่นใจว่าไม่ว่าเราจะยกเลิกออเดอร์จากหน้าเว็บ หรือจากคำสั่ง CLI (หน้าจอพิมพ์คำสั่ง) กฎการยกเลิกก็จะเหมือนเดิมเสมอ
นี่คือตัวอย่างการย้ายตรรกะการยกเลิกไปไว้ใน Model ซึ่งจะช่วยให้โค้ดของเราสะอาดและเป็นระเบียบตามแนวทางของ Pragmatic DDD (การทำ DDD แบบเน้นใช้งานจริง ไม่เน้นทฤษฎีจ๋าจนเกินไป) ครับ
// ในไฟล์ App\Domain\Order.php
public function cancel(): void
{
// ตรวจสอบกฎว่าสถานะต้องเป็น pending เท่านั้น
if ($this->status !== 'pending') {
throw new OrderCannotBeCancelled();
}
$this->status = 'cancelled';
}
อธิบายโค้ด: ฟังก์ชัน cancel() ถูกสร้างขึ้นภายใน Model ของ Order เพื่อตรวจสอบเงื่อนไขก่อนเปลี่ยนสถานะ หากสถานะไม่ใช่ pending จะส่งข้อผิดพลาดกลับไปทันที ทำให้เราไม่ต้องเขียนเงื่อนไขซ้ำซ้อนใน Controller
ผลลัพธ์ที่ควรเห็น: เมื่อเราเรียกใช้ $order->cancel() โค้ดจะตรวจสอบสถานะเองโดยอัตโนมัติ หากสถานะไม่ถูกต้องโปรแกรมจะหยุดทำงานและแจ้งเตือน ทำให้ระบบมีความปลอดภัยและคาดเดาผลลัพธ์ได้แม่นยำขึ้น
ใช้ Action เพื่อประสานงาน
เมื่อเรามีกฎใน Model แล้ว เราต้องมีตัวประสานงานที่เรียกว่า Action (การกระทำ) เพื่อทำงานให้ครบวงจร เช่น การดึงข้อมูลออเดอร์มาตรวจสอบสิทธิ์ แล้วค่อยสั่ง cancel() และบันทึกลงฐานข้อมูล ทั้งหมดนี้ควรทำในขั้นตอนเดียวเพื่อให้มั่นใจว่าข้อมูลจะถูกต้องเสมอ
การใช้ Action จะช่วยให้ Controller ของเราสั้นลงมากและทำหน้าที่แค่รับค่าจากผู้ใช้เท่านั้น ส่วนที่เหลือจะถูกส่งต่อให้ Action จัดการทั้งหมด สิ่งนี้ทำให้เราสามารถนำ Action ไปใช้ซ้ำได้ในหลายๆ ส่วนของแอปพลิเคชันโดยไม่ต้องเขียนโค้ดซ้ำซ้อน
การใช้ Laravel Boost จะช่วยให้เราสร้าง Action เหล่านี้ได้รวดเร็วขึ้น โดยกำหนดให้ทุก Action ต้องมีฟังก์ชัน handle() เพื่อให้เป็นมาตรฐานเดียวกันทั้งโปรเจกต์ ซึ่งจะช่วยให้คนในทีมอ่านโค้ดของกันและกันได้ง่ายขึ้นในเวลาอันรวดเร็ว
// ตัวอย่างการใช้ Action
class CancelOrderAction
{
public function handle(int $orderId): void
{
$order = Order::findOrFail($orderId);
// ตรวจสอบสิทธิ์ผ่าน Policy
Gate::authorize('cancel', $order);
// เรียกใช้กฎจาก Model
$order->cancel();
$order->save();
}
}
อธิบายโค้ด: CancelOrderAction ทำหน้าที่รวบรวมขั้นตอนการทำงานไว้ในที่เดียว เริ่มจากการค้นหาข้อมูลออเดอร์ ตรวจสอบสิทธิ์การเข้าถึงด้วย Gate (ระบบตรวจสอบสิทธิ์ของ Laravel) เรียกใช้ฟังก์ชัน cancel() จาก Model และบันทึกข้อมูลลงฐานข้อมูล
ผลลัพธ์ที่ควรเห็น: Controller ของเราจะเหลือเพียงบรรทัดเดียวคือ $action->handle($id) ทำให้โค้ดดูสะอาดตาและโฟกัสไปที่การรับ-ส่งข้อมูลเท่านั้น ส่วนขั้นตอนธุรกิจที่ซับซ้อนถูกย้ายไปอยู่ใน Action เรียบร้อยแล้ว
การทดสอบสถาปัตยกรรมด้วย Pest
หัวใจสำคัญของการรักษาโครงสร้างคือการป้องกันไม่ให้คนอื่นในทีมเขียนโค้ดผิดที่ Pest (เครื่องมือเขียนทดสอบโค้ด) สามารถช่วยเราเขียนกฎเพื่อตรวจสอบได้ว่า ไฟล์ใน Domain ห้ามไปเรียกใช้ไฟล์ใน Infrastructure ตรงๆ เพราะจะทำให้โครงสร้างพัง
ถ้าเราตั้งกฎไว้ว่า App\Domain ห้ามอ้างอิงถึง App\Infrastructure ถ้าใครเผลอเขียนโค้ดแบบนั้นขึ้นมา Pest จะแจ้งเตือนทันทีตอนเราสั่งรันการทดสอบ วิธีนี้จะช่วยให้ทีมรักษามาตรฐานสถาปัตยกรรมได้โดยไม่ต้องมานั่งตรวจโค้ดทีละบรรทัดด้วยตัวเอง
นี่คือตัวอย่างการเขียนกฎทดสอบง่ายๆ เพื่อคุมขอบเขตของโปรเจกต์ ลองนำไปปรับใช้ในโปรเจกต์ของคุณเพื่อสร้างวินัยในการวางโค้ดให้ถูกต้องตามแนวทาง DDD ที่เราได้วางไว้ตั้งแต่วันแรก
// ตัวอย่างการทดสอบด้วย Pest
test('domain code does not depend on infrastructure')
->expect('App\Domain')
->not->toUse('App\Infrastructure');
อธิบายโค้ด: การทดสอบนี้ใช้ฟังก์ชัน expect เพื่อตรวจสอบว่าโค้ดในโฟลเดอร์ Domain จะต้องไม่เรียกใช้ (not->toUse) โค้ดใน Infrastructure หากมีการเรียกใช้เกิดขึ้น การทดสอบนี้จะล้มเหลวทันทีเพื่อเตือนให้เราแก้ไข
ผลลัพธ์ที่ควรเห็น: หากเราเผลอทำโค้ดผิดกฎ ระบบจะแสดงข้อความเตือนตอนรัน php artisan test ทำให้เราแก้ไขได้ทันทีตั้งแต่ช่วงพัฒนา ช่วยป้องกันไม่ให้โปรเจกต์กลายเป็นก้อนโค้ดที่พันกันจนแกะไม่ออกในอนาคต
สรุป
การนำ DDD มาใช้ใน Laravel ไม่ใช่เรื่องยากอย่างที่คิด เพียงแค่เราค่อยๆ เริ่มปรับโครงสร้างในฟีเจอร์ใหม่ๆ ก็เพียงพอแล้ว ไม่จำเป็นต้องรื้อโค้ดเก่าทั้งหมดทันที เริ่มจากสร้างโฟลเดอร์ Domain, Application และ Infrastructure แล้วย้ายตรรกะธุรกิจเข้าไปไว้ให้ถูกที่
จำไว้ว่าเป้าหมายของเราคือการทำให้โค้ด "อ่านง่าย" และ "ดูแลรักษาง่าย" ไม่ใช่การทำตามทฤษฎีให้เป๊ะจนลืมความเรียบง่ายของ Laravel หากคุณทำโปรเจกต์เล็กๆ อยู่ อาจจะเริ่มจากแค่ย้ายกฎธุรกิจไปไว้ใน Model ก็ถือว่าเป็นการเริ่มต้นที่ดีเยี่ยมแล้วในการฝึกเป็นโปรแกรมเมอร์มืออาชีพ
สุดท้ายนี้ ลองใช้ Laravel Boost เพื่อช่วยสร้างโครงสร้างเหล่านี้ให้เป็นนิสัย เมื่อคุณทำแบบนี้ไปเรื่อยๆ คุณจะพบว่าการเพิ่มฟีเจอร์ใหม่ๆ ในอนาคตจะเป็นเรื่องง่ายและสนุกขึ้น เพราะคุณมี "บ้าน" ที่เป็นระเบียบสำหรับโค้ดทุกส่วนของคุณแล้ว ขอให้สนุกกับการเขียนโค้ดและพัฒนาฝีมือไปพร้อมๆ กันนะครับ
ที่มา: Pragmatic Domain-Driven Design in Laravel, with Laravel Boost — DEV Community: laravel