Estoy aprendiendo DDD y estoy pensando en lanzar excepciones en ciertas situaciones. Entiendo que un objeto no puede entrar en un mal estado, así que aquí las excepciones están bien, pero en muchos ejemplos también se lanzan excepciones, por ejemplo, si intentamos agregar un nuevo usuario con un correo electrónico existente en la base de datos.
public function doIt(UserData $userData)
{
$user = $this->userRepository->byEmail($userData->email());
if ($user) {
throw new UserAlreadyExistsException();
}
$this->userRepository->add(
new User(
$userData->email(),
$userData->password()
)
);
}
Entonces, si el usuario con este correo electrónico existe, entonces podemos detectar una excepción en el servicio de aplicaciones, pero no debemos controlar el funcionamiento de la aplicación utilizando el bloque try-catch.
¿Cuál es la mejor manera para esto?
Respuestas:
Comencemos esto con una breve revisión del espacio del problema: uno de los principios fundamentales de DDD es colocar las reglas de negocio lo más cerca posible de los lugares donde deben hacerse cumplir. Este es un concepto extremadamente importante porque hace que su sistema sea más "coherente". Mover las reglas "hacia arriba" es generalmente un signo de un modelo anémico; donde los objetos son solo bolsas de datos y las reglas se inyectan con esos datos para que se apliquen.
Un modelo anémico puede tener mucho sentido para los desarrolladores que recién comienzan con DDD. Crea un
User
modelo yEmailMustBeUnqiueRule
se le inyecta la información necesaria para validar el correo electrónico. Sencillo. Elegante. La cuestión es que este "tipo" de pensamiento es fundamentalmente de naturaleza procesal. No DDD Lo que termina sucediendo es que te queda un módulo con docenas deRules
envoltorios y encapsulados, pero están completamente desprovistos de contexto hasta el punto en que ya no se pueden cambiar porque no está claro cuándo / dónde se aplican. ¿Tiene sentido? Se puede ser auto-evidente queEmailMustBeUnqiueRule
se aplicará sobre la creación de unaUser
, pero ¿qué pasaUserIsInGoodStandingRule
?. Lento pero seguro, la granularización de extraer elRules
fuera de su contexto te deja con un sistema que es difícil de entender (y por lo tanto no se puede cambiar). Las reglas deben encapsularse solo cuando el procesamiento / ejecución real es tan detallado que su modelo comienza a perder el foco.Ahora pase a su pregunta específica: el problema con tener el
Service
/CommandHandler
throw theException
es que su lógica de negocios está comenzando a filtrarse ("hacia arriba") fuera de su dominio. ¿Por qué suService
/CommandHandler
necesidad de saber que un correo electrónico debe ser único? La capa de servicio de la aplicación se usa generalmente para la coordinación en lugar de la implementación. La razón de esto puede ilustrarse simplemente si agregamos unChangeEmail
método / comando a su sistema. Ahora AMBOS métodos / manejadores de comandos necesitarían incluir su verificación única. Aquí es donde un desarrollador puede verse tentado a "extraer" unEmailMustBeUniqueRule
. Como se explicó anteriormente, no queremos seguir esa ruta.Un poco de conocimiento adicional puede llevarnos a una respuesta más DDD. La singularidad de un correo electrónico es una invariante que debe aplicarse en una colección de
User
objetos. ¿Hay un concepto en su dominio que represente una "colección deUser
objetos"? Creo que probablemente puedas ver a dónde voy aquí.Para este caso particular (y muchos más que implican hacer cumplir invariantes en las colecciones), el mejor lugar para implementar esta lógica será en su
Repository
. Esto es especialmente conveniente porqueRepository
también "conoce" la infraestructura adicional necesaria para ejecutar este tipo de validación (el almacén de datos). En su caso, colocaría esta verificación en eladd
método. Esto tiene sentido ¿verdad? Conceptualmente, es este método el que realmente agregaUser
a su sistema. El almacén de datos es un detalle de implementación.fuente
Moving rules "upwards" is generally a sign of an anemic model
? ¿Medios ascendentes a la capa de aplicación? o al modelo de dominio?Email
modelo debe ser una bolsa de datos que se inspecciona para detectar invariantes antes de enviarlo. Ver: softwareengineering.stackexchange.com/questions/372338/…User
) o aceptar que este tipo específico de invariante no se encapsulará perfectamente en tu dominio. El primero representa una separación de datos y comportamiento que resulta en un paradigma de procedimiento. Esto es menos que ideal. La validación debería existir con datos, no alrededor de datos. Además, ¿qué beneficio hay para crear un servicio de dominio que solo sirve para verificar esta regla? Ostensiblemente, este servicio ...Repository
,User
y esta invariante y, por lo tanto, no logra la visión purista que estás presionando de todos modos. Las implementaciones de repositorio son parte del dominio y, por lo tanto, pueden recibir ciertas responsabilidades.Puede imponer la validación en cada capa de su aplicación. Dónde imponer qué reglas dependen de su aplicación.
Por ejemplo, las entidades implementan métodos que representan reglas comerciales mientras que los casos de uso implementan reglas específicas para su aplicación.
Si está creando un servicio de correo electrónico como Gmail, se podría argumentar que la regla "los usuarios deben tener una dirección de correo electrónico única" es una regla comercial.
Si está creando un proceso de registro para un nuevo sistema CRM, esta regla probablemente se implemente mejor en un caso de uso, ya que es probable que esta regla también sea parte de su caso de uso. Además, también podría ser más fácil de probar, ya que puede bloquear fácilmente las llamadas al repositorio en sus pruebas de casos de uso.
Para seguridad adicional, también puede aplicar esta regla en su repositorio.
fuente