Welcome to DTO hell

Am intalnit in unele proiecte urmatorul scenariu. E un DTO de genul

class User
{
    int Id;
    string FirstName;
    string LastName
}

caci asta e nevoie in ecranul 1.
In ecranul 2, e nevoie in schimb de

class User
{
    int Id;
    string FullName;
    string Email;
}

In consecinta, exista doar un singur DTO care contine toate campurile:

class User 
{
  int Id;
  string FirstName;
  strign LastName;
  string FullName;
  string Email;
}

Which drives me nuts. Pentru ca in acest mod in cod apar locuri unde se returneaza obiecte despre care nu stii niciodata daca au sau nu anumite campuri cu informatie, trebuie sa verifici implementarea (I shouldn’t!) care construieste instanta respectiva si ii umple campurile.

Care e abordarea voastra?
Ce am scris eu cu mana mea, am incercat sa separ in DTOs mici (in exemplul de mai sus ar fi User1 si User2)
Nu discut nevoia de a avea sau nu DTOs, it is what it is.

1 Like

Am câteva observații:

  1. În primul rând, a venit de mult vremea să trecem peste câmpuri / proprietăți separate pentru first name și last name. “Falsehoods Programmers Believe About Names”. Dar cel mai probabil în acest punct al proiectului nu ai control asupra acestui aspect. Știu cum e.

  2. Nu da niciodată nume de clase User1, User2, etc. Folosește nume descriptive, din care să îți dai ușor seama care e rolul unui obiect. Chit că le spui UserWithFirstLastName și UserWithFullname, orice e mai bine decât User1 și User2. Recomand episodul 2 din seria video Clean Code, sau capitolele corespunzătoare din carte.

  3. Îți recomand să nu folosești DTOs direct în “ecrane”. Folosește obiectele respective numai pentru data transfer, iar în restul aplicației folosești clase separate, care fac wrap la acele DTOs. În exemplul tău, presupunând că păstrezi un singur DTO cu toate proprietățile, și pe care îl redenumești în UserDTO:

public class User {

    private UserDTO dto;

    public User(UserDTO dto) {
        this.dto = dto;
    }

    public String getFullName() {
    }

    public String getFirstName() {
    }

    public String getLastName() {
    }

}

Iar apoi în “ecrane” folosești doar clasa User. În felul ăsta poți muta logica de verificare a proprietăților DTO-ului într-un singur loc, și nu o împrăștii prin toată aplicația. Și nici nu e treaba “ecranelor” să știe că tu folosești pattern-ul DTO.

1 Like

Ce intelegi prin DTO si ce rol are?

Daca prin DTO , intelegi un entity, nu cred ca e OK sa scrii cod in entities pentru a servi nevoilor din view (ecrane).

Thank you!
Intersante observatii

  1. Buna observatie, a) da, nu am control si b) poate uneori depinde de tipul de soft sau piata, poate chiar e nevoie de FirstName si LastName

  2. User1 User2 erau doar exemple :slightly_smiling:

  3. Inteleg, dar cred ca asta e alta discutie.

DTOs au rolul de a salva și transfera date elementare (numere, stringuri, și tipuri compozite ale acestora) valide de la un subsistem la altul.

1 Like

No man, DTO != Entity.

A, păi stai, nu am înțeles întrebarea atunci. Eu ziceam că abordarea mea ar fi să nu las view-urile / “ecranele” să știe de existența DTO-urilor, ci le-aș face wrap într-o clasă unde poți pune acea logică de verificare a implementării de care spuneai. Poți să clarifici întrebarea te rog?

@Catalin_Banu: Aici pe scurt, și aici pe larg despre DTO.

Cand am spus de verificarea implementarii, ma refeream efectiv la a inspecta logic codul care returneaza instanta DTO-ului ca sa vezi ce campuri sunt setate :slightly_smiling:

Eu prefer să fac o validare preliminară a datelor în DTO, de exemplu dacă email are format valid, pentru că folosesc acel DTO și pentru input, și un DTO e mai aproape de user feedback, deci nu trebuie să pun la muncă alte subsisteme - de exemplu baza de date, ca să verific dacă e-mail există deja.

Iar în view în aplicații web prefer tot DTO, nu trag modelul după mine în view.

Entity, sau Model cum mai e cunoscut, reprezinta conceptul la nivelul intregii aplicatii / intregului domeniu daca aderam la DDD. Poate fi, si se recomanda a fi, un obiect compozit, format din substructuri, pentru flexibilitate si minimizarea accesorilor.

DTO este o reprezentare a entitatii, de regula cu amprenta mai mica dpdv al informatiilor, cu scop precis determinat, de a muta un set precis de date la un alt layer al aplicatiei. Un dto poate fi flat, nu e nevoie de eleganta organizarii datelor in clase, pt ca el trebuie cat mai repede impachetat-despachetat.

Dpdv al intrebarii punctuale din thread, eu as face doi impachetatori-despachetatori si un dto gras dpdv al membrilor sai.

5 Likes

interesant, si va fi continuat
https://www.ibuildings.nl/blog/2016/02/programming-guidelines-part-3-the-life-and-death-objects

la final sunt consideratii bune legate de rolul unui dto

1 Like

Validarea tipului de date și validarea sintactică (dacă aplicabilă) unde preferi să o faci?

Pentru mine, când preiau inputul de la utilizator, curat este să verific tipul de date în DTO, și dacă am o validare mai complexă (dar doar sintactică, gen adresă de e-mail sau url), să instanțiez value objects și să le salvez în DTO.

Raționamentul meu e că business logic va avea oricum nevoie de acel value object.

În același timp, mai apare și o consecință frumoasă în model: mă limitez la business logic, nu mai am nevoie de validări obositoare, și codul de business e mai expresiv.

Altfel spus, un DTO are și rolul important de validare (parțială), nu mă lasă să transfer bullshit total dintr-un layer într-altul.

Alt exemplu de value obiect salvat în DTO este adresa. Fiecare țară are particularitățile sale, us sau uk sunt destul de diferite de alte țări.

Din ce știu eu, un DTO e un dumb object care nu trebuie să conțină nici un fel de logică și al cărui unic rol este acela de a transfera informația de la un sistem la altul. Cel mai elocvent exemplu este o structură JSON, care nu poate conține decât informații.

Ceea ce descrii tu este de fapt un Model care definește proprietățile, comportamentul și validitatea unei Entity. Modelul ține de logica aplicației, în timp ce DTO-ul definește un contract referitor la informațiile disponibile printr-un endpoint. În cele din urmă, aplicația stabilește validitatea datelor conținute de un DTO și ce reprezintă acestea în contextul său.

Vorbind pe exemplele tale, un DTO poate fi un obiect returnat de un sistem OAuth ce conține email-ul și numele utilizatorului. Nu este treaba DTO-ului să stabilească validitatea email-ului pentru că, deși formatul poate fi valid, logica aplicației poate prevedea că nu poate fi acceptat un email de pe domeniul Yahoo. Dacă tu consideri că ambele validări își au locul în DTO, atunci părerea mea e că faci o confuzie.

Mai pe scurt, eu sunt de părere că DTO-ul este obiectul ce conține datele pe care le dai ca parametri de intrare unui model pentru a crea o entitate. Cam așa ceva:

$addressEntity = AddressModel::create($addressDTO);

Ca să fac și o metaforă, respectând principiul Separation of Concerns, nu este treaba plicului să verifice validitatea documentelor pe care le conține. :smiley:

5 Likes

Nu inteleg firul discutiei, dar fiind ultimul reply dupa Flavius, sa inteleg ca i-ai raspuns lui Flavius, nu intrebarii mele?

Nu mi se pare cea mai buna idee sa folosesti un DTO gras care sa fie populat in doua moduri diferit, adica sa nu aiba toate proprietatile setate. Nu poti stii cine/cand il mai foloseste si se asteapta sa contina toate datele.

Vad cateva variante:

  1. DTO gras cu toate proprietatile, iar metodele care-l returneaza sa-i populeze toate proprietatile. Chiar daca se vor folosi doar anumite proprietati la un moment dat, as lasa aceasta alegere pentru cine foloseste datele, nu pentru cine le genereaza.

  2. DTO-uri diferite si atunci e clara treaba. S-ar putea chiar folosi “inheritance” daca exista proprietati comune, nu zice nimeni ca orice DTO trebuie sa fie flat.

  3. Separarea fluxurilor read/write, asta implicand automat mai multe DTO-uri (separate pentru read si write). CQRS (Command Query Responsibility Segregation) foloseste aceasta abordare si mi se pare unul dintre cele mai puternice “pattern”-uri.

1 Like

@octavian_mirica yep, ultimile doua. Corelat cu ceva gen AutoMapper. Partea nasoala: cod mai mult de scris si de intretinut, caci unele sunt DTO-uri cu aceleasi campuri. Dar e for a good cause.
Prima optiune e buna daca nu vrei sa faci mult refactoring, metodele ajuta, dar tot cam acelasi ghiveci e.
Anyway, thanks, cel mai bun raspuns.