DOMAIN-DRIVEN DESIGN EN PHP: CONCEPTOS Y APLICACIÓN
Domain-Driven Design (DDD) es una metodología de desarrollo de software introducida por Eric Evans. Se centra en construir software alineado con el dominio del negocio, es decir, las reglas, procesos y conocimientos del área específica para la que se desarrolla la aplicación. El objetivo es crear sistemas que sean más sostenibles, escalables y reflejen con precisión las necesidades del negocio.Conceptos clave de DDD
- Lenguaje Ubicuo: Un vocabulario común compartido entre desarrolladores, expertos del dominio (negocio) y otros involucrados, para garantizar que todos hablen el mismo "idioma" y evitar malentendidos.
- Entidades: Objetos que tienen una identidad única y persisten a lo largo del tiempo (por ejemplo, un "Cliente" con un ID específico).
- Value Objects: Objetos sin identidad propia que representan valores (por ejemplo, una dirección o un monto de dinero).
- Agregados: Grupos de objetos relacionados que se tratan como una sola unidad para mantener la consistencia de los datos. Cada agregado tiene una raíz (Aggregate Root) que controla el acceso a los objetos dentro de él.
- Contextos Delimitados: O Bounded Contexts son áreas específicas dentro del dominio donde un término o concepto tiene un significado claro y único, evitando ambigüedades entre diferentes partes del sistema.
- Servicios de Dominio: Lógica que no encaja naturalmente en una entidad o un objeto de valor, pero que es parte del dominio.
- Repositorios: Objetos que gestionan el acceso y almacenamiento de agregados en una base de datos u otro medio de persistencia.
- Eventos de Dominio: Representan algo que ha sucedido en el dominio y que puede ser relevante para otras partes del sistema (por ejemplo, "Pedido Realizado").
Enfoques principales
- Modelado del dominio: El diseño del software debe reflejar el modelo mental del negocio, creado en colaboración con expertos del dominio.
- Enfoque en el núcleo del dominio: Identificar las partes más importantes del negocio (core domain) y priorizarlas en el desarrollo.
- Separación de preocupaciones: Dividir el sistema en contextos delimitados para reducir la complejidad y evitar que los conceptos se mezclen.
Un ejemplo práctico de Domain-Driven Design en PHP sería un caso simple de un sistema de comercio electrónico. Se va a modelar un dominio relacionado con la gestión de un Carrito de Compras, incluyendo conceptos como Entidades,Value Objects, Agregados y un Repositorio. El código será claro y estará enfocado en los principios de DDD.
- Value Object: Product
declare(strict_types=1);
namespace Domain\ValueObjects;
class Product
{
private string $name;
private float $price;
public function __construct(string $name, float $price)
{
if ($price < 0) {
throw new \InvalidArgumentException('Price cannot be negative.');
}
$this->name = $name;
$this->price = $price;
}
public function getName(): string
{
return $this->name;
}
public function getPrice(): float
{
return $this->price;
}
// Igualdad basada en atributos
public function equals(Product $other): bool
{
return $this->name === $other->name && $this->price === $other->price;
}
}
- Entidad y Raíz del Agregado: Cart, El carrito es una entidad con un ID único y actúa como la raíz del agregado, gestionando los ítems.
declare(strict_types=1);
namespace Domain\Entities;
use Domain\ValueObjects\Product;
class Cart
{
private string $id;
private array $items = [];
public function __construct(string $id)
{
$this->id = $id;
}
public function getId(): string
{
return $this->id;
}
public function addItem(Product $product, int $quantity): void
{
if ($quantity <= 0) {
throw new \InvalidArgumentException('Quantity must be positive.');
}
$this->items[] = [
'product' => $product,
'quantity' => $quantity,
];
}
public function getTotal(): float
{
$total = 0.0;
foreach ($this->items as $item) {
$total += $item['product']->getPrice() * $item['quantity'];
}
return $total;
}
public function getItems(): array
{
return $this->items;
}
}
- Repositorio: CartRepository. El repositorio maneja la persistencia de los carritos (en este caso, simulado con un array, pero podría usar una base de datos).
eclare(strict_types=1);
namespace Domain\Repositories;
use Domain\Entities\Cart;
interface CartRepositoryInterface
{
public function findById(string $id): ?Cart;
public function save(Cart $cart): void;
}
class InMemoryCartRepository implements CartRepositoryInterface
{
private array $carts = [];
public function findById(string $id): ?Cart
{
return $this->carts[$id] ?? null;
}
public function save(Cart $cart): void
{
$this->carts[$cart->getId()] = $cart;
}
}
- Servicio de Dominio (Opcional): Un servicio de dominio puede coordinar operaciones complejas. Aquí, un servicio para manejar la lógica de agregar productos al carrito.
declare(strict_types=1);
namespace Domain\Services;
use Domain\Entities\Cart;
use Domain\Repositories\CartRepositoryInterface;
use Domain\ValueObjects\Product;
class CartService
{
private CartRepositoryInterface $repository;
public function __construct(CartRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function addProductToCart(string $cartId, Product $product, int $quantity): void
{
$cart = $this->repository->findById($cartId);
if (!$cart) {
$cart = new Cart($cartId);
}
$cart->addItem($product, $quantity);
$this->repository->save($cart);
}
public function getCartTotal(string $cartId): float
{
$cart = $this->repository->findById($cartId);
if (!$cart) {
throw new \InvalidArgumentException('Cart not found.');
}
return $cart->getTotal();
}
}
- Uso del sistema: Ejemplo de cómo usar estas clases en una aplicación.
require_once __DIR__ . '/vendor/autoload.php'; // Si usas Composer
use Domain\Services\CartService;
use Domain\Repositories\InMemoryCartRepository;
use Domain\ValueObjects\Product;
// Crear dependencias
$repository = new InMemoryCartRepository();
$cartService = new CartService($repository);
// Crear un producto
$product = new Product('Laptop', 999.99);
// Agregar producto al carrito
$cartId = 'cart-123';
$cartService->addProductToCart($cartId, $product, 2);
// Obtener el total
$total = $cartService->getCartTotal($cartId);
echo "Total del carrito: $total\n"; // Salida: Total del carrito: 1999.98
Explicación del ejemplo
- Lenguaje Ubicuo Términos como "Cart", "Product" y "addItem" reflejan el lenguaje del dominio de comercio electrónico.
- Entidad: Cart tiene un ID único ($id) y es la raíz del agregado.
- Objeto de Valor Product no tiene ID, solo atributos (name, price) y lógica para validar (precio no negativo).
- Agregado El carrito (Cart) controla los ítems, asegurando consistencia (por ejemplo, no permitir cantidades negativas).
- Repositorio InMemoryCartRepository abstrae la persistencia, permitiendo cambiar a una base de datos sin afectar el dominio.
- Servicio de Dominio CartService coordina operaciones entre el dominio y la infraestructura.
- Modelado del dominio: El diseño del software debe reflejar el modelo mental del negocio, creado en colaboración con expertos del dominio.
- Enfoque en el núcleo del dominio: Identificar las partes más importantes del negocio (core domain) y priorizarlas en el desarrollo.
- Separación de preocupaciones: Dividir el sistema en contextos delimitados para reducir la complejidad y evitar que los conceptos se mezclen.
- Value Object: Product
declare(strict_types=1); namespace Domain\ValueObjects; class Product { private string $name; private float $price; public function __construct(string $name, float $price) { if ($price < 0) { throw new \InvalidArgumentException('Price cannot be negative.'); } $this->name = $name; $this->price = $price; } public function getName(): string { return $this->name; } public function getPrice(): float { return $this->price; } // Igualdad basada en atributos public function equals(Product $other): bool { return $this->name === $other->name && $this->price === $other->price; } } - Entidad y Raíz del Agregado: Cart, El carrito es una entidad con un ID único y actúa como la raíz del agregado, gestionando los ítems.
declare(strict_types=1); namespace Domain\Entities; use Domain\ValueObjects\Product; class Cart { private string $id; private array $items = []; public function __construct(string $id) { $this->id = $id; } public function getId(): string { return $this->id; } public function addItem(Product $product, int $quantity): void { if ($quantity <= 0) { throw new \InvalidArgumentException('Quantity must be positive.'); } $this->items[] = [ 'product' => $product, 'quantity' => $quantity, ]; } public function getTotal(): float { $total = 0.0; foreach ($this->items as $item) { $total += $item['product']->getPrice() * $item['quantity']; } return $total; } public function getItems(): array { return $this->items; } } - Repositorio: CartRepository. El repositorio maneja la persistencia de los carritos (en este caso, simulado con un array, pero podría usar una base de datos).
eclare(strict_types=1); namespace Domain\Repositories; use Domain\Entities\Cart; interface CartRepositoryInterface { public function findById(string $id): ?Cart; public function save(Cart $cart): void; } class InMemoryCartRepository implements CartRepositoryInterface { private array $carts = []; public function findById(string $id): ?Cart { return $this->carts[$id] ?? null; } public function save(Cart $cart): void { $this->carts[$cart->getId()] = $cart; } } - Servicio de Dominio (Opcional): Un servicio de dominio puede coordinar operaciones complejas. Aquí, un servicio para manejar la lógica de agregar productos al carrito.
declare(strict_types=1); namespace Domain\Services; use Domain\Entities\Cart; use Domain\Repositories\CartRepositoryInterface; use Domain\ValueObjects\Product; class CartService { private CartRepositoryInterface $repository; public function __construct(CartRepositoryInterface $repository) { $this->repository = $repository; } public function addProductToCart(string $cartId, Product $product, int $quantity): void { $cart = $this->repository->findById($cartId); if (!$cart) { $cart = new Cart($cartId); } $cart->addItem($product, $quantity); $this->repository->save($cart); } public function getCartTotal(string $cartId): float { $cart = $this->repository->findById($cartId); if (!$cart) { throw new \InvalidArgumentException('Cart not found.'); } return $cart->getTotal(); } } - Uso del sistema: Ejemplo de cómo usar estas clases en una aplicación.
require_once __DIR__ . '/vendor/autoload.php'; // Si usas Composer use Domain\Services\CartService; use Domain\Repositories\InMemoryCartRepository; use Domain\ValueObjects\Product; // Crear dependencias $repository = new InMemoryCartRepository(); $cartService = new CartService($repository); // Crear un producto $product = new Product('Laptop', 999.99); // Agregar producto al carrito $cartId = 'cart-123'; $cartService->addProductToCart($cartId, $product, 2); // Obtener el total $total = $cartService->getCartTotal($cartId); echo "Total del carrito: $total\n"; // Salida: Total del carrito: 1999.98