C'è qualche "vera" ragione per cui l'ereditarietà multipla è odiata?


123

Mi è sempre piaciuta l'idea di avere l'ereditarietà multipla supportata in una lingua. Molto spesso però è stato intenzionalmente dimenticato, e la presunta "sostituzione" sono interfacce. Le interfacce semplicemente non coprono tutto lo stesso terreno dell'ereditarietà multipla, e questa restrizione può occasionalmente portare a più codice della caldaia.

L'unica ragione di base che io abbia mai sentito per questo è il problema dei diamanti con le classi base. Non posso accettarlo. Per me, viene fuori molto, come "Beh, è possibile rovinare tutto, quindi automaticamente è una cattiva idea". Puoi rovinare qualsiasi cosa in un linguaggio di programmazione, e intendo qualsiasi cosa. Non riesco proprio a prenderlo sul serio, almeno non senza una spiegazione più approfondita.

Essere consapevoli di questo problema è il 90% della battaglia. Inoltre, credo di aver sentito anni fa qualcosa su una soluzione generale che coinvolge un algoritmo di "inviluppo" o qualcosa del genere (questo suona un campanello, qualcuno?).

Per quanto riguarda il problema del diamante, l'unico problema potenzialmente genuino che mi viene in mente è se stai cercando di utilizzare una libreria di terze parti e non riesci a vedere che due classi apparentemente non correlate in quella libreria hanno una classe base comune, ma oltre a documentazione, una semplice funzionalità linguistica potrebbe, diciamo, richiedere che tu dichiari espressamente la tua intenzione di creare un diamante prima che ne compili uno per te. Con una tale caratteristica, qualsiasi creazione di un diamante è intenzionale, sconsiderata o perché non si è consapevoli di questa trappola.

In modo che tutto ciò che viene detto ... C'è qualche vero motivo per cui la maggior parte delle persone odia l'eredità multipla o è solo un mucchio di isteria che causa più danni che benefici? C'è qualcosa che non vedo qui? Grazie.

Esempio

L'automobile estende WheeledVehicle, KIASpectra estende l'automobile e l'elettronica, KIASpectra contiene la radio. Perché KIASpectra non contiene elettronica?

  1. Perché è un elettronico. L'ereditarietà contro la composizione dovrebbe sempre essere una relazione is-a vs a have-a.

  2. Perché è un elettronico. Ci sono cavi, circuiti stampati, interruttori, ecc. Su e giù per quella cosa.

  3. Perché è un elettronico. Se la batteria si esaurisce in inverno, ci sono tanti problemi come se all'improvviso perdessero tutte le ruote.

Perché non usare le interfacce? Prendi il n. 3, ad esempio. Non voglio scriverlo ancora e ancora, e non voglio davvero creare una bizzarra classe di supporto proxy per fare questo:

private void runOrDont()
{
    if (this.battery)
    {
        if (this.battery.working && this.switchedOn)
        {
            this.run();
            return;
        }
    }
    this.dontRun();
}

(Non capiremo se questa implementazione sia buona o cattiva.) Puoi immaginare come possano esserci diverse di queste funzioni associate a Electronic che non sono correlate a nulla in WheeledVehicle e viceversa.

Non ero sicuro di accontentarmi di quell'esempio o meno, poiché lì c'è spazio per l'interpretazione. Potresti anche pensare in termini di Piano che estende Veicolo e FlyingObject e Bird che estende Animal e FlyingObject, o in termini di un esempio molto più puro.


24
incoraggia anche l'eredità sulla composizione ... (quando dovrebbe essere il contrario)
maniaco del cricchetto

34
"Puoi rovinare tutto" è un motivo perfettamente valido per rimuovere una funzione. Il moderno design del linguaggio è in qualche modo frammentato su questo punto, ma in genere è possibile classificare i linguaggi in "Potenza da restrizione" e "Potenza da flessibilità". Nessuno dei due è "corretto", non credo, entrambi abbiano punti forti da sottolineare. L'MI è una di quelle cose che viene usata per il Male più spesso, e quindi i linguaggi restrittivi lo rimuovono. Quelli flessibili no, perché "il più delle volte" non è "letteralmente sempre". Detto questo, i mixin / tratti sono una soluzione migliore per il caso generale, credo.
Phoshi,

8
Esistono alternative più sicure all'ereditarietà multipla in diverse lingue, che vanno oltre le interfacce. Dai un'occhiata a Scala Traits: funzionano come interfacce con l'implementazione opzionale, ma hanno alcune restrizioni che aiutano a prevenire l'insorgere di problemi come il problema dei diamanti.
KChaloux,

5
Vedi anche questa domanda precedentemente chiusa: programmers.stackexchange.com/questions/122480/…
Doc Brown

19
KiaSpectranon è un Electronic ; esso ha Elettronica, e può essere una ElectronicCar(che estenderebbe Car...)
Brian S

Risposte:


68

In molti casi, le persone usano l'ereditarietà per fornire un tratto a una classe. Ad esempio, pensa a un Pegaso. Con l'eredità multipla potresti essere tentato di dire che Pegasus estende Cavallo e Uccello perché hai classificato l'uccello come un animale con le ali.

Tuttavia, gli uccelli hanno altri tratti che Pegasi non ha. Ad esempio, gli uccelli depongono le uova, Pegasi ha una nascita viva. Se l'ereditarietà è il tuo unico mezzo per trasmettere i tratti di condivisione, allora non c'è modo di escludere il tratto di deposizione delle uova dal Pegaso.

Alcune lingue hanno optato per rendere i tratti un costrutto esplicito all'interno della lingua. Gli altri ti guidano delicatamente in quella direzione rimuovendo l'MI dalla lingua. Ad ogni modo, non riesco a pensare a un singolo caso in cui ho pensato "Amico, ho davvero bisogno dell'MI per farlo correttamente".

Parliamo anche di quale eredità sia DAVVERO. Quando erediti da una classe, prendi una dipendenza da quella classe, ma devi anche supportare i contratti che la classe supporta, sia impliciti che espliciti.

Prendi il classico esempio di un quadrato che eredita da un rettangolo. Il rettangolo espone una proprietà di lunghezza e larghezza e anche un metodo getPerimeter e getArea. Il quadrato sovrascriverà la lunghezza e la larghezza in modo che quando uno è impostato l'altro è impostato in modo che corrisponda a getPerimeter e getArea funzionerebbe allo stesso modo (2 * lunghezza + 2 * larghezza per perimetro e lunghezza * larghezza per area).

Esiste un singolo caso di test che si interrompe se si sostituisce questa implementazione di un quadrato con un rettangolo.

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

È abbastanza difficile ottenere le cose giuste con una singola catena di eredità. Peggio ancora quando ne aggiungi un altro al mix.


Le insidie ​​che ho citato con Pegasus in MI e le relazioni Rettangolo / Quadrato sono entrambi i risultati di un progetto inesperto per le classi. Fondamentalmente evitare l'ereditarietà multipla è un modo per aiutare gli sviluppatori principianti a evitare di spararsi ai piedi. Come tutti i principi di progettazione, avere una disciplina e una formazione basate su di essi ti consente di scoprire in tempo quando va bene staccarli. Vedi il Modello di acquisizione di abilità Dreyfus , a livello di Esperto, la tua conoscenza intrinseca trascende la dipendenza da massime / principi. Puoi "sentire" quando una regola non si applica.

E sono d'accordo sul fatto che in qualche modo ho tradito un esempio del "mondo reale" del perché l'MI è malvisto.

Diamo un'occhiata a un framework UI. In particolare esaminiamo alcuni widget che a prima vista potrebbero sembrare come se fossero semplicemente una combinazione di altri due. Come un ComboBox. Un ComboBox è un TextBox che ha un DropDownList di supporto. Vale a dire che posso digitare un valore o posso selezionare da un elenco di valori preordinato. Un approccio ingenuo sarebbe quello di ereditare ComboBox da TextBox e DropDownList.

Ma la tua casella di testo deriva il suo valore da ciò che l'utente ha digitato. Mentre il DDL ottiene il suo valore da ciò che l'utente seleziona. Chi prende il precedente? Il DDL potrebbe essere stato progettato per verificare e rifiutare qualsiasi input che non fosse nel suo elenco di valori originale. Sostituiamo questa logica? Ciò significa che dobbiamo esporre la logica interna affinché gli eredi possano scavalcare. O peggio, aggiungi la logica alla classe base che è presente solo per supportare una sottoclasse (violando il principio di inversione di dipendenza ).

Evitare l'MI ti aiuta a eludere del tutto questa trappola. E potrebbe portarti a estrarre tratti comuni e riutilizzabili dei tuoi widget UI in modo che possano essere applicati secondo necessità. Un esempio eccellente di ciò è la proprietà associata WPF che consente a un elemento framework in WPF di fornire una proprietà che un altro elemento framework può utilizzare senza ereditare dall'elemento framework genitore.

Ad esempio, una griglia è un pannello di layout in WPF e ha proprietà associate a colonne e righe che specificano dove posizionare un elemento figlio nella disposizione della griglia. Senza le proprietà associate, se voglio disporre un pulsante all'interno di una griglia, il pulsante dovrebbe derivare dalla griglia in modo da poter accedere alle proprietà di colonna e riga.

Gli sviluppatori hanno approfondito questo concetto e hanno usato le proprietà associate come un modo per strutturare il comportamento (ad esempio, ecco il mio post su come rendere GridView ordinabile usando le proprietà associate scritte prima che WPF includesse un DataGrid). L'approccio è stato riconosciuto come XAML Design Pattern chiamato Attached Behaviors .

Si spera che ciò abbia fornito un po 'più di comprensione sul perché l'ereditarietà multipla è generalmente disapprovata.


14
"Amico, ho davvero bisogno dell'MI per farlo correttamente" - vedi la mia risposta per un esempio. In breve, i concetti ortogonali che soddisfano una relazione is-a hanno senso per MI. Sono molto rari, ma esistono.
MrFox,

13
Amico, odio quell'esempio di rettangolo quadrato. Esistono molti esempi più illuminanti che non richiedono mutabilità.
asmeurer,

9
Adoro che la risposta a "fornire un esempio reale" sia stata quella di discutere di una creatura immaginaria. Eccellente ironia +1
jjathman

3
Quindi stai dicendo che l'ereditarietà multipla è cattiva, perché l'eredità è cattiva (gli esempi riguardano l'ereditarietà a interfaccia singola). Pertanto, basandosi solo su questa risposta, una lingua dovrebbe essere libera da eredità e interfacce o avere ereditarietà multipla.
ctrl-alt-delor,

12
Non credo che questi esempi funzionino davvero ... nessuno dice che un pegasus sia un uccello e un cavallo ... dici che è un WingedAnimal e un HoofedAnimal. L'esempio del quadrato e del rettangolo ha un po 'più senso perché ti ritrovi con un oggetto che si comporta in modo diverso rispetto a quanto pensi sulla base della sua affermata definizione. Tuttavia, penso che questa situazione sarebbe colpa dei programmatori per non averlo colto (se davvero questo si fosse rivelato un problema).
richard

60

C'è qualcosa che non vedo qui?

Consentire l'ereditarietà multipla rende le regole sui sovraccarichi delle funzioni e sull'invio virtuale decisamente più complicate, così come l'implementazione del linguaggio attorno ai layout degli oggetti. Questi impattano un po 'i progettisti / implementatori di linguaggi e innalzano il livello già alto per ottenere un linguaggio fatto, stabile e adottato.

Un altro argomento comune che ho visto (e fatto a volte) è che avendo due + classi di base, il tuo oggetto viola quasi invariabilmente il principio di responsabilità singola. O le due + classi di base sono belle classi autonome con la propria responsabilità (che causa la violazione) o sono tipi parziali / astratti che lavorano l'uno con l'altro per formare una singola responsabilità coesiva.

In questo altro caso, hai 3 scenari:

  1. A non sa nulla di B - Fantastico, potresti unire le classi perché sei stato fortunato.
  2. A sa di B - Perché A non ha ereditato da B?
  3. A e B si conoscono - Perché non hai fatto solo una lezione? Quale vantaggio deriva dal rendere queste cose così accoppiate ma parzialmente sostituibili?

Personalmente, penso che l'ereditarietà multipla abbia un brutto rap e che un sistema ben fatto di composizione in stile tratto sia davvero potente / utile ... ma ci sono molti modi in cui può essere implementato male, e molte ragioni sono non è una buona idea in un linguaggio come C ++.

[modifica] riguardo al tuo esempio, è assurdo. Una Kia ha elettronica. Esso ha un motore. Allo stesso modo, è l'elettronica ha una fonte di alimentazione, che sembra essere una batteria per auto. Eredità, per non parlare dell'eredità multipla non ha posto lì.


8
Un'altra domanda interessante è come vengono inizializzate le classi base + le loro classi base. Incapsulamento> Ereditarietà.
jozefg,

"un sistema ben fatto di composizione in stile tratto sarebbe davvero potente / utile ..." - Questi sono conosciuti come mixin e sono potenti / utili. I mixin possono essere implementati utilizzando l'MI, ma l' ereditarietà multipla non è richiesta per i mixin. Alcune lingue supportano i mixin intrinsecamente, senza IM.
BlueRaja - Danny Pflughoeft

Per Java, in particolare, disponiamo già di più interfacce e INVOKEINTERFACE, quindi MI non avrebbe evidenti implicazioni sulle prestazioni.
Daniel Lubarov,

Tetastyn, concordo sul fatto che l'esempio è stato negativo e non ha risposto alla sua domanda. L'ereditarietà non è per gli aggettivi (elettronico), è per i nomi (veicolo).
richard

29

L'unico motivo per cui è vietato è perché rende facile per le persone spararsi al piede.

Ciò che di solito segue in questo tipo di discussione sono gli argomenti sul fatto che la flessibilità di avere gli strumenti sia più importante della sicurezza di non sparare dal piede. Non esiste una risposta decisamente corretta a tale argomento, perché come la maggior parte delle altre cose nella programmazione, la risposta dipende dal contesto.

Se i tuoi sviluppatori sono a proprio agio con l'MI e l'MI ha senso nel contesto di ciò che stai facendo, allora ti mancherà molto in un linguaggio che non lo supporta. Allo stesso tempo, se il team non è a suo agio con esso, o se non ce n'è davvero bisogno e le persone lo usano "solo perché possono", allora è controproducente.

Ma no, non esiste un argomento assolutamente vero e convincente che dimostri che l'eredità multipla sia una cattiva idea.

MODIFICARE

Le risposte a questa domanda sembrano essere unanimi. Per il bene di essere il difensore del diavolo fornirò un buon esempio di eredità multipla, dove non farlo porta a degli hack.

Supponiamo che tu stia progettando un'applicazione per i mercati dei capitali. È necessario un modello di dati per i titoli. Alcuni titoli sono prodotti azionari (azioni, fondi comuni di investimento immobiliare, ecc.) Altri sono debito (obbligazioni, obbligazioni societarie), altri sono derivati ​​(opzioni, futures). Quindi, se stai evitando l'MI, realizzerai un albero ereditario molto chiaro e semplice. Un titolo erediterà l'equità, Bond erediterà il debito. Ottimo finora, ma per quanto riguarda i derivati? Possono essere basati su prodotti simili a titoli azionari o prodotti simili a debito? Ok, immagineremo di estendere ulteriormente il nostro albero ereditario. Tieni presente che alcuni derivati ​​sono basati su prodotti azionari, prodotti di debito o nessuno dei due. Quindi il nostro albero ereditario si sta complicando. Poi arriva l'analista di business e ti dice che ora supportiamo titoli indicizzati (opzioni su indici, opzioni su indici futuri). E queste cose possono essere basate su azioni, debiti o derivati. Questo sta diventando disordinato! La mia opzione futura sull'indice deriva Azioni-> Azioni-> Opzioni-> Indice? Perché non Equity-> Stock-> Index-> ​​Option? E se un giorno trovassi entrambi nel mio codice (questo è successo; storia vera)?

Il problema qui è che questi tipi fondamentali possono essere mescolati in qualsiasi permutazione che non deriva naturalmente l'uno dall'altro. Gli oggetti sono definiti da una è una relazione, quindi la composizione non ha alcun senso. L'eredità multipla (o il concetto simile di mixin) è l'unica rappresentazione logica qui.

La vera soluzione per questo problema è di definire e miscelare i tipi di capitale, debito, derivato, indice utilizzando l'ereditarietà multipla per creare il modello di dati. Ciò creerà oggetti che hanno entrambi senso e si prestano facilmente al riutilizzo del codice.


27
Ho lavorato in finanza. C'è un motivo per cui la programmazione funzionale è preferita a OO nei software finanziari. E l'hai sottolineato. MI non risolve questo problema, lo aggrava. Credimi, non posso dirti quante volte ho sentito "È proprio così ... tranne" quando ho a che fare con clienti finanziari Che tranne diventa la rovina della mia esistenza.
Michael Brown,

8
Questo mi sembra molto risolvibile con le interfacce ... in effetti, sembra più adatto alle interfacce di ogni altra cosa. Equityed Debtentrambi implementano ISecurity. Derivativeha una ISecurityproprietà. Può essere esso stesso un ISecuritycaso appropriato (non conosco la finanza). IndexedSecuritiescontiene nuovamente una proprietà di interfaccia che viene applicata ai tipi su cui è possibile basarsi. Se lo sono tutti ISecurity, hanno tutti ISecurityproprietà e possono essere nidificati arbitrariamente ...
Bobson,

11
@Bobson il problema è che devi reimplementare l'interfaccia per ciascun implementatore, e in molti casi è esattamente la stessa. È possibile utilizzare la delega / composizione ma si perde l'accesso ai membri privati. E poiché Java non ha delegati / lambda ... non c'è letteralmente un buon modo per farlo. Tranne forse con uno strumento Aspect come Aspect4J
Michael Brown,

10
@Bobson esattamente quello che ha detto Mike Brown. Sì, puoi progettare una soluzione con interfacce, ma sarà ingombrante. La tua intuizione di usare le interfacce è molto corretta, è un desiderio nascosto di mixin / eredità multipla :).
MrFox,

11
Ciò tocca una stranezza in cui i programmatori OO preferiscono pensare in termini di eredità e spesso non riescono ad accettare la delega come un approccio OO valido, che in genere offre una soluzione più snella, più mantenibile ed estensibile. Avendo lavorato nel settore finanziario e sanitario, ho visto il disordine ingestibile che l'MI può creare, soprattutto perché le leggi fiscali e sanitarie cambiano di anno in anno e la definizione di un oggetto ULTIMO anno non è valida per QUESTO anno (ma deve comunque svolgere la funzione di entrambi gli anni e deve essere intercambiabile). La composizione produce un codice più snello che è più facile da testare e costa meno nel tempo.
Shaun Wilson,

11

Le altre risposte qui sembrano essere per lo più in teoria. Quindi, ecco un esempio concreto di Python, semplificato, in cui mi sono davvero distrutto, che ha richiesto una buona dose di refactoring:

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()

Barè stato scritto supponendo che avesse una sua implementazione zeta(), che è generalmente un presupposto piuttosto buono. Una sottoclasse dovrebbe sostituirla come appropriato in modo che faccia la cosa giusta. Sfortunatamente, i nomi erano solo casualmente gli stessi - hanno fatto cose piuttosto diverse, ma Barora chiamavano Fool'implementazione:

foozeta
---
barstuff
foozeta

È piuttosto frustrante quando non vengono generati errori, l'applicazione inizia a comportarsi in modo leggermente errato e la modifica del codice che l'ha causata (creando Bar.zeta) non sembra essere il problema.


Come viene evitato il problema Diamond tramite le chiamate a super()?
Panzercrisis,

@Panzercrisis Siamo spiacenti, non dimenticare mai quella parte. Ho frainteso a quale problema si riferisce di solito il "problema del diamante" - Python aveva un problema separato causato dall'eredità a forma di diamante che super()si aggirava
Izkata

5
Sono contento che l'MI di C ++ sia progettato molto meglio. Il bug sopra non è semplicemente possibile. Devi disambiguare manualmente prima ancora che il codice venga compilato.
Thomas Eding,

2
Cambiare l'ordine di successione in Banga Bar, Foosarebbe anche risolvere il problema - ma il vostro punto è una buona, +1.
Sean Vieira,

1
@ThomasEding È possibile disambiguare manualmente ancora: Bar.zeta(self).
Tyler Crompton,

9

Direi che non ci sono problemi reali con l'MI nella lingua giusta . La chiave è consentire le strutture a diamante, ma è necessario che i sottotipi forniscano il proprio override, invece che il compilatore scelga una delle implementazioni in base a una regola.

Lo faccio in guava , una lingua su cui sto lavorando. Una caratteristica di Guava è che possiamo invocare l'implementazione di un metodo di un supertipo specifico. Quindi è facile indicare quale implementazione del supertipo debba essere "ereditata", senza alcuna sintassi speciale:

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);
  }
}

Se non abbiamo dato OrderedSetla propria toString, ci sarebbe un errore di compilazione. Niente sorprese.

Trovo che MI sia particolarmente utile con le collezioni. Ad esempio, mi piace usare un RandomlyEnumerableSequencetipo per evitare di dichiarare getEnumeratorper array, deques e così via:

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

Se non avessimo l'MI, potremmo scrivere un RandomAccessEnumeratorper diverse raccolte da usare, ma dovendo scrivere un breve getEnumeratormetodo si aggiunge ancora il bollettino.

Allo stesso modo, MI è utile per ereditare implementazioni standard di equals, hashCodee toStringper collezioni.


1
@AndyzSmith, vedo il tuo punto, ma non tutti i conflitti di ereditarietà sono veri e propri problemi semantici. Considera il mio esempio: nessuno dei tipi fa promesse sul toStringcomportamento, quindi ignorare il suo comportamento non viola il principio di sostituzione. A volte si ottengono anche "conflitti" tra metodi che hanno lo stesso comportamento ma algoritmi diversi, in particolare con le raccolte.
Daniel Lubarov,

1
@Asmageddon concordato, i miei esempi riguardano solo la funzionalità. Quali sono i vantaggi dei mixin? Almeno a Scala, i tratti sono essenzialmente solo classi astratte che consentono l'MI - possono ancora essere utilizzati per l'identità e portare ancora al problema del diamante. Ci sono altre lingue in cui i mixin hanno differenze più interessanti?
Daniel Lubarov,

1
Le classi tecnicamente astratte possono essere usate come interfacce, per me, il vantaggio è semplicemente il fatto che il costrutto stesso è un "suggerimento d'uso", il che significa che so cosa aspettarmi quando vedo il codice. Detto questo, sto attualmente codificando Lua, che non ha un modello OOP intrinseco - i sistemi di classe esistenti comunemente consentono di includere tabelle come mixin, e quello che sto codificando utilizza le classi come mixin. L'unica differenza è che puoi utilizzare (singola) ereditarietà per identità, mixin per funzionalità (senza informazioni sull'identità) e interfacce per ulteriori controlli. Forse non è in qualche modo migliore, ma per me ha senso.
Llamageddon,

2
Perché chiami Ordered extends Set and Sequencea Diamond? È solo un join. Manca il hatvertice. Perché lo chiami un diamante? Ho chiesto qui, ma sembra una domanda tabù. Come sapevi che hai bisogno di chiamare questa struttura triangolare un diamante anziché unire?
Val

1
@RecognizeEvilas Spreca quel linguaggio che ha un Toptipo implicito che dichiara toString, quindi in realtà c'era una struttura a diamante. Ma penso che tu abbia un punto: un "join" senza una struttura a diamante crea un problema simile e la maggior parte delle lingue gestisce entrambi i casi allo stesso modo.
Daniel Lubarov,

7

L'ereditarietà, multipla o meno, non è così importante. Se due oggetti di tipo diverso sono sostituibili, questo è ciò che conta, anche se non sono collegati per eredità.

Un elenco collegato e una stringa di caratteri hanno poco in comune e non devono necessariamente essere collegati per ereditarietà, ma è utile se posso usare una lengthfunzione per ottenere il numero di elementi in uno di essi.

L'ereditarietà è un trucco per evitare l'implementazione ripetuta del codice. Se l'ereditarietà ti salva il lavoro e l'ereditarietà multipla ti fa risparmiare ancora più lavoro rispetto alla singola eredità, allora questa è tutta la giustificazione necessaria.

Ho il sospetto che alcune lingue non implementino molto bene l'ereditarietà multipla, e per i professionisti di quelle lingue, questo è ciò che significa eredità multipla. Indica l'ereditarietà multipla a un programmatore C ++ e ciò che viene in mente è qualcosa che riguarda i problemi quando una classe finisce con due copie di una base tramite due diversi percorsi di ereditarietà e se usare virtualsu una classe base e confusione su come vengono chiamati i distruttori , e così via.

In molte lingue, l'eredità della classe si fonde con l'eredità dei simboli. Quando si ricava una classe D da una classe B, non solo si sta creando una relazione di tipo, ma poiché queste classi fungono anche da spazi dei nomi lessicali, si ha a che fare con l'importazione di simboli dallo spazio dei nomi B allo spazio dei nomi D, oltre a la semantica di ciò che sta accadendo con i tipi B e D stessi. L'ereditarietà multipla comporta quindi problemi di scontro di simboli. Se ereditiamo da card_decke graphic, entrambi i quali "hanno" un drawmetodo, cosa significa per drawl'oggetto risultante? Un sistema a oggetti che non presenta questo problema è quello in Common Lisp. Forse non a caso, l'ereditarietà multipla viene utilizzata nei programmi Lisp.

Mal implementato, qualsiasi cosa scomoda (come eredità multipla) dovrebbe essere odiata.


2
Il tuo esempio con il metodo length () dei contenitori list e string è negativo, poiché questi due stanno facendo cose completamente diverse. Lo scopo dell'ereditarietà non è ridurre la ripetizione del codice. Se si desidera ridurre la ripetizione del codice, si esegue il refactoring delle parti comuni in una nuova classe.
BЈовић

1
@ BЈовић I due non stanno affatto facendo cose "completamente" diverse.
Kaz,

1
Combinando la stringa e l' elenco , si possono vedere molte ripetizioni. L'uso dell'ereditarietà per implementare questi metodi comuni sarebbe semplicemente sbagliato.
BЈовић

1
@ BЈовић Nella risposta non si dice che una stringa e un elenco debbano essere collegati per ereditarietà; piuttosto il contrario. Il comportamento di poter sostituire un elenco o una stringa in lengthun'operazione è utile indipendentemente dal concetto di ereditarietà (che in questo caso non aiuta: non è probabile che siamo in grado di ottenere qualcosa cercando di condividere l'implementazione tra i due metodi della lengthfunzione). Potrebbe tuttavia esserci un'eredità astratta: ad esempio sia list che string sono di tipo sequenza (ma che non fornisce alcuna implementazione).
Kaz,

2
Inoltre, l'ereditarietà è un modo per eseguire il refactoring delle parti in una classe comune: la classe base.
Kaz,

1

Per quanto ne so, parte del problema (oltre a rendere il tuo progetto un po 'più difficile da capire (tuttavia più facile da codificare)) è che il compilatore risparmierà abbastanza spazio per i dati della tua classe, permettendogli un'enorme quantità di spreco di memoria nel seguente caso:

(Il mio esempio potrebbe non essere il migliore, ma cerca di ottenere l'essenza dello spazio di memoria multipla per lo stesso scopo, è stata la prima cosa che mi è venuta in mente: P)

Considerando un DDD in cui il cane di classe si estende da canino e animale domestico, un canino ha una variabile che indica la quantità di cibo che dovrebbe mangiare (un numero intero) sotto il nome dietKg, ma un animale domestico ha anche un'altra variabile a tale scopo di solito sotto lo stesso name (a meno che tu non imposti un altro nome di variabile, dovrai codificare il codice aggiuntivo, che era il problema iniziale che volevi evitare, per gestire e mantenere l'integrità delle variabili bouth), quindi avrai due spazi di memoria per l'esatto stesso scopo, per evitarlo dovrai modificare il tuo compilatore per riconoscere quel nome sotto lo stesso spazio dei nomi e assegnare un solo spazio di memoria a quei dati, che purtroppo è suscettibile di determinare in tempo di compilazione.

È possibile, ovviamente, progettare un linguaggio per specificare che tale variabile potrebbe avere già uno spazio definito altrove, ma alla fine il programmatore dovrebbe specificare dove si trova quello spazio di memoria a cui fa riferimento questa variabile (e di nuovo codice extra).

Fidati di me, le persone che implementano questo pensiero davvero duramente su tutto questo, ma sono contento che tu l'abbia chiesto, la tua gentile prospettiva è quella che cambia i paradigmi;) e ammettilo, non sto dicendo che è impossibile (ma molte ipotesi e un compilatore a più fasi deve essere implementato, e molto complesso), sto solo dicendo che non esiste ancora, se avvii un progetto per il tuo compilatore in grado di fare "questo" (ereditarietà multipla) per favore fammi sapere, io saremo lieti di unirti alla tua squadra.


1
A che punto un compilatore potrebbe mai supporre che le variabili con lo stesso nome provenienti da classi genitore diverse siano sicure da combinare? Che garanzia hai che caninus.diet e pet.diet hanno effettivamente lo stesso scopo? Cosa faresti con la funzione / i metodi con lo stesso nome?
Mr.Mindor,

hahaha sono totalmente d'accordo con te @ Mr.Mindor è esattamente quello che sto dicendo nella mia risposta, l'unico modo che mi viene in mente per avvicinarmi a quell'approccio è la programmazione orientata ai prototipi, ma non sto dicendo che è impossibile , e questo è esattamente il motivo per cui ho detto che durante il periodo di compilazione devono essere fatte molte ipotesi, e alcuni "dati" / specifiche extra devono essere scritti dal programmatore (tuttavia è più codifica, che era il problema originale)
Ordiel

-1

Da un po 'di tempo ormai non mi sono mai reso conto di quanto totalmente diversi siano alcuni compiti di programmazione dagli altri - e quanto sia utile se i linguaggi e i modelli utilizzati sono adattati allo spazio del problema.

Quando lavori da solo o per lo più isolato sul codice che hai scritto è uno spazio problematico completamente diverso dall'ereditare una base di codice di 40 persone in India che ci hanno lavorato per un anno prima di consegnartelo senza alcun aiuto di transizione.

Immagina di essere stato appena assunto dalla compagnia dei tuoi sogni e quindi ereditato tale base di codice. Inoltre, immagina che i consulenti stessero imparando (e quindi infatuati) dell'eredità e dell'ereditarietà multipla ... Potresti immaginare su cosa potresti lavorare.

Quando erediti il ​​codice, la caratteristica più importante è che è comprensibile e i pezzi sono isolati in modo da poterli lavorare indipendentemente. Sicuramente quando stai scrivendo per la prima volta le strutture di codice come l'ereditarietà multipla potrebbero farti risparmiare un po 'di duplicazione e sembrano adattarsi al tuo umore logico al momento, ma il ragazzo successivo ha solo più cose da districare.

Ogni interconnessione nel codice rende anche più difficile la comprensione e la modifica dei pezzi in modo indipendente, doppiamente con eredità multipla.

Quando lavori come parte di un team, vuoi scegliere come target il codice più semplice possibile che non ti dia assolutamente nessuna logica ridondante (Questo è ciò che significa DRY, non che non dovresti digitare molto solo perché non devi mai cambiare il tuo codice in 2 posti per risolvere un problema!)

Esistono modi più semplici per ottenere il codice DRY rispetto all'ereditarietà multipla, quindi includerlo in una lingua può solo aprirti ai problemi inseriti da altri che potrebbero non essere allo stesso livello di comprensione. È anche allettante solo se la tua lingua non è in grado di offrirti un modo semplice / meno complesso per mantenere il tuo codice ASCIUTTO.


Inoltre, immagina che i consulenti stessero imparando qualcosa : ho visto così tante lingue non MI (es. .Net) in cui i consulenti impazzivano con DI e IoC basati sull'interfaccia per rendere il tutto un casino impenetrabilmente contorto. L'MI non lo peggiora, probabilmente lo migliora.
gbjbaanb,

@gbjbaanb Hai ragione, è facile per qualcuno che non è stato bruciato rovinare qualsiasi funzione - in effetti è quasi un requisito per imparare una funzione che si rovina per vedere come utilizzarla al meglio. Sono un po 'curioso perché sembra che tu stia dicendo che non avere l'MI forza più DI / IoC che non ho visto - non penserei che il codice del consulente che hai ottenuto con tutte le interfacce scadenti / DI sarebbe stato meglio avere anche un folle albero ereditario molti-a-molti, ma potrebbe essere la mia inesperienza con l'MI. Hai una pagina che potrei leggere sulla sostituzione di DI / IoC con MI?
Bill K,

-3

La più grande argomentazione contro l'ereditarietà multipla è che alcune abilità utili possono essere fornite e alcuni utili assiomi possono reggere, in un quadro che lo restringe gravemente (*), ma non può essere fornito e / o trattenuto senza tali restrizioni. Tra loro:

  • La possibilità di avere più moduli compilati separatamente include le classi che ereditano dalle classi di altri moduli e ricompila un modulo contenente una classe di base senza dover ricompilare tutti i moduli che ereditano da quella classe di base.

  • La capacità di un tipo di ereditare un'implementazione membro da una classe padre senza che il tipo derivato debba implementarlo nuovamente

  • L'assioma secondo cui qualsiasi istanza di oggetto può essere trasferita o downcast direttamente su se stessa o su uno qualsiasi dei suoi tipi di base e tali upcasts e downcasts, in qualsiasi combinazione o sequenza, mantengono sempre l'identità

  • L'assioma che se una classe derivata esegue l'override e le catene a un membro della classe base, il membro base verrà invocato direttamente dal codice di concatenamento e non verrà invocato altrove.

(*) In genere richiedendo che i tipi che supportano l'ereditarietà multipla siano dichiarati come "interfacce" anziché come classi, e non permettendo alle interfacce di fare tutto ciò che le normali classi possono fare.

Se si desidera consentire l'eredità multipla generalizzata, qualcos'altro deve cedere. Se X e Y ereditano entrambi da B, entrambi sovrascrivono lo stesso membro M e concatenano l'implementazione di base, e se D eredita da X e Y ma non sovrascrive M, quindi data un'istanza q di tipo D, cosa dovrebbe (( B) q) .M () fare? Non consentire tale cast violerebbe l'assioma secondo cui qualsiasi oggetto può essere trasferito a qualsiasi tipo di base, ma ogni possibile comportamento del cast e l'invocazione dei membri violerebbe l'assioma riguardo al concatenamento del metodo. Si potrebbe richiedere che le classi vengano caricate solo in combinazione con la particolare versione di una classe base su cui sono state compilate, ma ciò è spesso imbarazzante. Avere il runtime rifiutare di caricare qualsiasi tipo in cui qualsiasi antenato possa essere raggiunto da più di una rotta potrebbe essere praticabile, ma limiterebbe notevolmente l'utilità dell'eredità multipla. Consentire percorsi di ereditarietà condivisi solo quando non esistono conflitti creerebbe situazioni in cui una vecchia versione di X sarebbe compatibile con una vecchia o nuova Y e una vecchia Y sarebbe compatibile con una vecchia o nuova X, ma la nuova X e la nuova Y sarebbero essere compatibile, anche se nessuno dei due ha fatto qualcosa che di per sé dovrebbe essere considerato un cambiamento radicale.

Alcuni linguaggi e framework consentono l'ereditarietà multipla, in base alla teoria che ciò che è guadagnato dall'MI è più importante di ciò che deve essere rinunciato per consentirlo. I costi dell'MI sono significativi, tuttavia, e in molti casi le interfacce forniscono il 90% dei vantaggi dell'MI a una piccola frazione del costo.


I votanti svantaggiati affermerebbero di commentare? Credo che la mia risposta fornisca informazioni non presenti in altre lingue, per quanto riguarda i vantaggi semantici che possono essere ricevuti vietando l'eredità multipla. Il fatto che consentire l'ereditarietà multipla richiederebbe di rinunciare ad altre cose utili sarebbe una buona ragione per chiunque apprezzi queste altre cose più dell'ereditarietà multipla per opporsi all'MI.
supercat

2
I problemi con MI sono facilmente risolti o evitati del tutto usando le stesse tecniche che useresti con la singola eredità. Nessuna delle abilità / assiomi che hai citato è intrinsecamente ostacolata da MI tranne la seconda ... e se stai ereditando da due classi che forniscono comportamenti diversi per lo stesso metodo, spetta a te risolvere comunque quell'ambiguità. Ma se ti ritrovi a doverlo fare, probabilmente stai già abusando dell'eredità.
cHao,

@cHao: se si richiede che le classi derivate debbano sovrascrivere i metodi parent e non concatenarli (stessa regola delle interfacce) si possono evitare i problemi, ma questa è una limitazione piuttosto grave. Se si utilizza un modello in cui FirstDerivedFoo's override Barnon fa altro che la catena a un non-virtuale protetto FirstDerivedFoo_Bar, e SecondDerivedFoo' s catene di override di protezione non virtuale SecondDerivedBar, e se il codice che vuole accedere metodo di base utilizza questi metodi non virtuali protette, che potrebbe essere un approccio praticabile ...
supercat,

... (evitando la sintassi base.VirtualMethod) ma non conosco alcun supporto linguistico per facilitare in particolare tale approccio. Vorrei che ci fosse un modo chiaro per collegare un blocco di codice sia a un metodo protetto non virtuale sia a un metodo virtuale con la stessa firma senza richiedere un metodo virtuale "one-liner" (che finisce per prendere molto più di una riga nel codice sorgente).
supercat,

Non vi è alcun requisito intrinseco in MI che le classi non chiamino i metodi di base e non è necessaria una ragione particolare. Sembra un po 'strano dare la colpa a MI, considerando che il problema riguarda anche SI.
cHao,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.