Czy dobrym pomysłem jest korzystanie z dziedziczenia wielokrotnego, czy mogę zamiast tego robić inne rzeczy?
Czy dobrym pomysłem jest korzystanie z dziedziczenia wielokrotnego, czy mogę zamiast tego robić inne rzeczy?
Dziedziczenie wielokrotne (w skrócie MI) pachnie , co oznacza, że zwykle odbywało się to ze złych powodów i powróci w twarz opiekuna.
Dotyczy to dziedziczenia, a więc jeszcze bardziej dotyczy dziedziczenia wielokrotnego.
Czy Twój obiekt naprawdę musi dziedziczyć po innym? A Car
nie musi dziedziczyć z Engine
do pracy ani z Wheel
. A Car
ma Engine
i cztery Wheel
.
Jeśli używasz dziedziczenia wielokrotnego do rozwiązania tych problemów zamiast kompozycji, oznacza to, że zrobiłeś coś złego.
Zazwyczaj masz klasę A
, potem B
i C
zarówno dziedziczyć po A
. I (nie pytaj mnie dlaczego) ktoś zdecyduje, że D
musi dziedziczyć zarówno po, jak B
i C
.
Spotkałem się z tego rodzaju problemem dwa razy w ciągu 8 ośmiu lat i jest to zabawne z powodu:
D
nie powinien był odziedziczyć po obu B
i C
), bo to była zła architektura (właściwie C
nie powinna w ogóle istnieć ...)A
była obecna dwukrotnie w swojej klasie wnuków D
, a zatem aktualizacja jednego pola nadrzędnego A::field
oznaczała albo aktualizację go dwukrotnie (przez B::field
i C::field
), albo po cichu coś poszło nie tak i awarię później (nowy wskaźnik w B::field
i usuń C::field
...)Użycie słowa kluczowego virtual w C ++ do zakwalifikowania dziedziczenia pozwala uniknąć podwójnego układu opisanego powyżej, jeśli nie tego chcesz, ale w każdym razie, z mojego doświadczenia, prawdopodobnie robisz coś źle ...
W hierarchii obiektów powinieneś starać się zachować hierarchię jako drzewo (węzeł ma JEDNEGO rodzica), a nie jako wykres.
Prawdziwy problem z Diamentem Strachu w C ++ ( zakładając, że projekt jest dobry - poproś o sprawdzenie kodu! ), Polega na tym, że musisz dokonać wyboru :
A
występowała dwukrotnie w Twoim układzie i co to oznacza? Jeśli tak, to z całą pewnością odziedzicz po nim dwukrotnie.Ten wybór jest nieodłącznym elementem problemu, aw C ++, w przeciwieństwie do innych języków, można to zrobić bez dogmatów narzucających projekt na poziomie języka.
Ale jak wszystkie moce, z tą mocą wiąże się odpowiedzialność: poproś o sprawdzenie swojego projektu.
Wielokrotne dziedziczenie zera lub jednej konkretnej klasy i zera lub większej liczby interfejsów jest zwykle OK, ponieważ nie napotkasz Diamentu Strachu opisanego powyżej. W rzeczywistości tak się robi w Javie.
Zwykle to, co masz na myśli, gdy C dziedziczy po A
i B
to, że użytkownicy mogą używać C
tak, jakby to był A
i / lub tak, jakby był B
.
W C ++ interfejs jest klasą abstrakcyjną, która ma:
Dziedziczenie wielokrotne od zera do jednego rzeczywistego obiektu i zera lub większej liczby interfejsów nie jest uważane za „śmierdzące” (a przynajmniej nie tak bardzo).
Po pierwsze, wzorzec NVI może być użyty do stworzenia interfejsu, ponieważ rzeczywiste kryteria to brak stanu (tj. Brak zmiennych składowych , z wyjątkiem this
). Celem twojego abstrakcyjnego interfejsu jest opublikowanie umowy („możesz nazywać mnie tak i tak”), nic więcej, nic mniej. Ograniczeniem posiadania tylko abstrakcyjnej metody wirtualnej powinien być wybór projektu, a nie obowiązek.
Po drugie, w C ++ sensowne jest dziedziczenie wirtualne z abstrakcyjnych interfejsów (nawet z dodatkowymi kosztami / pośrednimi). Jeśli tego nie zrobisz, a dziedziczenie interfejsu pojawia się wielokrotnie w Twojej hierarchii, będziesz mieć niejasności.
Po trzecie, orientacja obiektu jest super, ale nie jest to jedyna prawda Out There TM w C ++. Używaj właściwych narzędzi i zawsze pamiętaj, że w C ++ masz inne paradygmaty, które oferują różnego rodzaju rozwiązania.
Czasem tak.
Zazwyczaj twoja C
klasa dziedziczy z A
i B
i A
i B
są dwóch niepowiązanych obiektów (czyli nie w tej samej hierarchii, nic wspólnego, różnych koncepcji, etc.).
Na przykład, możesz mieć system o Nodes
współrzędnych X, Y, Z, zdolny do wykonywania wielu obliczeń geometrycznych (być może punkt, część obiektów geometrycznych), a każdy Węzeł jest Automatycznym Agentem, zdolnym do komunikowania się z innymi agentami.
Być może masz już dostęp do dwóch bibliotek, z których każda ma własną przestrzeń nazw (kolejny powód, aby używać przestrzeni nazw ... Ale używasz przestrzeni nazw, prawda?), Jedna istota, geo
a drugaai
Więc masz własne own::Node
pochodzenie zarówno z, jak ai::Agent
i geo::Point
.
To jest moment, w którym powinieneś zadać sobie pytanie, czy zamiast tego nie powinieneś używać kompozycji. Jeśli own::Node
naprawdę jest zarówno a, jak ai::Agent
i a geo::Point
, to kompozycja nie wystarczy.
Wtedy będziesz potrzebować wielokrotnego dziedziczenia, aby own::Node
komunikować się z innymi agentami zgodnie z ich pozycją w przestrzeni 3D.
(Zauważysz, że ai::Agent
i geo::Point
jesteś całkowicie, całkowicie, całkowicie NIEZWIĄZANY ... To drastycznie zmniejsza niebezpieczeństwo wielokrotnego dziedziczenia)
Istnieją inne przypadki:
this
)Czasami możesz użyć kompozycji, a czasami MI jest lepsze. Chodzi o to, że masz wybór. Zrób to odpowiedzialnie (i poproś o sprawdzenie kodu).
Z mojego doświadczenia wynika, że przez większość czasu nie. MI nie jest odpowiednim narzędziem, nawet jeśli wydaje się działać, ponieważ może być używane przez leniwych do łączenia elementów bez zdawania sobie sprawy z konsekwencji (takich jak tworzenie Car
zarówno an, jak Engine
i a Wheel
).
Ale czasami tak. A wtedy nic nie będzie działać lepiej niż MI.
Ale ponieważ MI jest śmierdzący, przygotuj się na obronę swojej architektury w przeglądach kodu (a obrona jej jest dobra, ponieważ jeśli nie jesteś w stanie jej obronić, nie powinieneś tego robić).
Z wywiadu z Bjarne Stroustrupem :
źródło
const
- musiałem pisać niezgrabne obejścia (zwykle przy użyciu interfejsów i kompozycji), gdy klasa naprawdę potrzebowała mutowalnych i niezmiennych wariantów. Jednakże, nigdy nie raz brakowało wielokrotne dziedziczenie, i nigdy nie czułem, że muszę napisać obejścia ze względu na brak tej funkcji. To jest różnica. W każdym przypadku, jaki kiedykolwiek widziałem, nie używanie MI jest lepszym wyborem projektowym, a nie obejściem.Nie ma powodu, aby tego unikać i może być bardzo przydatne w sytuacjach. Musisz jednak zdawać sobie sprawę z potencjalnych problemów.
Największy z nich to diament śmierci:
Masz teraz dwie „kopie” GrandParent in Child.
C ++ pomyślał o tym jednak i pozwala na wirtualne dziedziczenie w celu obejścia problemów.
Zawsze sprawdzaj swój projekt, upewnij się, że nie używasz dziedziczenia, aby zaoszczędzić na ponownym wykorzystaniu danych. Jeśli możesz przedstawić to samo z kompozycją (a zazwyczaj możesz), jest to znacznie lepsze podejście.
źródło
GrandParent
wChild
. Istnieje strach przed MI, ponieważ ludzie po prostu myślą, że mogą nie rozumieć zasad językowych. Ale każdy, kto nie może zrozumieć tych prostych zasad, nie może również napisać nietrywialnego programu.Zobacz w: wielokrotne dziedziczenie .
Nowoczesny sposób rozwiązania tego problemu polega na użyciu interfejsu (klasa czysto abstrakcyjna), takiego jak interfejs COM i Java.
Tak, możesz. Mam zamiar okraść GoF .
źródło
Dziedziczenie publiczne jest relacją IS-A i czasami klasa będzie rodzajem kilku różnych klas i czasami ważne jest, aby to odzwierciedlić.
Czasami przydatne są także „miksy”. Na ogół są to małe klasy, zwykle nie dziedziczące po niczym, zapewniające użyteczną funkcjonalność.
Tak długo, jak hierarchia dziedziczenia jest dość płytka (jak prawie zawsze powinna być) i dobrze zarządzana, jest mało prawdopodobne, że otrzymasz przerażające dziedzictwo diamentowe. Diament nie jest problemem we wszystkich językach, które używają wielokrotnego dziedziczenia, ale sposób jego traktowania w C ++ jest często niezręczny i czasami zagadkowy.
Chociaż spotkałem się z przypadkami, w których wielokrotne dziedziczenie jest bardzo przydatne, w rzeczywistości są one dość rzadkie. Jest to prawdopodobne, ponieważ wolę używać innych metod projektowania, gdy tak naprawdę nie potrzebuję dziedziczenia wielokrotnego. Wolę unikać mylących konstrukcji językowych i łatwo jest tworzyć przypadki dziedziczenia, w których trzeba naprawdę dobrze przeczytać podręcznik, aby dowiedzieć się, co się dzieje.
źródło
Nie należy „unikać” wielokrotnego dziedziczenia, ale należy być świadomym problemów, które mogą się pojawić, takich jak „problem z diamentami” ( http://en.wikipedia.org/wiki/Diamond_problem ) i ostrożnie traktować przekazaną władzę , tak jak powinieneś ze wszystkimi mocami.
źródło
Ryzykując nieco abstrakcyjności, myślę o dziedziczeniu w ramach teorii kategorii wydaje mi się pouczające.
Jeśli pomyślimy o wszystkich naszych klasach i strzałkach między nimi oznaczających relacje dziedziczenia, to coś takiego
oznacza, że
class B
pochodzi zclass A
. Zauważ, że podanemówimy, że C pochodzi od B, które pochodzi od A, więc mówi się również, że C pochodzi od A, a zatem
Ponadto mówimy, że dla każdej klasy, z
A
którejA
wywodzi się trywialnieA
, nasz model dziedziczenia spełnia definicję kategorii. W bardziej tradycyjnym języku mamy kategorięClass
zawierającą obiekty, wszystkie klasy i morfizmy, relacje dziedziczenia.To trochę konfiguracji, ale spójrzmy na nasz Diament Zagłady:
To podejrzany schemat, ale wystarczy. Więc
D
dziedziczy wszystkoA
,B
iC
. Co więcej, i coraz bliżej odpowiedzi na pytanie OP,D
dziedziczy również z dowolnej nadklasyA
. Możemy narysować diagramTeraz problemy związane z Diamonda Śmierci oto kiedy
C
iB
podzielić nieruchomość / nazwy metod i robi się niejednoznaczne; jeśli jednak przeniesiemy do tego jakieś wspólne zachowanie,A
niejednoznaczność znika.Ujmując kategorycznie, chcemy
A
,B
iC
być takim, że jeśliB
iC
dziedziczenie zQ
tego czasuA
może być przepisane jako podklasaQ
. To powodujeA
coś, co nazywa się wypychaniem .Istnieje również symetryczna konstrukcja
D
zwana pullback . Jest to w zasadzie najbardziej użyteczna klasa, jaką można skonstruować, która dziedziczy po obuB
iC
. Oznacza to, że jeśli masz jakąkolwiek inną klasę,R
dziedzicząc mnożenie poB
iC
, toD
jest to klasa, w którejR
można ją przepisać jako podklasęD
.Upewnienie się, że twoje wierzchołki diamentu są wycofane i wypchnięte, daje nam dobry sposób na ogólne radzenie sobie z konfliktami nazw lub problemami z konserwacją, które mogą wystąpić w przeciwnym razie.
Zauważ , że odpowiedź Paercebala zainspirowała to, ponieważ jego napomnienia wynikają z powyższego modelu, biorąc pod uwagę, że pracujemy w pełnej kategorii Klasa wszystkich możliwych klas.
Chciałem uogólnić jego argument na coś, co pokazuje, jak skomplikowane relacje wielokrotnego dziedziczenia mogą być zarówno potężne, jak i bezproblemowe.
TL; DR Pomyśl o relacjach dziedziczenia w swoim programie jako tworzących kategorię. Następnie możesz uniknąć problemów z Diamentem Zagłady, wykonując wypychania klas dziedziczonych wielokrotnie i symetrycznie, tworząc wspólną klasę nadrzędną, która jest wycofaniem.
źródło
Używamy Eiffla. Mamy doskonałe MI. Bez obaw. Nie ma problemów. Łatwe zarządzanie. Są chwile, aby NIE używać MI. Jest to jednak przydatne bardziej niż ludzie zdają sobie sprawę, ponieważ: A) posługują się niebezpiecznym językiem, który nie radzi sobie dobrze -LUB- B) są zadowoleni z tego, jak pracowali z MI przez lata -LUB- C) z innych powodów ( zbyt liczne, aby je wymienić, jestem pewien - zobacz odpowiedzi powyżej).
Dla nas używanie Eiffla, MI jest tak samo naturalne jak wszystko inne i jest kolejnym doskonałym narzędziem w zestawie narzędzi. Szczerze mówiąc, nie obchodzi nas, że nikt inny nie używa Eiffla. Bez obaw. Cieszymy się z tego, co mamy i zapraszamy do obejrzenia.
Kiedy patrzysz: zwróć szczególną uwagę na bezpieczeństwo Pustki i wyeliminowanie dereferencji wskaźnika zerowego. Kiedy wszyscy tańczymy dookoła MI, twoje wskazówki gubią się! :-)
źródło
Każdy język programowania ma nieco inne podejście do programowania obiektowego z zaletami i wadami. Wersja C ++ kładzie nacisk bezpośrednio na wydajność i ma towarzyszącą wadę, że pisanie nieprawidłowego kodu jest niepokojąco łatwe - i jest to prawdą w przypadku wielokrotnego dziedziczenia. W konsekwencji istnieje tendencja do odciągania programistów od tej funkcji.
Inne osoby zadały sobie pytanie, do czego dziedziczenie wielokrotne nie jest dobre. Ale widzieliśmy wiele komentarzy, które mniej więcej sugerują, że powodem, dla którego należy tego unikać, jest to, że nie jest to bezpieczne. Cóż, tak i nie.
Jak to często bywa w C ++, jeśli zastosujesz się do podstawowych wskazówek, możesz z nich bezpiecznie korzystać bez ciągłego „patrzenia przez ramię”. Główną ideą jest to, że rozróżnia się specjalny rodzaj definicji klasy zwany „mieszanką”; class jest mieszanką, jeśli wszystkie jej funkcje składowe są wirtualne (lub czysto wirtualne). Wtedy możesz dziedziczyć z jednej głównej klasy i tylu „miksów”, ile chcesz - ale powinieneś dziedziczyć mieszanki ze słowem kluczowym „wirtualny”. na przykład
Moja sugestia jest taka, że jeśli zamierzasz używać klasy jako klasy mieszanej, zastosuj także konwencję nazewnictwa, aby ułatwić każdemu czytającemu kod, aby zobaczyć, co się dzieje i sprawdzić, czy grasz zgodnie z zasadami podstawowych wytycznych . Przekonasz się, że działa to znacznie lepiej, jeśli twoje miksy mają również domyślne konstruktory, tylko ze względu na sposób działania wirtualnych klas podstawowych. I pamiętaj, aby wszystkie destruktory też były wirtualne.
Zwróć uwagę, że moje użycie tutaj słowa „mix-in” nie jest tym samym, co sparametryzowana klasa szablonu (zobacz ten link po dobre wyjaśnienie), ale myślę, że jest to uczciwe użycie terminologii.
Nie chcę teraz sprawiać wrażenia, że jest to jedyny sposób na bezpieczne korzystanie z dziedziczenia wielokrotnego. To tylko jeden sposób, który jest dość łatwy do sprawdzenia.
źródło
Powinieneś używać go ostrożnie, są takie przypadki, jak problem diamentów , kiedy sprawy mogą się skomplikować.
(źródło: learncpp.com )
źródło
Printer
nie powinno być nawetPoweredDevice
. APrinter
oznacza drukowanie, a nie zarządzanie energią. Implementacja określonej drukarki może wymagać zarządzania energią, ale te polecenia zarządzania energią nie powinny być ujawniane bezpośrednio użytkownikom drukarki. Nie mogę sobie wyobrazić żadnego rzeczywistego wykorzystania tej hierarchii.Wykorzystywanie i nadużywanie dziedziczenia.
Artykuł świetnie wyjaśnia dziedziczenie i związane z nim niebezpieczeństwa.
źródło
Poza wzorem rombu wielokrotne dziedziczenie sprawia, że model obiektu jest trudniejszy do zrozumienia, co z kolei zwiększa koszty utrzymania.
Kompozycja jest z natury łatwa do zrozumienia, zrozumienia i wyjaśnienia. Pisanie kodu może być uciążliwe, ale dobre środowisko IDE (minęło kilka lat, odkąd pracowałem z Visual Studio, ale z pewnością wszystkie środowiska Java IDE mają świetne narzędzia automatyzujące skróty do kompozycji) powinny pokonać tę przeszkodę.
Jeśli chodzi o utrzymanie, „problem diamentów” pojawia się również w niedosłownych przypadkach dziedziczenia. Na przykład, jeśli masz A i B, a twoja klasa C rozszerza je oba, a A ma metodę `` makeJuice '', która wytwarza sok pomarańczowy i rozszerzasz ją, aby zrobić sok pomarańczowy z odrobiną limonki: co się stanie, gdy projektant B „dodaje metodę„ makeJuice ”, która generuje prąd elektryczny? „A” i „B” może być zgodny „rodzice” już teraz , ale to nie znaczy zawsze będą tak!
Ogólnie rzecz biorąc, maksyma polegająca na unikaniu dziedziczenia, a zwłaszcza dziedziczenia wielokrotnego, jest rozsądna. Jak wszystkie maksymy, są wyjątki, ale musisz upewnić się, że miga zielony neon wskazujący wszystkie wyjątki, które kodujesz (i trenuj swój mózg, aby za każdym razem, gdy zobaczysz takie drzewa dziedziczenia, rysujesz własnym migającym zielonym neonem znak) i od czasu do czasu sprawdzasz, czy wszystko ma sens.
źródło
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?
Uhhh, oczywiście pojawia się błąd kompilacji (jeśli użycie jest niejednoznaczne).Kluczową kwestią związaną z MI dla konkretnych obiektów jest to, że rzadko masz obiekt, który zgodnie z prawem powinien „być A i być B”, więc rzadko jest to poprawne rozwiązanie ze względów logicznych. Znacznie częściej masz obiekt C, który jest posłuszny „C może działać jako A lub B”, co można osiągnąć poprzez dziedziczenie i kompozycję interfejsu. Ale nie popełnij błędu - dziedziczenie wielu interfejsów to nadal MI, tylko jej podzbiór.
W szczególności w przypadku języka C ++ kluczową słabością tej funkcji nie jest faktyczne ISTNIENIE wielokrotnego dziedziczenia, ale niektóre konstrukcje, na które zezwala, są prawie zawsze źle sformułowane. Na przykład dziedziczenie wielu kopii tego samego obiektu, na przykład:
jest zniekształcony WEDŁUG DEFINICJI. W tłumaczeniu na język angielski jest to „B to A i A”. Tak więc nawet w ludzkim języku istnieje poważna dwuznaczność. Czy chodziło Ci o „B ma 2 As” czy po prostu „B to A” ?. Dopuszczenie takiego patologicznego kodu, a co gorsza uczynienie go przykładem użycia, nie przyniosło C ++ żadnych korzyści, jeśli chodzi o argumentowanie za utrzymaniem tej funkcji w kolejnych językach.
źródło
Możesz użyć kompozycji zamiast dziedziczenia.
Ogólne wrażenie jest takie, że kompozycja jest lepsza i jest bardzo dobrze omówiona.
źródło
The general feeling is that composition is better, and it's very well discussed.
To nie znaczy, że kompozycja jest lepsza.zajmuje 4/8 bajtów na zaangażowaną klasę. (Jeden ten wskaźnik na klasę).
To może nigdy nie być problemem, ale jeśli pewnego dnia będziesz mieć strukturę mikrodanych, która jest instancją miliardów czasu, to będzie.
źródło