Usa composizione ed eredità per DTO


13

Abbiamo un'API Web ASP.NET che fornisce un'API REST per la nostra applicazione a pagina singola. Utilizziamo DTO / POCO per trasferire dati attraverso questa API.

Il problema è ora che questi DTO stanno diventando più grandi nel tempo, quindi ora vogliamo riformattare i DTO.

Sto cercando "migliori pratiche" su come progettare un DTO: attualmente abbiamo piccoli DTO che consistono solo di campi di tipo valore, ad esempio:

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

    public string Name { get; set; }
}

Altri DTO utilizzano questo UserDto per composizione, ad esempio:

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

    public UserDto AssignedTo { get; set; }
}

Inoltre, ci sono alcuni DTO estesi che sono definiti ereditando da altri, ad esempio:

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

Poiché alcuni DTO sono stati utilizzati per diversi endpoint / metodi (ad esempio GET e PUT), nel tempo sono stati estesi in modo incrementale da alcuni campi. E per eredità e composizione anche altri DTO sono diventati più grandi.

La mia domanda è ora che l'ereditarietà e la composizione non sono buone pratiche? Ma quando non li riutilizziamo, sembra di scrivere lo stesso codice più volte. È una cattiva pratica usare un DTO per più endpoint / metodi o dovrebbero esserci diversi DTO, che differiscono solo in alcune sfumature?


6
Non posso dirti cosa dovresti fare, ma alla mia esperienza di lavoro con DTO, eredità e composizione prima o poi ti cacciano come un caffè cattivo. Non riutilizzerò mai più DTO. Mai. Inoltre, non considero DTO simili una violazione SECCA. Due endpoint che restituiscono la stessa rappresentazione possono riutilizzare gli stessi DTO. Due endpoint che restituiscono rappresentazioni simili non stanno restituendo gli stessi DTO, quindi creo DTO specifici per ciascuno . Se fossi costretto a scegliere, la composizione sarebbe meno problematica a lungo termine.
Laiv

@Laiv questa è la risposta corretta alla domanda, semplicemente no. Non so perché lo metti come commento
TheCatWhisperer,

2
@Laiv: cosa usi invece? Nella mia esperienza, le persone che lottano con questo lo stanno semplicemente pensando troppo. Un DTO è solo un contenitore di dati, ed è tutto.
Robert Harvey,

TheCatWhisperer perché i miei argomenti sarebbero principalmente basati sull'opinione. Sto ancora cercando di affrontare questo tipo di problemi nei miei progetti. @RobertHarvey vero, non so perché tendo a vedere le cose più difficili di quanto non siano in realtà. Sto ancora lavorando alla soluzione. Ero abbastanza convinto che HAL fosse il modello destinato a risolvere questi problemi, ma leggendo la tua risposta mi sono reso conto che faccio anche il mio DTO troppo granulars. Quindi metterò in pratica prima il tuo approccio. I cambiamenti saranno meno drammatici del passaggio completo a HATEOAS.
Laiv

Prova questo per una buona discussione che potrebbe aiutarti: stackoverflow.com/questions/6297322/…
johnny,

Risposte:


9

Come best practice, cerca di rendere i tuoi DTO il più concisi possibile. Restituisci solo ciò di cui hai bisogno per tornare. Usa solo ciò che devi usare. Se ciò significa alcuni DTO extra, così sia.

Nel tuo esempio, un'attività contiene un utente. Uno probabilmente non ha bisogno di un oggetto utente completo lì, forse solo il nome dell'utente a cui è assegnata l'attività. Non abbiamo bisogno del resto delle proprietà dell'utente.

Supponiamo che tu voglia riassegnare un'attività a un altro utente, potresti essere tentato di passare un oggetto utente completo e un oggetto attività completo durante il post di riassegnazione. Ma ciò che è veramente necessario è solo l'ID attività e l'ID utente. Queste sono le uniche due informazioni necessarie per riassegnare un'attività, quindi modella il DTO in quanto tale. Questo di solito significa che c'è un DTO separato per ogni chiamata di riposo se cerchi un modello DTO snello.

Inoltre, a volte uno potrebbe aver bisogno di ereditarietà / composizione. Diciamo che uno ha un lavoro. Un lavoro ha più attività. In tal caso, ottenere un lavoro può anche restituire l'elenco delle attività per il lavoro. Quindi, non esiste una regola contro la composizione. Dipende da cosa viene modellato.


Il più conciso possibile significa anche che dovrei ricreare DTO se differiscono solo in alcuni dettagli - non è vero?
ufficiale

1
@officer - In generale, sì.
Jon Raynor,

2

A meno che il tuo sistema non sia basato rigorosamente su operazioni CRUD, i tuoi DTO sono troppo granulari. Prova a creare endpoint che incarnano processi aziendali o artefatti. Questo approccio si adatta perfettamente a un livello di logica aziendale e al "design guidato dal dominio" di Eric Evans.

Ad esempio, supponiamo che tu abbia un endpoint che restituisce i dati per una fattura in modo che possano essere visualizzati su uno schermo o un modulo per l'utente finale. In un modello CRUD, sono necessarie diverse chiamate agli endpoint per assemblare le informazioni necessarie: nome, indirizzo di fatturazione, indirizzo di spedizione, elementi pubblicitari. In un contesto di transazione commerciale, un singolo DTO da un singolo endpoint può restituire tutte queste informazioni contemporaneamente.


Robert, intendi per radice aggregata qui?
johnny,

Attualmente i nostri DTO sono progettati per fornire tutti i dati per un modulo, ad esempio quando si visualizza un elenco di cose da fare, il TodoListDTO ha un Elenco di TaskDTO. Ma dal momento che stiamo riutilizzando TaskDTO, ognuno di essi ha anche un UserDTO. Quindi il problema è la grande quantità di dati che vengono interrogati nel back-end e inviati via cavo.
ufficiale

Tutti i dati sono richiesti?
Robert Harvey,

1

È difficile stabilire le migliori pratiche per qualcosa di "flessibile" o astratto come un DTO. In sostanza, i DTO sono solo oggetti per il trasferimento di dati, ma a seconda della destinazione o del motivo del trasferimento, è possibile che si desideri applicare diverse "migliori pratiche".

Consiglio di leggere Patterns of Enterprise Application Architecture di Martin Fowler . C'è un intero capitolo dedicato ai modelli, in cui i DTO ottengono una sezione davvero dettagliata.

Inizialmente, erano "progettati" per essere utilizzati in costose chiamate remote, dove probabilmente avresti bisogno di molti dati da diverse parti della tua logica; I DTO effettuerebbero il trasferimento di dati in una singola chiamata.

Secondo l'autore, i DTO non erano destinati ad essere utilizzati in ambienti locali, ma alcune persone hanno trovato un uso per loro. Di solito vengono utilizzati per raccogliere informazioni da diversi POCO in un'unica entità per GUI, API o livelli diversi.

Ora, con l'ereditarietà, il riutilizzo del codice è più simile a un effetto collaterale dell'eredità piuttosto che al suo obiettivo principale; la composizione, d'altra parte, è implementata con il riutilizzo del codice come obiettivo principale.

Alcune persone raccomandano l'uso della composizione e dell'eredità insieme, usando i punti di forza di entrambi e cercando di mitigare i loro punti deboli. Quanto segue fa parte del mio processo mentale quando scelgo o creo nuovi DTO, o qualsiasi nuova classe / oggetto per quella materia:

  • Uso l'ereditarietà con DTO all'interno dello stesso layer o stesso contesto. Un DTO non erediterà mai da un POCO, un DTO BLL non erediterà mai da un DTO DAL, ecc.
  • Se mi trovo a cercare di nascondere un campo a un DTO, rifoterò e magari userò la composizione.
  • Se sono sufficienti pochissimi campi diversi da un DTO di base, li inserirò in un DTO universale. I DTO universali vengono utilizzati solo internamente.
  • Un POCO / DTO di base non sarà quasi mai usato per nessuna logica, in questo modo la base risponde solo ai bisogni dei suoi figli. Se mai dovessi usare la base, evito di aggiungere nuovi campi che i suoi figli non useranno mai.

Alcuni di loro forse non sono le "migliori" pratiche, funzionano abbastanza bene per i progetti a cui sto lavorando, ma è necessario ricordare che nessuna dimensione si adatta a tutti. Nel caso del DTO universale dovresti stare attento, le firme dei miei metodi si presentano così:

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

Se uno qualsiasi dei metodi ha mai bisogno del proprio DTO, faccio l'ereditarietà e di solito l'unica modifica che devo fare è il parametro, anche se a volte ho bisogno di scavare più a fondo per casi specifici.

Dai tuoi commenti ho capito che stai usando DTO nidificati. Se i tuoi DTO nidificati consistono solo in un elenco di altri DTO, penso che la cosa migliore da fare sia scartare l'elenco.

A seconda della quantità di dati che è necessario visualizzare o utilizzare, potrebbe essere una buona idea creare nuovi DTO che limitano i dati; ad esempio, se il tuo UserDTO ha molti campi e hai bisogno solo di 1 o 2, potrebbe essere meglio avere un DTO con solo quei campi. Definire il livello, il contesto, l'utilizzo e l'utilità di un DTO aiuterà molto nella progettazione.


Non ho potuto aggiungere più di 2 collegamenti nella mia risposta, ecco alcune ulteriori informazioni sui DTO locali. Sono consapevole che si tratta di informazioni molto vecchie, ma penso che alcune di esse possano essere ancora pertinenti.
IvanGrasp,

1

L'uso della composizione nei DTO è una pratica perfettamente valida.

L'ereditarietà tra tipi concreti di DTO è una cattiva pratica.

Per prima cosa, in un linguaggio come C #, una proprietà implementata automaticamente ha pochissime spese generali di manutenibilità, quindi duplicarle (io detesto veramente la duplicazione) non è così dannosa come spesso accade.

Un motivo per non usare l'eredità concreta tra i DTO è che alcuni strumenti li mapperanno felicemente su tipi sbagliati.

Ad esempio, se si utilizza un'utilità di database come Dapper (non lo consiglio ma è popolare) che esegue l' class name -> table nameinferenza, si potrebbe facilmente finire per salvare un tipo derivato come tipo di base concreto da qualche parte nella gerarchia e quindi perdere dati o peggio .

Un motivo più profondo per non utilizzare l'ereditarietà tra i DTO è che non dovrebbe essere usato per condividere implementazioni tra tipi che non hanno una relazione "is a" ovvia. A mio avviso, TaskDetailnon sembra un sottotipo di Task. Potrebbe essere facilmente una proprietà di un Tasko, peggio, potrebbe essere un supertipo di Task.

Ora, una cosa di cui potresti essere preoccupato è mantenere la coerenza tra i nomi e i tipi delle proprietà dei vari DTO correlati.

Mentre l'ereditarietà di tipi concreti contribuirebbe, di conseguenza, a garantire tale coerenza, è molto meglio usare interfacce (o pure classi di base virtuali in C ++) per mantenere quel tipo di coerenza.

Considera i seguenti DTO

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
}
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.