Testy jednostkowe klasy dziedziczącej po klasie bazowej

0

Hej, takie pytanie mi się nasunęło. Mam jakąś ABSTRAKCYJNĄ klasę bazową Item. Po niej dziedziczą inne klasy: A, B, C, D, E itd.

I teraz chciałbym zrobić testy jednostkowe klasy A. Czy powinienem zawrzeć tutaj również testy dla klasy bazowej i to samo zrobić w reszcie klas, czy jakoś inaczej. Jak takie rzeczy testujecie u siebie?

1
  1. W jaki sposób chcesz napisać testy do klasy abstrakcyjnej bez zabawy w sondę analną?
  2. Przerobiłbym hierarchię klas na kompozycję i napisał testy zgodne z tym, jakie każda z klas ma zachowanie.
0

Ad. 1. Nie wiem. Dlatego pytam.
Ad. 2. Nie chcę tego robić, bo mam już dziedziczenie ogarnięte. Ja wiem, że są fanatycy, którzy krzyczą, że kompozycja uber alles, ale bez przesady :)

Więc tak to ogarnąć poprawnie bez zmiany dziedziczenia na kompozycję?

1
Juhas napisał(a):

Ad. 1. Nie wiem. Dlatego pytam.
Ad. 2. Nie chcę tego robić, bo mam już dziedziczenie ogarnięte. Ja wiem, że są fanatycy, którzy krzyczą, że kompozycja uber alles, ale bez przesady :)

Więc tak to ogarnąć poprawnie bez zmiany dziedziczenia na kompozycję?

Ad. 2. Czyli jedynym argumentem za tym żeby zostawić bad design (dziedziczenie), jest to że już to zrobiłeś? Outstanding move

Jedynym sposobem wtedy jest w testach zrobić kolejną implementację klasy abstrakcyjnej, która nie będzie miała żadnej dodatkowej logiki. Będzie służyła tylko do tego żeby w testach zainstancjonować i przetestować. Jeśli natomiast ta Twoja klasa abstrakcyjna udostępnia logikę przez metody protected albo pola protected, no to takiego ograniczenia (które sam sobie stworzyłeś) bez sond analnych (jak pokazał @Patryk27) się nie obejdzie.

PS: Albo ewentualnie z klasy abstrakcyjnej wydzielić zachowanie do innej klasy (kompozycja) i tą nową wydzieloną klasę przetestować. Wtedy właściwie Twoja klasa abstrakcyjna mogłaby się stać interfejsem (chyba że ma pola i trzyma stan).

0

Dlaczego uważasz, że podstawowe założenie programowania obiektowego to bad design?

0

W uproszczeniu:
Kiedy dziedziczysz A -> B -> C -> ...
W klasie B masz wspólne zachowanie klasy A.
W klasie C masz wspólne zachowanie z klasy A i klasy B.

Niekoniecznie, ale możliwe, że będziesz musiał napisać testy dla:
w A (testy dla A)
w B (testy dla A oraz testy dla B)
w C (testy dla A, testy dla B oraz testy dla C)
itd...

Kompozycją elminujesz powtarzające się testy. Wszystko zależy od konkretnego przypadku.
Jeśli zachowanie B zależy od zachowania A to wtedy musisz testować wszystko od początku, itd.
Jest to silne wiązanie którego warto unikać, może że są ku temu przesłanki.

0

Ja wiem, że dziedziczenie może przeszkadzać w testach. Ale nadal nie uważam tego jako bad design. No, chyba że się tego nadużywa. Tak jak ze wszystkim.

0
Juhas napisał(a):

Ja wiem, że dziedziczenie może przeszkadzać w testach. Ale nadal nie uważam tego jako bad design. No, chyba że się tego nadużywa. Tak jak ze wszystkim.

Wady dziedziczenia/Zalety kompozycji:

  • Mniejszy coupling klas
  • Można użyć interfejsów: co oznacza możliwość wielu implementacji; zmiany/ustalenia zachowanie w runtime'ie zamiast compile-time, etc.
  • Łatwiejsze testowanie
  • Łatwiejsze mockowanie
  • Łatwiej użyć ponownie już istniejącej logki
  • Łatwiej zmienić kompozycję na np. kontenery DI jak Spring
  • Jest pierdyliard wzorców projektowych, które wykorzystują kompozycję. Wzorców z dziedziczeniem nie znam żadnych
  • Zmiana interfejsu klasy bazowej nie oznacza zmiany interfejsu klasy dziedziczącej
  • Dziedziczyć możesz tylko z jednej klasy, z kompozycją możesz mieć tyle zależności ile chcesz

Zalety dziedziczenia/Wady kompozycji:

  • ?

Chyba już znasz powód.

1
Juhas napisał(a):

Hej, takie pytanie mi się nasunęło. Mam jakąś ABSTRAKCYJNĄ klasę bazową Item. Po niej dziedziczą inne klasy: A, B, C, D, E itd.

I teraz chciałbym zrobić testy jednostkowe klasy A. Czy powinienem zawrzeć tutaj również testy dla klasy bazowej i to samo zrobić w reszcie klas, czy jakoś inaczej. Jak takie rzeczy testujecie u siebie?

A co to są testy klasy bazowej? Powinieneś testować API klas A, B, C i tak dalej, to czy one z czegoś dziedziczą nie ma żadnego znaczenia.

0
somekind napisał(a):
Juhas napisał(a):

Hej, takie pytanie mi się nasunęło. Mam jakąś ABSTRAKCYJNĄ klasę bazową Item. Po niej dziedziczą inne klasy: A, B, C, D, E itd.

I teraz chciałbym zrobić testy jednostkowe klasy A. Czy powinienem zawrzeć tutaj również testy dla klasy bazowej i to samo zrobić w reszcie klas, czy jakoś inaczej. Jak takie rzeczy testujecie u siebie?

A co to są testy klasy bazowej? Powinieneś testować API klas A, B, C i tak dalej, to czy one z czegoś dziedziczą nie ma żadnego znaczenia.

Niby tak, ale wtedy albo miałby zduplikowane testy, albo musiałby mieć jeden test który testuje np A+Bazową i pozostałe B i C.

4
TomRiddle napisał(a):

Wady dziedziczenia/Zalety kompozycji:

  • Mniejszy coupling klas

Nieprawda - coupling jest inny, ale nie znaczy to, że mniejszy.

  • Można użyć interfejsów: co oznacza możliwość wielu implementacji; zmiany/ustalenia zachowanie w runtime'ie zamiast compile-time, etc.

Czyli błędy zobaczymy w runtime zamiast compile-time.

  • Łatwiejsze testowanie
  • Łatwiejsze mockowanie
  • Łatwiej użyć ponownie już istniejącej logki

To zależy.

  • Łatwiej zmienić kompozycję na np. kontenery DI jak Spring

Bez związku.

  • Jest pierdyliard wzorców projektowych, które wykorzystują kompozycję. Wzorców z dziedziczeniem nie znam żadnych

W takim razie masz duże braki w wiedzy na temat wzorców.

TomRiddle napisał(a):

Niby tak, ale wtedy albo miałby zduplikowane testy, albo musiałby mieć jeden test który testuje np A+Bazową i pozostałe B i C.

Co to jest testowanie klasy bazowej? W klasie bazowej nie ma niczego do testowania, bo nawet nie ma publicznych metod, które można byłoby wywołać.
A jeśli są, to taka klasa nie powinna być bazowa, a więc jest klasą z końca hierarchii i wymaga własnych testów.

0
TomRiddle napisał(a):

Zalety dziedziczenia/Wady kompozycji:

  • ?

Zalety dziedziczenia - można zaiplementować Elipsę, która jest (dziedziczy) z Koła, które jest (dziedziczy) z Punktu i patrzeć jak ludziom się mózgi skręcają.
Okraśmy to podstawą programowania imperatywnego czyli x = x + 1 i bez problemu niszczymy każdego, kto przykłada jakąś wagę do matematyki i logiki.
Broń doskonała.

0
somekind napisał(a):
TomRiddle napisał(a):

Wady dziedziczenia/Zalety kompozycji:

  • Mniejszy coupling klas

Nieprawda - coupling jest inny, ale nie znaczy to, że mniejszy.

Yyy... interfejs klasy bazowej = 1:1 interfejs klasy dziecka? Jakakolwiek zmiana interfejsu klasy bazowej = zmieniony interfejs klasy dziecka.

PS1: Dodatkowo, może powinienem zaznaczyć że miałem na myśli: kompozycję + dependency inversion. Jeśli dostajemy zależność skądś - wtedy coupling'u w zasadzie nie ma. Możemy dostać różne implementacje, mocki, klasy dziedziczące z zależności, etc.

PS2: Jak dziedziczysz ze swojej klasy to jeszcze pół biedy, gorzej jak wprowadzasz dziedziczenie z jakiejś klasy z biblioteki. Wtedy nie można bezpiecznie zupdate'ować wersji przez starch że może API się zmieni i każde użycie klasy dziecka też trzeba będzie potencjalnie update'ować.

PS3: Jak w klasie dziecka nadpiszesz metodę i dodasz @Override, a ktoś w klasie bazowej zmieni tą metodę; to teraz już jest compile-error bo @Override jest na metodzie która nie jest override'owana.

  • Można użyć interfejsów: co oznacza możliwość wielu implementacji; zmiany/ustalenia zachowanie w runtime'ie zamiast compile-time, etc.

Czyli błędy zobaczymy w runtime zamiast compile-time.

Konkretnie jaki błąd zobaczyłbyś w compile-time, korzystając z dziedziczenia, którego nie zobaczysz korzystając z kompozycji?

  • Łatwiejsze testowanie
  • Łatwiejsze mockowanie
  • Łatwiej użyć ponownie już istniejącej logki

To zależy.

Z argumentem "To zależy" faktycznie ciężko się kłócić :D

  • Łatwiej zmienić kompozycję na np. kontenery DI jak Spring

Bez związku.

  • Jak można by wziąć klasę bazową z kontenera?
  • Jaki sens w ogóle, jest brać klasę bazową z kontenera?
  • Jest pierdyliard wzorców projektowych, które wykorzystują kompozycję. Wzorców z dziedziczeniem nie znam żadnych

W takim razie masz duże braki w wiedzy na temat wzorców.

Wymień, proszę kilka, których nie znam (takie które stosują dziedziczenie)?

PS1: Haha, czułem że ktoś się odniesie do tego że napisałem "nie znam" :D

TomRiddle napisał(a):

Niby tak, ale wtedy albo miałby zduplikowane testy, albo musiałby mieć jeden test który testuje np A+Bazową i pozostałe B i C.

Co to jest testowanie klasy bazowej? W klasie bazowej nie ma niczego do testowania, bo nawet nie ma publicznych metod, które można byłoby wywołać.
A jeśli są, to taka klasa nie powinna być bazowa, a więc jest klasą z końca hierarchii i wymaga własnych testów.

Po pierwsze, skąd pomysł że nie ma metod publicznych?

Po drugie, nawet jeśli nie ma publiców - to ma logikę która powinna być przetestowana (np w metodach protected, private i blokach inicjalizacyjnych i metodach statycznych). Jeśli natomiast nie ma logiki, to powinna być zamieniona na interfejs (więc już nie ma dziedziczenia). Jeśli w takiej bazowej byłby tylko pola, to to byłby szczegół implementacyjny którego nie ma sensu przywiązywać do klasy bazowej.

0

Może zaimplementuj test abstrakcyjny i z niego zrób dziedziczenie dla innych testów ;-)

0
yarel napisał(a):

Może zaimplementuj test abstrakcyjny i z niego zrób dziedziczenie dla innych testów ;-)

Uruchamianie tych samych testów wiele razy? Po co?

PS: Chyba że to żart. W takim razie: "haha".

2

Jeśli już licytujemy się co jest większym rakiem to ja zaproponuję technikę miotania lambdami.

Mamy taki kod:

class KlasaWejściowa {
  coś metoda(coś) {
    klasa1.zróbCoś(parametr -> akcja(parametr));
  }

  String akcja(String coś) {
    return coś.toUpperCase();
  }
}

class Klasa1 {
  coś zróbCoś(Function<String, String> akcja) {
    metodka(akcja, 5);
  }

  coś metodka(Function<String, String> akcja, int coś) {
    metodka2(akcja, coś + 8);
  }

  coś metodka2(Function<String, String> akcja, int coś) {
    klasa2.zróbCoś(akcja, coś / 2);
  }
}

class Klasa2 {
  coś zróbCoś(Function<String, String> akcja, int coś) {
    // hmm, co zrobić z argumentem `akcja`? jak sprawdzić co to robi? dla jakich argumentów użyć tej funkcji?
    // mam tylko typ Function<String, String> - z tego wynika tylko tyle, że produkuje Stringa ze Stringa i tyle
    // nawet nie jestem pewien jakie to może mieć efekty uboczne
    // nie ma też opisowych nazw parametrów czy metod. jest tylko Function.apply(t)
    // skąd mam wiedzieć jaki kontrakt ma parametr `akcja`? dokumentować go w każdej metodzie?
  }
}

Wracając do tematu:

  1. skomplikowane hierarchie dziedziczenia są złe, bo są skomplikowane.
  2. jak ocenić nieskomplikowane hierarchie dziedziczenia? skoro klepiemy w OOP to chyba nie po to, by unikać OOP, no nie?

Jeśli już mam taki przypadek, że mam klasę abstrakcyjną do przetestowania, która nie jest na tyle skomplikowana by ją refaktorować (tzn byłoby to refaktorowanie na siłę, bardziej dla zasady niż z potrzeby) to tworzę jej pełną implementację testową (w testach of koz). Jest to moim zdaniem znacznie lepsze niż stosowanie całego arsenału dziwacznych metod mockowania na potrzeby testowania klasy abstrakcyjnej bądź wielokrotnego duplikowania testów, które dodatkowo testują dwa rodzaje funkcjonalności naraz (te z klasy bazowej i te z dziedziczącej).

Podstawą do życia jest to, by kod był zrozumiały, dobrze pokryty testami i by dało się go efektywnie modyfikować (by zaimplementować nowe funkcjonalności, usunąć stare lub zmienić działanie obecnych). Jeśli n.p. zrobimy całą aplikację bez użycia rozszerzania klas i pokryjemy ją testami w 90%+, ale byle jaka zmiana w kodzie produkcyjnym będzie wymagać 10x większych zmian w testach to niestety nasza architektura jest słaba.

4
TomRiddle napisał(a):

Uruchamianie tych samych testów wiele razy? Po co?

Żeby przetestować w ten sam sposób inną klasę. Przecież to nic dziwnego. Jeśli mam np. jakiś interfejs do persystencji to podstawowe operacje będą się zachowywać tak samo, niezależnie od tego, czy interfejs jest zaimplementowany jako in-memory albo SQL.

0
Michał Sikora napisał(a):
TomRiddle napisał(a):

Uruchamianie tych samych testów wiele razy? Po co?

Żeby przetestować w ten sam sposób inną klasę. Przecież to nic dziwnego. Jeśli mam np. jakiś interfejs do persystencji to podstawowe operacje będą się zachowywać tak samo, niezależnie od tego, czy interfejs jest zaimplementowany jako in-memory albo SQL.

@yarel Mówił o uruchamianiu tych samych testów, przy okazji testowania różnych dzieci klasy bazowej. W jego przykładzie klasa bazowa jest przywiązana do klas dziedziczących, więc o testowaniu różbych implementacji tego samego interfejsu nie ma mowy.


W Twoim przykładzie faktycznie ma to sens, ale to też nie jest do końca co miałem na myśli. Kiedy pisałem

TomRiddle napisał(a):

Uruchamianie tych samych testów wiele razy? Po co?

Miałem na myśli to, że odpalanie tych samych testów na tym samym kodzie (np 3x na in-memory) nie ma sensu. Jasne, że jest sens odpalać te same testy na różnych implementacjach tego samego interfejsu.

1
TomRiddle napisał(a):

Yyy... interfejs klasy bazowej = 1:1 interfejs klasy dziecka? Jakakolwiek zmiana interfejsu klasy bazowej = zmieniony interfejs klasy dziecka.

Nie ma czegoś takiego jak zmiana interfejsu klasy bazowej, bo poprawnie napisana klasa bazowa nie ma swojego interfejsu. (Kontrakt dla dzieci, czyli wymuszenie, które metody mają dzieci zaimplementować to nie jest interfejs.)

PS1: Dodatkowo, może powinienem zaznaczyć że miałem na myśli: kompozycję + dependency inversion. Jeśli dostajemy zależność skądś - wtedy coupling'u w zasadzie nie ma. Możemy dostać różne implementacje, mocki, klasy dziedziczące z zależności, etc.

Ale co z tego? Klasy dziedziczące nadal można wstrzykiwać i komponować, dodawać rózne implementacje i mocki.

PS2: Jak dziedziczysz ze swojej klasy to jeszcze pół biedy, gorzej jak wprowadzasz dziedziczenie z jakiejś klasy z biblioteki. Wtedy nie można bezpiecznie zupdate'ować wersji przez starch że może API się zmieni i każde użycie klasy dziecka też trzeba będzie potencjalnie update'ować.

Pełna zgoda.
Dlatego właśnie powinno się plombować klasy w publicznych bibliotekach, żeby odebrać ludziom możliwość zepsucia swojego kodu.

Z argumentem "To zależy" faktycznie ciężko się kłócić :D

Podobnie jak z argumentem "łatwiejsze".
Te rzeczy po prostu w ogóle nie mają związku ze sobą.

  • Jak można by wziąć klasę bazową z kontenera?
  • Jaki sens w ogóle, jest brać klasę bazową z kontenera?

No właśnie - jaki?

Sorry, ale mam wrażenie, że Ty nie rozumiesz do czego służy dziedziczenie i jak je stosować, a swoje wywody konstruujesz w oparciu o założenie, że dziedziczenie wyklucza kompozycję. Tymczasem obie te rzeczy są stosowane w różnych sytuacjach.

Wymień, proszę kilka, których nie znam (takie które stosują dziedziczenie)?

Metoda szablonowa, fabryki.

Po pierwsze, skąd pomysł że nie ma metod publicznych?

Bo jeśli ma, to znaczy, że autor nie rozumie dziedziczenia i w efekcie zamiast klasy bazowej stworzył jakiegoś frankensteina.

Po drugie, nawet jeśli nie ma publiców - to ma logikę która powinna być przetestowana (np w metodach protected, private i blokach inicjalizacyjnych i metodach statycznych). Jeśli natomiast nie ma logiki, to powinna być zamieniona na interfejs (więc już nie ma dziedziczenia). Jeśli w takiej bazowej byłby tylko pola, to to byłby szczegół implementacyjny którego nie ma sensu przywiązywać do klasy bazowej.

No i ta logika zostanie przetestowana podczas testów klas dziedziczących.

0
somekind napisał(a):
TomRiddle napisał(a):

Yyy... interfejs klasy bazowej = 1:1 interfejs klasy dziecka? Jakakolwiek zmiana interfejsu klasy bazowej = zmieniony interfejs klasy dziecka.

Nie ma czegoś takiego jak zmiana interfejsu klasy bazowej, bo poprawnie napisana klasa bazowa nie ma swojego interfejsu. (Kontrakt dla dzieci, czyli wymuszenie, które metody mają dzieci zaimplementować to nie jest interfejs.)

Trochę jest, bo kiedy piszę class Human extends Animal to mówię że Human JEST Animalem. W przypadku kompozycji takiej relacji nie ma.

Ale tak, zgadzam się że metody abstrakcyjne w klasie abstrakcyjnej to nie interfejs.

No ok, jeśli umówimy się że klasy abstrakcyjne nie mają żadnych metod i pól publicznych, to faktycznie interfejs bazowej != 1:1 klasy dziecka. W takim razie klasy bazowe mogą dostarczyć swoim dzieciom to:

  • metody protected
  • trzymanie stanu
  • "widzenie" wszystkich implementacji klasy bazowej (aka Liskov Substitution Principle) - np. każdy Human to Animal.

Pierwszy i drugi punkt może dostarczyć bez problemu kompozycja, trzeci (ja przynajmniej) załatwiłbym interfejsem, żeby mogły być też Animale zupełnie inne - takie które nie mają nic wspólnego z Human.

PS1: Dodatkowo, może powinienem zaznaczyć że miałem na myśli: kompozycję + dependency inversion. Jeśli dostajemy zależność skądś - wtedy coupling'u w zasadzie nie ma. Możemy dostać różne implementacje, mocki, klasy dziedziczące z zależności, etc.

Ale co z tego? Klasy dziedziczące nadal można wstrzykiwać i komponować, dodawać rózne implementacje i mocki.

Nie wukluczają się. Ale po co (wśród dodatkowych mocków/implementacji) korzystać z dziedziczenia? Czemu tej logiki która jest w klasie bazowej też nie wstrzyknąć?

Z argumentem "To zależy" faktycznie ciężko się kłócić :D

Podobnie jak z argumentem "łatwiejsze".
Te rzeczy po prostu w ogóle nie mają związku ze sobą.

Wydawało mi się że wyraziłem się wystarczająco jasno, chętnie edytuję moją odpowiedź i umieszę powody dla których uważam że te 3 rzeczy robi się łatwiej mając do dyspozycji klasę która preferuje kompozycję.

  • Jak można by wziąć klasę bazową z kontenera?
  • Jaki sens w ogóle, jest brać klasę bazową z kontenera?

No właśnie - jaki?

Odpowiedziałem na Twój argument że "injectowanie klasy bazowej z kontenera jest bez związku". Chciałem powiedzieć że dobrze byłoby, że jeśli ktoś korzysta z kontenerów zależności, żeby mógł z tego kontenera wyciągać którą implementację chcę. Jeśli mamy klasę class A extends Baza, i chce korzystać z logiki A ale Baza chce zamienić na Baza2, cóż nie ma szczęścia.

Oczywiście przyjdzie ktoś kto powie że logikę z A można wydzielić i wtedy sobie zdziedziczyć z Baza2, ale po co to robić skoro można po prostu nie dziedziczyć, nie przywiązywać jednych klas do innych i ta da.

Sorry, ale mam wrażenie, że Ty nie rozumiesz do czego służy dziedziczenie i jak je stosować, a swoje wywody konstruujesz w oparciu o założenie, że dziedziczenie wyklucza kompozycję. Tymczasem obie te rzeczy są stosowane w różnych sytuacjach.

Nie sądzę, żeby się wykluczały.

Możliwe, że faktycznie nie wiem do czego Ty używasz dziedziczenia/jakie zachowanie mu przypisujesz; ale rozumiem co oferują/jakie mają konsekwencje. I na podstawie tego zajmuje stanowisko, że 99.99% (jeśli nie 100%) można załatwić kompozycją i będzie to bardziej robust, reliable.

Wymień, proszę kilka, których nie znam (takie które stosują dziedziczenie)?

Metoda szablonowa, fabryki.

Ale metoda szablonowa nie mówi nic o tym czy szczegóły implementacyjne mają być dostarczane przez klasy-dzieci czy przez wstrzykniętą implementację interfejsu. Nawet bym powiedział że to drugie jest (niespodzianka) bardziej elastyczne, bo wtedy dodatkowa implementacja może dziedziczyć z czegoś innego. Mówi o tym chociażby to źródło: https://fillumina.wordpress.com/2012/03/09/comparison-between-composition-and-template-pattern/

Po drugie, nawet jeśli nie ma publiców - to ma logikę która powinna być przetestowana (np w metodach protected, private i blokach inicjalizacyjnych i metodach statycznych). Jeśli natomiast nie ma logiki, to powinna być zamieniona na interfejs (więc już nie ma dziedziczenia). Jeśli w takiej bazowej byłby tylko pola, to to byłby szczegół implementacyjny którego nie ma sensu przywiązywać do klasy bazowej.

No i ta logika zostanie przetestowana podczas testów klas dziedziczących.

W odpowiedzi, wracam do mojej poprzedniej odpowiedzi:

TomRiddle napisał(a):
somekind napisał(a):

A co to są testy klasy bazowej? Powinieneś testować API klas A, B, C i tak dalej, to czy one z czegoś dziedziczą nie ma żadnego znaczenia.

Niby tak, ale wtedy albo miałby zduplikowane testy, albo musiałby mieć jeden test który testuje np A+Bazową i pozostałe B i C.

0

@TomRiddle: mam wrażenie, że naprawdę nie ogarniasz dziedziczenia. Mam też pewne zastrzeżenia do @somekind, który, z tego co zrozumiałem, mówi że bazowa klasa abstrakcyjna nie powinna mieć publicznych metod.

Prosty przykład:

public abstract class DbItem
{
  public Guid Id {get; set;}
  string name;
  public string Name
  {
    get { return name; }
    set { if(name != value) name = value;}
  }
  public override int GetHashCode(); //pomijam implementację
  //pomijam przeciążone operatory
  //pomijam equals

  public DbItem DeepClone()
  {
    DbItem result = MemberwiseClone() as DbItem;
    result.name = string.Copy(name);

    return result;
  }
}

Nie powiecie mi, że to jest nieprawidłowa abstrakcyjna klasa bazowa. Tutaj np. do testowania jest DeepClone.

0
Juhas napisał(a):

Nie powiecie mi, że to jest nieprawidłowa abstrakcyjna klasa bazowa. Tutaj np. do testowania jest DeepClone.

Powiem, i to conajmniej z 3ech powodów:

  • Klasa bazowa dokłada pola id i name klasy dziecku (jak również ich propertisy get i set), mimo że dziecko może ich nie chcieć/nie potrzebować
  • W językach w których jest pojedyncze dziedziczenie (np. Java) blokujesz możliwość dziedziczenia z innych klas (są takie głupie frameworki/libki w których jedyny sposób na skorzystanie z API to dziedziczenie).
  • Powód który już wymieniałem: Dziedzicząc z takiej klasy przywiązujesz implementację klonowania do klasy dziecka. Nie można jej podmienić, korzystać z żadnych cudów OOP jak np. Decorator

I do Twoim przykładu oczywiście tyczą sie wszystkie elementy które wymieniłem w drugiej odpowiedzi.

0

Jak na razie wymieniłem bardzo dużo powodów dla których ktoś miałby wybrać kompozycję, ale nie poznałem żadnego czemu ktoś miałby preferować dziedziczenie?

1

@TomRiddle: w tym momencie mam już pewność, że nie za bardzo ogarniasz dziedziczenie. Jeśli klasa bazowa ma jakieś pole, to KAŻDA KLASA dziedzicząca po niej też musi mieć to pole. Jeśli z jakiegoś powodu go nie potrzebuje, to świadczy o złej architekturze i błędach. To samo tyczy się Twojego drugiego punktu o frameworkach. Jeśli potrzebuję stworzyć kompletnie nowy super przycisk, to dziedziczę go z klasy button (albo control) i tyle.

Jeśli potrzebuję, żeby wszystkie klasy w moim modelu były zapisywane w bazie danych, to wszystkie dziedziczą po DbItem, który to umożliwia (dając chociażby pole Id).

0
Juhas napisał(a):

@TomRiddle: w tym momencie mam już pewność, że nie za bardzo ogarniasz dziedziczenie. Jeśli klasa bazowa ma jakieś pole, to KAŻDA KLASA dziedzicząca po niej też musi mieć to pole.

Jestem świadom jak działa dziedziczenie :) Pola i metody z bazowej są wtedy częścią dziecka, i dodatkowo dziecko dostaje przywiązaną relację is-a rodzic. Z każdego z tych 3ech powodów dziedziczenie ssie.

Napisałem, że trochę głupio że Twoja nowa logika w klasie abstrakcyjnej wymusza te dwa pola (przez dziedziczenie).

Jeśli z jakiegoś powodu go nie potrzebuje, to świadczy o złej architekturze i błędach. To samo tyczy się Twojego drugiego punktu o frameworkach. Jeśli potrzebuję stworzyć kompletnie nowy super przycisk, to dziedziczę go z klasy button (albo control) i tyle.

No to lepiej żebyś nie pisał w Javie, bo możesz dziedziczyć tylko z jednej klasy :D

Masz kod:

class BazowaZJakimsClonem {}
class Dziecko extends BazowaZJakimsClonem {} // konkretnie jak chcesz tutaj dodać Button/Control?

Natomiast gdybyś chciał dodać BazowaZJakimsClonem extends Button, to nagle wszystkie inne klasy które chciały się klonować stają się Buttonem.

Jeśli potrzebuję, żeby wszystkie klasy w moim modelu były zapisywane w bazie danych, to wszystkie dziedziczą po DbItem, który to umożliwia (dając chociażby pole Id).

No ok, i? To ma być argument za dziedziczeniem? To nic nie wnosi.

Z modelami niech będzie że dziedziczenie jest okej, bo na pewno do modelu nikt nie dołoży żadnej logiki, więc ok. Dziedziczenie w modelach z bazy danych jest ok. W innych przypadkach ma wszystkie wady o których mówiłem w poprzednich odpowiedziach.

0
TomRiddle napisał(a):

No to lepiej żebyś nie pisał w Javie, bo możesz dziedziczyć tylko z jednej klasy :D

Piszę aktualnie w C++ i C#. Wcześniej w Delphi i trochę w Javie. Tylko w jednym z tych języków można dziedziczyć z więcej niż jednej klasy i jakoś nie jest to problem nigdzie indziej.
Czy dziedziczenie z dwóch klas jest złe? A z trzech? A z pięciu? Jak zwykle - to zależy. ZAZWYCZAJ jeśli dziedziczysz z więcej niż jednej klasy, to prawdopodobnie masz złą architekturę. ZAZWYCZAJ.

Masz kod:

class BazowaZJakimsClonem {}
class Dziecko extends BazowaZJakimsClonem {} // konkretnie jak chcesz tutaj dodać Button/Control?

Ale po co? Jeśli chcę zrobić kontrolkę, to moją klasą bazową jest klasa bazowa dla kontrolek. Jeśli potrzebuję dodatkowego zachowania, to wtedy używam kompozycji albo interfejsów. Ale prawdopodobnie w 99% przypadków samo dziedziczenie z bazowej klasy dla kontrolek będzie wystarczające. W 99% jeśli tworząc kontrolkę, musisz dziedziczyć jeszcze z czegoś innego, to robisz coś źle.

Jeśli potrzebuję, żeby wszystkie klasy w moim modelu były zapisywane w bazie danych, to wszystkie dziedziczą po DbItem, który to umożliwia (dając chociażby pole Id).

No ok, i? To ma być argument za dziedziczeniem? To nic nie wnosi.

Nie, argument za dziedziczeniem jest taki, że jest to PODSTAWA programowania obiektowego i bardzo dużo ułatwia. Ułatwia też trzymanie reguły DRY.

Z modelami niech będzie że dziedziczenie jest okej, bo na pewno do modelu nikt nie dołoży żadnej logiki, więc ok. Dziedziczenie w modelach z bazy danych jest ok. W innych przypadkach ma wszystkie wady o których mówiłem w poprzednich odpowiedziach.

Tu wychodzi trochę brak wiedzy i doświadczenia w temacie architektury oprogramowania. Model bez logiki to tzw. model anemiczny. Dla niektórych jest to antywzorzec. Chyba, że to DTO, ale DTO raczej nie potrzebuje dziedziczenia.

1
TomRiddle napisał(a):

Trochę jest, bo kiedy piszę class Human extends Animal to mówię że Human JEST Animalem. W przypadku kompozycji takiej relacji nie ma.

To jest właśnie klasyczny akademicki przykład błędnego dziedziczenia i błędnego myślenia o dziedziczeniu. Dziedziczenie nie powinno być używane do tworzenia kategorii obiektów lecz do organizacji kodu.

Czemu tej logiki która jest w klasie bazowej też nie wstrzyknąć?

Ależ przecież można ją wstrzyknąć i używać w klasie bazowej. Ja nie widzę problemu, dopóki ma to sens.

Odpowiedziałem na Twój argument że "injectowanie klasy bazowej z kontenera jest bez związku".

Jest bez związku, bo klas bazowych się nie wstrzykuje, z nich się po prostu dziedziczy.
To co Ty robisz, to próba udowodnienia, że samochód jest lepszy od roweru, bo do roweru nie ma gdzie nalać paliwa. A to przecież oczywiste, bo rower nie potrzebuje paliwa.

Chciałem powiedzieć że dobrze byłoby, że jeśli ktoś korzysta z kontenerów zależności, żeby mógł z tego kontenera wyciągać którą implementację chcę. Jeśli mamy klasę class A extends Baza, i chce korzystać z logiki A ale Baza chce zamienić na Baza2, cóż nie ma szczęścia.

O ile brak projektu to brak szczęścia. ;) Klasa bazowa ma sens w celu uniknięcia duplikacji pewnych szczegółów implementacji, a nie w celu budowy kodu dającego się podmienić w locie.

Ale metoda szablonowa nie mówi nic o tym czy szczegóły implementacyjne mają być dostarczane przez klasy-dzieci czy przez wstrzykniętą implementację interfejsu.

W definicji podanej w Twoim linku masz istnienie klas bazowych i dziedziczenia: It consists of leaving some abstract methods in a base class to be implemented in derived specialized classes.
Ten wzorzec jest oparty o dziedziczenie i tyle. To, czy jakieś operacje zostaną oddelegowane, czy będą fizycznie w klasie bazowej to kolejny szczegół implementacyjny.

W odpowiedzi, wracam do mojej poprzedniej odpowiedzi:

Ale jakie to ma znaczenie?
Masz klasy: A, B i C, które mają jakieś publiczne metody. Metody te mają być przetestowane bez znaczenia, czy pod spodem jest jakiś kod, który się wykona wiele razy, czy nie, czy mają wspólną klasę bazową, czy nie, czy mają jakieś zależności, czy nie.
Testuje się to, co kod robi, a nie jego strukturę.

Juhas napisał(a):

Nie powiecie mi, że to jest nieprawidłowa abstrakcyjna klasa bazowa. Tutaj np. do testowania jest DeepClone.

No jest nieprawidłowa - publiczne DeepClone nie ma sensu, bo nie można go wywołać na obiekcie klasy, w której się znajduje.A skoro nie da się jej użyć, to nie ma też po co jej testować.
Taka metoda ma sens tylko jeśli jest protected i używana przez klasy potomne.

TomRiddle napisał(a):

Jak na razie wymieniłem bardzo dużo powodów dla których ktoś miałby wybrać kompozycję, ale nie poznałem żadnego czemu ktoś miałby preferować dziedziczenie?

Ja po prostu nie uznaję czegoś takiego jak "preferuję kompozycję" czy "preferuję dziedziczenie". Kompozycja ma swoje zastosowania, dziedziczenie swoje. Jeśli ktoś coś "preferuje" i zastępuje prawidłowy kod tym "preferowanym", to najprawdopodobniej efekty są słabe.
Jeśli Ty w ogóle nie używasz dziedziczenia, to jest spore prawdopodobieństwo, że są sytuacje, w których piszesz kod bardziej skomplikowany niż to konieczne, albo nie używasz sensownych wzorców.
Kompozycja nie jest darmowa - trzeba jakoś tworzyć i/lub przekazywać obiekty. Jest dodatkowy kod do utrzymania, bardziej rozbudowane konstruktory, itd. W przypadku kilku linijek kodu operujących na stanie obiektu to często może być gra nie warta świeczki. Wszystko zależy od sytuacji.

0

@Juhas: @somekind

Na razie nie usłyszałem jeszcze żadnego argumentu który powiedziałby w czym dziedziczenie jest lepsze od kompozycji. Jedyne co widzę to:

  • Kontr argumenty czemu dziedziczeniem da się zrobić wszystko to samo co kompozycją (których może 25% ma sens)
  • Nie merytoryczne argumenty, typu właśnie "dziedziczenie to podstawa", "nie umiesz z niego skorzystać", "ten przykład to zły przykład dziedziczenia"
0
somekind napisał(a):

Ależ przecież można ją wstrzyknąć i używać w klasie bazowej. Ja nie widzę problemu, dopóki ma to sens.

Wstrzyknąć klasę której konkretna klasa jest ustalone w compile-time'ie? Niby jak?

Klasa bazowa ma sens w celu uniknięcia duplikacji pewnych szczegółów implementacji, a nie w celu budowy kodu dającego się podmienić w locie.

Klasa bazowa (dziedziczenie) ma sens w celu uniknięcia duplikacji.
Klasa delegująca (kopozycja) ma sens w celu uniknięcia duplikacji.

Czemu miałbym używać dziedziczenia? Bo ciągle nie wiem?

Ale metoda szablonowa nie mówi nic o tym czy szczegóły implementacyjne mają być dostarczane przez klasy-dzieci czy przez wstrzykniętą implementację interfejsu.

W definicji podanej w Twoim linku masz istnienie klas bazowych i dziedziczenia: It consists of leaving some abstract methods in a base class to be implemented in derived specialized classes.
Ten wzorzec jest oparty o dziedziczenie i tyle. To, czy jakieś operacje zostaną oddelegowane, czy będą fizycznie w klasie bazowej to kolejny szczegół implementacyjny.

We wzorcu chodzi o to że jakiś algorytm (czy to w klasie abstrakcyjnej czy zwykłej) wymaga szczegółów implementacyjnych dostarczanych:

  • albo przez implementację metod abstrakcyjnych (jeśli jest to rozwiązane przez dziedziczenie)
  • albo poprzez wstrzyknięcie odpowiedniego interfejsu implementującego te metody (jeśli jest to rozwiązane przez kompozycję).

Zauważcie, że drugie rozwiązanie ma wszystkie słodkie cechy OOP: Interface segregation, Open/Close, Polimorfizm, z tej wstrzykniętej implementacji można zrobić decorator, kompozyt, adapter, mniam oop.

Z na sztywno wpisaną klasą bazową można... nic.

PS: To ma jeszcze dodatkową wadę, bo jeśli zrobisz sobie klasę dziedziczącą-bazową (w języku z jedno-dziedziczeniem), i potem będziesz chciał w niej skorzystać ze zwalonej libki która udostępnia tylko API poprzez dziedziczenie - cóż (znów), nie masz szczęścia :>

TomRiddle napisał(a):

Jak na razie wymieniłem bardzo dużo powodów dla których ktoś miałby wybrać kompozycję, ale nie poznałem żadnego czemu ktoś miałby preferować dziedziczenie?

Ja po prostu nie uznaję czegoś takiego jak "preferuję kompozycję" czy "preferuję dziedziczenie". Kompozycja ma swoje zastosowania, dziedziczenie swoje. Jeśli ktoś coś "preferuje" i zastępuje prawidłowy kod tym "preferowanym", to najprawdopodobniej efekty są słabe.

Zgadzam się co do preferowania.

Kompozycja ma swoje zastosowania - zgadzam się, wymieniam je od kilku godzin.
Dziedziczenie ma swoje zastosowania - pytam znów - jakie? Do tej pory słyszałem "że to podstawa programowania" i że "trzeba umieć z tego skorzystać" (nie merytoryczne)

Jeśli Ty w ogóle nie używasz dziedziczenia, to jest spore prawdopodobieństwo, że są sytuacje, w których piszesz kod bardziej skomplikowany niż to konieczne, albo nie używasz sensownych wzorców.

Kompozycja nie jest darmowa - trzeba jakoś tworzyć i/lub przekazywać obiekty. Jest dodatkowy kod do utrzymania, bardziej rozbudowane konstruktory, itd. W przypadku kilku linijek kodu operujących na stanie obiektu to często może być gra nie warta świeczki. Wszystko zależy od sytuacji.

  • Jeden skrót w IDE
  • dodatkowy kod (to prawda), ale za to lepszej jakości
  • Kontenery zależności albom Lombok

W IntelliJ jest nawet gotowy tool: "Replace inheritance with delegation" (czy jakoś tak)

Nic się nie traci na wprowadzeniu kompozycji.

Zalet dziedziczenia nie poznałem ciągle.

0
Juhas napisał(a):
TomRiddle napisał(a):

No to lepiej żebyś nie pisał w Javie, bo możesz dziedziczyć tylko z jednej klasy :D

Piszę aktualnie w C++ i C#. Wcześniej w Delphi i trochę w Javie. Tylko w jednym z tych języków można dziedziczyć z więcej niż jednej klasy i jakoś nie jest to problem nigdzie indziej.
Czy dziedziczenie z dwóch klas jest złe? A z trzech? A z pięciu? Jak zwykle - to zależy. ZAZWYCZAJ jeśli dziedziczysz z więcej niż jednej klasy, to prawdopodobnie masz złą architekturę. ZAZWYCZAJ.

Ever heard of the diamond problem?

Masz kod:

class BazowaZJakimsClonem {}
class Dziecko extends BazowaZJakimsClonem {} // konkretnie jak chcesz tutaj dodać Button/Control?

Ale po co? Jeśli chcę zrobić kontrolkę, to moją klasą bazową jest klasa bazowa dla kontrolek. Jeśli potrzebuję dodatkowego zachowania, to wtedy używam kompozycji albo interfejsów. Ale prawdopodobnie w 99% przypadków samo dziedziczenie z bazowej klasy dla kontrolek będzie wystarczające. W 99% jeśli tworząc kontrolkę, musisz dziedziczyć jeszcze z czegoś innego, to robisz coś źle.

Sprytnie sobie to odwróciłeś. Ja mówiłem o sytuacji w której masz już klasę bazową/abstrakcyjną z logiką, i np. chcesz nową stworzyć kontrolkę, (extends Control) która z niej korzysta...
(Tutaj widać czemu dziedziczenie się tutaj nie nadaje, bo jesteś zmuszony dziedziczyć z dwóch klas)

...nie z sytuacji w której masz już kontrolkę, i chcesz dołożyć do niej nową logikę.
(Wtedy faktycznie kompozycja jest dobrym (i jedynym) rozwiązaniem)

Jeśli potrzebuję, żeby wszystkie klasy w moim modelu były zapisywane w bazie danych, to wszystkie dziedziczą po DbItem, który to umożliwia (dając chociażby pole Id).

No ok, i? To ma być argument za dziedziczeniem? To nic nie wnosi.

Nie, argument za dziedziczeniem jest taki, że jest to PODSTAWA programowania obiektowego i bardzo dużo ułatwia. Ułatwia też trzymanie reguły DRY.

Ja mogę sobie powiedzieć że kompozycja to PODSTAWA programowania obiektowego, dużo ułatwia i pomaga z DRY. Co z tego?

Ciągle próbuję Wam pokazać że jeszcze nie usłyszałem jednej zalety dziedziczenia, której kompozycja też by nie miała i przy okazji korzystała z niej lepiej.

Tu wychodzi trochę brak wiedzy i doświadczenia w temacie architektury oprogramowania. Model bez logiki to tzw. model anemiczny. Dla niektórych jest to antywzorzec. Chyba, że to DTO, ale DTO raczej nie potrzebuje dziedziczenia.

Dziękuję, wcześniej jeszcze miałem z tyłu głowy że może dziedziczenie jest ok w rzadkich przypadkach (np. modele).

Ale teraz, dzięki Twojemu argumentowi wiem, że (skoro jest tak jak mówisz) dziedziczenie nie ma żadnej racji bytu.

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