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, che non andrai troppo lontano fuori strada.
Basato su ciò che ho visto ...
1) I gestori di comandi di solito utilizzano il repository per caricare l'aggregato tramite un repository. I comandi mirano a una singola istanza specifica dell'aggregato; il repository carica la radice per ID. Non c'è, che io possa vedere, un caso in cui i comandi sono eseguiti contro una raccolta di aggregati (invece, si dovrebbe prima eseguire una query per ottenere la raccolta di aggregati, quindi enumerare la raccolta e inviare un comando a ciascuno.
Pertanto, nei contesti in cui modificherete l'aggregato, mi aspetto 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 AggregateDTO, e hai l'idea giusta.
Nei contesti in cui si stanno eseguendo query sull'aggregato, preparandolo per la visualizzazione e così via, mi aspetto di vedere restituito un DTO o una raccolta DTO piuttosto che un'entità.
Tutte le tue chiamate getCustomerByProperty
mi sembrano simili, quindi rientrerebbero in quest'ultima categoria. Probabilmente vorrei utilizzare un singolo punto di ingresso per generare la raccolta, quindi dovrei cercare di vedere se
getCustomersThatSatisfy(Specification spec)
è una scelta ragionevole; i gestori di query costruiranno quindi la specifica appropriata dai parametri forniti e passeranno tale specifica al repository. Lo svantaggio è che la firma suggerisce davvero che il repository è una raccolta in memoria; non mi è chiaro che il predicato ti comprenda molto se il repository è solo un'astrazione di eseguire un'istruzione SQL su un database relazionale.
Ci sono alcuni schemi che possono aiutare, però. Ad esempio, invece di compilare manualmente le specifiche, passare al repository una descrizione dei vincoli e consentire l'implementazione del repository per decidere cosa fare.
Attenzione: digitata java come rilevata
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 si aspetta che il consumatore faccia con esso. La mia ipotesi sarebbe una implementazione concreta che supporti un'interfaccia per ogni contesto.