Verwenden Sie Komposition und Vererbung für DTOs

13

Wir haben eine ASP.NET-Web-API, die eine REST-API für unsere Einzelseitenanwendung bereitstellt. Wir verwenden DTOs / POCOs, um Daten über diese API weiterzuleiten.

Das Problem ist nun, dass diese DTOs im Laufe der Zeit immer größer werden. Jetzt wollen wir die DTOs umgestalten.

Ich suche nach "Best Practices" zum Entwerfen eines DTO: Derzeit gibt es kleine DTOs, die nur aus Feldern vom Typ "Wert" bestehen, z.

public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }
}

Andere DTOs verwenden diese UserDto nach Zusammensetzung, z.

public class TaskDto
{
    public int Id { get; set; }

    public UserDto AssignedTo { get; set; }
}

Es gibt auch einige erweiterte DTOs, die durch Erben von anderen definiert werden, z.

public class TaskDetailDto : TaskDto
{
    // some more fields
}

Da einige DTOs für mehrere Endpunkte / Methoden (z. B. GET und PUT) verwendet wurden, wurden sie im Laufe der Zeit schrittweise um einige Felder erweitert. Und aufgrund der Vererbung und Zusammensetzung wurden auch andere DTOs größer.

Meine Frage ist nun, ob Vererbung und Zusammensetzung keine guten Praktiken sind. Aber wenn wir sie nicht wiederverwenden, fühlt es sich an, als würde man denselben Code mehrmals schreiben. Ist es eine schlechte Praxis, ein DTO für mehrere Endpunkte / Methoden zu verwenden, oder sollte es unterschiedliche DTOs geben, die sich nur in einigen Nuancen unterscheiden?

Offizier
quelle
6
Ich kann Ihnen nicht sagen, was Sie tun sollen, aber nach meiner Erfahrung mit DTO, Vererbung und Komposition jagen Sie lieber früher als später einen schlechten Kaffee. Ich werde DTO nie wieder verwenden. Je. Außerdem halte ich ähnliche DTOs nicht für eine DRY-Verletzung. Zwei Endpunkte, die dieselbe Darstellung zurückgeben, können dieselben DTOs wiederverwenden. Zwei Endpunkte, die ähnliche Darstellungen zurückgeben, geben nicht dieselben DTOs zurück, daher erstelle ich für jeden spezifische DTOs . Wenn ich mich entscheiden müsste, wäre die Komposition auf lange Sicht weniger problematisch.
Laiv,
@Laiv das ist die richtige Antwort auf die Frage, tu es einfach nicht. Nicht sicher, warum Sie es als Kommentar setzen
TheCatWhisperer
2
@Laiv: Was benutzt du stattdessen? Nach meiner Erfahrung überdenken Leute, die damit zu kämpfen haben, es einfach. Ein DTO ist nur ein Container für Daten, und das ist alles, was es ist.
Robert Harvey
TheCatWhisperer, weil meine Argumente hauptsächlich meinungsbasiert wären. Ich versuche immer noch, diese Art von Problemen in meinen Projekten anzugehen. @RobertHarvey stimmt, ich weiß nicht, warum ich Dinge schwieriger sehe, als sie wirklich sind. Ich arbeite immer noch an der Lösung. Ich war ziemlich überzeugt, dass HAL das Modell war, das diese Probleme lösen sollte, aber als ich Ihre Antwort las, wurde mir klar, dass ich mein DTO auch zu granular mache. Also werde ich Ihren Ansatz zuerst in die Praxis umsetzen. Die Änderungen werden weniger dramatisch sein als die vollständige Verlagerung auf HATEOAS.
Laiv,
Versuchen Sie dies für eine gute Diskussion , die Ihnen helfen könnten: stackoverflow.com/questions/6297322/...
johnny

Antworten:

9

Versuchen Sie als Best Practice, Ihre DTOs so präzise wie möglich zu gestalten. Geben Sie nur das zurück, was Sie für die Rücksendung benötigen. Verwenden Sie nur das, was Sie benötigen. Wenn das ein paar zusätzliche DTOs bedeutet, dann sei es so.

In Ihrem Beispiel enthält eine Aufgabe einen Benutzer. Wahrscheinlich wird dort kein vollständiges Benutzerobjekt benötigt, sondern nur der Name des Benutzers, dem die Aufgabe zugewiesen wurde. Wir brauchen den Rest der Benutzereigenschaften nicht.

Angenommen, Sie möchten eine Aufgabe einem anderen Benutzer neu zuweisen. Möglicherweise besteht die Versuchung, während des Postens der Neuzuweisung ein vollständiges Benutzerobjekt und ein vollständiges Aufgabenobjekt zu übergeben. Was jedoch wirklich benötigt wird, ist nur die Task-ID und die Benutzer-ID. Dies sind die einzigen beiden Informationen, die zum erneuten Zuweisen einer Aufgabe erforderlich sind. Modellieren Sie das DTO als solches. Dies bedeutet normalerweise, dass für jeden Rest-Aufruf ein separates DTO vorhanden ist, wenn Sie ein Lean-DTO-Modell anstreben.

Manchmal braucht man auch Vererbung / Komposition. Nehmen wir an, man hat einen Job. Ein Job hat mehrere Aufgaben. In diesem Fall wird beim Abrufen eines Jobs möglicherweise auch die Liste der Aufgaben für den Job zurückgegeben. Es gibt also keine Regel gegen Komposition. Es kommt darauf an, was modelliert wird.

Jon Raynor
quelle
Möglichst übersichtlich zu sein bedeutet auch, dass ich DTOs neu erstellen sollte, wenn sie sich nur in einigen Details unterscheiden - nicht wahr?
Offizier
1
@officer - Im Allgemeinen ja.
Jon Raynor
2

Sofern Ihr System nicht ausschließlich auf CRUD-Operationen basiert, sind Ihre DTOs zu detailliert. Versuchen Sie, Endpunkte zu erstellen, die Geschäftsprozesse oder Artefakte verkörpern. Dieser Ansatz lässt sich gut auf eine Geschäftslogikebene und auf Eric Evans '"domänenorientiertes Design" übertragen.

Angenommen, Sie haben einen Endpunkt, der Daten für eine Rechnung zurückgibt, damit diese dem Endbenutzer auf einem Bildschirm oder in einem Formular angezeigt werden können. In einem CRUD-Modell benötigen Sie mehrere Anrufe an Ihre Endpunkte, um die erforderlichen Informationen zusammenzustellen: Name, Rechnungsadresse, Versandadresse, Werbebuchungen. In einem Geschäftstransaktionskontext kann ein einzelner DTO von einem einzelnen Endpunkt alle diese Informationen auf einmal zurückgeben.

Robert Harvey
quelle
Robert, meinst du hier eine Gesamtwurzel?
Johnny
Gegenwärtig sind unsere DTOs so konzipiert, dass sie die gesamten Daten für ein Formular bereitstellen. Wenn beispielsweise eine Aufgabenliste angezeigt wird, verfügt das TodoListDTO über eine Liste von Aufgaben. Aber da wir das TaskDTO wiederverwenden, hat jedes von ihnen auch ein UserDTO. Das Problem ist also die große Datenmenge, die im Backend abgefragt und über das Netzwerk gesendet wird.
Offizier
Sind alle Daten erforderlich?
Robert Harvey
1

Es ist schwierig, Best Practices für etwas so "Flexibles" oder Abstraktes wie ein DTO zu etablieren. Im Wesentlichen sind DTOs nur Objekte für die Datenübertragung. Abhängig vom Ziel oder dem Grund für die Übertragung möchten Sie möglicherweise unterschiedliche "Best Practices" anwenden.

Ich empfehle, Patterns of Enterprise Application Architecture von Martin Fowler zu lesen . Es gibt ein ganzes Kapitel über Muster, in dem DTOs einen wirklich detaillierten Abschnitt erhalten.

Ursprünglich waren sie für die Verwendung in teuren Fernaufrufen konzipiert, bei denen Sie wahrscheinlich viele Daten aus verschiedenen Teilen Ihrer Logik benötigen würden. DTOs würden die Datenübertragung in einem einzigen Anruf durchführen.

Laut dem Autor waren DTOs nicht für die Verwendung in lokalen Umgebungen vorgesehen, aber einige Leute fanden eine Verwendung für sie. Normalerweise werden sie verwendet, um Informationen von verschiedenen POCOs in einer einzigen Entität für GUIs, APIs oder verschiedene Ebenen zu sammeln.

Mit der Vererbung ähnelt die Wiederverwendung von Code eher einer Nebenwirkung der Vererbung als ihrem Hauptziel. Komposition hingegen wird mit der Wiederverwendung von Code als Hauptziel implementiert.

Einige Leute empfehlen die Verwendung von Komposition und Vererbung zusammen, wobei sie die Stärken beider nutzen und versuchen, ihre Schwächen zu mildern. Folgendes ist Teil meines mentalen Prozesses, wenn ich neue DTOs oder neue Klassen / Objekte auswähle oder erstelle:

  • Ich verwende Vererbung mit DTOs innerhalb derselben Ebene oder desselben Kontexts. Ein DTO wird niemals von einem POCO erben, ein BLL-DTO wird niemals von einem DAL-DTO erben usw.
  • Wenn ich versuche, ein Feld vor einem DTO zu verbergen, überarbeite ich es und verwende stattdessen möglicherweise die Komposition.
  • Wenn ich nur sehr wenige verschiedene Felder von einem Basis-DTO benötige, füge ich sie einem universellen DTO hinzu. Universelle DTOs werden nur intern verwendet.
  • Ein Basis-POCO / DTO wird so gut wie nie für eine Logik verwendet, so dass die Basis nur auf die Bedürfnisse ihrer Kinder reagiert. Wenn ich jemals die Basis verwenden muss, vermeide ich es, ein neues Feld hinzuzufügen, das seine Kinder niemals verwenden werden.

Einige von ihnen sind vielleicht nicht die "besten" Praktiken, sie funktionieren ziemlich gut für die Projekte, an denen ich gearbeitet habe, aber Sie müssen sich daran erinnern, dass keine Größe für alle passt. Beim universellen DTO sollten Sie vorsichtig sein, meine Methodensignaturen sehen folgendermaßen aus:

public void DoSomething(BaseDTO base) {
    //Some code 
}

Wenn eine der Methoden jemals ein eigenes DTO benötigt, übernehme ich die Vererbung und normalerweise muss ich nur den Parameter ändern, obwohl ich in bestimmten Fällen manchmal tiefer gehen muss.

Aus Ihren Kommentaren gehe hervor, dass Sie verschachtelte DTOs verwenden. Wenn Ihre verschachtelten DTOs nur aus einer Liste anderer DTOs bestehen, ist es meines Erachtens das Beste, die Liste auszupacken.

Abhängig von der Datenmenge, die Sie anzeigen oder bearbeiten müssen, ist es möglicherweise eine gute Idee, neue DTOs zu erstellen, die die Daten begrenzen. Wenn Ihr UserDTO beispielsweise viele Felder hat und Sie nur 1 oder 2 benötigen, ist es möglicherweise besser, ein DTO nur mit diesen Feldern zu haben. Das Definieren der Ebene, des Kontexts, der Verwendung und des Nutzens eines DTO wird beim Entwerfen sehr hilfreich sein.

IvanGrasp
quelle
Ich konnte nicht mehr als 2 Links in meine Antwort einfügen. Hier sind einige weitere Informationen zu lokalen DTOs. Mir ist bewusst, dass es sich um sehr alte Informationen handelt, aber ich denke, dass einige davon möglicherweise noch relevant sind.
IvanGrasp
1

Die Verwendung von Komposition in DTOs ist eine sehr gute Übung.

Die Vererbung zwischen konkreten Arten von DTOs ist eine schlechte Praxis.

Zum einen hat eine automatisch implementierte Eigenschaft in einer Sprache wie C # einen sehr geringen Wartungsaufwand, so dass das Duplizieren (ich verabscheue wirklich das Duplizieren) nicht so schädlich ist wie es oft ist.

Ein Grund , die konkrete Vererbung unter DTOs nicht zu verwenden, ist, dass bestimmte Tools sie gerne den falschen Typen zuordnen.

Wenn Sie beispielsweise ein Datenbankdienstprogramm wie Dapper verwenden (ich empfehle es nicht, aber es ist sehr beliebt), das class name -> table nameInferenzen ausführt , können Sie einen abgeleiteten Typ leicht irgendwo in der Hierarchie als konkreten Basistyp speichern und dadurch Daten verlieren oder schlimmeres .

Ein tieferer Grund nicht Gebrauch Erbe unter DTOs ist , dass es nicht zu teilen Implementierungen zwischen Typen verwendet werden soll , dass eine offensichtlich nicht haben „ist eine“ Beziehung. Meiner Meinung nach TaskDetailklingt das nicht nach einem Untertyp von Task. Es könnte genauso gut eine Eigenschaft von Taskoder, schlimmer noch, ein Supertyp von sein Task.

Eine Sache, die Sie möglicherweise befürchten, ist die Wahrung der Konsistenz zwischen den Namen und Typen der Eigenschaften verschiedener verwandter DTOs.

Die Vererbung konkreter Typen würde folglich dazu beitragen, eine solche Konsistenz zu gewährleisten. Es ist jedoch viel besser, Schnittstellen (oder reine virtuelle Basisklassen in C ++) zu verwenden, um diese Art von Konsistenz aufrechtzuerhalten.

Betrachten Sie die folgenden DTOs

interface IIdentity
{
    int Id { get; set; }
}

interface INamed
{
    string Name { get; set; }
}

public class UserDto: IIdentity, INamed
{
    public int Id { get; set; }

    public string Name { get; set; }

    // User specific properties
}

public class TaskDto: IIdentity
{
    public int Id { get; set; }

    // Task specific properties
}
Aluan Haddad
quelle