Czy istnieje jakiś „prawdziwy” powód, dla którego nienawidzono wielokrotnego dziedziczenia?

123

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?

  1. Ponieważ jest to elektronika. Dziedziczenie vs. składanie zawsze powinno być relacją typu „jest relacją a relacją typu relacja”.

  2. Ponieważ jest to elektronika. Przewody, płytki drukowane, przełączniki itp. To wszystko w górę iw dół.

  3. 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.

Panzercrisis
źródło
24
zachęca także do dziedziczenia nad kompozycją ... (kiedy powinno być na odwrót)
maniak zapadkowy
34
„Możesz to zepsuć” to doskonale uzasadniony powód do usunięcia funkcji. Współczesny projekt językowy jest w tym miejscu nieco rozczłonkowany, ale ogólnie można klasyfikować języki na „Moc z ograniczeń” i „Moc z elastyczności”. Żadna z tych rzeczy nie jest „poprawna”, nie sądzę, obie mają mocne strony. MI jest jedną z tych rzeczy, które są używane do Zła częściej niż nie, a zatem restrykcyjne języki go usuwają. Elastyczne nie, ponieważ „częściej niż nie” nie jest „dosłownie zawsze”. To powiedziawszy, mieszanki / cechy są lepszym rozwiązaniem w ogólnym przypadku, tak myślę.
Phoshi
8
Istnieją bezpieczniejsze alternatywy dla wielokrotnego dziedziczenia w kilku językach, które wykraczają poza interfejsy. Sprawdź Scali Traits- działają one jak interfejsy z opcjonalną implementacją, ale mają pewne ograniczenia, które pomagają zapobiegać problemom takim jak problem z diamentem.
KChaloux
5
Zobacz także to wcześniej zamknięte pytanie: programmers.stackexchange.com/questions/122480/…
Doc Brown
19
KiaSpectranie jest Electronic ; to jest elektronika, a może być ElectronicCar(co byłoby rozszerzyć Car...)
Brian S

Odpowiedzi:

68

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.

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

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.

Michael Brown
źródło
14
„Człowieku, naprawdę potrzebuję MI, aby zrobić to poprawnie” - zobacz moją odpowiedź na przykład. W skrócie, ortogonalne koncepcje, które spełniają relację is-a, mają sens w MI. Są bardzo rzadkie, ale istnieją.
MrFox
13
Człowieku, nienawidzę tego przykładu z kwadratowym prostokątem. Istnieje wiele bardziej pouczających przykładów, które nie wymagają modyfikacji.
asmeurer
9
Uwielbiam to, że odpowiedzią na „podać prawdziwy przykład” było omówienie fikcyjnego stworzenia. Doskonała ironia +1
jjathman
3
Mówisz więc, że wielokrotne dziedziczenie jest złe, ponieważ dziedziczenie jest złe (twoje przykłady dotyczą dziedziczenia pojedynczego interfejsu). Dlatego na podstawie samej odpowiedzi język powinien albo pozbyć się dziedziczenia i interfejsów, albo posiadać wielokrotne dziedziczenie.
ctrl-alt-delor
12
Nie sądzę, żeby te przykłady naprawdę działały ... nikt nie mówi, że pegaz jest ptakiem i koniem ... mówisz, że to skrzydlaty zwierzę i kopytny zwierzę. Przykład kwadratu i prostokąta ma nieco więcej sensu, ponieważ znajdujesz się w obiekcie, który zachowuje się inaczej, niż myślisz na podstawie przyjętej definicji. Myślę jednak, że taka sytuacja byłaby winą programistów za to, że nie wychwycili tego (jeśli rzeczywiście okazałoby się to problemem).
richard
60

Czy jest coś, czego tu nie widzę?

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:

  1. A nic nie wie o B - Świetnie, mogłeś łączyć klasy, bo miałeś szczęście.
  2. A wie o B - dlaczego A nie odziedziczył po B?
  3. A i B wiedzą o sobie - dlaczego nie zrobiłeś tylko jednej klasy? Jaka jest korzyść z tego, że te elementy są tak sprzężone, ale częściowo wymienne?

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.

Telastyn
źródło
8
Innym interesującym pytaniem jest, w jaki sposób inicjowane są klasy podstawowe + ich klasy podstawowe. Hermetyzacja> Dziedziczenie.
jozefg
„dobrze wykonany system kompozycji w stylu cechy byłby naprawdę potężny / użyteczny ...” - Są one znane jako mixiny i potężne / przydatne. Mixiny można wdrożyć za pomocą MI, ale dla mixin nie jest wymagane wielokrotne dziedziczenie. Niektóre języki z natury obsługują miksy, bez MI.
BlueRaja - Danny Pflughoeft
W szczególności w Javie mamy już wiele interfejsów i INVOKEINTERFACE, więc MI nie miałoby żadnych oczywistych konsekwencji dla wydajności.
Daniel Lubarov,
Tetastyn, zgadzam się, że przykład był zły i nie służył jego pytaniu. Dziedziczenie nie dotyczy przymiotników (elektronicznych), lecz rzeczowników (pojazdów).
richard
29

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.

MrFox
źródło
27
Pracowałem w finansach. Istnieje powód, dla którego w oprogramowaniu finansowym preferowane jest programowanie funkcjonalne niż OO. I wskazałeś na to. MI nie rozwiązuje tego problemu, tylko go zaostrza. Uwierz mi, nie mogę ci powiedzieć, ile razy słyszałem „tak po prostu… oprócz” w kontaktach z klientami finansowymi, które stały się zmorą mojego istnienia.
Michael Brown
8
Wydaje mi się to bardzo łatwe do rozwiązania w przypadku interfejsów ... w rzeczywistości wydaje się, że lepiej nadaje się do interfejsów niż cokolwiek innego. Equityi Debtoba wdrażają ISecurity. Derivativema ISecuritywłaściwość. Może to być samo w sobie ISecuritywłaściwe (nie znam finansów). IndexedSecuritiesponownie zawiera właściwość interfejsu, która zostanie zastosowana do typów, na których może być oparta. Jeśli są wszystkie ISecurity, wszystkie mają ISecuritywłaściwości i można je dowolnie zagnieżdżać ...
Bobson,
11
@ Bobson pojawia się problem polegający na tym, że trzeba ponownie zaimplementować interfejs dla każdego implementatora, aw wielu przypadkach jest on taki sam. Możesz użyć delegacji / składu, ale wtedy utracisz dostęp do prywatnych członków. A ponieważ Java nie ma delegatów / lambdów ... dosłownie nie ma dobrego sposobu na zrobienie tego. Z wyjątkiem ewentualnie narzędzia Aspect, takiego jak Aspect4J
Michael Brown,
10
@ Bobson dokładnie to, co powiedział Mike Brown. Tak, możesz zaprojektować rozwiązanie z interfejsami, ale będzie niezgrabne. Twoja intuicja w korzystaniu z interfejsów jest jednak bardzo poprawna, jest to ukryte pragnienie mieszania / wielokrotnego dziedziczenia :).
MrFox
11
Dotyka to dziwności, w której programiści OO wolą myśleć w kategoriach dziedziczenia i często nie akceptują delegacji jako ważnego podejścia do OO, które zazwyczaj daje lżejsze, łatwiejsze w utrzymaniu i rozszerzalne rozwiązanie. Pracując w dziale finansów i opieki zdrowotnej widziałem niemożliwy do opanowania bałagan, jaki MI może stworzyć, zwłaszcza, że ​​przepisy podatkowe i zdrowotne zmieniają się z roku na rok, a definicja obiektu OSTATNI rok jest niepoprawna w TYM roku (ale wciąż musi pełnić funkcję któregokolwiek roku i muszą być wymienne). Kompozycja pozwala uzyskać łatwiejszy do przetestowania kod, który z czasem kosztuje mniej.
Shaun Wilson
11

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:

class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()

Barzostał 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, ale Barteraz nazywali Fooimplementację:

foozeta
---
barstuff
foozeta

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.

Izkata
źródło
Jak można uniknąć problemu z diamentem poprzez połączenia z super()?
Panzercrisis
@Panzercrisis Przepraszamy, nieważne, że ta część. I misremembered jakim problemem „problem diament” zazwyczaj odnosi się do - Python miał oddzielny problem spowodowany przez kształcie rombu dziedziczenia że super()obeszli
Izkata
5
Cieszę się, że MI C ++ jest znacznie lepiej zaprojektowany. Powyższy błąd jest po prostu niemożliwy. Musisz ręcznie ujednoznacznić go, zanim kod się skompiluje.
Thomas Eding,
2
Zmiana kolejności dziedziczenia Bangna Bar, Foorównież rozwiązałaby problem - ale masz rację, +1.
Sean Vieira,
1
@ThomasEding można ręcznie disambiguate nadal: Bar.zeta(self).
Tyler Crompton
9

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:

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

Gdybyśmy nie dali OrderedSetswojego toString, otrzymalibyśmy błąd kompilacji. Bez niespodzianek.

Uważam, że MI jest szczególnie przydatny w przypadku kolekcji. Na przykład lubię używać RandomlyEnumerableSequencetypu, aby uniknąć deklarowania getEnumeratortablic, deques i tak dalej:

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

Gdybyśmy nie mieli MI, moglibyśmy napisać RandomAccessEnumeratordla kilku kolekcji do użycia, ale konieczność napisania krótkiej getEnumeratormetody wciąż dodaje płytę podstawową.

Podobnie MI jest przydatna dla dziedziczy standardowe implementacje equals, hashCodea toStringdo zbiorów.

Daniel Lubarov
źródło
1
@AndyzSmith, rozumiem twój punkt widzenia, ale nie wszystkie konflikty dziedziczenia są faktycznymi problemami semantycznymi. Rozważ mój przykład - żaden z typów nie składa obietnic dotyczących toStringzachowania, 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.
Daniel Lubarov,
1
@Asmageddon zgodził się, moje przykłady dotyczą tylko funkcjonalności. Jakie są zalety miksin? Przynajmniej w Scali cechy są w gruncie rzeczy jedynie abstrakcyjnymi klasami, które pozwalają na MI - nadal można je wykorzystać do identyfikacji i nadal powodować problem z diamentem. Czy istnieją inne języki, w których mixiny mają bardziej interesujące różnice?
Daniel Lubarov,
1
Technicznie abstrakcyjne klasy mogą być używane jako interfejsy, dla mnie korzyść polega po prostu na tym, że sam konstrukt jest „wskazówką użytkowania”, co oznacza, że ​​wiem, czego się spodziewać, kiedy zobaczę kod. To powiedziawszy, obecnie koduję w Lua, który nie ma nieodłącznego modelu OOP - istniejące systemy klas zwykle pozwalają na dołączanie tabel jako mixin, a ten, który koduję, wykorzystuje klasy jako mixiny. Jedyną różnicą jest to, że możesz używać (pojedynczego) dziedziczenia dla tożsamości, miksów dla funkcjonalności (bez informacji o tożsamości) i interfejsów do dalszych kontroli. Może nie jest to jakoś lepsze, ale ma dla mnie sens.
Llamageddon
2
Dlaczego nazywacie „ Ordered extends Set and Sequencea” Diamond? To tylko Join. Brakuje hatwierzchoł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ć?
Val
1
@RecognizeEvilasWaste ten język ma domyślny Toptyp, który deklaruje toString, 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.
Daniel Lubarov,
7

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ć lengthfunkcji, 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ć virtualna 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_decki graphicoba z nich „mają” drawmetodę, co to oznacza dla drawwynikowego 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.

Kaz
źródło
2
Twój przykład z metodą length () kontenerów list i łańcuchów jest zły, ponieważ te dwie rzeczy robią zupełnie inne rzeczy. Dziedziczenie nie ma na celu zmniejszenia liczby powtórzeń kodu. Jeśli chcesz zmniejszyć liczbę powtórzeń kodu, przefiltruj części wspólne do nowej klasy.
BЈовић
1
@ BЈовић Obaj nie robią „zupełnie” różnych rzeczy.
Kaz
1
Składając ciąg i listę , widać wiele powtórzeń. Używanie dziedziczenia do implementacji tych powszechnych metod byłoby po prostu błędne.
BЈовић
1
@ BЈовић W odpowiedzi nie powiedziano, że ciąg i lista powinny być połączone przez dziedziczenie; wręcz przeciwnie. Zachowanie możliwości zastąpienia listy lub łańcucha w lengthoperacji 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 metody lengthfunkcji). 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).
Kaz
2
Dziedziczenie to także sposób na refaktoryzację części do wspólnej klasy: mianowicie klasy podstawowej.
Kaz
1

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.

Ordiel
źródło
1
W którym momencie kompilator mógłby kiedykolwiek założyć, że zmienne o tej samej nazwie z różnych klas nadrzędnych można bezpiecznie łączyć? Jaką masz gwarancję, że caninus.diet i pet.diet faktycznie służą temu samemu celowi? Co zrobiłbyś z funkcją / metodami o tej samej nazwie?
Mr.Mindor,
hahaha Całkowicie się z tobą zgadzam @ Mr.Mindor to dokładnie to, co mówię w mojej odpowiedzi, jedynym sposobem, który przychodzi mi na myśl, aby zbliżyć się do tego podejścia, jest programowanie prototypowe, ale nie mówię, że to niemożliwe i to jest dokładnie powód, dla którego powiedziałem, że w czasie kompilacji trzeba poczynić wiele domniemań, a programista musi napisać dodatkowe „dane” / specyfikacje (jednak jest to więcej kodowania, co było pierwotnym problemem)
Ordiel
-1

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.

Bill K.
źródło
Ponadto wyobraź sobie, że konsultanci się uczyli - widziałem tak wiele języków spoza MI (np. .Net), w których konsultanci oszaleli na punkcie interfejsów DI i IoC, aby sprawić, że cała ta sprawa była nieprzeniknionym chaosem. MI nie pogarsza tego, prawdopodobnie czyni to lepszym.
gbjbaanb
@gbjbaanb Masz rację, łatwo jest komuś, kto nie został spalony, zepsuć żadnej funkcji - w rzeczywistości jest prawie wymagane nauczenie się tej funkcji, aby ją zepsuć, aby zobaczyć, jak najlepiej z niej korzystać. Jestem trochę ciekawy, ponieważ wydaje się, że mówisz, że brak MI zmusza więcej DI / IoC, których nie widziałem - nie sądzę, że kod konsultanta, który dostałeś ze wszystkimi gównianymi interfejsami / DI, byłby lepiej też mieć całe zwariowane drzewo dziedzictwa wiele do wielu, ale może to być mój brak doświadczenia w MI. Czy masz stronę, którą mógłbym przeczytać o zamianie DI / IoC na MI?
Bill K
-3

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.

supercat
źródło
Czy głosujący w dół głosowaliby, że komentują? Uważam, że moja odpowiedź zawiera informacje, które nie występują w innych językach, w odniesieniu do korzyści semantycznych, które można uzyskać poprzez niedopuszczenie do wielokrotnego dziedziczenia. Fakt, że dopuszczenie wielokrotnego dziedziczenia wymagałoby porzucenia innych przydatnych rzeczy, byłby dobrym powodem dla każdego, kto ceni te inne rzeczy bardziej niż wielokrotne dziedziczenie, aby sprzeciwić się MI.
supercat
2
Problemy z MI można łatwo rozwiązać lub całkowicie uniknąć, stosując te same techniki, których używa się w przypadku pojedynczego dziedziczenia. Żadne z wymienionych przez ciebie umiejętności / aksjomatów nie jest z natury hamowane przez MI, z wyjątkiem drugiej ... a jeśli dziedziczysz po dwóch klasach, które zapewniają różne zachowania dla tej samej metody, to i tak musisz rozwiązać tę dwuznaczność. Ale jeśli musisz to zrobić, prawdopodobnie już nadużywasz dziedziczenia.
cHao
@ cHao: Jeśli ktoś wymaga, aby klasy pochodne musiały zastępować metody nadrzędne, a nie łączyć się z nimi (ta sama reguła co interfejsy), można uniknąć problemów, ale jest to dość poważne ograniczenie. Jeśli ktoś użyje wzorca, w którym FirstDerivedFoozastąpienie Barnie robi nic poza łańcuchem do chronionego nie-wirtualnego FirstDerivedFoo_Bar, i SecondDerivedFoozastępuje łańcuchy do chronionego nie-wirtualnego SecondDerivedBar, 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 ...
supercat
... (unikając składni 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).
supercat
W MI nie ma nieodłącznego wymogu, aby klasy nie wywoływały metod bazowych i nie ma konkretnego powodu, dla którego byłby potrzebny. To trochę dziwne, aby obwiniać MI, biorąc pod uwagę, że problem dotyczy również SI.
cHao