¿Existe una manera elegante de verificar restricciones únicas en los atributos de los objetos de dominio sin mover la lógica empresarial a la capa de servicio?

10

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 Nameya 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 Producto 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.ProductCollectionclase 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.

Andy
fuente
¿Por qué extraer una lista completa de nombres cuando puede pedir uno y basar la lógica en si se encontró o no? Le está diciendo a la capa de persistencia qué hacer y no cómo hacerlo, siempre que haya un acuerdo con la interfaz.
JeffO
1
Al utilizar el término Servicio, debemos esforzarnos por lograr una mayor claridad, como la diferencia entre los servicios de dominio en la capa de dominio y los servicios de aplicación en la capa de aplicación. Ver gorodinski.com/blog/2012/04/14/…
Erik Eidt
Excelente artículo, @ErikEidt, gracias. Supongo que mi diseño no está mal, entonces, si puedo confiar en el Sr. Gorodinski, está diciendo lo mismo: una mejor solución es hacer que un servicio de aplicaciones recupere la información requerida por una entidad, establezca de manera efectiva el entorno de ejecución y proporcione a la entidad, y tiene la misma objeción contra la inyección de repositorio en el modelo de Dominio directamente que yo, principalmente al romper el SRP.
Andy
Una cita de la referencia "tell, don't ask": "Pero personalmente, no uso tell-dont-ask".
radarbob

Respuestas:

7

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í.

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 .

Eufórico
fuente
Gracias por la aportación. ¿Podría el repositorio representar realmente lo Domain.ProductCollectionque tenía en mente, considerando que son responsables de recuperar objetos de la capa de Dominio?
Andy
@DavidPacker Realmente no entiendo lo que quieres decir. Porque no debería ser necesario mantener todos los elementos en la memoria. La implementación del método "DoesNameExist" debería ser (probablemente) consulta SQL en el lado del almacén de datos.
Eufórico
Lo que significa es que, en lugar de almacenar los datos en una colección en la memoria, cuando quiero conocer todo el Producto, no necesito obtenerlos, Domain.ProductCollectionsino preguntar al repositorio, al igual que preguntar, si Domain.ProductCollectioncontiene 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.
Andy
Lo que lleva a otra pregunta, ¿debería saber el repositorio si un atributo debe ser único? Siempre he implementado repositorios como componentes bastante tontos, guardando lo que pasa para guardarlos e intentando recuperar datos basados ​​en las condiciones pasadas y poner la decisión en los servicios.
Andy
@DavidPacker Bueno, el "conocimiento del dominio" está en la interfaz. La interfaz dice que "el dominio necesita esta funcionalidad", y el almacén de datos proporciona esa funcionalidad. Si tiene IProductRepositoryinterfaz con DoesNameExist, 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.
Eufórico
4

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?

El problema con los requisitos de "singularidad" es que, bueno, muy a menudo hay una razón subyacente más profunda por la cual la gente los quiere - Yves Reynhout

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.

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

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.

No me gusta la necesidad de extraer todas las SKU del producto de la base de datos para realizar la operación en la memoria.

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).

VoiceOfUnreason
fuente
No me gusta la necesidad de extraer todas las SKU del producto de la base de datos para realizar la operación en la memoria. Es la solución obvia que sugerí en la pregunta inicial y cuestioné su rendimiento. Además, la OMI, confiando en la restricción en su DB, para ser responsable de la unicidad, es mala. Si tuviera que cambiar a un nuevo motor de base de datos y de alguna manera la restricción única se perdiera durante una transformación, habría roto el código, porque la información de la base de datos que existía antes ya no está allí. De todos modos gracias por el enlace, lectura interesante.
Andy
Quizás un filtro Bloom (ver edición).
VoiceOfUnreason