¿Cómo tratar la validación de referencias entre agregados?

11

Estoy luchando un poco con la referencia entre agregados. Supongamos que el agregado Cartiene una referencia al agregado Driver. Esta referencia será modelada por tener Car.driverId.

Ahora mi problema es qué tan lejos debo ir para validar la creación de un Caragregado en CarFactory. ¿Debo confiar en que lo aprobado se DriverIdrefiere a un valor existente Driver o debo verificarlo como invariante?

Para verificar, veo dos posibilidades:

  • Podría cambiar la firma de la fábrica de automóviles para aceptar una entidad de conductor completa. La fábrica simplemente seleccionaría la identificación de esa entidad y construiría el automóvil con eso. Aquí el invariante se verifica implícitamente.
  • Podría tener una referencia de DriverRepositoryen el CarFactoryy explícitamente llamar driverRepository.exists(driverId).

Pero ahora me pregunto, ¿no es eso demasiada comprobación invariable? Me imagino que esos agregados podrían vivir en un contexto acotado separado, y ahora contaminaría el automóvil BC con dependencias del DriverRepository o la entidad Driver del controlador BC.

Además, si hablara con expertos en dominios, nunca cuestionarían la validez de tales referencias. Tengo la sensación de que contamina mi modelo de dominio con preocupaciones no relacionadas. Pero, de nuevo, en algún momento la entrada del usuario debe validarse.

Markus Malkusch
fuente

Respuestas:

6

Podría cambiar la firma de la fábrica de automóviles para aceptar una entidad de conductor completa. La fábrica simplemente seleccionaría la identificación de esa entidad y construiría el automóvil con eso. Aquí el invariante se verifica implícitamente.

Este enfoque es atractivo ya que recibe el cheque de forma gratuita y está bien alineado con el lenguaje omnipresente. A Carno es impulsado por a driverId, sino por a Driver.

De hecho, este enfoque es utilizado por Vaughn Vernon en su contexto limitado de muestra de Identidad y Acceso donde pasa un Useragregado a un Groupagregado, pero el Groupúnico se mantiene en un tipo de valor GroupMember. Como puede ver, esto también le permite verificar la habilitación del usuario (somos conscientes de que la verificación puede estar obsoleta).

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

Sin embargo, al pasar la Driverinstancia también te abres a una modificación accidental del Driverinterior Car. Pasar la referencia del valor hace que sea más fácil razonar sobre los cambios desde el punto de vista de un programador, pero al mismo tiempo, DDD tiene que ver con el lenguaje ubicuo, por lo que quizás valga la pena el riesgo.

Si realmente puede encontrar buenos nombres para aplicar el Principio de segregación de interfaz (ISP), entonces puede confiar en una interfaz que no tenga los métodos de comportamiento. Tal vez también podría crear un concepto de objeto de valor que represente una referencia de controlador inmutable y que solo se pueda crear una instancia desde un controlador existente (por ejemplo DriverDescriptor driver = driver.descriptor()).

Me imagino que esos agregados podrían vivir en un contexto acotado separado, y ahora contaminaría el automóvil BC con dependencias del DriverRepository o la entidad Driver del controlador BC.

No, en realidad no lo harías. Siempre hay una capa anticorrupción para garantizar que los conceptos de un contexto no se filtren en otro. En realidad, es mucho más fácil si tiene un BC dedicado a las asociaciones de conductores de automóviles porque puede modelar conceptos existentes como Cary Driverespecíficamente para ese contexto.

Por lo tanto, es posible que tenga un DriverLookupServiceBC definido para administrar las asociaciones de conductores de automóviles. Este servicio puede llamar a un servicio web expuesto por el contexto de Gestión de controladores que devuelve Driverinstancias que probablemente serán objetos de valor en este contexto.

Tenga en cuenta que los servicios web no son necesariamente el mejor método de integración entre los BC. También puede confiar en la mensajería donde, por ejemplo UserCreated, se consumiría un mensaje del contexto de Gestión de controladores en un contexto remoto que almacenaría una representación del controlador en su propia base de datos. El DriverLookupServicepodría entonces utilizar estos datos del conductor DB y se mantienen actualizados con nuevos mensajes (por ejemplo DriverLicenceRevoked).

Realmente no puedo decirte qué enfoque es mejor para tu dominio, pero espero que esto te brinde información suficiente para tomar una decisión.

plalx
fuente
3

La forma en que hace la pregunta (y propone dos alternativas) es como si la única preocupación es que el driverId todavía es válido en el momento en que se crea el automóvil.

Sin embargo, también debe preocuparse de que el conductor asociado con driverId no se elimine antes de que el automóvil se elimine o se le dé otro conductor (y posiblemente también que el conductor no esté asignado a otro automóvil (esto si el dominio restringe a un conductor a solo estar asociado con un automóvil)).

Sugiero que en lugar de la validación, asigne (lo que incluiría la validación de presencia). A continuación, no permitirá las eliminaciones mientras esté asignado, evitando así la condición de carrera de los datos obsoletos durante la construcción, así como el otro problema a largo plazo. (Tenga en cuenta que la asignación valida y marca, y opera atómicamente).

Por cierto, estoy de acuerdo con @PriceJones en que la asociación entre el automóvil y el conductor probablemente sea una responsabilidad separada del automóvil o del conductor. Este tipo de asociación solo crecerá en complejidad con el tiempo, porque suena como un problema de programación (conductores, automóviles, franjas horarias / ventanas, sustitutos, etc.) Incluso si es más como un problema de registro, uno puede querer un historial registros, así como registros actuales. Por lo tanto, es muy posible que merezca su propio BC completamente.

Puede proporcionar un esquema de asignación (como un conteo booleano o de referencia) dentro del BC de las entidades agregadas que se asignan, o dentro de un BC separado, por ejemplo, el responsable de hacer la asociación entre el automóvil y el conductor. Si hace lo primero, puede permitir operaciones de eliminación (válidas) emitidas para el automóvil o el conductor BC; si hace lo último, deberá evitar las eliminaciones de los BC del automóvil y el conductor y, en su lugar, enviarlos a través del programador de asociación de automóvil y conductor.

También puede dividir algunas de las responsabilidades de asignación entre BC de la siguiente manera. El coche y el conductor BC proporcionan un esquema de "asignación" que valida y establece el booleano asignado con ese BC; cuando se establece su asignación booleana, el BC impide la eliminación de las entidades correspondientes. (Y el sistema está configurado para que el BC del automóvil y el conductor solo permita la asignación y la desasignación desde el BC de la asociación del automóvil / conductor).

El BC que programa el automóvil y el conductor mantiene un calendario de conductores asociados con el automóvil durante algunos períodos de tiempo / duraciones, ahora y en el futuro, y notifica a los otros BC de la desasignación solo en el último uso de un automóvil o conductor programado.


Como una solución más radical, puede tratar a los BC de automóviles y conductores como fábricas de registros históricos solo anexables, dejando la propiedad al programador de asociación de automóvil / conductor. El automóvil BC puede generar un automóvil nuevo, completo con todos los detalles del automóvil, junto con su VIN. La propiedad del automóvil es manejada por el programador de asociación de automóvil / conductor. Incluso si se elimina una asociación de automóvil / conductor, y el automóvil en sí mismo se destruye, los registros del automóvil aún existen en el automóvil BC por definición, y podemos usar el automóvil BC para buscar datos históricos; mientras que las asociaciones / propietarios de automóviles / conductores (pasados, presentes y potencialmente futuros programados) están siendo manejados por otro BC.

Erik Eidt
fuente
2

Supongamos que el automóvil agregado tiene una referencia al conductor agregado. Esta referencia será modelada teniendo Car.driverId.

Sí, esa es la forma correcta de acoplar un agregado a otro.

si hablara con expertos en dominios, nunca cuestionarían la validez de tales referencias

No es la pregunta correcta para hacerle a sus expertos en dominios. Pruebe "¿cuál es el costo para el negocio si el controlador no existe?"

Probablemente no usaría DriverRepository para verificar el driverId. En cambio, usaría un servicio de dominio para hacerlo. Creo que hace un mejor trabajo al expresar la intención: bajo las cubiertas, el servicio de dominio aún verifica el sistema de registro.

Entonces algo como

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

Realmente consulta el dominio sobre el ID del controlador en varios puntos diferentes

  • Desde el cliente, antes de enviar el comando
  • En la aplicación, antes de pasar el comando al modelo
  • Dentro del modelo de dominio, durante el procesamiento del comando

Cualquiera o todas estas comprobaciones pueden reducir los errores en la entrada del usuario. Pero todos están trabajando a partir de datos obsoletos; el otro agregado puede cambiar inmediatamente después de hacer la pregunta. Por lo tanto, siempre existe el peligro de falsos negativos / positivos.

  • En un informe de excepción, ejecute después de que se haya completado el comando

Aquí, todavía está trabajando con datos obsoletos (los agregados pueden estar ejecutando comandos mientras está ejecutando el informe, es posible que no pueda ver las escrituras más recientes en todos los agregados). Pero las comprobaciones entre agregados nunca serán perfectas (Car.create (driver: 7) ejecutándose al mismo tiempo que Driver.delete (driver: 7)) Así que esto le brinda una capa adicional de defensa contra el riesgo.

VoiceOfUnreason
fuente
1
Driver.deleteno debería existir Realmente nunca vi un dominio donde los agregados se destruyen. Al mantener los AR a su alrededor, nunca puede terminar con huérfanos.
plalx
1

Podría ser útil preguntar: ¿Está seguro de que los automóviles están construidos con conductores? Nunca he oído hablar de un automóvil compuesto por un conductor en el mundo real. La razón por la cual esta pregunta es importante es porque podría indicarle la dirección de crear independientemente automóviles y conductores y luego crear algún mecanismo externo que asigne un conductor a un automóvil. Un automóvil puede existir sin una referencia del conductor y seguir siendo un automóvil válido.

Si un automóvil debe tener un conductor en su contexto, entonces es posible que desee considerar el patrón del constructor. Este patrón será responsable de garantizar que los automóviles se construyan con los conductores existentes. Las fábricas servirán autos y conductores validados independientemente, pero el constructor se asegurará de que el auto tenga la referencia que necesita antes de que lo haga.

Price Jones
fuente
También pensé en la relación automóvil / conductor, pero la introducción de un agregado DriverAssignment solo mueve la referencia que debe validarse.
VoiceOfUnreason
1

Pero ahora me pregunto, ¿no es eso demasiada comprobación invariable?

Creo que sí. Obtener un DriverId determinado de la base de datos devuelve un conjunto vacío si no existe. Por lo tanto, verificar el resultado devuelto hace que sea innecesario preguntar si existe (y luego buscar).

Entonces el diseño de clase hace innecesario también

  • Si existe un requisito "un automóvil estacionado puede o no tener un conductor"
  • Si un objeto Driver requiere a DriverIdy se establece en el constructor.
  • Si Carsolo necesita el DriverId, tenga un Driver.Idcaptador. Sin setter.

El repositorio no es el lugar para las reglas comerciales

  • A Carle importa si tiene un Driver(o su ID al menos). A Driverle importa si tiene un DriverId. Se Repositorypreocupa por la integridad de los datos y no podría importarle menos los automóviles sin conductor.
  • El DB tendrá reglas de integridad de datos. Claves no nulas, restricciones no nulas, etc. Pero la integridad de los datos se trata del esquema de datos / tablas, no de las reglas de negocio. Tenemos una relación simbiótica fuertemente correlacionada en este caso, pero no mezclemos los dos.
  • El hecho de que a DriverIdes un asunto de dominio comercial se maneja en las clases apropiadas.

Violación de separación de preocupaciones

... sucede cuando Repository.DriverIdExists()hace la pregunta.

Construye un objeto de dominio. Si no es un, Driverentonces tal vez un objeto DriverInfo(solo un DriverIdy Name, digamos). El DriverIdse valida en la construcción. Debe existir, y ser del tipo correcto, y cualquier otra cosa. Entonces es un problema de diseño de clase de cliente cómo tratar con un driver / driverId inexistente.

Quizás a Carestá bien sin conductor hasta que llame Car.Drive(). En cuyo caso, el Carobjeto, por supuesto, garantiza su propio estado. No se puede conducir sin Driver... bueno, todavía no.

Separar una propiedad de su clase es malo

Claro, ten un Car.DriverIdsi lo deseas. Pero debería verse más o menos así:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

No esta:

public class Car {
    public int DriverId {get; protected set;}
}

Ahora se Cardebe tratar con todos los DriverIdproblemas de validez: una violación del principio de responsabilidad única; y código redundante probablemente.

radarbob
fuente