Dlaczego dziedziczenie i polimorfizm są tak szeroko stosowane?

18

Im więcej dowiaduję się o różnych paradygmatach programowania, takich jak programowanie funkcjonalne, tym bardziej zaczynam kwestionować mądrość koncepcji OOP, takich jak dziedziczenie i polimorfizm. Po raz pierwszy dowiedziałem się o dziedziczeniu i polimorfizmie w szkole, a polimorfizm wydawał się wtedy wspaniałym sposobem na pisanie kodu ogólnego, który pozwalał na łatwą rozszerzalność.

Ale w obliczu pisania kaczego (zarówno dynamicznego, jak i statycznego) oraz funkcji takich jak funkcje wyższego rzędu, zacząłem patrzeć na dziedziczenie i polimorfizm jako narzucanie niepotrzebnych ograniczeń opartych na kruchym zestawie relacji między obiektami. Ogólna idea polimorfizmu polega na tym, że piszesz funkcję raz, a później możesz dodać nową funkcjonalność do swojego programu bez zmiany pierwotnej funkcji - wszystko, co musisz zrobić, to stworzyć kolejną klasę pochodną, ​​która implementuje niezbędne metody.

Ale jest to o wiele łatwiejsze do osiągnięcia przez pisanie kaczych, czy to w dynamicznym języku, takim jak Python, czy w języku statycznym, takim jak C ++.

Jako przykład weźmy pod uwagę następującą funkcję Python, a następnie jej statyczny odpowiednik C ++:

def foo(obj):
   obj.doSomething()

template <class Obj>
void foo(Obj& obj)
{
   obj.doSomething();
}

Odpowiednik OOP byłby podobny do następującego kodu Java:

public void foo(DoSomethingable obj)
{
  obj.doSomething();
}

Główną różnicą jest oczywiście to, że wersja Java wymaga utworzenia interfejsu lub hierarchii dziedziczenia, zanim będzie działać. Wersja Java wymaga zatem więcej pracy i jest mniej elastyczna. Ponadto uważam, że większość rzeczywistych hierarchii dziedziczenia jest nieco niestabilna. Wszyscy widzieliśmy wymyślone przykłady kształtów i zwierząt, ale w prawdziwym świecie, gdy zmieniają się wymagania biznesowe i dodaje się nowe funkcje, trudno jest wykonać jakąkolwiek pracę, zanim naprawdę trzeba rozciągnąć relację „to-a” między podklasy, albo przebuduj / przebuduj swoją hierarchię, aby uwzględnić dodatkowe klasy podstawowe lub interfejsy w celu dostosowania do nowych wymagań. Dzięki pisaniu kaczek nie musisz się martwić o modelowanie - po prostu martwisz się o potrzebną funkcjonalność .

Jednak dziedziczenie i polimorfizm są tak popularne, że wątpię, czy nazwanie ich dominującą strategią rozszerzalności i ponownego użycia kodu byłoby przesadą. Dlaczego więc dziedziczenie i polimorfizm są tak szalenie skuteczne? Czy pomijam jakieś poważne zalety dziedziczenia / polimorfizmu w porównaniu z pisaniem kaczek?

Channel72
źródło
Nie jestem ekspertem w Pythonie, więc muszę zapytać: co by się stało, gdyby objnie było doSomethingmetody? Czy zgłoszono wyjątek? Czy nic się nie dzieje
FrustratedWithFormsDesigner
6
@Frustrated: Pojawia się bardzo wyraźny, specyficzny wyjątek.
dsimcha
11
Czy pisanie kaczek nie zajmuje tylko polimorfizmu do jedenastu? Domyślam się, że nie jesteś sfrustrowany OOP, ale jego statycznie wpisanymi wcieleniami. Naprawdę to rozumiem, ale to nie sprawia, że ​​OOP jako całość jest złym pomysłem.
3
Najpierw przeczytaj „szeroko” jako „dziko” ...

Odpowiedzi:

22

W większości się z tobą zgadzam, ale dla zabawy zagram Adwokat diabła. Jawne interfejsy dają jedno miejsce, aby poszukać wyraźnie określonej, formalnie określonej umowy, informując o tym, co powinien zrobić dany typ. Może to być ważne, gdy nie jesteś jedynym programistą w projekcie.

Co więcej, te wyraźne interfejsy można zaimplementować wydajniej niż pisanie kaczką. Wirtualne wywołanie funkcji ma niewiele więcej narzutu niż normalne wywołanie funkcji, z tym wyjątkiem, że nie można go wstawić. Pisanie kaczek ma znaczny koszt. Pisanie strukturalne w stylu C ++ (przy użyciu szablonów) może generować ogromne ilości wzdęć pliku obiektowego (ponieważ każda instancja jest niezależna na poziomie pliku obiektu) i nie działa, gdy potrzebujesz polimorfizmu w czasie wykonywania, a nie kompilacji.

Konkluzja: Zgadzam się, że dziedziczenie w stylu Java i polimorfizm mogą być PITA, a alternatywy powinny być stosowane częściej, ale nadal ma swoje zalety.

dsimcha
źródło
29

Dziedziczenie i polimorfizm są szeroko stosowane, ponieważ działają w przypadku niektórych rodzajów problemów programistycznych.

Nie chodzi o to, że są powszechnie nauczani w szkołach, to jest odwrotnie: są powszechnie nauczani w szkołach, ponieważ ludzie (inaczej rynek) stwierdzili, że działali lepiej niż stare narzędzia, a więc szkoły zaczęły ich uczyć. [Anegdota: kiedy po raz pierwszy uczyłem się OOP, bardzo trudno było znaleźć szkołę, która uczyłaby dowolnego języka OOP. Dziesięć lat później trudno było znaleźć szkołę, która nie uczyłaby języka OOP.]

Powiedziałeś:

Ogólna idea polimorfizmu polega na tym, że piszesz funkcję raz, a później możesz dodać nową funkcjonalność do swojego programu bez zmiany pierwotnej funkcji - wystarczy stworzyć kolejną klasę pochodną, ​​która implementuje niezbędne metody

Mówię:

Nie, nie jest

Opisujesz nie polimorfizm, ale dziedziczenie. Nic dziwnego, że masz problemy z OOP! ;-)

Cofnij się o krok: polimorfizm jest zaletą przekazywania wiadomości; oznacza to po prostu, że każdy obiekt może swobodnie odpowiadać na wiadomość na swój własny sposób.

Więc ... Wpisywanie kaczek to (a raczej umożliwia) polimorfizm

Istotą twojego pytania wydaje się nie być to, że nie rozumiesz OOP lub że go nie lubisz , ale że nie lubisz definiować interfejsów . W porządku i dopóki będziesz ostrożny, wszystko będzie dobrze. Wadą jest to, że jeśli popełniłeś błąd - na przykład pominęłeś metodę - nie dowiesz się tego do czasu wykonania.

To jest statyczna vs dynamiczna rzecz, która jest dyskusją tak starą jak Lisp i nie ogranicza się do OOP.

Steven A. Lowe
źródło
2
+1 za „typowanie kaczki to polimorfizm”. Polimorfizm i dziedziczenie zostały podświadomie powiązane zbyt długo, a ścisłe wpisywanie tekstu jest jedynym prawdziwym powodem, dla którego musiały być.
cHao
+1. Myślę, że statyczne i dynamiczne rozróżnienie powinno być czymś więcej niż uwaga - leży u podstaw rozróżnienia między pisaniem kaczek a polimorfizmem w stylu java.
Sava B.
8

Zacząłem patrzeć na dziedziczenie i polimorfizm jako narzucanie niepotrzebnych ograniczeń opartych na kruchym zestawie relacji między obiektami.

Dlaczego?

Dziedziczenie (z pisaniem kaczek lub bez) zapewnia ponowne użycie wspólnej funkcji. Jeśli jest powszechny, możesz zapewnić, że będzie on konsekwentnie wykorzystywany w podklasach.

To wszystko. Nie ma „niepotrzebnych ograniczeń”. To uproszczenie.

Podobnie polimorfizm oznacza „pisanie kaczek”. Te same metody. Wiele klas z identycznym interfejsem, ale różne implementacje.

To nie jest ograniczenie. To uproszczenie.

S.Lott
źródło
7

Dziedziczenie jest nadużywane, ale podobnie jak pisanie kaczek. Oba mogą i prowadzą do problemów.

Dzięki silnemu pisaniu masz wiele „testów jednostkowych” wykonanych w czasie kompilacji. Podczas pisania kaczką często trzeba je pisać.

ElGringoGrande
źródło
3
Twój komentarz na temat testowania dotyczy tylko dynamicznego pisania kaczego. Pisanie statyczne w stylu C ++ w stylu C ++ (z szablonami) powoduje błędy podczas kompilacji. (Chociaż przyznane błędy szablonu C ++ są dość trudne do rozszyfrowania, ale to zupełnie inna kwestia.)
Channel72
2

Dobrą rzeczą w uczeniu się rzeczy w szkole jest to, że się ich uczysz. Niezbyt dobre jest to, że możesz zaakceptować je trochę zbyt dogmatycznie, nie rozumiejąc, kiedy są przydatne, a kiedy nie.

Następnie, jeśli nauczyłeś się tego dogmatycznie, możesz później „buntować się” tak samo dogmatycznie w innym kierunku. To też nie jest dobre.

Podobnie jak w przypadku takich pomysłów, najlepiej jest przyjąć pragmatyczne podejście. Rozwijaj zrozumienie, gdzie pasują, a gdzie nie. I zignoruj ​​wszystkie sposoby, w jakie zostały wyprzedane.

Mike Dunlavey
źródło
2

Tak, statyczne pisanie i interfejsy są ograniczeniami. Ale od czasu wynalezienia programowania strukturalnego (tj. „Goto uważany za szkodliwy”) chodziło o ograniczenie nas. Wujek Bob ma świetne zdanie na ten temat na swoim blogu wideo .

Teraz można argumentować, że zwężenie jest złe, ale z drugiej strony wprowadza porządek, kontrolę i znajomość do bardzo złożonego tematu.

Złagodzenie ograniczeń poprzez (ponowne) wprowadzenie dynamicznego pisania, a nawet bezpośredni dostęp do pamięci jest bardzo skuteczną koncepcją, ale może również utrudnić wielu programistom. Zwłaszcza programiści zwykle polegali na kompilatorze i na bezpieczeństwie typu przez większą część swojej pracy.

Martin Wickman
źródło
1

Dziedziczenie jest bardzo silnym związkiem między dwiema klasami. Nie sądzę, żeby Java była silniejsza. Dlatego powinieneś go używać tylko wtedy, gdy masz na myśli. Dziedziczenie publiczne to relacja „jest-a”, a nie „zwykle jest-a”. Naprawdę bardzo łatwo jest nadużyć dziedzictwa i skończyć z bałaganem. W wielu przypadkach dziedziczenie jest używane do reprezentowania „ma-a” lub „bierze-funkcjonalność-z-a”, a zazwyczaj lepiej to zrobić przez kompozycję.

Polimorfizm jest bezpośrednią konsekwencją relacji „jest-a”. Jeśli Pochodna dziedziczy z Podstawy, to każda Pochodna „jest-” Podstawą, dlatego możesz używać Pochodnej wszędzie tam, gdzie będziesz używać Podstawy. Jeśli nie ma to sensu w hierarchii dziedziczenia, hierarchia jest błędna i prawdopodobnie dzieje się za dużo dziedziczenia.

Wpisywanie kaczych jest fajną funkcją, ale kompilator nie ostrzeże Cię, jeśli będziesz go niewłaściwie używać. Jeśli nie chcesz zajmować się wyjątkami w czasie wykonywania, musisz upewnić się, że za każdym razem osiągasz właściwe wyniki. Łatwiej może być po prostu zdefiniować statyczną hierarchię dziedziczenia.

Nie jestem prawdziwym fanem pisania statycznego (uważam, że często jest to forma przedwczesnej optymalizacji), ale eliminuje pewne klasy błędów i wiele osób uważa, że ​​warto je wyeliminować.

Jeśli lubisz pisanie dynamiczne i pisanie kaczek lepiej niż pisanie statyczne i zdefiniowane hierarchie dziedziczenia, nie ma problemu. Jednak sposób Java ma swoje zalety.

David Thornley
źródło
0

Zauważyłem, że im częściej używam zamknięć C #, tym mniej robię tradycyjne OOP. Dziedziczenie było kiedyś jedynym sposobem łatwego udostępniania implementacji, więc myślę, że często było ono nadmiernie wykorzystywane, a granice koncepcji były zbyt daleko posunięte.

Chociaż zwykle można użyć zamknięć, aby wykonać większość czynności związanych z dziedziczeniem, może to również stać się brzydkie.

Zasadniczo jest to odpowiednie narzędzie do pracy: tradycyjne OOP może działać naprawdę dobrze, jeśli masz model, który mu odpowiada, a zamknięcia mogą działać naprawdę dobrze, gdy nie masz.

Ben Hughes
źródło
-1

Prawda leży gdzieś pośrodku. Podoba mi się, jak język C # 4.0, który jest statycznie typowany, obsługuje „pisanie kaczką” przez słowo kluczowe „dynamiczne”.

SiberianGuy
źródło
-1

Dziedziczenie, nawet jeśli rozum z perspektywy PR, jest świetną koncepcją; nie tylko oszczędza dużo czasu, ale nadaje sens relacji między niektórymi obiektami w twoim programie.

public class Animal {
    public virtual string Sound () {
        return "Some Sound";
    }
}

public class Dog : Animal {
    public override string Sound () {
        return "Woof";
    }
}

public class Cat : Animal {
    public override string Sound () {
        return "Mew";
    }
}

public class GoldenRetriever : Dog {

}

Tutaj klasa GoldenRetrieverma to samo, Soundco Dogbezpłatne podziękowania za spadek.

Napiszę ten sam przykład z moim poziomem Haskell, aby zobaczyć różnicę

data Animal = Animal | Dog | Cat | GoldenRetriever

sound :: Animal -> String
sound Animal = "Some Sound"
sound Dog = "Woof"
sound Cat = "Mew"
sound GoldenRetriever = "Woof"

Tu nie uciec konieczności określania sounddla GoldenRetriever. Najłatwiej byłoby to zrobić ogólnie

sound GoldenRetriever = sound Dog

ale wyobraź sobie, jeśli masz 20 funkcji! Jeśli jest ekspert Haskell, pokaż nam łatwiejszy sposób.

To powiedziawszy, byłoby wspaniale mieć dopasowanie wzorców i dziedziczenie w tym samym czasie, w którym funkcja byłaby domyślnie ustawiona na klasę podstawową, jeśli bieżąca instancja nie ma implementacji.

Cristian Garcia
źródło
5
Przysięgam, Animalto jest anty-tutorial OOP.
Telastyn
@Telastyn Nauczyłem się przykładu od Dereka Banasa na Youtube. Świetny nauczyciel. Moim zdaniem jest to bardzo łatwe do wizualizacji i uzasadnienia. Możesz swobodnie edytować post z lepszym przykładem.
Cristian Garcia,
wydaje się, że nie dodaje to nic znaczącego w porównaniu z poprzednimi 10 odpowiedziami
komnata
@gnat Chociaż „substancja” już tam jest, osoba początkująca może znaleźć przykład łatwiejszy do zrozumienia.
Cristian Garcia,
2
Zwierzęta i kształty są naprawdę złymi zastosowaniami polimorfizmu z kilku powodów, które zostały omówione na stronie na stronie. Zasadniczo powiedzenie, że kot „jest” zwierzęciem, nie ma znaczenia, ponieważ nie ma konkretnego „zwierzęcia”. To, co naprawdę chcesz wymodelować, to zachowanie, a nie tożsamość. Sensowne jest zdefiniowanie interfejsu „CanSpeak”, który mógłby zostać zaimplementowany jako miauczenie lub kora. Zdefiniowanie interfejsu „HasArea”, który może realizować kwadrat i okrąg, ma sens, ale nie ogólny kształt.