1. DDD là gì
DDD là viết tắt của chữ Domain-Driven Design, dịch sang Tiếng Việt là Thiết Kế Phần Mềm Hướng Nghiệp Vụ, là nền tảng lý thuyết cho rằng code nên phản ánh nghiệp vụ business, không phải là ngôn từ kỹ thuật.
Các logic của nghiệp vụ phải được CÔ LẬP, có thể tuỳ ý NÂNG CẤP hoặc THAY FRAMEWORK nhưng điều đó không được phép ảnh hưởng tới code business.
Quan trọng nhất, đó là khả năng TESTABLE, code của nghiệp vụ, phải có khả năng Unit Test, mọi cách thiết kế code không đem lại Unit Test thì đó là một thiết kế không chính xác.
2. Clean Architecture
Nếu ví DDD là đích đến, thì Clean Architecture là đường đi. Clean Architecture là một kiến trúc mô tả luật phụ thuộc theo các vòng tròn, lớp bên trong không được sử dụng code của lớp bên ngoài. Cụ thể bao gồm 4 lớp chính:
- Enterprise Business Rules: Entity và Domain Service
- Application Business Rules: UseCase (hoặc còn gọi là Application Service), có chứa các interface của Repository.
- Interface Adapters: chứa các Presenter, các implement của Interface Repository (gọi vào Framework)
- Infrastructure: chứa code của Framework, tương tác với Database, UI, Web, API, các hàm lấy DateTime
Giải nghĩa các từ khoá:
- Entity: đây là các class chứa cấu trúc dữ liệu của nghiệp vụ, ví dụ như User, Game, Invoice.
- Domain Service: là class thực thi logic có liên quan nhiều Entity trong nghiệp vụ.
- UseCase hoặc Application Service: là nơi chứa logic của nhiều Domain Service.
- Repository: là class đảm nhiệm việc lấy dữ liệu/ghi dữ liệu theo Entity.
- Presenter: là nơi format và show dữ liệu ra Infrastructure như Web, Api, Console Command.
Ngoài ra còn có một vài khái niệm khác như:
- ValueObject: đây là các class không chứa Id (không định danh), bất biến, ví dụ Money, Address, Email (giống một kiểu dữ liệu mới)
- DTO: viết tắt của chữ Data Transfer Object, là Object cấu trúc dữ liệu có 2 method cho phép serialize thành string/byte, và unserialize từ string/bytes thành Object ban đầu. Mục đích là để đưa dữ liệu từ Domain này qua Domain khác, có thể đưa trực tiếp hoặc đưa thông qua Message Broker (như Redis/Rabbimq/Kafka..).
- Aggregate Root: là Entity đại diện cho các Entity nhỏ bên trong, bảo vệ nghiệp vụ, chống update lẻ tẻ bên trong gây sai nghiệp vụ và giữ domain dễ hiểu khi nghiệp vụ lớn. Nói đơn giản và thực dụng hơn: Aggregate Root là Entity đặc biệt, tự chứa đủ dữ liệu để ra quyết định business.
3. Ví dụ
Giả sử mình cần làm tính năng cho thuê game theo ngày, nếu game quá ngày được thuê thì cần ném Exception là GameExpiredException.
<?php
// đầu tiên code Domain Exception cho nghiệp vụ thuê game
class GameExpiredException extends DomainException
{
public function __construct()
{
parent::__construct('GAME_EXPIRED');
}
}
//sau đó tạo value object thể hiện lịch thuê game
class LaunchGamePeriod
{
public function __construct(
public \DateTimeImmutable $from,
public \DateTimeImmutable $to
)
{
if ($to->getTimestamp() < $this->from->getTimestamp()) {
throw new InvalidArgumentException('PERIOD_INVALID');
}
}
}
//tao entity
class Game
{
public function __construct(
public string $name,
public \DateTimeImmutable $launchFrom,
public DateTimeImmutable $launchTo
)
{
}
public static function make(
string $name,
LaunchGamePeriod $period
): static
{
return new Game($name, $period->from, $period->to);
}
public function canPlay(): bool
{
$now = time(); // time là hàm của php trả ra timestamp
return $this->launchTo->getTimestamp() > $now;
}
}
//Domain Service
class PlayGameService
{
public function startGame(Game $game): void
{
if (!$game->canPlay()) {
throw new GameExpiredException();
}
// logic tạo session game hay gì đó ở đây
}
}
//test
$period = new LaunchGamePeriod(new DateTimeImmutable('2025-11-01'), new DateTimeImmutable('2025-11-02'));
$game = Game::make('mario', $period);
$service = new PlayGameService();
$service->startGame($game);
//phai nem exception do bay giờ là 2026
Code ở trên đã vi phạm DDD và Clean Architecture.
Thứ nhất: các hàm get DateTime là Infrastructure, đoạn code canPlay trong Entity Game đã trực tiếp lấy time() là call vào Infrastructure là sai DDD.
Điều đó dẫn đến điều thứ hai: KHÔNG THỂ UNIT TEST khi truyền thời gian quá khứ, vì khi đó Exception luôn ném là GameExpiredException, dẫn đến KHÔNG có khả năng PREPRODUCE BUG đã xảy ra trong quá khứ.
Do vậy cần sửa lại hàm canPlay như sau:
<?php
public function canPlay(int $now): bool
{
return $this->launchTo->getTimestamp() > $now;
}
tách hẳn Infrastructure ra khỏi Domain.
Tuy nhiên điều này tăng thêm độ phức tạp chương trình, nên thực chiến sửa như sau để tiện cho việc code, mà vẫn unit test được (mặc dù code trông không được clean):
<?php
public function canPlay(?int $now = null): bool
{
if (!$now) {
$now = time();
}
return $this->launchTo->getTimestamp() > $now;
}
Đôi lúc cũng nên chấp nhận code hơi xấu, để quá trình dev cho tiện, quan trọng nhất vẫn là unit test, đừng bao giờ viết code không thể test.