in DDD, i repository dovrebbero esporre un'entità o oggetti di dominio?


11

A quanto ho capito, in DDD è opportuno utilizzare un modello di repository con una radice aggregata. La mia domanda è: devo restituire i dati come entità o oggetti di dominio / DTO?

Forse un po 'di codice spiegherà ulteriormente la mia domanda:

Entità

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

Dovrei fare qualcosa del genere?

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

O qualcosa del genere?

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

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

Domanda aggiuntiva:

  1. In un repository, devo restituire IQueryable o IEnumerable?
  2. In un servizio o un repository, devo fare qualcosa di simile .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? o semplicemente creare un metodo simile GetCustomerBy(Func<string, bool> predicate)?

Cosa GetCustomerByName('John Smith')tornerà se hai venti John Smith nel tuo database? Sembra che tu stia supponendo che due persone non abbiano lo stesso nome.
bdsl,

Risposte:


8

dovrei restituire i dati come entità o oggetti dominio / DTO

Bene, dipende interamente dai tuoi casi d'uso. L'unica ragione per cui riesco a pensare di restituire un DTO invece di un'entità completa è se la tua entità è enorme e devi solo lavorare su un sottoinsieme di essa.

In questo caso, forse dovresti riconsiderare il tuo modello di dominio e dividere la tua grande entità in entità minori correlate.

  1. In un repository, devo restituire IQueryable o IEnumerable?

Una buona regola empirica è quella di restituire sempre il tipo più semplice (il più alto nella gerarchia dell'ereditarietà) possibile. Quindi, restituire a IEnumerablemeno che non si desideri consentire al consumatore del repository di funzionare con un IQueryable.

Personalmente, penso che restituire un IQueryablesia un'astrazione che perde ma ho incontrato diversi sviluppatori che sostengono appassionatamente che non lo è. Nella mia mente tutta la logica dell'interrogazione dovrebbe essere contenuta e nascosta dal repository. Se si consente al codice chiamante di personalizzare la propria query, qual è il punto del repository?

  1. In un servizio o repository, dovrei fare qualcosa come .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? o semplicemente creare un metodo simile a GetCustomerBy (predicato di Func)?

Per lo stesso motivo di cui ho parlato al punto 1, sicuramente non lo uso GetCustomerBy(Func<string, bool> predicate). All'inizio può sembrare allettante, ma questo è esattamente il motivo per cui le persone hanno imparato a odiare i repository generici. Perdono.

Cose come GetByPredicate(Func<T, bool> predicate)sono utili solo quando sono nascoste dietro classi concrete. Quindi, se avessi una classe base astratta chiamata RepositoryBase<T>quale esposto protected T GetByPredicate(Func<T, bool> predicate)che è stata utilizzata solo da repository concreti (ad es. public class CustomerRepository : RepositoryBase<Customer>) , Allora andrebbe bene.


Quindi il tuo detto, va bene avere classi DTO nel livello di dominio?
codefish

Non è quello che stavo cercando di dire. Non sono un guru DDD, quindi non posso dire se sia accettabile. Per curiosità, perché non dovevi restituire l'intera entità? È troppo grande?
MetaFight,

Oh non proprio. Voglio solo sapere qual è la cosa giusta e accettabile da fare. Restituisce un'entità completa o solo il suo sottoinsieme. Immagino che dipenda dalla base della tua risposta.
codefish

6

C'è una considerevole comunità di persone che usano CQRS per implementare i loro domini. La mia sensazione è che, se l'interfaccia del tuo repository è analoga alle migliori pratiche utilizzate da loro, non andrai troppo fuori strada.

Basato su quello che ho visto ...

1) I gestori dei comandi di solito usano il repository per caricare l'aggregato tramite un repository. I comandi indirizzano una singola istanza specifica dell'aggregato; il repository carica la radice per ID. Non riesco a vedere un caso in cui i comandi vengono eseguiti su una raccolta di aggregati (invece, eseguiresti prima una query per ottenere la raccolta di aggregati, quindi enumerare la raccolta e inviare un comando a ciascuno di essi.

Pertanto, in contesti in cui modificherete l'aggregato, mi aspetterei che il repository restituisca l'entità (ovvero la radice aggregata).

2) I gestori di query non toccano affatto gli aggregati; invece, funzionano con le proiezioni degli aggregati - oggetti valore che descrivono lo stato dell'aggregato / aggregati in un determinato momento. Quindi pensa a ProjectionDTO, piuttosto che a AggregateDTO, e hai l'idea giusta.

In contesti in cui eseguirai query sull'aggregato, preparandolo per la visualizzazione e così via, mi aspetterei di visualizzare un DTO, o una raccolta DTO, anziché un'entità.

Tutte le tue getCustomerByPropertychiamate mi sembrano domande, quindi rientrerebbero in quest'ultima categoria. Probabilmente vorrei usare un singolo punto di ingresso per generare la raccolta, quindi vorrei vedere se

getCustomersThatSatisfy(Specification spec)

è una scelta ragionevole; i gestori di query costruiscono quindi la specifica appropriata dai parametri forniti e passano tale specifica al repository. Il rovescio della medaglia è che la firma suggerisce davvero che il repository è una raccolta in memoria; non è chiaro per me che il predicato ti compra molto se il repository è solo un'astrazione dell'esecuzione di un'istruzione SQL su un database relazionale.

Ci sono alcuni modelli che possono aiutare, però. Ad esempio, invece di creare manualmente le specifiche, passare al repository una descrizione dei vincoli e consentire all'implementazione del repository di decidere cosa fare.

Attenzione: rilevata la digitazione come Java

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);
    }
}

In conclusione: la scelta tra fornire un aggregato e fornire un DTO dipende da cosa ci si aspetta dal consumatore. La mia ipotesi sarebbe un'implementazione concreta che supporti un'interfaccia per ogni contesto.


È tutto bello e buono, ma il richiedente non menziona l'uso di CQRS. Hai ragione però, il suo problema sarebbe inesistente se lo facesse.
MetaFight,

Non ho alcuna conoscenza di CQRS, anche se ne ho sentito parlare. A quanto ho capito, quando penso a cosa restituire se AggregateDTO o ProjectionDTO, restituirò ProjectionDTO. Quindi, su getCustomersThatSatisfy(Specification spec), elencherò solo le proprietà necessarie per le opzioni di ricerca. Lo sto capendo bene?
codefish

Poco chiaro. Non credo che AggregateDTO dovrebbe esistere. Il punto di un aggregato è garantire che tutte le modifiche soddisfino l'invariante del business. È l'incapsulamento delle regole di dominio che devono essere soddisfatte, ovvero è un comportamento. Le proiezioni, d'altra parte, sono rappresentazioni di alcune istantanee dello stato accettabili per l'azienda. Vedi modifica per tentare di chiarire la specifica.
VoiceOfUnreason

Concordo con VoiceOfUnreason che un AggregateDTO non dovrebbe esistere - penso a ProjectionDTO come un modello di visualizzazione solo per i dati. Può adattare la sua forma a ciò che è richiesto dal chiamante, estraendo i dati da varie fonti se necessario. La radice aggregata dovrebbe essere una rappresentazione completa di tutti i dati correlati in modo che tutte le regole di riferimento possano essere testate prima del salvataggio. Cambia forma solo in circostanze più drastiche, come le modifiche alla tabella DB o le relazioni dati modificate.
Brad Irby,

1

In base alla mia conoscenza di DDD dovresti avere questo,

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

In un repository, devo restituire IQueryable o IEnumerable?

Dipende, per questa domanda troverai viste miste di popoli diversi. Ma personalmente ritengo che se il tuo repository verrà utilizzato da un servizio come CustomerService, puoi usare IQueryable altrimenti rimani con IEnumerable.

I repository dovrebbero restituire IQueryable?

In un servizio o repository, dovrei fare qualcosa come .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? o semplicemente creare un metodo simile a GetCustomerBy (predicato di Func)?

Nel tuo repository dovresti avere una funzione generica come hai detto, GetCustomer(Func predicate)ma nel tuo livello di servizio aggiungi tre diversi metodi, questo perché c'è una possibilità che il tuo servizio venga chiamato da client diversi e che abbiano bisogno di DTO diversi.

È inoltre possibile utilizzare il modello di repository generico per CRUD comune, se non si è già a conoscenza.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.