dans DDD, les référentiels devraient-ils exposer une entité ou des objets de domaine?

11

Si je comprends bien, en DDD, il convient d'utiliser un modèle de référentiel avec une racine agrégée. Ma question est, dois-je renvoyer les données en tant qu'entités ou objets de domaine / DTO?

Peut-être qu'un code expliquera ma question plus en détail:

Entité

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

Dois-je faire quelque chose comme ça?

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

Ou quelque chose comme ça?

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

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

Question supplémentaire:

  1. Dans un référentiel, dois-je retourner IQueryable ou IEnumerable?
  2. Dans un service ou un dépôt, dois - je faire quelque chose comme .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? ou tout simplement faire une méthode qui ressemble à quelque chose GetCustomerBy(Func<string, bool> predicate)?
poisson-code
la source
Que GetCustomerByName('John Smith')reviendra si vous avez vingt John Smith dans votre base de données? On dirait que vous supposez que deux personnes n'ont pas le même nom.
bdsl

Réponses:

8

dois-je renvoyer les données en tant qu'entités ou objets de domaine / DTO

Eh bien, cela dépend entièrement de vos cas d'utilisation. La seule raison pour laquelle je peux penser à retourner un DTO au lieu d'une entité complète est si votre entité est énorme et que vous n'avez besoin que de travailler sur un sous-ensemble de celui-ci.

Si tel est le cas, vous devriez peut-être reconsidérer votre modèle de domaine et diviser votre grande entité en entités plus petites connexes.

  1. Dans un référentiel, dois-je retourner IQueryable ou IEnumerable?

Une bonne règle est de toujours renvoyer le type le plus simple (le plus élevé dans la hiérarchie d'héritage) possible. Donc, revenez IEnumerablesauf si vous voulez laisser le consommateur du référentiel travailler avec un IQueryable.

Personnellement, je pense que le retour d'un IQueryableest une abstraction qui fuit, mais j'ai rencontré plusieurs développeurs qui soutiennent avec passion que ce n'est pas le cas. Dans mon esprit, toute logique d'interrogation devrait être contenue et cachée par le référentiel. Si vous autorisez le code appelant à personnaliser leur requête, quel est l'intérêt du référentiel?

  1. Dans un service ou un référentiel, dois-je faire quelque chose comme .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? ou simplement créer une méthode qui ressemble à GetCustomerBy (prédicat Func)?

Pour la même raison que je l'ai mentionné au point 1, ne l' utilisez certainement pasGetCustomerBy(Func<string, bool> predicate) . Cela peut sembler tentant au premier abord, mais c'est exactement pourquoi les gens ont appris à détester les référentiels génériques. C'est fuyant.

Des choses comme GetByPredicate(Func<T, bool> predicate)ne sont utiles que lorsqu'elles sont cachées derrière des classes concrètes. Donc, si vous aviez une classe de base abstraite appelée RepositoryBase<T>qui exposait protected T GetByPredicate(Func<T, bool> predicate)qui n'était utilisée que par des référentiels concrets (par exemple, public class CustomerRepository : RepositoryBase<Customer>) alors ce serait bien.

MetaFight
la source
Donc, votre dicton, c'est bien d'avoir des classes DTO dans la couche domaine?
codefish
Ce n'est pas ce que j'essayais de dire. Je ne suis pas un gourou DDD donc je ne peux pas dire si c'est acceptable. Par curiosité, pourquoi ne devais-tu pas rendre l'entité complète? Est-il trop grand?
MetaFight
Oh pas vraiment. Je veux juste savoir quelle est la bonne et acceptable chose à faire. Est-ce pour renvoyer une entité complète ou juste le sous-ensemble de celle-ci. Je suppose que cela dépend de votre réponse.
codefish
6

Il existe une importante communauté de personnes qui utilisent CQRS pour implémenter leurs domaines. Mon sentiment est que, si l'interface de votre référentiel est analogue aux meilleures pratiques utilisées par eux, vous ne vous égarerez pas trop.

D'après ce que j'ai vu ...

1) Les gestionnaires de commandes utilisent généralement le référentiel pour charger l'agrégat via un référentiel. Les commandes ciblent une seule instance spécifique de l'agrégat; le référentiel charge la racine par ID. Il n'y a pas, comme je peux le voir, un cas où les commandes sont exécutées sur une collection d'agrégats (à la place, vous devez d'abord exécuter une requête pour obtenir la collection d'agrégats, puis énumérer la collection et émettre une commande pour chacun.

Par conséquent, dans des contextes où vous allez modifier l'agrégat, je m'attendrais à ce que le référentiel retourne l'entité (aka la racine d'agrégat).

2) Les gestionnaires de requêtes ne touchent pas du tout aux agrégats; au lieu de cela, ils fonctionnent avec des projections des agrégats - objets de valeur qui décrivent l'état de l'agrégat / agrégats à un moment donné. Pensez donc à ProjectionDTO, plutôt qu'à AggregateDTO, et vous avez la bonne idée.

Dans des contextes où vous allez exécuter des requêtes sur l'agrégat, le préparer pour l'affichage, etc., je m'attends à voir un DTO, ou une collection DTO, renvoyé, plutôt qu'une entité.

Tous vos getCustomerByPropertyappels me ressemblent à des requêtes, donc ils entrent dans cette dernière catégorie. Je voudrais probablement utiliser un seul point d'entrée pour générer la collection, donc je chercherais à voir si

getCustomersThatSatisfy(Specification spec)

est un choix raisonnable; les gestionnaires de requêtes construiraient alors la spécification appropriée à partir des paramètres donnés et passeraient cette spécification au référentiel. L'inconvénient est que la signature suggère vraiment que le référentiel est une collection en mémoire; il n'est pas clair pour moi que le prédicat vous achète beaucoup si le référentiel n'est qu'une abstraction de l'exécution d'une instruction SQL sur une base de données relationnelle.

Cependant, il existe certains modèles qui peuvent aider. Par exemple, au lieu de construire la spécification à la main, transmettez au référentiel une description des contraintes et laissez l'implémentation du référentiel décider quoi faire.

Avertissement: Java comme la frappe détectée

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 conclusion: le choix entre fournir un agrégat et fournir un DTO dépend de ce que vous attendez du consommateur. Je suppose qu'une implémentation concrète prend en charge une interface pour chaque contexte.

VoiceOfUnreason
la source
C'est bien beau, mais le demandeur ne mentionne pas l'utilisation de CQRS. Vous avez raison cependant, son problème serait inexistant s'il le faisait.
MetaFight
Je n'ai aucune connaissance du CQRS alors que j'en ai entendu parler. Si je comprends bien, quand je pense à quoi retourner si AggregateDTO ou ProjectionDTO, je vais retourner ProjectionDTO. Ensuite, getCustomersThatSatisfy(Specification spec)je vais simplement énumérer les propriétés dont j'avais besoin pour les options de recherche. Suis-je bien compris?
codefish
Pas clair. Je ne pense pas que AggregateDTO devrait exister. Le but d'un agrégat est de s'assurer que toutes les modifications satisfont l'invariant métier. C'est l'encapsulation des règles de domaine qui doivent être satisfaites - c'est-à-dire, c'est le comportement. Les projections, en revanche, sont des représentations d'un instantané de l'état qui était acceptable pour l'entreprise. Voir modifier pour tenter de clarifier la spécification.
VoiceOfUnreason
Je suis d'accord avec le VoiceOfUnreason selon lequel un AggregateDTO ne devrait pas exister - je pense au ProjectionDTO comme un modèle d'affichage pour les données uniquement. Il peut adapter sa forme à ce qui est requis par l'appelant, en extrayant les données de diverses sources si nécessaire. La racine agrégée doit être une représentation complète de toutes les données associées afin que toutes les règles référentielles puissent être testées avant l'enregistrement. Il ne change de forme que dans des circonstances plus drastiques, telles que des changements de table DB ou des relations de données modifiées.
Brad Irby
1

Sur la base de ma connaissance de DDD, vous devriez avoir ceci,

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

Dans un référentiel, dois-je retourner IQueryable ou IEnumerable?

Cela dépend, vous trouverez des vues mixtes de différents peuples pour cette question. Mais je crois personnellement que si votre référentiel sera utilisé par un service comme CustomerService, vous pouvez utiliser IQueryable, sinon restez avec IEnumerable.

Les référentiels doivent-ils renvoyer IQueryable?

Dans un service ou un référentiel, dois-je faire quelque chose comme .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? ou simplement créer une méthode qui ressemble à GetCustomerBy (prédicat Func)?

Dans votre référentiel, vous devriez avoir une fonction générique comme vous l'avez dit, GetCustomer(Func predicate)mais dans votre couche de service, ajoutez trois méthodes différentes, car il est possible que votre service soit appelé à partir de différents clients et qu'ils nécessitent des DTO différents.

Vous pouvez également utiliser le modèle de référentiel générique pour les CRUD courants, si vous n'êtes pas déjà au courant.

Muhammad Raja
la source