Por qué DDD no se trata de diagramas aburridos, sino de arquitectura viva
\nCuando un proyecto en C++ supera el tamaño de una aplicación "de juguete", los desarrolladores a menudo se enfrentan al caos: la lógica de negocio está dispersa por los controladores, la base de datos dicta la estructura de las clases, y los novatos temen tocar el código legacy. Domain-Driven Design (DDD) no es un conjunto de reglas abstractas, sino una herramienta práctica que ayuda a domar la complejidad. En este artículo, analizaremos cómo implementar DDD en C++ usando ejemplos concretos de código y por qué esto salvará su proyecto de convertirse en un "gran montón de barro".
\n\n¿Qué es DDD y por qué lo necesita un desarrollador de C++?
\nDomain-Driven Design (diseño orientado al dominio) es un enfoque donde la atención principal se centra en el modelo del dominio, no en los detalles técnicos. En C++, esto es especialmente relevante: el lenguaje ofrece herramientas potentes (plantillas, RAII, punteros inteligentes), pero sin una arquitectura adecuada, el proyecto se convierte en un monolito donde cambiar una característica lo rompe todo.
\nDDD propone dos niveles:
\n- \n
- Modelado estratégico: división del sistema en contextos (Bounded Contexts) y configuración de su interacción. \n
- Modelado táctico: bloques de construcción: Entity, Value Object, Aggregate, Repository, Domain Service. \n
Nos centraremos en el modelado táctico, ya que es el que proporciona patrones concretos para el código en C++.
\n\nArquitectura en capas: el fundamento de DDD en C++
\nDDD presupone una división clara en capas. La estructura típica para un proyecto de C++ se ve así:
\n- \n
- Capa de dominio (núcleo): entidades, value objects, agregados, servicios de dominio. Sin dependencias de frameworks o bibliotecas externas. \n
- Capa de aplicación: casos de uso (use cases), orquestación. Depende solo del dominio. \n
- Capa de infraestructura: implementación de repositorios (SQL, Redis, archivos), trabajo con APIs externas. Implementa interfaces del dominio. \n
- Capa de presentación: interfaz de usuario (consola, API REST, GUI). \n
Esto no es una regla estricta, sino una recomendación. Lo principal es que las dependencias apuntan hacia adentro: las capas externas pueden depender de las internas, pero no al revés.
\n\nEjemplo: cómo no hacerlo
\n// ❌ Mal: lógica de negocio mezclada con SQL\nclass UserController {\n void createUser(const std::string& name) {\n // Consulta SQL directamente aquí\n db.execute("INSERT INTO users (name) VALUES (?)", name);\n // La lógica de validación también está aquí\n if (name.empty()) throw std::runtime_error("Name required");\n }\n};\n\n\nEjemplo: estilo DDD
\n// ✅ Bien: división clara\nclass User : public Entity<User, int> {\n std::string name_;\npublic:\n explicit User(int id, std::string name) : Entity(id), name_(std::move(name)) {\n if (name_.empty()) throw std::invalid_argument("User name cannot be empty");\n }\n const std::string& name() const { return name_; }\n};\n\n// Interfaz del repositorio (Dominio)\nclass IUserRepository {\npublic:\n virtual ~IUserRepository() = default;\n virtual void save(const User& user) = 0;\n};\n\n// Implementación (Infraestructura)\nclass SqlUserRepository : public IUserRepository {\n void save(const User& user) override {\n // Solo SQL, sin lógica de negocio\n }\n};\n\n\nBloques de construcción de DDD en C++
\n\nEntity (Entidad)
\nEs un objeto con un identificador único que persiste en el tiempo. En C++, es conveniente crear una clase base con plantilla:
\ntemplate<typename T, typename IdType>\nclass Entity {\n IdType id_;\npublic:\n explicit Entity(IdType id) : id_(std::move(id)) {}\n IdType id() const { return id_; }\n bool operator==(const Entity& other) const { return id_ == other.id_; }\n