en DDD, ¿deberían los repositorios exponer una entidad u objetos de dominio?

11

Según tengo entendido, en DDD, es apropiado usar un patrón de repositorio con una raíz agregada. Mi pregunta es, ¿debo devolver los datos como una entidad u objetos de dominio / DTO?

Quizás algún código explique más mi pregunta:

Entidad

public class Customer
{
  public Guid Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

¿Debo hacer algo como esto?

public Customer GetCustomerByName(string name) { /*some code*/ }

¿O algo como esto?

public class CustomerDTO
{
  public Guid Id { get; set; }
  public FullName { get; set; }
}

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

Pregunta adicional

  1. En un repositorio, ¿debo devolver IQueryable o IEnumerable?
  2. En un servicio o repositorio, debería hacer algo así .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? o simplemente hacer un método similar GetCustomerBy(Func<string, bool> predicate)?
bacalao
fuente
¿Qué GetCustomerByName('John Smith')devolverá si tiene veinte John Smiths en su base de datos? Parece que estás asumiendo que no hay dos personas que tengan el mismo nombre.
bdsl

Respuestas:

8

¿Debo devolver los datos como una entidad u objetos de dominio / DTO

Bueno, eso depende completamente de sus casos de uso. La única razón por la que puedo pensar en devolver un DTO en lugar de una entidad completa es si su entidad es enorme y solo necesita trabajar en un subconjunto de ella.

Si este es el caso, entonces quizás debería reconsiderar su modelo de dominio y dividir su entidad grande en entidades más pequeñas relacionadas.

  1. En un repositorio, ¿debo devolver IQueryable o IEnumerable?

Una buena regla general es devolver siempre el tipo más simple (el más alto en la jerarquía de herencia) posible. Por lo tanto, regrese a IEnumerablemenos que desee dejar que el consumidor del repositorio trabaje con un IQueryable.

Personalmente, creo que devolver un IQueryablemensaje es una abstracción permeable, pero he conocido a varios desarrolladores que argumentan apasionadamente que no lo es. En mi opinión, toda la lógica de consulta debe estar contenida y oculta por el Repositorio. Si permite que el código de llamada personalice su consulta, ¿cuál es el punto del repositorio?

  1. En un servicio o repositorio, ¿debería hacer algo como ... GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? o simplemente hacer un método que sea algo como GetCustomerBy (predicado Func)?

Por la misma razón que mencioné en el punto 1, definitivamente no lo use GetCustomerBy(Func<string, bool> predicate). Puede parecer tentador al principio, pero esta es exactamente la razón por la cual la gente ha aprendido a odiar los repositorios genéricos. Es permeable.

Cosas como GetByPredicate(Func<T, bool> predicate)solo son útiles cuando están ocultos detrás de clases concretas. Por lo tanto, si tuviera una clase base abstracta llamada RepositoryBase<T>que expuso y protected T GetByPredicate(Func<T, bool> predicate)que solo fue utilizada por repositorios concretos (por ejemplo, public class CustomerRepository : RepositoryBase<Customer>) entonces estaría bien.

MetaFight
fuente
Entonces, ¿está bien tener clases DTO en la capa de dominio?
codefish
Eso no es lo que estaba tratando de decir. No soy un gurú DDD, así que no puedo decir si eso es aceptable. Por curiosidad, ¿por qué no no devolviste la entidad completa? ¿Es demasiado grande?
MetaFight el
Oh no realmente. Solo quiero saber qué es lo correcto y aceptable. ¿Es para devolver una entidad completa o solo el subconjunto de ella? Supongo que depende de tu respuesta.
codefish
6

Hay una comunidad considerable de personas que usan CQRS para implementar sus dominios. Mi sensación es que, si la interfaz de su repositorio es análoga a las mejores prácticas utilizadas por ellos, no se desviará demasiado.

Basado en lo que he visto ...

1) Los manejadores de comandos usualmente usan el repositorio para cargar el agregado a través de un repositorio. Los comandos se dirigen a una única instancia específica del agregado; el repositorio carga la raíz por ID. No hay, por lo que puedo ver, un caso en el que los comandos se ejecuten contra una colección de agregados (en su lugar, primero ejecutaría una consulta para obtener la colección de agregados, luego enumeraría la colección y emitiría un comando para cada uno.

Por lo tanto, en contextos en los que va a modificar el agregado, esperaría que el repositorio devuelva la entidad (también conocida como raíz del agregado).

2) Los manejadores de consultas no tocan los agregados en absoluto; en cambio, trabajan con proyecciones de los agregados: objetos de valor que describen el estado de los agregados / agregados en algún momento. Entonces piense en ProjectionDTO, en lugar de AggregateDTO, y tiene la idea correcta.

En contextos en los que va a ejecutar consultas contra el agregado, prepararlo para su visualización, etc., esperaría ver un DTO, o una colección de DTO, devuelta, en lugar de una entidad.

Todas sus getCustomerByPropertyllamadas me parecen consultas, por lo que caerían en la última categoría. Probablemente quiera usar un único punto de entrada para generar la colección, por lo que estaría buscando para ver si

getCustomersThatSatisfy(Specification spec)

es una elección razonable; los manejadores de consultas luego construirían la especificación apropiada a partir de los parámetros dados y pasarían esa especificación al repositorio. La desventaja es que la firma realmente sugiere que el repositorio es una colección en memoria; No está claro para mí que el predicado te compre mucho si el repositorio es solo una abstracción de ejecutar una instrucción SQL en una base de datos relacional.

Sin embargo, hay algunos patrones que pueden ayudar. Por ejemplo, en lugar de construir la especificación a mano, pase al repositorio una descripción de las restricciones y permita que la implementación del repositorio decida qué hacer.

Advertencia: Java como mecanografía detectado

interface CustomerRepository {
    interface ConstraintBuilder {
        void setLastName();
        void setFirstName();
    }

    interface ConstraintDescriptor {
        void copyTo(ConstraintBuilder builder);
    }

    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor);
}

SQLBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        WhereClauseBuilder builder = new WhereClauseBuilder();
        descriptor.copyTo(builder);
        Query q = createQuery(builder.build());
        //...
     }
}

CollectionBackedCustomerRepository implements CustomerRepository {
    List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
        PredicateBuilder builder = new PredicateBuilder();
        descriptor.copyTo(builder);
        Predicate p = builder.build();
        // ...
}

class MatchLastName implements CustomerRepository.ConstraintDescriptor {
    private final lastName;
    // ...

    void copyTo(CustomerRepository.ConstraintBuilder builder) {
        builder.setLastName(this.lastName);
    }
}

En conclusión: la elección entre proporcionar un agregado y proporcionar un DTO depende de lo que espera que el consumidor haga con él. Mi conjetura sería una implementación concreta que soporte una interfaz para cada contexto.

VoiceOfUnreason
fuente
Todo eso es bueno y bueno, pero el autor de la pregunta no menciona el uso de CQRS. Sin embargo, tienes razón, su problema sería inexistente si lo hiciera.
MetaFight el
No tengo conocimiento sobre CQRS aunque escuché sobre eso. Según tengo entendido, al pensar en qué devolver si AggregateDTO o ProjectionDTO, devolveré ProjectionDTO. Luego, en la getCustomersThatSatisfy(Specification spec)lista, enumeraré las propiedades que necesitaba para las opciones de búsqueda. ¿Lo estoy entendiendo bien?
codefish
Poco claro. No creo que AggregateDTO deba existir. El objetivo de un agregado es garantizar que todos los cambios satisfagan a la empresa invariable. Es la encapsulación de las reglas de dominio que deben cumplirse, es decir, es el comportamiento. Las proyecciones, por otro lado, son representaciones de alguna instantánea del estado que era aceptable para el negocio. Consulte editar para intentar aclarar la especificación.
VoiceOfUnreason
Estoy de acuerdo con VoiceOfUnreason en que un AggregateDTO no debería existir; pienso en ProjectionDTO como un modelo de vista solo para datos. Puede adaptar su forma a lo que requiere la persona que llama, obteniendo datos de varias fuentes según sea necesario. La raíz agregada debe ser una representación completa de todos los datos relacionados para que todas las reglas de referencia se puedan probar antes de guardar. Solo cambia de forma en circunstancias más drásticas, como cambios en la tabla DB o relaciones de datos modificadas.
Brad Irby
1

Según mi conocimiento de DDD, deberías tener esto,

public CustomerDTO GetCustomerByName(string name) { /*some code*/ }

En un repositorio, ¿debo devolver IQueryable o IEnumerable?

Depende, usted encontrará opiniones mixtas de diferentes personas para esta pregunta. Pero personalmente creo que si su repositorio será utilizado por un servicio como CustomerService, entonces puede usar IQueryable, de lo contrario, quédese con IEnumerable.

¿Deben los repositorios devolver IQueryable?

En un servicio o repositorio, ¿debería hacer algo como ... GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? o simplemente hacer un método que sea algo como GetCustomerBy (predicado Func)?

En su repositorio debe tener una función genérica como dijo, GetCustomer(Func predicate)pero en su capa de servicio agregue tres métodos diferentes, esto se debe a que existe la posibilidad de que su servicio sea llamado desde diferentes clientes y necesiten diferentes DTO.

También puede usar el patrón de repositorio genérico para CRUD común, si aún no lo sabe.

Muhammad Raja
fuente