Sollten Repositorys in DDD eine Entität oder Domänenobjekte verfügbar machen?

11

Soweit ich weiß, ist es in DDD angebracht, ein Repository-Muster mit einem aggregierten Stamm zu verwenden. Meine Frage ist, sollte ich die Daten als Entität oder Domänenobjekte / DTO zurückgeben?

Vielleicht erklärt ein Code meine Frage weiter:

Entität

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

Soll ich so etwas machen?

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

Oder sowas?

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

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

Zusätzliche Frage:

  1. Soll ich in einem Repository IQueryable oder IEnumerable zurückgeben?
  2. In einem Dienst oder Repository, sollte ich so etwas tun .. GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? oder einfach eine Methode machen, die so etwas wie ist GetCustomerBy(Func<string, bool> predicate)?
Codefisch
quelle
Was wird GetCustomerByName('John Smith')zurückgegeben, wenn Sie zwanzig John Smiths in Ihrer Datenbank haben? Es sieht so aus, als würden Sie davon ausgehen, dass keine zwei Personen denselben Namen haben.
BDSL

Antworten:

8

sollte ich die Daten als Entität oder Domänenobjekte / DTO zurückgeben

Nun, das hängt ganz von Ihren Anwendungsfällen ab. Der einzige Grund, warum ich daran denken kann, ein DTO anstelle einer vollständigen Entität zurückzugeben, ist, wenn Ihre Entität riesig ist und Sie nur an einer Teilmenge davon arbeiten müssen.

Wenn dies der Fall ist, sollten Sie möglicherweise Ihr Domain-Modell überdenken und Ihre große Entität in verwandte kleinere Entitäten aufteilen.

  1. Soll ich in einem Repository IQueryable oder IEnumerable zurückgeben?

Eine gute Faustregel ist, immer den einfachsten (höchsten in der Vererbungshierarchie) Typ zurückzugeben. IEnumerableKehren Sie also zurück, es sei denn, Sie möchten, dass der Repository-Konsument mit einem arbeitet IQueryable.

Persönlich denke ich, dass die Rückgabe IQueryableeine undichte Abstraktion ist, aber ich habe mehrere Entwickler getroffen, die leidenschaftlich argumentieren, dass dies nicht der Fall ist. Meiner Meinung nach sollte die gesamte Abfragelogik im Repository enthalten und verborgen sein. Wenn Sie zulassen, dass aufrufender Code die Abfrage anpasst, wozu dient das Repository dann?

  1. Sollte ich in einem Dienst oder Repository Folgendes tun: GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? oder einfach eine Methode erstellen, die so etwas wie GetCustomerBy (Func-Prädikat) ist?

Aus dem gleichen Grund, den ich in Punkt 1 erwähnt habe, definitiv nicht verwenden GetCustomerBy(Func<string, bool> predicate). Es mag zunächst verlockend erscheinen, aber genau deshalb haben die Leute gelernt, generische Repositories zu hassen. Es ist undicht.

Dinge wie GetByPredicate(Func<T, bool> predicate)sind nur dann nützlich, wenn sie sich hinter konkreten Klassen verstecken. Wenn Sie also eine abstrakte Basisklasse namens " RepositoryBase<T>Belichtet" hätten, protected T GetByPredicate(Func<T, bool> predicate)die nur von konkreten Repositorys (z. B. public class CustomerRepository : RepositoryBase<Customer>) verwendet wurde, wäre das in Ordnung.

MetaFight
quelle
Sie sagen also, es ist in Ordnung, DTO-Klassen in der Domänenschicht zu haben?
Codefisch
Das wollte ich nicht sagen. Ich bin kein DDD-Guru, daher kann ich nicht sagen, ob das akzeptabel ist. Warum sollten Sie aus Neugier nicht die gesamte Einheit zurückgeben? Ist es zu groß?
MetaFight
Oh nicht wirklich. Ich möchte nur wissen, was das Richtige und Akzeptable ist. Ist es eine vollständige Entität oder nur die Teilmenge davon zurückzugeben. Ich denke, es hängt von Ihrer Antwort ab.
Codefisch
6

Es gibt eine beträchtliche Community von Leuten, die CQRS verwenden, um ihre Domänen zu implementieren. Ich bin der Meinung, dass Sie nicht zu weit in die Irre gehen werden, wenn die Schnittstelle Ihres Repositorys den von ihnen verwendeten Best Practices entspricht.

Basierend auf dem, was ich gesehen habe ...

1) Befehlshandler verwenden normalerweise das Repository, um das Aggregat über ein Repository zu laden. Befehle zielen auf eine einzelne bestimmte Instanz des Aggregats ab. Das Repository lädt den Stamm nach ID. Wie ich sehen kann, gibt es keinen Fall, in dem die Befehle für eine Sammlung von Aggregaten ausgeführt werden (stattdessen würden Sie zuerst eine Abfrage ausführen, um die Sammlung von Aggregaten abzurufen, dann die Sammlung aufzählen und jeweils einen Befehl ausgeben.

In Kontexten, in denen Sie das Aggregat ändern möchten, würde ich daher erwarten, dass das Repository die Entität (auch als Aggregatstamm bezeichnet) zurückgibt.

2) Abfragehandler berühren die Aggregate überhaupt nicht. Stattdessen arbeiten sie mit Projektionen der Aggregate - Wertobjekte, die den Status des Aggregats / der Aggregate zu einem bestimmten Zeitpunkt beschreiben. Denken Sie also eher an ProjectionDTO als an AggregateDTO, und Sie haben die richtige Idee.

In Kontexten, in denen Sie Abfragen für das Aggregat ausführen, es für die Anzeige vorbereiten usw., würde ich erwarten, dass ein DTO oder eine DTO-Sammlung anstelle einer Entität zurückgegeben wird.

Alle Ihre getCustomerByPropertyAnrufe sehen für mich wie Anfragen aus, sodass sie in die letztere Kategorie fallen würden. Ich würde wahrscheinlich einen einzelnen Einstiegspunkt verwenden wollen, um die Sammlung zu generieren, also würde ich nachsehen, ob

getCustomersThatSatisfy(Specification spec)

ist eine vernünftige Wahl; Die Abfragehandler würden dann die entsprechende Spezifikation aus den angegebenen Parametern erstellen und diese Spezifikation an das Repository übergeben. Der Nachteil ist, dass die Signatur wirklich darauf hindeutet, dass das Repository eine Sammlung im Speicher ist. Mir ist nicht klar, dass das Prädikat Ihnen viel kostet, wenn das Repository nur eine Abstraktion zum Ausführen einer SQL-Anweisung für eine relationale Datenbank ist.

Es gibt jedoch einige Muster, die helfen können. Anstatt die Spezifikation manuell zu erstellen, übergeben Sie beispielsweise eine Beschreibung der Einschränkungen an das Repository und lassen Sie die Implementierung des Repositorys entscheiden, was zu tun ist.

Warnung: Java-ähnliche Eingabe erkannt

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

Fazit: Die Wahl zwischen der Bereitstellung eines Aggregats und der Bereitstellung eines DTO hängt davon ab, was Sie vom Verbraucher erwarten. Meine Vermutung wäre eine konkrete Implementierung, die eine Schnittstelle für jeden Kontext unterstützt.

VoiceOfUnreason
quelle
Das ist alles schön und gut, aber der Fragesteller erwähnt die Verwendung von CQRS nicht. Sie haben jedoch Recht, sein Problem wäre nicht vorhanden, wenn er es tun würde.
MetaFight
Ich habe keine Kenntnisse über CQRS, obwohl ich davon gehört habe. So wie ich es verstehe, werde ich ProjectionDTO zurückgeben, wenn ich überlege, was ich zurückgeben soll, wenn AggregateDTO oder ProjectionDTO. Dann getCustomersThatSatisfy(Specification spec)liste ich einfach die Eigenschaften auf, die ich für die Suchoptionen benötigte. Verstehe ich es richtig?
Codefisch
Unklar. Ich glaube nicht, dass AggregateDTO existieren sollte. Der Zweck eines Aggregats besteht darin, sicherzustellen, dass alle Änderungen die Geschäftsinvariante erfüllen. Es ist die Kapselung der Domänenregeln, die erfüllt werden müssen - dh es ist Verhalten. Projektionen hingegen sind Darstellungen einer Momentaufnahme des Staates, der für das Unternehmen akzeptabel war. Siehe Bearbeiten für den Versuch, die Spezifikation zu verdeutlichen.
VoiceOfUnreason
Ich stimme VoiceOfUnreason zu, dass ein AggregateDTO nicht existieren sollte - ich betrachte das ProjectionDTO als ein Ansichtsmodell nur für Daten. Es kann seine Form an die Anforderungen des Anrufers anpassen und bei Bedarf Daten aus verschiedenen Quellen abrufen. Das Aggregatstammverzeichnis sollte eine vollständige Darstellung aller zugehörigen Daten sein, damit alle Referenzregeln vor dem Speichern getestet werden können. Die Form ändert sich nur unter drastischeren Umständen, z. B. bei Änderungen der DB-Tabelle oder geänderten Datenbeziehungen.
Brad Irby
1

Basierend auf meinem Wissen über DDD sollten Sie dies haben,

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

Soll ich in einem Repository IQueryable oder IEnumerable zurückgeben?

Es kommt darauf an, dass Sie für diese Frage gemischte Ansichten von verschiedenen Völkern finden. Ich persönlich bin jedoch der Meinung, dass Sie IQueryable verwenden können, wenn Ihr Repository von einem Dienst wie CustomerService verwendet wird, andernfalls bleiben Sie bei IEnumerable.

Sollten Repositories IQueryable zurückgeben?

Sollte ich in einem Dienst oder Repository Folgendes tun: GetCustomerByLastName, GetCustomerByFirstName, GetCustomerByEmail? oder einfach eine Methode erstellen, die so etwas wie GetCustomerBy (Func-Prädikat) ist?

In Ihrem Repository sollten Sie eine generische Funktion haben, wie Sie gesagt haben, GetCustomer(Func predicate)aber in Ihrer Service-Schicht drei verschiedene Methoden hinzufügen. Dies liegt daran, dass Ihr Service möglicherweise von verschiedenen Clients aufgerufen wird und diese unterschiedliche DTOs benötigen.

Sie können auch das generische Repository-Muster für die allgemeine CRUD verwenden, wenn Sie dies noch nicht wissen.

Muhammad Raja
quelle