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.