Obiekty typu DTO -> DAO - oddzielne klasy + mapowanie vs dziedziczenie

0

Cześć,
tworząc np. web api tworzy się oddzielnie obiekty typu DTO oraz Entity (DAO). Następnie mapuje się jedne do drugich.
Wydaje mi się jednak, że w większości przypadków obiekty typu DTO w zasadzie są podzbiorem Entity.

Zastanawiam się więc czy w takim razie zamiast tworzyć dwa oddzielne typy obiektów nie lepiej:

  1. Zrobić obiekt typu DTO a następnie dziedziczyć po nim do DAO np.
    class PersonDto{...}
    class PersonDao : PersonDto {...}

  2. Zrobić klasę bazową - wspólną dla obu obiektów i dziedziczyć po niej w obu typach obiektach i mapować różnice np.
    class PersonBase{...}
    class PersonDto : PersonBase{...}
    class PersonDao : PersonBase{...}

Jakie waszym zdaniem są wady obu podanych przeze mnie rozwiązań? Czy dziedziczenie po DTO może wpłynąć jakoś negatywnie na pracę / wydajność EF lub JsonSerializer?
Czy spotkaliście się z takim podejściem w praktyce?

2

Ja bym rekomendowal tak nie robic.

To sie moze sprawdzic tylko w najprostszych przypadkach (czyli tam gdzie to zawsze jest rzeczywiscie 1:1, a w takich napisanie mapera to tez nie jest wielki problem), ale nawet pod katem zaleznosci pomiedzy "warstwami" i architektury to wyglada nie halo. Jak na potrzeby wyswietlenia na froncie (zakladam przypadek ze to DTO jest na potrzeby zwrocenia z API na front) bedziesz potrzebowal dorzucic dodatkowe pole wyliaczne na podstawie innych danych biznesowych ktore juz masz to ma ono z automatu trafiac do DAO?

4

Nie idź tą drogą i nie wiąż tych modeli, bo szybko się okaże, że będziesz potrzebował zaraz czegoś na froncie, co nie bedzie potrzebne w bazie i masz klina bez wyjścia.

0

Hey,
dzięki za odpowiedzi. Odnośnie pytania co jeśli na froncie będę potrzebował pola, którego nie powinno być w bazie danych to rozwiązaniem jest druga podana przeze mnie opcja tj. w klasie bazowej mam wszystkie części wspólne, które powinny być na froncie i w bazie a w obiektach DTO i DAO mam tylko różnice.

4

Po to się stosuje DTO żeby kompletnie ukryć encję i odseparować je od wpływu świata zewnętrznego. Wprowadzają dziedziczenie rezygnujesz z benefitów które ta sepraracja zapewnia i wprowadzasz tylko dodatkową złożoność. Równie dobrze możesz kompletnie zrezygnować z DTO i pchać encje na zewnątrz, i wyjdzie na to samo przy mniejszej złożoności.

3
Kofcio napisał(a):

Cześć,
tworząc np. web api tworzy się oddzielnie obiekty typu DTO oraz Entity (DAO). Następnie mapuje się jedne do drugich.
Wydaje mi się jednak, że w większości przypadków obiekty typu DTO w zasadzie są podzbiorem Entity.

Zastanawiam się więc czy w takim razie zamiast tworzyć dwa oddzielne typy obiektów nie lepiej:

  1. Zrobić obiekt typu DTO a następnie dziedziczyć po nim do DAO np.
    class PersonDto{...}
    class PersonDao : PersonDto {...}

  2. Zrobić klasę bazową - wspólną dla obu obiektów i dziedziczyć po niej w obu typach obiektach i mapować różnice np.
    class PersonBase{...}
    class PersonDto : PersonBase{...}
    class PersonDao : PersonBase{...}

Jakie waszym zdaniem są wady obu podanych przeze mnie rozwiązań? Czy dziedziczenie po DTO może wpłynąć jakoś negatywnie na pracę / wydajność EF lub JsonSerializer?
Czy spotkaliście się z takim podejściem w praktyce?

Trochę gubisz cały sens.

DAO są po to żeby nie dodawać logiki związanej z persystencją do logiki biznesowej.

DTO są po to żeby nie zaśmiecać logiki biznesowej elementami transportowymi/widokiem.

Dla przykładu. Masz w logice biznesowej obiekt Book który ma cenę Money price. W bazie chcesz ją zapisać jako int, a wyświetlić z dwoma miejscami po przecinku. Mógłbyś zrobić wszystko w logice biznesowej, ale to the byłoby słabe. Dlatego zmianę Money na int pod bazę robisz w DAO, a zamianę na cenę z dwoma miejscami po przecinku wsadzasz w DTO.

Więc teraz jak mówisz że zauważasz pewne elementy wspólne, i dałoby się obejść bez DTO i bez DAO - masz rację. Nawet nie musiałbyś odziedziczyć nic , Mógłbyś po prostu całkowicie usunąć DTO i DAO i wszytko trzymać w logice biznesowej. Mógłbyś to zrobić ofc.

Tylko wtedy nie masz Dependency Inversion, i ogólnie Twój kod jest gorszej jakości.

Także:

  • jeśli nie rozumiesz po co DTO/DAO jest , to faktycznie to się wydaje bez sensu i można by to usunąć
  • ale jeśli chcesz osiągnąć DI i wiesz po co to jest, to wtedy to co porównujesz żeby je współdzielić nie ma kompletnie sensu.
2
Kofcio napisał(a):

dzięki za odpowiedzi. Odnośnie pytania co jeśli na froncie będę potrzebował pola, którego nie powinno być w bazie danych to rozwiązaniem jest druga podana przeze mnie opcja tj. w klasie bazowej mam wszystkie części wspólne, które powinny być na froncie i w bazie a w obiektach DTO i DAO mam tylko różnice.

To jest słabe rozwiązanie.

Co jak będziesz chciał zmienić typ pola z int na double w widoku, ale nie w bazie?

0

Dziękuję wszystkim za komentarze i dyskusję.

neves napisał(a):

Po to się stosuje DTO żeby kompletnie ukryć encję i odseparować je od wpływu świata zewnętrznego. Wprowadzają dziedziczenie rezygnujesz z benefitów które ta sepraracja zapewnia i wprowadzasz tylko dodatkową złożoność. Równie dobrze możesz kompletnie zrezygnować z DTO i pchać encje na zewnątrz, i wyjdzie na to samo przy mniejszej złożoności.

W jakim kontekście piszesz o złożoności? Czy chodzi Ci o to, że dziedziczenie coś "kosztuje" czy że projekt jest bardziej złożony?

Riddle napisał(a):

To jest słabe rozwiązanie.

Co jak będziesz chciał zmienić typ pola z int na double w widoku, ale nie w bazie?

To wtedy usuwam tą zmienną z base i w DAO dodaję zmienną int a w DTO dodaję double. Ale szczerze to nie kojarzę potrzeby zmiany double na int - szczególnie w obiektach typu DTO.

Nie mam dużego doświadczenia w pisaniu API etc. ale wydaje mi się, że ogólną koncepcję rozdzielenia DTO i DAO jako tako rozumiem. Natomiast nie jestem pewny czy dobrze zobrazowałem cel mojego działania i korzyści.
Nie ważne jakie rozwiązanie przyjmę to i tak muszę stworzyć jakiś mechanizm, który zmieni mi DTO -> DAO oraz DAO -> DTO.
Poza drobnymi wyjątkami praktycznie zawsze oba obiekty są prawie identyczne (różnice są niewielki i głównie sprowadzają się do wyboru właściwości DAO, które mają być umieszczone w DTO).

Do mapowania tych pól są różnego rodzaju narzędzia typu automappery etc., ale pytanie jest po co kopiować jeden typ do drugiego, jeśli można zrobić tak jak pokazałem poniżej (przykład z podaną książką, którą zasugerował @Riddle):
Kod może nie jest idealny, ale chodzi o pokazanie idei. Nie ma tu automappera, martwimy się tylko o różnice między DTO a DAO - cała reszta się sama kopiuje.
W dalszym ciągu mam dwie klasy (DTO i DAO), ale część wspólna należy do obu klas. Bo jaki jest sens, aby to rozdzielać?
Dodatkowo każdą z tych klas mogę mieć w oddzielnym folderze / projekcie / bibliotece etc.
Gdy klasy są małe lub różnic między DTO a DAO jest dużo to korzyści są mniejsze, ale dla dużych klas, gdzie część wspólna jest duża to takie rozwiązanie wydaje mi się całkiem spoko - przynajmniej moim zdaniem.

BookDAO bookDAO = new BookDAO();
bookDAO.Author = "Mickiewicz";
bookDAO.Title = "Dziady";
bookDAO.Price = 12345; //int
bookDAO.IsBook = true; //jakaś inna zmienna

//DAO -> DTO
BookDTO bookDTO = bookDAO.GetDTO(); //tworzenie nowego obiektu i kopiowanie części wspólnej
bookDTO.ReleaseDate = DateOnly.FromDateTime(DateTime.Now); //jakaś zmienna


//DTO -> DAO
BookDAO bookDAO2 = bookDTO.GetDAO(); //tworzenie nowego obiektu i kopiowanie części wspólnej
bookDAO2.Price = 321;
bookDAO2.IsBook = false;


public record BookBase
	{
	//część wspólna dla DAO i DTO
	public string Author { get; set; }
	public string Title { get; set; }
	}

public record BookDAO : BookBase
	{
	public BookDAO() { }
	public BookDAO(BookBase book_base) : base(book_base) { }
	
	//właściwości typowe dla DAO
	public int Price { get; set; }
	public bool IsBook { get; set; }
	
public BookDTO GetDTO()
		{
		var tmp = new BookDTO(this);
		tmp.Price = Price / 100.00;
        return tmp;
		}
	}

public record BookDTO : BookBase
	{
	public BookDTO() { }
	public BookDTO(BookBase book_base) : base(book_base) { }
	
	//właściwości typowe dla DTO:
	public double Price { get; set; }
	public DateOnly ReleaseDate { get; set; }
    public BookDAO GetDAO()
        {
        var tmp = new BookDAO(this);
        tmp.Price = (int)(Price * 100.00);
        return tmp;
        }
    }
0
Kofcio napisał(a):

Nie ważne jakie rozwiązanie przyjmę to i tak muszę stworzyć jakiś mechanizm, który zmieni mi DTO -> DAO oraz DAO -> DTO.

No właśnie nie.

Jak nie chcesz osiągnąć dependency inversion to nie musisz mieć ani DTO ani DAO. Możesz wszystko "upchać" do jednego wora.

Kofcio napisał(a):

Poza drobnymi wyjątkami praktycznie zawsze oba obiekty są prawie identyczne (różnice są niewielki i głównie sprowadzają się do wyboru właściwości DAO, które mają być umieszczone w DTO).

To często oznacza że ich autor nie do końca zrozumiał po co one są.

Do mapowania tych pól są różnego rodzaju narzędzia typu automappery etc., ale pytanie jest po co kopiować jeden typ do drugiego, jeśli można zrobić tak jak pokazałem poniżej (przykład z podaną książką, którą zasugerował @Riddle):

No jeśli są takie same to nie ma sensu, masz rację.

Ale one nie powinny być takie same.

Te obiekty DTO i DAO, w sposób w jaki o nich mówisz - one nie mają w sobie żadnej wartości same w sobie. Ich użyteczność zaczyna mieć znaczenie jak chcesz osiągnąć Dependency Inversion. Bez tego są bez sensu i można się ich pozbyć całkowicie.

0

Ale szczerze to nie kojarzę potrzeby zmiany double na int - szczególnie w obiektach typu DTO.

Ale ktoś, kto jest odpowiedzialny za wymagania w Twoim projekcie może stwierdzić że taka potrzeba jest. Praktycznie wszystko może (powinno móc) się zmienić, to jest słaby argument. A jeżeli przez zbyt ciasne powiązania między warstwami jest to niemożliwe lub utrudnione - jest skopany design.

0
dargenn napisał(a):

Ale szczerze to nie kojarzę potrzeby zmiany double na int - szczególnie w obiektach typu DTO.

Ale ktoś, kto jest odpowiedzialny za wymagania w Twoim projekcie może stwierdzić że taka potrzeba jest. Praktycznie wszystko może (powinno móc) się zmienić, to jest słaby argument. A jeżeli przez zbyt ciasne powiązania między warstwami jest to niemożliwe lub utrudnione - jest skopany design.

No tak, ale pokazałem, że moje rozwiązanie również umożliwia takie wygibasy - nawet jak jest to bez sensu.
Chodzi właśnie o to, że według mnie podane przeze mnie podejście jest łatwiejsze do takich rzeczy, bo koncentruję się tylko na rzeczach specyficznych dla danego obiektu a część wspólną olewam.

0
Kofcio napisał(a):

Ale ktoś, kto jest odpowiedzialny za wymagania w Twoim projekcie może stwierdzić że taka potrzeba jest. Praktycznie wszystko może (powinno móc) się zmienić, to jest słaby argument. A jeżeli przez zbyt ciasne powiązania między warstwami jest to niemożliwe lub utrudnione - jest skopany design.

No tak, ale pokazałem, że moje rozwiązanie również umożliwia takie wygibasy - nawet jak jest to bez sensu.
Chodzi właśnie o to, że według mnie podane przeze mnie podejście jest łatwiejsze do takich rzeczy, bo koncentruję się tylko na rzeczach specyficznych dla danego obiektu a część wspólną olewam.

Tylko że to nadal nie ma sensu.

Albo chcesz odseparować widok od logiki albo nie. Jeśli chcesz je odseparować, to nie możesz mieć żadnych części wspólnych. A jeśli nie chcesz, to równie dobrze możesz wywalić DTO/DAO całkiem i trzymać wszystko w jednym obiekcie.

0

A DAO nie jest od tego, że trzyma funkcje, które uderzają bezpośrednio do bazy? Np. IList< Book> GetBooksById()? U mnie w projekcie tak jest. Korzystamy z NHibernate.

0
Riddle napisał(a):
Kofcio napisał(a):

Ale ktoś, kto jest odpowiedzialny za wymagania w Twoim projekcie może stwierdzić że taka potrzeba jest. Praktycznie wszystko może (powinno móc) się zmienić, to jest słaby argument. A jeżeli przez zbyt ciasne powiązania między warstwami jest to niemożliwe lub utrudnione - jest skopany design.

No tak, ale pokazałem, że moje rozwiązanie również umożliwia takie wygibasy - nawet jak jest to bez sensu.
Chodzi właśnie o to, że według mnie podane przeze mnie podejście jest łatwiejsze do takich rzeczy, bo koncentruję się tylko na rzeczach specyficznych dla danego obiektu a część wspólną olewam.

Tylko że to nadal nie ma sensu.

Albo chcesz odseparować widok od logiki albo nie. Jeśli chcesz je odseparować, to nie możesz mieć żadnych części wspólnych. A jeśli nie chcesz, to równie dobrze możesz wywalić DTO/DAO całkiem i trzymać wszystko w jednym obiekcie.

Tylko pytanie czy celem DTO faktycznie jest odseparowanie widoku od logiki czy może przekazanie między frontem a backend-em tylko tych danych, które są faktycznie istotne (potrzebne) po drugiej stronie.

Trzymanie wszystkiego w jednym obiekcie nie ma sensu bo:

  1. nie chcę przekazywać wszystkiego na front (nie ma sensu wysyłać wszystkiego do klienta -> nadmiarowość, wpływ )
  2. niektóre dane chcę przekazać w zmienionej formie (niektóre dane są np. poufne)

Ale jeśli część danych jest wspólna to jaki jest sens robienia dwóch całkowicie niezależnych obiektów i mapować każdą właściwość 1:1 jak można je po prostu przypisać do siebie (bez żadnego automappera etc.)? Nie widzę w tym żądnych korzyści.

Na pewno brakuje mi doświadczenia, ale jeśli masz dwie klasy: UserDTO i UserDAO i obie klasy mają właściwość typu UserName i nic w niej nie zmieniamy przed wysłaniem na front to po co mi niezależny obiekt do tej właściwości jeśli to jest to samo UserName?
A jeśli mam coś co faktycznie wymaga zmiany to robię to niezależnie w obiekcie DTO i oddzielnie w DAO.

Przynajmniej ja to tak widzę.

2
Szadek95 napisał(a):

A DAO nie jest od tego, że trzyma funkcje, które uderzają bezpośrednio do bazy? Np. IList< Book> GetBooksById()? U mnie w projekcie tak jest. Korzystamy z NHibernate.

Tak, pod warunkiem że Book nie jest obiektem bazo-danowym, tylko czystą encją biznesową.

PS: Może tylko nie "trzyma". To nie jest tak że DAO ma być workiem na funkcje, których nie można wsadzić nigdzie. To powinna być szczelna abstrakcja na persystencję.

Kofcio napisał(a):

Tylko pytanie czy celem DTO faktycznie jest odseparowanie widoku od logiki czy może przekazanie między frontem a backend-em tylko tych danych, które są faktycznie istotne (potrzebne) po drugiej stronie.

No tak, to jest ich jedyny cel.

Kofcio napisał(a):

Trzymanie wszystkiego w jednym obiekcie nie ma sensu bo:

  1. nie chcę przekazywać wszystkiego na front (nie ma sensu wysyłać wszystkiego do klienta -> nadmiarowość, wpływ )
  2. niektóre dane chcę przekazać w zmienionej formie (niektóre dane są np. poufne)

Przecież możesz to wszystko zrobić w encjach biznesowych (jeśli tak jak mówisz, nie chcesz odseparować tych warstw od siebie).

Jest taki cytat Abrahama Lincolna: "Ile nóg ma owca, jeśli nazwiemy jej ogon nogą?". Odpowiedź brzmi 4, bo nazwanie ogona nogą, nie sprawi że będzie np. mogła na nim biegać.

Możesz sobie nazwać Twój obiekt DTO, ale to nie znaczy że on na prawdę będzie funkcjonował jako DTO.

Kofcio napisał(a):

Ale jeśli część danych jest wspólna to jaki jest sens robienia dwóch całkowicie niezależnych obiektów i mapować każdą właściwość 1:1 jak można je po prostu przypisać do siebie (bez żadnego automappera etc.)? Nie widzę w tym żądnych korzyści.

Korzyść to Dependency Inversion, mówiłem Ci już.

Jeśli nie interesuje Cię Dependency Inversion, to to nie ma sensu - ale również posiadanie DTO i DAO wtedy nie ma sensu.

0

Ok, dziękuję wszystkim za dyskusję (szczególnie @Riddle) :)

0

a po co to komu a na co. dawniej pisało sie caly program w jednym pliku a dzis jakies projekty solucje dao srao. programowanie nie jest juz takie fajne jak dawniej. zostało zrujnowane przez wzorce i zasady, a praca dla kogos w firmie w ogole nie jest tak przyjemna jak pisanie projektu dla siebie byle jak, wazne by dzialal, bo ciagle ktos odrzuca twoj kod w review az w koncu dajesz wypowiedzenie szuksz nowej pracy, usuwasz dwie najstarsze pozycje z doswiadczenia z cv i historia sie powtarza, udając co roku coraz to młodszą osobę

0
Kofcio napisał(a):

Trzymanie wszystkiego w jednym obiekcie nie ma sensu bo:

  1. nie chcę przekazywać wszystkiego na front (nie ma sensu wysyłać wszystkiego do klienta -> nadmiarowość, wpływ )
  2. niektóre dane chcę przekazać w zmienionej formie (niektóre dane są np. poufne)

Przecież możesz to wszystko zrobić w encjach biznesowych (jeśli tak jak mówisz, nie chcesz odseparować tych warstw od siebie).

Jest taki cytat Abrahama Lincolna: "Ile nóg ma owca, jeśli nazwiemy jej ogon nogą?". Odpowiedź brzmi 4, bo nazwanie ogona nogą, nie sprawi że będzie np. mogła na nim biegać.

Możesz sobie nazwać Twój obiekt DTO, ale to nie znaczy że on na prawdę będzie funkcjonował jako DTO.

Jaka jest oficjalna definicja wzorca projektowego 'encja biznesowa', w jakiej książce został on opublikowany, kto jest jego autorem?

Riddle napisał(a):
Szadek95 napisał(a):

A DAO nie jest od tego, że trzyma funkcje, które uderzają bezpośrednio do bazy? Np. IList< Book> GetBooksById()? U mnie w projekcie tak jest. Korzystamy z NHibernate.

Tak, pod warunkiem że Book nie jest obiektem bazo-danowym, tylko czystą encją biznesową.

PS: Może tylko nie "trzyma". To nie jest tak że DAO ma być workiem na funkcje, których nie można wsadzić nigdzie. To powinna być szczelna abstrakcja na persystencję.

Dlatego masz fetchAllProperties w NH i konfigurowalny maper, abyś nie musiał pisać selectów i mapować wszystkiego na inny obiekt jak "encje biznesowe".
Może problem rozwiąże nazwanie tego obiektu Transaction Script?

0
Jaro26 napisał(a):

Jaka jest oficjalna definicja wzorca projektowego 'encja biznesowa', w jakiej książce został on opublikowany, kto jest jego autorem?

Tak na prawdę nie ma jednej definicji. Początkowa pewnie wzięła się z książki Erica Evansa, "Domain Driven Design", i z tego co pamiętam miała pewne konkretne definicje; ale ja w moim poście użyłem określenia "encja biznesowa" bardziej luźno. W moich poprzednich postach miałem na myśli element logiki programu odrębny od widoku i persystencji. Można też to określić "essential complexity", w odróżnieniu od "accidental complexity". W skrócie - po prostu część programu którą warto odseparować od innych elementów programów, i faktycznie - nie musi to koniecznie wyczerpać definicji "encji biznesowej".

Riddle napisał(a):

Dlatego masz fetchAllProperties w NH i konfigurowalny maper, abyś nie musiał pisać selectów i mapować wszystkiego na inny obiekt jak "encje biznesowe".
Może problem rozwiąże nazwanie tego obiektu Transaction Script?

Mówimy co czymś innym.

To o czym mówisz, dotyczy w bardzo konkretnym stopniu persystencji - sposobu na zapisanie stanu systemu w jakiś sposób (np. w bazie). Żeby dało się taki stan zapisać musisz podjąć pewne kroki (wymyślić ideę id, transakcji, selectów, etc.). To są elementy konieczne do działania aplikacji; ale tak na prawdę nie są niezbędne z punktu widzenia użytkownika (bo użytkownik nie wie czy jego aplikacja działa na bazie sql', mongo, na plikach, w cache'u, etc.). Tak długo jak wymagania są spełnione (bezpieczeństwo, prędkośc, etc.) tak długo taki wybór nie ma znaczenia. To miałem na myśli.

1 użytkowników online, w tym zalogowanych: 0, gości: 1