Usa composizione ed ereditarietà per DTO

9

Abbiamo un'API Web ASP.NET che fornisce un'API REST per la nostra applicazione per pagina singola. Utilizziamo DTO / POCO per trasmettere i dati tramite questa API.

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

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

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

    public string Name { get; set; }
}

Altri DTO usano 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), sono stati estesi in modo incrementale da alcuni campi nel tempo. E a causa dell'ereditarietà e della composizione anche altri DTO sono diventati più grandi.

La mia domanda ora è 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?

    
posta officer 17.07.2017 - 16:33
fonte

4 risposte

7

Come best practice, prova a rendere i tuoi DTO il più concisi possibile. Restituisci solo ciò di cui hai bisogno per tornare. Usa solo ciò di cui hai bisogno. Se questo significa qualche DTO in più, così sia.

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

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

Inoltre, a volte si può aver bisogno di ereditarietà / composizione. Diciamo che uno ha un lavoro. Un lavoro ha più compiti. 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.

    
risposta data 17.07.2017 - 17:23
fonte
1

A meno che il tuo sistema non sia basato rigorosamente sulle operazioni CRUD, i tuoi DTO sono troppo granulari. Prova a creare endpoint che incarnino processi aziendali o artefatti. Questo approccio si adatta perfettamente a un livello di business logic e al "domain-driven design" di Eric Evans.

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

    
risposta data 17.07.2017 - 17:25
fonte
1

È difficile stabilire le migliori pratiche per qualcosa di "flessibile" o astratto come un DTO. Essenzialmente, i DTO sono solo oggetti per il trasferimento dei dati ma, a seconda della destinazione o del motivo del trasferimento, potresti voler applicare diverse "best practice".

Raccomando di leggere Modelli di Enterprise Application Architecture di Martin Fowler . C'è un intero capitolo dedicato ai pattern, in cui le DTO hanno una sezione molto dettagliata.

Originariamente, erano "progettati" per essere usati in costose chiamate remote, dove probabilmente avresti bisogno di molti dati provenienti da parti diverse della tua logica; Le DTO effettuano il trasferimento di dati in un'unica chiamata.

Secondo l'autore, le DTO non erano pensate per essere utilizzate in ambienti locali, ma alcune persone ne hanno trovato un uso. Solitamente 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 le loro debolezze. 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 livello o dello stesso contesto. Un DTO non erediterà mai da un POCO, un DTO BLL non erediterà mai un DTO DAL, ecc.
  • Se mi trovo a cercare di nascondere un campo da un DTO, mi rifatterò e forse userò la composizione.
  • Se pochissimi campi diversi da un DTO di base sono tutto ciò di cui ho bisogno, li inserirò in un DTO universale. I DTO universali sono utilizzati solo internamente.
  • Un POCO / DTO di base non verrà quasi mai utilizzato per nessuna logica, in questo modo la base risponde solo alle esigenze dei suoi figli. Se dovessi mai usare la base, evito di aggiungere nuovi campi che i bambini non useranno mai.

Alcuni di loro forse non sono le migliori pratiche, funzionano piuttosto bene per i progetti su cui ho lavorato, ma è necessario ricordare che nessuna taglia si adatta a tutti. Nel caso del DTO universale dovresti stare attento, i miei metodi firme hanno questo aspetto:

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

Se uno qualsiasi dei metodi ha mai bisogno del proprio DTO, faccio ereditarietà e di solito l'unico cambiamento che devo apportare è 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 annidati. 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 devi mostrare o di lavorare, potrebbe essere una buona idea creare nuovi DTO che limitino i dati; per esempio, se il tuo UserDTO ha molti campi e hai solo bisogno 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 sarà di grande aiuto durante la progettazione.

    
risposta data 18.07.2017 - 17:11
fonte
0

L'utilizzo della composizione in DTO è una pratica perfetta.

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

Per prima cosa, in un linguaggio come C #, una proprietà auto-implementata ha un overhead di manutenzione molto piccolo, quindi duplicarli (ho davvero aborrito la duplicazione) non è così dannosa come spesso lo è.

Un motivo per cui non per usare l'ereditarietà concreta tra le DTO è che certi strumenti li mapperanno felicemente ai tipi sbagliati.

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

Un motivo più profondo per non usare l'ereditarietà tra le DTO è che non dovrebbe essere usato per condividere implementazioni tra tipi che non hanno una relazione ovvia "è una". A mio parere, TaskDetail non sembra un sottotipo di Task . Potrebbe facilmente essere una proprietà di Task o, peggio, potrebbe essere un supertipo di Task .

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

Mentre l'ereditarietà dei tipi concreti contribuirebbe, di conseguenza, a garantire tale coerenza, è molto meglio utilizzare le interfacce (o classi di base virtuali pure 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
}
    
risposta data 19.07.2017 - 19:18
fonte

Leggi altre domande sui tag