เหตุการณ์โดเมนและบริการโดเมน
บันทึกข้อเท็จจริงทางธุรกิจด้วยเหตุการณ์และบริการโดเมน
เหตุการณ์โดเมนและบริการโดเมน เป็นบทเรียน PHP Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน PHP Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน
การบันทึกข้อเท็จจริงทางธุรกิจ
พฤติกรรมไม่ได้ทุกอย่างจะเหมาะกับการอยู่ภายในเอนทิตีเดียว เหตุการณ์โดเมน จะบันทึกข้อเท็จจริงทางธุรกิจที่มีความหมายและเกิดขึ้นแล้ว เช่น OrderPlaced เพื่อให้ส่วนอื่นของระบบตอบสนองได้ ส่วน บริการโดเมน จะเก็บตรรกะของโดเมนที่ไม่ได้เป็นของเอนทิตีหรืออ็อบเจ็กต์ค่าใดโดยธรรมชาติ บทเรียนนี้จะแสดงให้เห็นว่าทั้งสองรูปแบบช่วยให้แบบจำลองสื่อความหมายได้ชัดเจนโดยไม่ทำให้แอกกริเกตใหญ่เกินไปอย่างไร
เหตุการณ์โดเมนคืออะไร
เหตุการณ์โดเมน คือบันทึกที่ไม่เปลี่ยนรูปของสิ่งที่เกิดขึ้นในโดเมน โดยตั้งชื่อเป็น รูปอดีตกาล ตามภาษากลางของโดเมน เช่น OrderPlaced PaymentReceived และ CustomerRelocated เหตุการณ์นี้เก็บข้อมูลที่อธิบายข้อเท็จจริง รวมถึงเวลาที่เกิดขึ้น เหตุการณ์นี้เป็นอ็อบเจ็กต์ค่า ซึ่งสร้างขึ้นครั้งเดียวและไม่เปลี่ยนแปลงอีก
<?php
final class OrderPlaced {
public function __construct(
public readonly string $orderId,
public readonly int $totalCents,
public readonly DateTimeImmutable $occurredOn = new DateTimeImmutable()
) {}
}
$e = new OrderPlaced('o1', 1300);
echo $e->orderId, ' @ ', $e->occurredOn->format('H:i'), PHP_EOL;
แอกกริเกตบันทึกเหตุการณ์
รากแอกกริเกตจะบันทึกเหตุการณ์เป็นส่วนหนึ่งของพฤติกรรม จากนั้นเปิดเผยเหตุการณ์เหล่านั้นเพื่อให้ชั้นแอปพลิเคชันส่งต่อ หลังจากธุรกรรมยืนยันสำเร็จ ตัวแอกกริเกตเองจะไม่ส่งต่อเหตุการณ์ แต่จะทำหน้าที่เพียง บันทึก เท่านั้น วิธีนี้ทำให้โดเมนไม่ผูกติดกับข้อกังวลด้านโครงสร้างพื้นฐาน
<?php
final class OrderPlaced {
public function __construct(public readonly string $orderId) {}
}
abstract class AggregateRoot {
private array $events = [];
protected function record(object $e): void { $this->events[] = $e; }
public function releaseEvents(): array {
$out = $this->events; $this->events = []; return $out;
}
}
final class Order extends AggregateRoot {
public function __construct(public readonly string $id) {
$this->record(new OrderPlaced($id));
}
}
$o = new Order('o1');
foreach ($o->releaseEvents() as $e) { echo get_class($e), PHP_EOL; }
ส่งต่อหลังยืนยันธุรกรรม
บริการแอปพลิเคชันจะจัดเก็บแอกกริเกต ยืนยันธุรกรรมให้ เสร็จสมบูรณ์ แล้วจึงส่งต่อเหตุการณ์ที่ถูกปล่อยออกมา การส่งต่อ หลังยืนยันธุรกรรม ทำให้มั่นใจว่าตัวจัดการจะไม่ตอบสนองต่อข้อเท็จจริงที่ถูกยกเลิกไปพร้อมกับธุรกรรม ลำดับด้านล่างเป็นรูปแบบที่ปลอดภัยสำหรับผลข้างเคียง เช่น อีเมลหรือการเรียกใช้ระบบภายนอก
<?php
final class PlaceOrderService {
public function __construct(
private object $repo,
private object $dispatcher
) {}
public function handle(object $order): void {
// 1. persist inside a transaction
$this->repo->save($order); // commit happens here
// 2. only AFTER commit, release and dispatch
foreach ($order->releaseEvents() as $event) {
$this->dispatcher->dispatch($event);
}
}
}
echo "order persisted, then events dispatched", PHP_EOL;
ตัวจัดการตอบสนอง
เหตุการณ์แต่ละรายการสามารถมีตัวจัดการได้หลายตัว โดยไม่ผูกติดกับแอกกริเกตที่ทำให้เกิดเหตุการณ์นั้น Dispatcher ตาม PSR-14 จะส่งเหตุการณ์ไปยังตัวรับฟังที่ลงทะเบียนไว้ การเพิ่มการตอบสนองใหม่ เช่น การวิเคราะห์ข้อมูลหรือการแจ้งเตือน หมายถึงการเพิ่มตัวรับฟังหนึ่งตัว โดยไม่ต้องแก้ไขแอกกริเกต
<?php
final class OrderPlaced { public function __construct(public readonly string $orderId) {} }
final class Dispatcher {
private array $listeners = [];
public function on(string $event, callable $l): void { $this->listeners[$event][] = $l; }
public function dispatch(object $event): void {
foreach ($this->listeners[$event::class] ?? [] as $l) { $l($event); }
}
}
$d = new Dispatcher();
$d->on(OrderPlaced::class, fn($e) => print("email for {$e->orderId}\n"));
$d->on(OrderPlaced::class, fn($e) => print("analytics for {$e->orderId}\n"));
$d->dispatch(new OrderPlaced('o1'));
เหตุการณ์ข้ามขอบเขต
เหตุการณ์โดเมนช่วยแยกแอกกริเกตที่ต้องรักษาความสอดคล้องโดยไม่ต้องใช้ธุรกรรมร่วมกัน OrderPlaced ในบริบทการขายสามารถกระตุ้นการจองสินค้าในอีกบริบทหนึ่งผ่านเหตุการณ์ได้ ภายในระบบ เหตุการณ์เหล่านี้เป็นอ็อบเจ็กต์ แต่เมื่อข้ามบริการ มักจะกลายเป็น เหตุการณ์เชื่อมต่อระบบ ที่ส่งผ่านตัวกลางรับส่งข้อความ ควรแยกเหตุการณ์โดเมนภายในออกจากเหตุการณ์เชื่อมต่อระบบภายนอกอย่างชัดเจน เพราะสัญญาสาธารณะไม่ควรเป็นแบบจำลองภายในของคุณ
บริการโดเมนคืออะไร
บริการโดเมน เก็บตรรกะของโดเมนที่:
- เกี่ยวข้องกับ แอกกริเกตหรือเอนทิตีหลายรายการ จึงไม่เหมาะจะอยู่ในรายการใดรายการหนึ่ง และ
- เป็นแนวคิดของโดเมนอย่างแท้จริง โดยแสดงออกผ่านภาษากลางของโดเมน
บริการนี้ ไร้สถานะ และทำงานกับอ็อบเจ็กต์โดเมน บริการ FundsTransfer ที่หักเงินจากบัญชีหนึ่งแล้วเพิ่มเงินให้บัญชีอีกบัญชีหนึ่งเป็นตัวอย่างที่พบได้ทั่วไป
บริการโดเมนในโค้ด
บริการนี้ประสานงานแอกกริเกตสองรายการ แต่ไม่เก็บสถานะของตนเอง ตรรกะนี้ไม่ได้เป็นของบัญชีใดบัญชีหนึ่ง จึงอยู่ในบริการที่ตั้งชื่อตามการดำเนินการของโดเมน
<?php
final class Account {
public function __construct(public readonly string $id, private int $cents) {}
public function withdraw(int $c): void {
if ($c > $this->cents) { throw new DomainException('insufficient funds'); }
$this->cents -= $c;
}
public function deposit(int $c): void { $this->cents += $c; }
public function balance(): int { return $this->cents; }
}
final class MoneyTransferService {
public function transfer(Account $from, Account $to, int $cents): void {
$from->withdraw($cents);
$to->deposit($cents);
}
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new MoneyTransferService())->transfer($a, $b, 400);
echo $a->balance(), '/', $b->balance(), PHP_EOL; // 600/400
บริการโดเมนเทียบกับบริการแอปพลิเคชัน
อย่าสับสนระหว่างสองสิ่งนี้:
- บริการโดเมน มีตรรกะทางธุรกิจและอยู่ในชั้นโดเมน โดยไม่รู้อะไรเกี่ยวกับธุรกรรมหรือการสื่อสารผ่านเว็บ
- บริการแอปพลิเคชัน ประสานงานกรณีการใช้งานหนึ่งกรณี โดยโหลดแอกกริเกตจากรีโพซิทอรี เรียกใช้ตรรกะของโดเมน ยืนยันธุรกรรม และส่งต่อเหตุการณ์ บริการนี้เป็นผู้ประสานงานที่มีหน้าที่บางเบาและไม่มีกฎทางธุรกิจของตนเอง
หลีกเลี่ยงกับดักของบริการ
บริการโดเมนถูกใช้มากเกินไปได้ง่าย หากตรรกะ สามารถอยู่บนเอนทิตีหรืออ็อบเจ็กต์ค่าได้ ก็ควรให้ตรรกะอยู่ที่นั่น นี่คือวิธีหลีกเลี่ยงแบบจำลองที่ไร้พฤติกรรม ควรใช้บริการโดเมนเฉพาะเมื่อการดำเนินการครอบคลุมหลายแอกกริเกตจริง ๆ หรือไม่มีที่อยู่ตามธรรมชาติ มิฉะนั้นคุณจะดึงพฤติกรรมออกจากเอนทิตี และสร้างโค้ดเชิงกระบวนงานขึ้นมาใหม่
บริการแอปพลิเคชันทำหน้าที่ประสานงาน
เมื่อนำทุกอย่างมาประกอบกัน: บริการแอปพลิเคชันแบบบางจะโหลดแอกเกรกตจากคลังข้อมูล เรียกใช้ บริการโดเมนสำหรับตรรกะที่ครอบคลุมหลายแอกเกรกต บันทึกผลลัพธ์ และส่งเหตุการณ์ต่อ บริการนี้ทำหน้าที่ประสานงานเท่านั้น — ไม่มีกฎธุรกิจเป็นของตนเอง
<?php
final class Account {
public function __construct(public readonly string $id, public int $cents) {}
}
final class MoneyTransferService { // domain service: the rule
public function transfer(Account $from, Account $to, int $c): void {
if ($c > $from->cents) { throw new DomainException('insufficient'); }
$from->cents -= $c; $to->cents += $c;
}
}
final class TransferApp { // application service: orchestration only
public function __construct(private MoneyTransferService $domain) {}
public function run(Account $a, Account $b, int $c): void {
$this->domain->transfer($a, $b, $c); // then repo->save + dispatch
}
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new TransferApp(new MoneyTransferService()))->run($a, $b, 250);
echo $a->cents, '/', $b->cents, PHP_EOL; // 750/250
ตรวจสอบอย่างรวดเร็ว
เหตุการณ์และบริการ
สรุปทบทวน
คุณได้บันทึกข้อเท็จจริงทางธุรกิจและตรรกะที่ไม่รู้ว่าจะวางไว้ที่ใด เหตุการณ์โดเมนคือระเบียนที่ไม่เปลี่ยนแปลงและใช้รูปอดีต ซึ่งแอกเกรกต บันทึกไว้ แล้วชั้นแอปพลิเคชัน ส่งต่อหลังการยืนยันการทำรายการ การทำเช่นนี้ช่วยแยกปฏิกิริยาต่าง ๆ ออกจากกันและข้ามขอบเขตของแอกเกรกต ใช้ บริการโดเมนเก็บตรรกะโดเมนที่ไม่มีสถานะและครอบคลุมหลายแอกเกรกต โดยแยกจากบริการแอปพลิเคชันแบบบางที่เพียงประสานงานกรณีใช้งาน ใช้ทั้งสองอย่างอย่างพอเหมาะ เพื่อให้พฤติกรรมยังคงอยู่บนเอนทิตีและออบเจ็กต์ค่าในจุดที่ควรอยู่
เรียนรู้ PHP ด้วย AI tutor — ฟรี
เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป
- คอร์ส
- 49
- บทเรียน
- 195
คำถามที่พบบ่อย
บทเรียน “เหตุการณ์โดเมนและบริการโดเมน” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “เหตุการณ์โดเมนและบริการโดเมน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส PHP Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “เหตุการณ์โดเมนและบริการโดเมน”
บันทึกข้อเท็จจริงทางธุรกิจด้วยเหตุการณ์และบริการโดเมน คุณปฏิบัติ PHP Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน PHP Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน PHP Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “เหตุการณ์โดเมนและบริการโดเมน” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน PHP Academy นี้ได้ไหม
ได้ บทเรียน PHP Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- องค์ประกอบพื้นฐาน DDD: เอนทิตีและออบเจ็กต์ค่า
- แอกกริเกต รีโพซิทอรี และแฟกทอรี
- เหตุการณ์โดเมนและบริการโดเมน
- บริบทที่มีขอบเขตและการทำแผนผังบริบท