Przypadki użycia wielokrotnego dziedziczenia

15

Java pomija wielokrotne dziedziczenie, ponieważ pomija cel projektowy polegający na uproszczeniu języka .

Zastanawiam się, czy Java (ze swoim ekosystemem) jest naprawdę „prosta”. Python nie jest złożony i ma wielokrotne dziedziczenie. Więc nie będąc zbyt subiektywnym, moje pytanie brzmi ...

Jakie są typowe wzorce problemów, które korzystają z kodu zaprojektowanego do częstego korzystania z wielokrotnego dziedziczenia

treecoder
źródło
8
Java pomija bardzo wiele doskonałych rzeczy z bardzo niewielkiego powodu. Nie spodziewałbym się dobrego uzasadnienia MI.
DeadMG
2
Wielokrotne dziedzictwo Pythona to zdecydowanie terytorium tu-smoków . Fakt, że korzysta z dokładnego rozpoznawania nazw od lewej do prawej, wiąże się z poważnymi problemami zarówno w zakresie łatwości konserwacji, jak i zrozumienia. Chociaż może być przydatny w płytkich hierarchiach klas, w głębokich może być niewiarygodnie intuicyjny.
Mark Booth
Myślę, że powodem, dla którego Java nie zawiera wielokrotnego dziedziczenia, jest to, że programiści Java chcieli, aby ich język był łatwy do nauczenia się. Wielokrotne dziedziczenie, choć w niektórych przypadkach niewiarygodnie potężne, jest trudne do uchwycenia, a jeszcze trudniejsze do uzyskania dobrego efektu; nie jest to rzecz, z którą chciałbyś skonfrontować początkującego programistę. Mam na myśli: jak wytłumaczyć wirtualne dziedzictwo komuś, kto zmaga się z samą koncepcją dziedziczenia? A ponieważ wielokrotne dziedziczenie również nie jest po prostu trywialne po stronie implementatorów, prawdopodobnie twórca oprogramowania Java może pominąć to rozwiązanie korzystne dla wszystkich stron.
cmaster
Java jest nominalnie wpisana. Python nie jest. To sprawia, że ​​wielokrotne dziedziczenie jest znacznie łatwiejsze zarówno do implementacji, jak i zrozumienia w Pythonie.
Jules

Odpowiedzi:

11

Plusy:

  1. Czasami pozwala to na bardziej oczywiste modelowanie problemu niż inne sposoby jego modelowania.
  2. Jeśli różne nurty mają cel ortogonalny, może pozwolić na pewnego rodzaju kompozycje

Cons :

  1. Jeśli różni rodzice nie mają celu ortogonalnego, utrudnia to zrozumienie tego typu.
  2. Nie jest łatwo zrozumieć, w jaki sposób jest on implementowany w języku (dowolnym języku).

W C ++ dobrym przykładem wielokrotnego dziedziczenia stosowanego do złożonych funkcji ortogonalnych jest użycie CRTP do, na przykład, skonfigurowania systemu składowego gry.

Zacząłem pisać przykład, ale wydaje mi się, że warto przyjrzeć się przykładowi z prawdziwego świata. Niektóre kody Ogre3D używają wielokrotnego dziedziczenia w przyjemny i bardzo intuicyjny sposób. Na przykład klasa Mes dziedziczy zarówno Resources, jak i AnimationContainer. Zasoby udostępniają interfejs wspólny dla wszystkich zasobów, a AnimationContainer udostępnia interfejs specyficzny dla manipulowania zestawem animacji. Nie są ze sobą powiązane, więc łatwo jest myśleć o Siatce jako o zasobie, który dodatkowo może zawierać zestaw animacji. Czy to naturalne, prawda?

Możesz spojrzeć na inne przykłady w tej bibliotece , takie jak sposób zarządzania alokacją pamięci w sposób drobnoziarnisty, powodując, że klasy dziedziczą po wariantach przeciążenia klasy CRTP nowym i usuwanym .

Jak powiedziano, główne problemy związane z wielokrotnym dziedziczeniem wynikają z mieszania powiązanych pojęć. To sprawia, że ​​język musi ustawiać złożone implementacje (zobacz, w jaki sposób C ++ pozwala grać z problemem z diamentem ...), a użytkownik nie jest pewien, co się dzieje w tej implementacji. Na przykład przeczytaj ten artykuł wyjaśniający, w jaki sposób jest on implementowany w C ++ .

Usunięcie go z języka pomaga unikać ludzi, którzy nie wiedzą, w jaki sposób ten język jest zmuszony, aby coś złego uczynić. Ale zmusza do myślenia w sposób, który czasami nie wydaje się naturalny, nawet jeśli są to skrajne przypadki, zdarza się częściej, niż myślisz.

Klaim
źródło
byłbym bardzo wdzięczny, gdybyś przyozdobił swoją odpowiedź przykładowym problemem - dzięki temu terminy takie jak „cel ortogonalny” będą wyraźniejsze - ale dzięki
treekoder
Ok, pozwól mi spróbować coś dodać.
Klaim
Ogre3D nie jest miejscem, w którym szukałbym inspiracji projektowych - czy widziałeś ich infekcję Singleton?
DeadMG,
Po pierwsze, dziedzic singleton nie jest tak naprawdę singletonem, konstrukcja i zniszczenie są wyraźne. Następnie Ogre jest warstwą nad systemem sprzętowym (lub sterownikiem graficznym, jeśli wolisz). Oznacza to, że powinna istnieć tylko jedna unikalna reprezentacja interfejsów systemowych (takich jak root lub inne). Mogą usunąć singletona, ale nie o to tutaj chodzi. Dobrowolnie unikałem tego, aby uniknąć dyskusji o trollach, więc proszę spójrz na przykłady, które wskazałem. Ich użycie Singletona może nie być idealne, ale jest wyraźnie przydatne w praktyce (ale tylko dla ich rodzaju systemu, nie wszystko).
Klaim
4

Istnieje koncepcja zwana mixinami, która jest powszechnie używana w bardziej dynamicznych językach. Wielokrotne dziedziczenie to jeden ze sposobów, w jaki miksy mogą być obsługiwane przez język. Mieszanki są na ogół używane jako sposób na zgromadzenie przez klasę różnych funkcji. Bez wielokrotnego dziedziczenia musisz użyć agregacji / delegacji, aby uzyskać zachowanie typu mixin z klasą, która jest nieco bardziej złożona.

RationalGeek
źródło
+1 to właściwie dobry powód do posiadania wielokrotnego dziedziczenia. Mixiny mają dodatkowe konotacje („tej klasy nie należy używać jako samodzielnej”)
ashes999
2

Myślę, że wybór opiera się głównie na problemach związanych z problemem diamentu .

Ponadto często można obejść stosowanie wielokrotnego dziedziczenia przez przekazanie uprawnień lub w inny sposób.

Nie jestem pewien znaczenia twojego ostatniego pytania. Ale jeśli jest to „w jakich przypadkach przydatne jest wielokrotne dziedziczenie?”, To we wszystkich przypadkach, w których chciałbyś, aby obiekt A miał w zasadzie funkcje obiektów B i C.

Dagnele
źródło
2

Nie będę tutaj zagłębiał się, ale z pewnością można zrozumieć wielokrotne dziedziczenie w pythonie za pomocą następującego linku http://docs.python.org/release/1.5.1p1/tut/multiple.html :

Jedyną regułą niezbędną do wyjaśnienia semantyki jest reguła rozstrzygania używana w odniesieniu do odwołań do atrybutów klas. Jest to najpierw głębokość, od lewej do prawej. Zatem jeśli atrybut nie zostanie znaleziony w DerivedClassName, jest przeszukiwany w Base1, a następnie (rekurencyjnie) w klasach bazowych Base1 i tylko jeśli go tam nie ma, jest przeszukiwany w Base2 i tak dalej.

...

Oczywiste jest, że masowe korzystanie z wielokrotnego dziedziczenia jest koszmarem konserwacyjnym, biorąc pod uwagę poleganie na konwencjach Pythona w celu uniknięcia przypadkowych konfliktów nazw. Dobrze znanym problemem wielokrotnego dziedziczenia jest klasa wywodząca się z dwóch klas, które mają wspólną klasę podstawową. Chociaż łatwo jest zorientować się, co dzieje się w tym przypadku (instancja będzie miała jedną kopię `` zmiennych instancji '' lub atrybutów danych używanych przez wspólną klasę podstawową), nie jest jasne, czy semantyka jest w jakikolwiek sposób przydatny.

To tylko mały akapit, ale wystarczająco duży, aby wyjaśnić wątpliwości.

Pankaj Upadhyay
źródło
1

Jednym z miejsc, w których przydałoby się wielokrotne dziedziczenie, jest sytuacja, w której klasa implementuje kilka interfejsów, ale chciałbyś mieć jakąś domyślną funkcjonalność wbudowaną w każdy interfejs. Jest to przydatne, jeśli większość klas implementujących jakiś interfejs chce zrobić coś w ten sam sposób, ale czasami trzeba zrobić coś innego. Możesz mieć każdą klasę z tą samą implementacją, ale sensowniej jest umieścić ją w jednym miejscu.

KeithB
źródło
1
Czy wymagałoby to uogólnionego wielokrotnego dziedziczenia, czy po prostu środków, za pomocą których interfejs może określać domyślne zachowania dla niezaimplementowanych metod? Gdyby interfejsy mogły określać domyślne implementacje tylko dla metod, które same wdrażają (w przeciwieństwie do tych, które dziedziczą z innych interfejsów), taka funkcja całkowicie unikałaby problemów podwójnego diamentu, które utrudniają wielokrotne dziedziczenie.
supercat
1

Jakie są typowe wzorce problemów, które korzystają z kodu zaprojektowanego do częstego korzystania z wielokrotnego dziedziczenia?

To tylko jeden przykład, ale uważam go za nieoceniony w celu poprawy bezpieczeństwa i złagodzenia pokus, aby zastosować zmiany kaskadowe w dzwoniących lub podklasach.

Tam, gdzie znalazłem wielokrotne dziedziczenie, niezwykle przydatne nawet dla najbardziej abstrakcyjnych, bezstanowych interfejsów, jest nie-wirtualny idiom interfejsu (NVI) w C ++.

Nie są nawet tak naprawdę abstrakcyjnymi klasami podstawowymi , jak interfejsy, które mają tylko odrobinę implementacji, aby egzekwować uniwersalne aspekty swoich umów, ponieważ tak naprawdę nie zawężają ogólności umowy, a jedynie lepiej ją egzekwują. .

Prosty przykład (niektórzy mogą sprawdzić, czy przekazany uchwyt pliku jest otwarty, czy coś takiego):

// Non-virtual interface (public methods are nonvirtual/final).
// Since these are modeling the concept of "interface", not ABC,
// multiple will often be inherited ("implemented") by a subclass.
class SomeInterface
{
public:
    // Pre: x should always be greater than or equal to zero.
    void f(int x) /*final*/
    {
        // Make sure x is actually greater than or equal to zero
        // to meet the necessary pre-conditions of this function.
        assert(x >= 0);

        // Call the overridden function in the subtype.
        f_impl(x);
    }

protected:
    // Overridden by a boatload of subtypes which implement
    // this non-virtual interface.
    virtual void f_impl(int x) = 0;
};

W tym przypadku być może fnazywa je tysiąc miejsc w bazie kodu, a f_implzastępuje je sto podklas.

Trudno byłoby przeprowadzić tego rodzaju kontrolę bezpieczeństwa we wszystkich 1000 wzywających miejscach flub we wszystkich 100 nadrzędnych miejscach f_impl.

Po prostu uczynienie tego punktu wejścia funkcją niewirtualną, daje mi jedno centralne miejsce do przeprowadzenia tej kontroli. Ta kontrola nie zmniejsza w najmniejszym stopniu abstrakcji, ponieważ po prostu zapewnia warunek konieczny do wywołania tej funkcji. W pewnym sensie jest to prawdopodobnie wzmocnienie umowy zapewnianej przez interfejs i zmniejszenie obciążenia związanego z sprawdzaniem xdanych wejściowych, aby upewnić się, że są one zgodne z prawidłowymi warunkami wstępnymi we wszystkich 100 miejscach, które je zastępują.

Jest to coś, czego chciałbym, aby każdy język miał, a także życzyłem sobie, nawet w C ++, aby była to trochę bardziej natywna koncepcja (np. Nie wymagająca od nas definiowania oddzielnej funkcji do zastąpienia).

Jest to niezwykle przydatne, jeśli nie zrobiłeś tego assertwcześniej, i zdałeś sobie sprawę, że potrzebujesz go później, gdy niektóre losowe miejsca w bazie kodu napotykały ujemne wartości f.


źródło
0

Po pierwsze: wiele kopii klasy podstawowej (problem C ++) i ścisłe powiązanie między klasami podstawowymi i pochodnymi.

Po drugie: wielokrotne dziedziczenie po abstrakcyjnych interfejsach

quant_dev
źródło
sugerujesz, że nie jest to przydatne w żadnym kontekście? I że bez tego wszystko można wygodnie zaprojektować / zakodować? Proszę również o rozwinięcie drugiego punktu.
treekoder