Zawsze podobał mi się pomysł obsługiwania wielokrotnego dziedziczenia w jednym języku. Najczęściej jednak jest celowo zapominany, a domniemanym „zamiennikiem” są interfejsy. Interfejsy po prostu nie obejmują tego samego gruntu, co wielokrotne dziedziczenie, a to ograniczenie może czasami prowadzić do większej liczby kodów wzorcowych.
Jedynym podstawowym powodem, dla którego kiedykolwiek to słyszałem, jest problem z diamentami w klasach podstawowych. Po prostu nie mogę tego zaakceptować. Dla mnie wychodzi to okropnie: „Cóż, można to zepsuć, więc automatycznie jest to zły pomysł”. Możesz jednak popsuć wszystko w języku programowania i mam na myśli wszystko. Po prostu nie mogę tego potraktować poważnie, przynajmniej nie bez dokładniejszego wyjaśnienia.
Świadomość tego problemu to 90% bitwy. Co więcej, wydaje mi się, że już dawno temu słyszałem o obejściu ogólnego zastosowania obejmującym algorytm „kopertowy” lub coś w tym rodzaju (czy to ktoś dzwoni, ktoś?).
Jeśli chodzi o problem z diamentem, jedynym potencjalnie autentycznym problemem, jaki mogę wymyślić, jest próba użycia biblioteki innej firmy i nie widzę, że dwie pozornie niezwiązane klasy w tej bibliotece mają wspólną klasę podstawową, ale oprócz dokumentacja, prosta funkcja językowa może wymagać, powiedzmy, wyraźnego zadeklarowania zamiaru stworzenia diamentu, zanim faktycznie go skompiluje. Dzięki takiej funkcji każde stworzenie diamentu jest celowe, lekkomyślne lub dlatego, że nie jest się świadomym tej pułapki.
Tak więc wszystko zostało powiedziane ... Czy jest jakiś prawdziwy powód, dla którego większość ludzi nienawidzi wielokrotnego dziedziczenia, czy może to tylko histeria, która powoduje więcej szkody niż pożytku? Czy jest coś, czego tu nie widzę? Dziękuję Ci.
Przykład
Samochód rozszerza WheeledVehicle, KIASpectra rozszerza samochód i elektronikę, KIASpectra zawiera radio. Dlaczego KIASpectra nie zawiera elektroniki?
Ponieważ jest to elektronika. Dziedziczenie vs. składanie zawsze powinno być relacją typu „jest relacją a relacją typu relacja”.
Ponieważ jest to elektronika. Przewody, płytki drukowane, przełączniki itp. To wszystko w górę iw dół.
Ponieważ jest to elektronika. Jeśli akumulator zużyje się w zimie, masz tyle samo kłopotów, jakby nagle wszystkie koła zniknęły.
Dlaczego nie skorzystać z interfejsów? Weźmy na przykład # 3. Nie chcę pisać tego od nowa i naprawdę nie chcę tworzyć dziwacznych klas pomocników proxy, aby to zrobić:
private void runOrDont()
{
if (this.battery)
{
if (this.battery.working && this.switchedOn)
{
this.run();
return;
}
}
this.dontRun();
}
(Nie zastanawiamy się, czy ta implementacja jest dobra czy zła.) Możesz sobie wyobrazić, jak może być kilka z tych funkcji związanych z elektroniką, które nie są powiązane z niczym w WheeledVehicle i odwrotnie.
Nie byłem pewien, czy zdecydować się na ten przykład, czy nie, ponieważ jest tam miejsce na interpretację. Możesz również pomyśleć w kategoriach samolotu rozszerzającego pojazd i FlyingObject i ptaka rozszerzającego zwierzę i FlyingObject, lub w kategoriach znacznie czystszego przykładu.
źródło
Traits
- działają one jak interfejsy z opcjonalną implementacją, ale mają pewne ograniczenia, które pomagają zapobiegać problemom takim jak problem z diamentem.KiaSpectra
nie jestElectronic
; to jest elektronika, a może byćElectronicCar
(co byłoby rozszerzyćCar
...)Odpowiedzi:
W wielu przypadkach ludzie używają dziedziczenia, aby zapewnić cechę klasie. Pomyśl na przykład o Pegazie. W przypadku wielokrotnego dziedziczenia możesz pokusić się o stwierdzenie, że Pegaz rozszerza konia i ptaka, ponieważ sklasyfikowałeś go jako zwierzę ze skrzydłami.
Ptaki mają jednak inne cechy, których Pegasi nie ma. Na przykład ptaki składają jaja, Pegasi rodzą się na żywo. Jeśli dziedzictwo jest twoim jedynym sposobem przekazywania cech dzielenia się, nie ma sposobu, aby wykluczyć cechę składania jaj z Pegaza.
Niektóre języki zdecydowały się na wyraźne skonstruowanie cech w tym języku. Inne delikatnie poprowadzą cię w tym kierunku, usuwając MI z języka. Tak czy inaczej, nie mogę wymyślić jednego przypadku, w którym pomyślałem „Człowieku, naprawdę potrzebuję MI, aby zrobić to poprawnie”.
Omówmy również, czym jest NAPRAWDĘ dziedzictwo. Dziedzicząc po klasie, bierzesz zależność od tej klasy, ale musisz także wspierać kontrakty obsługiwane przez klasę, zarówno niejawne, jak i jawne.
Weźmy klasyczny przykład kwadratu dziedziczącego z prostokąta. Prostokąt odsłania właściwość długości i szerokości, a także metodę getPerimeter i getArea. Kwadrat zastąpiłby długość i szerokość, więc gdy jeden jest ustawiony, drugi jest ustawiony tak, aby pasował do getPerimeter, a getArea działałby tak samo (2 * długość + 2 * szerokość dla obwodu i długość * szerokość dla obszaru).
Istnieje jeden przypadek testowy, który pęka, jeśli zastąpisz tę implementację kwadratu prostokątem.
Jest wystarczająco twardy, aby wszystko było w porządku z jednym łańcuchem spadkowym. Jeszcze gorzej jest, gdy dodasz kolejny do miksu.
Pułapki, o których wspominałem w przypadku Pegaza w MI, oraz relacje prostokąt / kwadrat są wynikiem niedoświadczonego projektu klas. Zasadniczo unikanie wielokrotnego dziedziczenia jest sposobem, aby pomóc początkującym programistom uniknąć strzelania sobie w stopę. Podobnie jak wszystkie zasady projektowania, dyscyplina i szkolenie oparte na nich pozwala z czasem odkryć, kiedy można się od nich oderwać. Zobacz model nabywania umiejętności Dreyfus , na poziomie eksperckim twoja wiedza wykracza poza poleganie na maksymach / zasadach. Możesz „poczuć”, gdy reguła nie ma zastosowania.
I zgadzam się, że nieco oszukiwałem przykładem „prawdziwego świata”, dlaczego MI jest niezadowolony.
Spójrzmy na środowisko interfejsu użytkownika. W szczególności spójrzmy na kilka widżetów, które mogą na początku wyglądać tak, jakby były po prostu kombinacją dwóch innych. Jak ComboBox. ComboBox to TextBox, który ma obsługiwaną DropDownList. Czyli mogę wpisać wartość lub wybrać z ustalonej wcześniej listy wartości. Naiwnym podejściem byłoby dziedziczenie ComboBox po TextBox i DropDownList.
Ale pole tekstowe czerpie swoją wartość z tego, co wpisał użytkownik. Podczas gdy DDL otrzymuje swoją wartość z tego, co wybiera użytkownik. Kto ma precedens? DDL mógł zostać zaprojektowany do weryfikacji i odrzucania wszelkich danych wejściowych, których nie było na oryginalnej liście wartości. Czy przekraczamy tę logikę? Oznacza to, że musimy ujawnić wewnętrzną logikę, aby dziedziczące mogły zastąpić. Lub, co gorsza, dodaj logikę do klasy podstawowej, która jest tam tylko w celu obsługi podklasy (naruszając zasadę inwersji zależności ).
Unikanie MI pomaga całkowicie ominąć tę pułapkę. Może to prowadzić do wyodrębnienia typowych cech wielokrotnego użytku widgetów interfejsu użytkownika, aby można je było stosować w razie potrzeby. Doskonałym tego przykładem jest właściwość dołączona WPF, która pozwala elementowi ramy w WPF na dostarczenie właściwości, z której inny element ramy może korzystać bez dziedziczenia po nadrzędnym elemencie ramy.
Na przykład Siatka jest panelem układu w WPF i ma właściwości związane z kolumną i rzędem, które określają, gdzie element potomny powinien zostać umieszczony w układzie siatki. Bez dołączonych właściwości, jeśli chcę ustawić przycisk w siatce, przycisk musiałby pochodzić z siatki, aby mógł mieć dostęp do właściwości kolumny i wiersza.
Deweloperzy posunęli tę koncepcję dalej i wykorzystali dołączone właściwości jako sposób komponowania zachowania (na przykład tutaj jest mój post na temat tworzenia sortowalnego GridView przy użyciu dołączonych właściwości napisanych przed WPF zawierającym DataGrid). Podejście to uznano za wzorzec projektowy XAML o nazwie Attached Behaviors .
Miejmy nadzieję, że dostarczyło to nieco więcej informacji na temat tego, dlaczego wielokrotne dziedziczenie jest zwykle odrzucane.
źródło
Zezwolenie na wielokrotne dziedziczenie sprawia, że reguły dotyczące przeciążeń funkcji i wirtualnej wysyłki są znacznie trudniejsze, a także implementacja języka wokół układów obiektów. Wpływają one w znacznym stopniu na projektantów / implementatorów języka i podnoszą już wysoki poziom, aby język był gotowy, stabilny i adoptowany.
Innym częstym argumentem, który widziałem (i czasami stawiałem), jest to, że mając dwie klasy podstawowe +, twój obiekt prawie niezmiennie narusza zasadę pojedynczej odpowiedzialności. Albo dwie + klasy podstawowe są ładnymi, samodzielnymi klasami z własną odpowiedzialnością (powodującą naruszenie), albo są częściowymi / abstrakcyjnymi typami, które współpracują ze sobą, tworząc jedną spójną odpowiedzialność.
W tym drugim przypadku masz 3 scenariusze:
Osobiście uważam, że wielokrotne dziedziczenie ma zły rap i że dobrze wykonany system kompozycji w stylu cechy byłby naprawdę potężny / użyteczny ... ale istnieje wiele sposobów, w których można go źle wdrożyć i wiele powodów niezbyt dobry pomysł w języku takim jak C ++.
[edytuj] odnośnie twojego przykładu, to absurdalne. Kia ma elektronikę. To ma silnik. Podobnie, jego elektronika ma źródło zasilania, które akurat jest akumulatorem samochodowym. Dziedziczenie, nie mówiąc już o wielokrotnym dziedziczeniu, nie ma tam miejsca.
źródło
Jedynym powodem, dla którego jest niedozwolony, jest to, że ułatwia ludziom strzelanie sobie w stopę.
Podczas tego rodzaju dyskusji zwykle pojawiają się argumenty, czy elastyczność posiadania narzędzi jest ważniejsza niż bezpieczeństwo nie zrzucania stopy. Nie ma zdecydowanie poprawnej odpowiedzi na ten argument, ponieważ jak większość innych rzeczy w programowaniu, odpowiedź zależy od kontekstu.
Jeśli Twoi programiści dobrze znają MI, a MI ma sens w kontekście tego, co robisz, to bardzo tęsknisz w języku, który go nie obsługuje. Jednocześnie, jeśli zespół nie czuje się z tym komfortowo lub nie ma takiej potrzeby, a ludzie używają go „tylko dlatego, że mogą”, to przynosi efekt przeciwny do zamierzonego.
Ale nie, nie istnieje przekonujący absolutnie prawdziwy argument, który dowodzi, że wielokrotne dziedziczenie jest złym pomysłem.
EDYTOWAĆ
Odpowiedzi na to pytanie wydają się być jednomyślne. Dla bycia rzecznikiem diabła podam dobry przykład wielokrotnego dziedziczenia, gdzie nie zrobienie tego prowadzi do włamań.
Załóżmy, że projektujesz aplikację na rynki kapitałowe. Potrzebujesz modelu danych dla papierów wartościowych. Niektóre papiery wartościowe są produktami kapitałowymi (akcje, fundusze inwestycyjne nieruchomości itp.), Inne są długami (obligacje, obligacje korporacyjne), inne są instrumentami pochodnymi (opcje, kontrakty futures). Więc jeśli unikasz MI, utworzysz bardzo jasne, proste drzewo dziedziczenia. Akcje odziedziczą kapitał własny, obligacje odziedziczą dług. Jak dotąd świetnie, ale co z instrumentami pochodnymi? Mogą być oparte na produktach podobnych do instrumentów kapitałowych lub debetowych? Ok, myślę, że sprawimy, że nasza gałąź dziedzictwa spadnie bardziej. Należy pamiętać, że niektóre instrumenty pochodne opierają się na produktach kapitałowych, produktach dłużnych lub żadnym z nich. Więc nasze drzewo spadkowe komplikuje się. Następnie pojawia się analityk biznesowy i mówi, że teraz obsługujemy indeksowane papiery wartościowe (opcje indeksowe, opcje indeksowane w przyszłości). I te rzeczy mogą być oparte na kapitale własnym, długu lub instrumencie pochodnym. Robi się bałagan! Czy moja przyszła opcja na indeks pochodzi z kapitału własnego-> akcji-> opcji-> indeksu? Dlaczego nie Equity-> Stock-> Index-> Option? Co jeśli pewnego dnia znajdę oba w swoim kodzie (To się stało; prawdziwa historia)?
Problem polega na tym, że te podstawowe typy można mieszać w dowolnej permutacji, która naturalnie nie wywodzi się od siebie. Te są definiowane przez obiekty to związek, więc kompozycja nie ma sensu w ogóle. Wielokrotne dziedziczenie (lub podobna koncepcja miksów) jest tutaj jedyną logiczną reprezentacją.
Prawdziwym rozwiązaniem tego problemu jest zdefiniowanie i połączenie typów kapitału własnego, zadłużenia, instrumentu pochodnego, indeksu przy użyciu wielokrotnego dziedziczenia w celu utworzenia modelu danych. Stworzy to obiekty, które zarówno będą miały sens, jak i umożliwią łatwe ponowne użycie kodu.
źródło
Equity
iDebt
oba wdrażająISecurity
.Derivative
maISecurity
właściwość. Może to być samo w sobieISecurity
właściwe (nie znam finansów).IndexedSecurities
ponownie zawiera właściwość interfejsu, która zostanie zastosowana do typów, na których może być oparta. Jeśli są wszystkieISecurity
, wszystkie mająISecurity
właściwości i można je dowolnie zagnieżdżać ...Wydaje się, że inne tutaj odpowiedzi dotyczą głównie teorii. Oto konkretny uproszczony przykład w języku Python, który tak naprawdę rozbiłem, co wymagało sporego refaktoryzacji:
Bar
został napisany przy założeniu, że ma własną implementacjęzeta()
, co ogólnie jest dość dobrym założeniem. Podklasa powinna zastąpić ją odpowiednio, aby działała poprawnie. Niestety, nazwy były przypadkowo takie same - robili raczej różne rzeczy, aleBar
teraz nazywaliFoo
implementację:Jest to dość frustrujące, gdy nie są zgłaszane żadne błędy, aplikacja zaczyna działać tak bardzo lekko, a zmiana kodu, która ją spowodowała (tworzenie
Bar.zeta
), nie wydaje się być przyczyną problemu.źródło
super()
?super()
obeszliBang
naBar, Foo
również rozwiązałaby problem - ale masz rację, +1.Bar.zeta(self)
.Twierdziłbym, że nie ma żadnych prawdziwych problemów z MI w odpowiednim języku . Kluczem jest zezwolenie na struktury diamentowe, ale wymaga, aby podtypy zapewniały własne zastępowanie, zamiast wybierania przez kompilator jednej z implementacji na podstawie jakiejś reguły.
Robię to w języku Guava , nad którym pracuję. Jedną z cech Guava jest to, że możemy wywoływać implementację metody określonego nadtypu. Łatwo więc wskazać, która implementacja typu nadrzędnego powinna zostać „odziedziczona”, bez specjalnej składni:
Gdybyśmy nie dali
OrderedSet
swojegotoString
, otrzymalibyśmy błąd kompilacji. Bez niespodzianek.Uważam, że MI jest szczególnie przydatny w przypadku kolekcji. Na przykład lubię używać
RandomlyEnumerableSequence
typu, aby uniknąć deklarowaniagetEnumerator
tablic, deques i tak dalej:Gdybyśmy nie mieli MI, moglibyśmy napisać
RandomAccessEnumerator
dla kilku kolekcji do użycia, ale konieczność napisania krótkiejgetEnumerator
metody wciąż dodaje płytę podstawową.Podobnie MI jest przydatna dla dziedziczy standardowe implementacje
equals
,hashCode
atoString
do zbiorów.źródło
toString
zachowania, więc zastąpienie jego zachowania nie narusza zasady substytucji. Czasami zdarzają się również „konflikty” między metodami, które mają takie samo zachowanie, ale różne algorytmy, szczególnie w przypadku kolekcji.Ordered extends Set and Sequence
a”Diamond
? To tylko Join. Brakujehat
wierzchołka. Dlaczego nazywasz to diamentem? Zapytałem tutaj, ale wydaje się, że jest to tabu. Skąd wiedziałeś, że musisz nazwać tę strukturę triangualarową Diamentem, a nie dołączyć?Top
typ, który deklarujetoString
, więc w rzeczywistości istniała struktura diamentowa. Ale myślę, że masz rację - „połączenie” bez struktury diamentowej stwarza podobny problem, a większość języków obsługuje oba przypadki w ten sam sposób.Dziedziczenie, wielokrotne lub inne, nie jest tak ważne. Jeśli dwa obiekty różnego typu są zastępowalne, to jest to ważne, nawet jeśli nie są połączone przez dziedziczenie.
Połączona lista i ciąg znaków nie mają ze sobą wiele wspólnego i nie muszą być połączone przez dziedziczenie, ale jest to przydatne, jeśli mogę użyć
length
funkcji, aby uzyskać liczbę elementów w jednym z nich.Dziedziczenie to sztuczka pozwalająca uniknąć powtarzającej się implementacji kodu. Jeśli dziedziczenie oszczędza ci pracę, a wielokrotne dziedziczenie oszczędza ci jeszcze więcej pracy w porównaniu do pojedynczego dziedziczenia, to wszystko, co jest potrzebne, jest uzasadnieniem.
Podejrzewam, że niektóre języki nie wdrażają zbyt dobrze wielokrotnego dziedziczenia, a dla praktyków tych języków oznacza to wielokrotne dziedziczenie. Wspomnij o wielokrotnym dziedziczeniu dla programisty w C ++, a przychodzi mi na myśl kwestia problemów, gdy klasa kończy się na dwóch kopiach bazy za pośrednictwem dwóch różnych ścieżek dziedziczenia, a także o tym, czy używać
virtual
na klasie bazowej, oraz zamieszanie na temat sposobu wywoływania destruktorów , i tak dalej.W wielu językach dziedziczenie klasy jest powiązane z dziedziczeniem symboli. Gdy wywodzisz klasę D z klasy B, nie tylko tworzysz relację typu, ale ponieważ klasy te służą również jako leksykalne przestrzenie nazw, zajmujesz się importem symboli z obszaru nazw B do obszaru nazw D, oprócz semantyka tego, co dzieje się z samymi typami B i D. Wielokrotne dziedziczenie powoduje zatem konflikt symboli. Jeśli odziedziczymy metodę
card_deck
igraphic
oba z nich „mają”draw
metodę, co to oznacza dladraw
wynikowego obiektu? System obiektowy, który nie ma tego problemu, jest systemem Common Lisp. Być może nieprzypadkowo w programach Lisp wykorzystywane jest wielokrotne dziedziczenie.Źle zaimplementowane, niewygodne rzeczy (takie jak wielokrotne dziedziczenie) powinny być znienawidzone.
źródło
length
operacji jest użyteczne niezależnie od koncepcji dziedziczenia (co w tym przypadku nie pomaga: prawdopodobnie nie będziemy w stanie niczego osiągnąć, próbując podzielić implementację między dwie metodylength
funkcji). Niemniej jednak może istnieć pewne dziedziczenie abstrakcyjne: np. Zarówno lista, jak i łańcuch są sekwencji typów (ale która nie zapewnia żadnej implementacji).O ile mogę stwierdzić, część problemu (oprócz tego, że projekt jest nieco trudniejszy do zrozumienia (ale łatwiejszy do kodowania)) polega na tym, że kompilator zaoszczędzi wystarczająco dużo miejsca na dane klasy, pozwalając na to ogromną ilość marnotrawstwo pamięci w następującym przypadku:
(Mój przykład może nie być najlepszy, ale spróbuj uzyskać sedno dotyczące wielu miejsc pamięci w tym samym celu, to była pierwsza rzecz, jaka przyszła mi do głowy: P)
Przyjmuj DDD w przypadku, gdy pies klasy wystaje od caninusa i zwierzaka, caninus ma zmienną, która wskazuje ilość pokarmu, którą powinien jeść (liczba całkowita) pod nazwą dietKg, ale zwierzę ma również inną zmienną do tego celu, zwykle pod tym samym name (chyba że ustawisz inną nazwę zmiennej, wtedy będziesz kodyfikował dodatkowy kod, który był początkowym problemem, którego chciałeś uniknąć, aby obsłużyć i zachować integralność zmiennych typu bouth), wtedy będziesz miał dwie przestrzenie pamięci dla dokładnego w tym samym celu, aby tego uniknąć, będziesz musiał zmodyfikować kompilator, aby rozpoznał tę nazwę w tej samej przestrzeni nazw i po prostu przypisał do tych danych jedną przestrzeń pamięci, co niestety jest możliwe do ustalenia w czasie kompilacji.
Można oczywiście zaprojektować język, aby określić, że taka zmienna może już mieć przestrzeń zdefiniowaną gdzie indziej, ale na końcu programista powinien określić, gdzie jest ta przestrzeń pamięci, do której odwołuje się ta zmienna (i ponownie dodatkowy kod).
Zaufaj mi, ludzie bardzo mocno wdrażają tę myśl o tym wszystkim, ale cieszę się, że zapytałeś, twój miły szacunek to ten, który zmienia paradygmaty;) i rozumiem, nie mówię, że jest to niemożliwe (ale wiele założeń i wielofazowy kompilator musi zostać zaimplementowany, a naprawdę skomplikowany), po prostu mówię, że jeszcze nie istnieje, jeśli uruchomisz projekt własnego kompilatora zdolnego do wykonania tego (wielokrotne dziedziczenie), daj mi znać, ja Z przyjemnością dołączę do twojego zespołu.
źródło
Od dłuższego czasu tak naprawdę nigdy nie przyszło mi do głowy, jak bardzo różne są niektóre zadania programistyczne od innych - i jak bardzo pomaga to, jeśli używane języki i wzorce są dostosowane do przestrzeni problemów.
Kiedy pracujesz sam lub w większości izolowany od kodu, który napisałeś, jest to zupełnie inna przestrzeń problemowa niż dziedziczenie bazy kodów od 40 osób w Indiach, które pracowały nad nią przez rok, zanim przekazały ci ją bez żadnej przejściowej pomocy.
Wyobraź sobie, że właśnie zostałeś zatrudniony przez swoją wymarzoną firmę, a następnie odziedziczyłeś taką bazę kodu. Ponadto wyobraź sobie, że konsultanci uczyli się (a więc i zauroczeni) dziedziczeniem i wielokrotnym spadkiem ... Czy możesz sobie wyobrazić, nad czym możesz pracować.
Po odziedziczeniu kodu najważniejszą cechą jest to, że jest zrozumiały, a elementy są izolowane, aby można było nad nimi pracować niezależnie. Pewnie, kiedy piszesz struktury kodu, takie jak wielokrotne dziedziczenie, może zaoszczędzić ci trochę duplikacji i wydaje się pasować do twojego logicznego nastroju w tym czasie, ale następny facet po prostu ma więcej rzeczy do rozwiązania.
Każde połączenie w twoim kodzie utrudnia również zrozumienie i modyfikację elementów niezależnie, podwójnie z wielokrotnym dziedziczeniem.
Kiedy pracujesz w zespole, chcesz kierować reklamy na najprostszy możliwy kod, który absolutnie nie daje żadnej redundantnej logiki (To właśnie tak naprawdę oznacza DRY, nie dlatego, że nie powinieneś pisać zbyt wiele, po prostu nigdy nie musisz zmieniać kodu w 2 miejsca do rozwiązania problemu!)
Istnieją prostsze sposoby na osiągnięcie kodu DRY niż wielokrotne dziedziczenie, więc włączenie go w języku może tylko otworzyć się na problemy wprowadzone przez inne osoby, które mogą nie być na tym samym poziomie zrozumienia co ty. To nawet kuszące, jeśli twój język nie jest w stanie zaoferować ci prostego / mniej złożonego sposobu na utrzymanie kodu w stanie SUCHYM.
źródło
Największym argumentem przeciwko wielokrotnemu dziedziczeniu jest to, że można zapewnić pewne użyteczne umiejętności, a niektóre przydatne aksjomaty można utrzymać w ramach, które poważnie je ograniczają (*), ale nie można ich zapewnić i / lub utrzymać bez takich ograniczeń. Pomiędzy nimi:
Możliwość posiadania wielu osobno skompilowanych modułów obejmuje klasy, które dziedziczą po klasach innych modułów, i rekompiluje moduł zawierający klasę podstawową bez konieczności ponownej kompilacji każdego modułu, który dziedziczy z tej klasy podstawowej.
Zdolność typu do dziedziczenia implementacji elementu z klasy nadrzędnej bez konieczności ponownego implementowania typu pochodnego
Aksjomat, że dowolna instancja obiektu może być skierowana w górę lub w dół bezpośrednio do siebie lub dowolnego z jej podstawowych typów, oraz takie upcasty i downcasty, w dowolnej kombinacji lub sekwencji, zawsze zachowują tożsamość
Aksjomat, że jeśli klasa pochodna przesłania i łańcuchuje do elementu klasy podstawowej, element podstawowy zostanie wywołany bezpośrednio przez kod łańcuchowy i nie będzie wywołany nigdzie indziej.
(*) Zazwyczaj wymagając, aby typy obsługujące wielokrotne dziedziczenie były zadeklarowane jako „interfejsy”, a nie klasy, i nie zezwalając interfejsom na robienie wszystkiego, co mogą zrobić normalne klasy.
Jeśli ktoś chce pozwolić na uogólnione wielokrotne dziedziczenie, coś innego musi ustąpić. Jeśli oba X i Y dziedziczą po B, oba zastępują ten sam element M i łańcuch do implementacji podstawowej, a jeśli D dziedziczy po X i Y, ale nie zastępuje M, to podając instancję q typu D, co powinno (( B) q) .M () zrobić? Odrzucenie takiego rzutowania naruszyłoby aksjomat, który mówi, że dowolny obiekt może być skierowany w górę do dowolnego typu podstawowego, ale każde możliwe zachowanie rzutowania i wywołania elementu naruszyłoby aksjomat dotyczący łączenia metod. Można wymagać, aby klasy były ładowane tylko w połączeniu z określoną wersją klasy podstawowej, dla której zostały skompilowane, ale często jest to niewygodne. Realizacja odmowy załadowania dowolnego typu, w którym do dowolnego przodka można dotrzeć więcej niż jedną drogą, może być wykonalna, ale znacznie ograniczyłoby użyteczność wielokrotnego dziedziczenia. Zezwalanie na wspólne ścieżki dziedziczenia tylko wtedy, gdy nie ma żadnych konfliktów, stworzyłoby sytuacje, w których stara wersja X byłaby kompatybilna ze starym lub nowym Y, a stare Y byłoby kompatybilne ze starym lub nowym X, ale nowe X i nowe Y być kompatybilnym, nawet jeśli nie zrobiło się nic, co samo w sobie powinno być przełomową zmianą.
Niektóre języki i struktury pozwalają na wielokrotne dziedziczenie, zgodnie z teorią, że to, co zyskało MI, jest ważniejsze niż to, co należy porzucić, aby na to pozwolić. Koszty MI są jednak znaczące i w wielu przypadkach interfejsy zapewniają 90% korzyści MI przy niewielkim ułamku kosztów.
źródło
FirstDerivedFoo
zastąpienieBar
nie robi nic poza łańcuchem do chronionego nie-wirtualnegoFirstDerivedFoo_Bar
, iSecondDerivedFoo
zastępuje łańcuchy do chronionego nie-wirtualnegoSecondDerivedBar
, a jeśli kod, który chce uzyskać dostęp do metody podstawowej, używa tych chronionych nie-wirtualnych metod, może być wykonalne podejście ...base.VirtualMethod
), ale nie znam żadnego wsparcia językowego, które by szczególnie ułatwiło takie podejście. Chciałbym, aby istniał czysty sposób na dołączenie jednego bloku kodu zarówno do niechronionej metody chronionej, jak i metody wirtualnej z tym samym podpisem, bez konieczności stosowania metody wirtualnej „one-lineer” (która kończy się na więcej niż jednej linii w kodzie źródłowym).