Llevo 8 años adaptando el diseño basado en dominios e incluso después de todos estos años, todavía hay una cosa que me ha estado molestando. Es verificar un registro único en el almacenamiento de datos contra un objeto de dominio.
En septiembre de 2013, Martin Fowler mencionó el principio TellDon'tAsk , que, si es posible, debería aplicarse a todos los objetos de dominio, que luego deberían devolver un mensaje, cómo fue la operación (en el diseño orientado a objetos, esto se hace principalmente a través de excepciones, cuando la operación no tuvo éxito).
Mis proyectos generalmente se dividen en muchas partes, donde dos de ellos son Dominio (que contiene reglas comerciales y nada más, el dominio es completamente ignorante de persistencia) y Servicios. Servicios que conocen la capa de repositorio utilizada para datos CRUD.
Debido a que la singularidad de un atributo que pertenece a un objeto es una regla de dominio / negocio, el módulo de dominio debería ser largo, por lo que la regla es exactamente donde se supone que debe estar.
Para poder verificar la unicidad de un registro, debe consultar el conjunto de datos actual, generalmente una base de datos, para averiguar si Name
ya existe otro registro con un digamos .
Teniendo en cuenta que la capa de dominio ignora la persistencia y no tiene idea de cómo recuperar los datos, sino solo cómo realizar operaciones en ellos, realmente no puede tocar los repositorios en sí.
El diseño que he estado adaptando se ve así:
class ProductRepository
{
// throws Repository.RecordNotFoundException
public Product GetBySKU(string sku);
}
class ProductCrudService
{
private ProductRepository pr;
public ProductCrudService(ProductRepository repository)
{
pr = repository;
}
public void SaveProduct(Domain.Product product)
{
try {
pr.GetBySKU(product.SKU);
throw Service.ProductWithSKUAlreadyExistsException("msg");
} catch (Repository.RecordNotFoundException e) {
// suppress/log exception
}
pr.MarkFresh(product);
pr.ProcessChanges();
}
}
Esto lleva a que los servicios definan reglas de dominio en lugar de la capa de dominio en sí y que las reglas estén dispersas en varias secciones de su código.
Mencioné el principio de TellDon'tAsk, porque como puedes ver claramente, el servicio ofrece una acción (guarda Product
o arroja una excepción), pero dentro del método estás operando objetos usando un enfoque de procedimiento.
La solución obvia es crear una Domain.ProductCollection
clase con un Add(Domain.Product)
método que arroje ProductWithSKUAlreadyExistsException
, pero le falta mucho rendimiento, porque necesitaría obtener todos los Productos del almacenamiento de datos para averiguar en código, si un Producto ya tiene el mismo SKU como el Producto que intentas agregar.
¿Cómo resuelven este problema específico? Esto no es realmente un problema per se, he tenido la capa de servicio que representa ciertas reglas de dominio durante años. La capa de servicio por lo general también sirve para operaciones de dominio más complejas, simplemente me pregunto si ha encontrado una solución mejor y más centralizada durante su carrera.
Respuestas:
No estaría de acuerdo con esta parte. Especialmente la última oración.
Si bien es cierto que el dominio debe ser ignorante de persistencia, sí sabe que existe una "Colección de entidades de dominio". Y que hay reglas de dominio que conciernen a esta colección como un todo. La unicidad es uno de ellos. Y debido a que la implementación de la lógica real depende en gran medida del modo de persistencia específico, debe haber algún tipo de abstracción en el dominio que especifique la necesidad de esta lógica.
Por lo tanto, es tan simple como crear una interfaz que pueda consultar si el nombre ya existe, que luego se implementa en su almacén de datos y quien lo necesite para saber si el nombre es único.
Y me gustaría destacar que los repositorios son servicios DOMAIN. Son abstracciones en torno a la persistencia. Es la implementación del repositorio la que debe estar separada del dominio. No hay absolutamente nada de malo en que la entidad de dominio llame a un servicio de dominio. No hay nada de malo en que una entidad pueda usar el repositorio para recuperar otra entidad o recuperar información específica, que no puede guardarse fácilmente en la memoria. Esta es una razón por la cual Repository es un concepto clave en el libro de Evans .
fuente
Domain.ProductCollection
que tenía en mente, considerando que son responsables de recuperar objetos de la capa de Dominio?Domain.ProductCollection
sino preguntar al repositorio, al igual que preguntar, siDomain.ProductCollection
contiene un Producto con el SKU pasado, esta vez, nuevamente, preguntando al repositorio en su lugar (este es realmente el ejemplo), que en lugar de iterar sobre los productos precargados consulta la base de datos subyacente. No pretendo almacenar todo el Producto en la memoria a menos que sea necesario, hacerlo sería una tontería completa.IProductRepository
interfaz conDoesNameExist
, es obvio lo que debe hacer el método. Pero asegurarse de que el Producto no pueda tener el mismo nombre que el producto existente debería estar en algún lugar del dominio.Debes leer a Greg Young en la validación del set .
Respuesta corta: antes de adentrarse demasiado en el nido de ratas, debe asegurarse de comprender el valor del requisito desde la perspectiva comercial. ¿Qué tan costoso es realmente detectar y mitigar la duplicación, en lugar de prevenirla?
Respuesta más larga: he visto un menú de posibilidades, pero todas tienen compensaciones.
Puede verificar si hay duplicados antes de enviar el comando al dominio. Esto se puede hacer en el cliente o en el servicio (su ejemplo muestra la técnica). Si no está satisfecho con la lógica que se escapa de la capa de dominio, puede lograr el mismo tipo de resultado con a
DomainService
.Por supuesto, de esta manera la implementación del DeduplicationService necesitará saber algo sobre cómo buscar los skus existentes. Entonces, aunque empuja parte del trabajo al dominio, aún enfrenta los mismos problemas básicos (necesita una respuesta para la validación establecida, problemas con las condiciones de carrera).
Puede hacer la validación en su propia capa de persistencia. Las bases de datos relacionales son realmente buenas para la validación de conjuntos. Ponga una restricción de unicidad en la columna de sku de su producto y estará listo. La aplicación solo guarda el producto en el repositorio y obtiene una violación de restricción que vuelve a aparecer si hay un problema. Por lo tanto, el código de la aplicación se ve bien y se elimina su condición de carrera, pero se están filtrando las reglas de "dominio".
Puede crear un agregado separado en su dominio que represente el conjunto de skus conocidos. Puedo pensar en dos variaciones aquí.
Uno es algo así como un ProductCatalog; los productos existen en otro lugar, pero la relación entre productos y skus se mantiene mediante un catálogo que garantiza la singularidad del sku. No es que esto implique que los productos no tienen skus; los skus son asignados por un ProductCatalog (si necesita que los skus sean únicos, puede lograr esto teniendo solo un agregado ProductCatalog). Revise el lenguaje omnipresente con sus expertos en dominios; si tal cosa existe, este podría ser el enfoque correcto.
Una alternativa es algo más como un servicio de reserva de sku. El mecanismo básico es el mismo: un agregado conoce todos los skus, por lo que puede evitar la introducción de duplicados. Pero el mecanismo es ligeramente diferente: adquiere un contrato de arrendamiento de un sku antes de asignarlo a un producto; al crear el producto, le pasa el contrato de arrendamiento al sku. Todavía hay una condición de carrera en juego (diferentes agregados, por lo tanto, distintas transacciones), pero tiene un sabor diferente. El verdadero inconveniente aquí es que está proyectando en el modelo de dominio un servicio de arrendamiento sin tener realmente una justificación en el idioma del dominio.
Puede extraer todas las entidades de productos en un solo agregado, es decir, el catálogo de productos descrito anteriormente. Absolutamente obtienes la singularidad de los skus cuando haces esto, pero el costo es una disputa adicional, modificar cualquier producto realmente significa modificar todo el catálogo.
Quizás no necesites hacerlo. Si prueba su sku con un filtro Bloom , puede descubrir muchos skus únicos sin cargar el conjunto.
Si su caso de uso le permite ser arbitrario sobre qué skus rechaza, podría eliminar todos los falsos positivos (no es un gran problema si permite que los clientes prueben los skus que proponen antes de enviar el comando). Eso le permitiría evitar cargar el conjunto en la memoria.
(Si quisieras ser más receptivo, podrías considerar cargar lentamente los skus en caso de que haya una coincidencia en el filtro de floración; todavía te arriesgas a cargar todos los skus en la memoria a veces, pero no debería ser el caso común si permites el código del cliente para verificar el comando en busca de errores antes de enviarlo).
fuente