Domain-Driven Design (DDD) adalah pendekatan pengembangan perangkat lunak yang berfokus pada pemodelan domain bisnis yang kompleks. NexaUI mengadopsi prinsip-prinsip DDD untuk membantu developer membangun aplikasi yang lebih terstruktur, mudah dipelihara, dan selaras dengan kebutuhan bisnis.
DDD menekankan kolaborasi antara tim teknis dan domain experts untuk menciptakan model domain yang mencerminkan proses bisnis dan aturan yang mendasarinya.
Ubiquitous Language adalah bahasa bersama yang digunakan oleh semua anggota tim, baik developer maupun domain experts. Bahasa ini tercermin dalam kode, dokumentasi, dan komunikasi sehari-hari.
Di NexaUI, Anda didorong untuk menggunakan nama class, method, dan variabel yang mencerminkan konsep bisnis, bukan istilah teknis.
Bounded Context adalah batas eksplisit di mana model domain tertentu berlaku. Ini membantu memisahkan model yang berbeda dan mencegah konflik konseptual.
NexaUI mendukung organisasi kode berdasarkan bounded context, misalnya:
app/
├── Billing/ # Bounded Context untuk penagihan
│ ├── Domain/
│ │ ├── Invoice.php
│ │ ├── Payment.php
│ │ └── ...
│ ├── Application/
│ │ ├── InvoiceService.php
│ │ └── ...
│ └── Infrastructure/
│ ├── InvoiceRepository.php
│ └── ...
├── Shipping/ # Bounded Context untuk pengiriman
│ ├── Domain/
│ │ ├── Shipment.php
│ │ ├── Package.php
│ │ └── ...
│ └── ...
└── UserManagement/ # Bounded Context untuk manajemen pengguna
├── Domain/
│ ├── User.php
│ ├── Role.php
│ └── ...
└── ...
Entity adalah objek yang memiliki identitas dan siklus hidup, sedangkan Value Object adalah objek immutable yang didefinisikan oleh atributnya.
Contoh Entity di NexaUI:
declare(strict_types=1);
namespace App\UserManagement\Domain;
use App\System\Domain\NexaEntity;
class User extends NexaEntity
{
private UserId $id;
private string $name;
private Email $email;
private Password $password;
public function __construct(UserId $id, string $name, Email $email, Password $password)
{
$this->id = $id;
$this->name = $name;
$this->email = $email;
$this->password = $password;
}
public function changeName(string $name): void
{
$this->name = $name;
}
public function changeEmail(Email $email): void
{
$this->email = $email;
}
// Getters and other methods...
}
Contoh Value Object di NexaUI:
declare(strict_types=1);
namespace App\UserManagement\Domain;
use App\System\Domain\NexaValueObject;
final class Email extends NexaValueObject
{
private string $value;
public function __construct(string $email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email address');
}
$this->value = $email;
}
public function getValue(): string
{
return $this->value;
}
public function equals(Email $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return $this->value;
}
}
Aggregate adalah cluster dari entities dan value objects yang diperlakukan sebagai satu unit. Setiap aggregate memiliki root entity yang menjadi titik akses utama.
declare(strict_types=1);
namespace App\Billing\Domain;
use App\System\Domain\NexaAggregate;
class Order extends NexaAggregate
{
private OrderId $id;
private CustomerId $customerId;
private array $lineItems;
private OrderStatus $status;
private Money $totalAmount;
public function __construct(OrderId $id, CustomerId $customerId)
{
$this->id = $id;
$this->customerId = $customerId;
$this->lineItems = [];
$this->status = OrderStatus::DRAFT();
$this->totalAmount = new Money(0);
}
public function addLineItem(ProductId $productId, int $quantity, Money $unitPrice): void
{
$lineItem = new OrderLineItem(
new OrderLineItemId(Uuid::uuid4()->toString()),
$productId,
$quantity,
$unitPrice
);
$this->lineItems[] = $lineItem;
$this->recalculateTotal();
}
public function removeLineItem(OrderLineItemId $lineItemId): void
{
$this->lineItems = array_filter(
$this->lineItems,
fn($item) => !$item->getId()->equals($lineItemId)
);
$this->recalculateTotal();
}
public function place(): void
{
if (count($this->lineItems) === 0) {
throw new \DomainException('Cannot place an empty order');
}
$this->status = OrderStatus::PLACED();
// Publish domain event
$this->recordEvent(new OrderPlaced($this->id));
}
private function recalculateTotal(): void
{
$this->totalAmount = array_reduce(
$this->lineItems,
fn(Money $total, OrderLineItem $item) => $total->add($item->getTotal()),
new Money(0)
);
}
// Getters and other methods...
}
Domain Service menangani operasi domain yang tidak cocok berada di entity atau value object.
declare(strict_types=1);
namespace App\Billing\Domain;
class PaymentService
{
public function processPayment(Order $order, PaymentMethod $paymentMethod): Payment
{
// Validasi order
if (!$order->canBePaid()) {
throw new \DomainException('Order cannot be paid');
}
// Proses pembayaran berdasarkan payment method
$payment = match ($paymentMethod->getType()) {
PaymentMethodType::CREDIT_CARD => $this->processCreditCardPayment($order, $paymentMethod),
PaymentMethodType::BANK_TRANSFER => $this->processBankTransferPayment($order, $paymentMethod),
PaymentMethodType::DIGITAL_WALLET => $this->processDigitalWalletPayment($order, $paymentMethod),
default => throw new \DomainException('Unsupported payment method'),
};
return $payment;
}
private function processCreditCardPayment(Order $order, PaymentMethod $paymentMethod): Payment
{
// Implementasi proses pembayaran kartu kredit
}
private function processBankTransferPayment(Order $order, PaymentMethod $paymentMethod): Payment
{
// Implementasi proses pembayaran transfer bank
}
private function processDigitalWalletPayment(Order $order, PaymentMethod $paymentMethod): Payment
{
// Implementasi proses pembayaran dompet digital
}
}
Repository menyediakan abstraksi untuk akses dan manipulasi aggregate.
declare(strict_types=1);
namespace App\Billing\Domain;
interface OrderRepository
array; public function save(Order $order): void; public function remove(Order $order): void;
Implementasi repository di NexaUI:
declare(strict_types=1);
namespace App\Billing\Infrastructure;
use App\Billing\Domain\Order;
use App\Billing\Domain\OrderId;
use App\Billing\Domain\CustomerId;
use App\Billing\Domain\OrderRepository;
use App\System\Database\NexaDatabase;
class MySqlOrderRepository implements OrderRepository
{
private NexaDatabase $db;
public function __construct(NexaDatabase $db)
{
$this->db = $db;
}
public function findById(OrderId $id): ?Order
{
$data = $this->db->query(
'SELECT * FROM orders WHERE id = :id',
['id' => $id->getValue()]
)->fetch();
if (!$data) {
return null;
}
return $this->hydrateOrder($data);
}
public function findByCustomer(CustomerId $customerId): array
{
$data = $this->db->query(
'SELECT * FROM orders WHERE customer_id = :customerId',
['customerId' => $customerId->getValue()]
)->fetchAll();
return array_map([$this, 'hydrateOrder'], $data);
}
public function save(Order $order): void
{
// Implementasi penyimpanan order ke database
}
public function remove(Order $order): void
{
// Implementasi penghapusan order dari database
}
private function hydrateOrder(array $data): Order
{
// Implementasi konversi data dari database ke objek Order
}
}
Domain Events merepresentasikan kejadian penting dalam domain yang mungkin menarik bagi bagian lain dari sistem.
declare(strict_types=1);
namespace App\Billing\Domain;
use App\System\Domain\NexaDomainEvent;
class OrderPlaced extends NexaDomainEvent
{
private OrderId $orderId;
public function __construct(OrderId $orderId)
{
parent::__construct();
$this->orderId = $orderId;
}
public function getOrderId(): OrderId
{
return $this->orderId;
}
}
NexaUI mendukung struktur direktori yang sesuai dengan prinsip DDD:
app/
├── [BoundedContext]/
│ ├── Domain/ # Domain model
│ │ ├── Entities/
│ │ ├── ValueObjects/
│ │ ├── Aggregates/
│ │ ├── Events/
│ │ ├── Exceptions/
│ │ └── Services/
│ ├── Application/ # Application services
│ │ ├── Commands/
│ │ ├── Queries/
│ │ ├── DTOs/
│ │ └── Services/
│ ├── Infrastructure/ # Infrastructure implementations
│ │ ├── Repositories/
│ │ ├── Persistence/
│ │ └── External/
│ └── Presentation/ # Presentation layer
│ ├── Controllers/
│ ├── Views/
│ └── Forms/
NexaUI menggunakan dependency injection untuk mengelola dependensi antar komponen:
declare(strict_types=1);
namespace App\Billing\Application;
use App\Billing\Domain\OrderRepository;
use App\Billing\Domain\PaymentService;
class OrderApplicationService
{
private OrderRepository $orderRepository;
private PaymentService $paymentService;
public function __construct(
OrderRepository $orderRepository,
PaymentService $paymentService
) {
$this->orderRepository = $orderRepository;
$this->paymentService = $paymentService;
}
public function placeOrder(PlaceOrderCommand $command): void
{
// Implementation...
}
public function payOrder(PayOrderCommand $command): void
{
// Implementation...
}
}
NexaUI menyediakan event dispatcher untuk menangani domain events:
declare(strict_types=1);
namespace App\System\Domain;
class NexaEventDispatcher
{
private array $listeners = [];
public function addListener(string $eventClass, callable $listener): void
{
if (!isset($this->listeners[$eventClass])) {
$this->listeners[$eventClass] = [];
}
$this->listeners[$eventClass][] = $listener;
}
public function dispatch(NexaDomainEvent $event): void
{
$eventClass = get_class($event);
if (!isset($this->listeners[$eventClass])) {
return;
}
foreach ($this->listeners[$eventClass] as $listener) {
$listener($event);
}
}
}
Mendaftarkan listeners:
// Di bootstrap aplikasi
$eventDispatcher = new NexaEventDispatcher();
$eventDispatcher->addListener(
OrderPlaced::class,
function (OrderPlaced $event) use ($notificationService) {
$notificationService->notifyCustomer($event->getOrderId(), 'Order placed successfully');
}
);
$eventDispatcher->addListener(
OrderPlaced::class,
function (OrderPlaced $event) use ($inventoryService) {
$inventoryService->reserveItems($event->getOrderId());
}
);
Mulailah dengan memahami domain bisnis sebelum menulis kode. Lakukan diskusi dengan domain experts untuk membangun model yang akurat.
Gunakan bahasa yang konsisten di seluruh kode, dokumentasi, dan komunikasi. Hindari istilah teknis yang tidak dipahami oleh domain experts.
Jaga domain model tetap bersih dari detail infrastruktur. Gunakan interfaces dan dependency injection untuk memisahkan domain dari implementasi teknis.
Pastikan validasi aturan bisnis dilakukan di domain model, bukan di lapisan presentasi atau aplikasi.
declare(strict_types=1);
namespace App\Billing\Domain;
class Order extends NexaAggregate
{
// ...
public function addLineItem(ProductId $productId, int $quantity, Money $unitPrice): void
{
if ($quantity <= 0) {
throw new \DomainException('Quantity must be positive');
}
if ($unitPrice->isNegative()) {
throw new \DomainException('Unit price cannot be negative');
}
// Proceed with adding line item
}
// ...
}
Buat value objects immutable untuk menghindari side effects dan memudahkan reasoning tentang kode.
Gunakan rich domain model dengan behavior, bukan anemic model yang hanya berisi data.
Definisikan bounded contexts dengan jelas dan kelola interaksi antar contexts dengan hati-hati.
Domain-Driven Design adalah pendekatan yang powerful untuk menangani kompleksitas dalam pengembangan perangkat lunak. NexaUI menyediakan infrastruktur dan konvensi yang mendukung implementasi DDD, membantu Anda membangun aplikasi yang lebih terstruktur dan selaras dengan kebutuhan bisnis.
Dengan mengadopsi DDD di NexaUI, Anda dapat membangun aplikasi yang tidak hanya berfungsi dengan baik secara teknis, tetapi juga mencerminkan domain bisnis dengan akurat dan dapat beradaptasi dengan perubahan kebutuhan bisnis.