Preferisci la composizione rispetto all'eredità?


1604

Perché preferire la composizione rispetto all'eredità? Quali sono i compromessi per ciascun approccio? Quando dovresti scegliere l'eredità rispetto alla composizione?



40
C'è un buon articolo su questa domanda qui . La mia opinione personale è che non esiste un principio "migliore" o "peggiore" da progettare. Esiste un design "appropriato" e "inadeguato" per l'attività concreta. In altre parole, utilizzo l'ereditarietà o la composizione, a seconda della situazione. L'obiettivo è produrre codice più piccolo, più facile da leggere, riutilizzare ed eventualmente estenderlo ulteriormente.
m_pGladiator,

1
in una frase l'ereditarietà è pubblica se si dispone di un metodo pubblico e lo si modifica cambia l'API pubblicato. se hai composizione e l'oggetto composto è cambiato non devi cambiare l'API pubblicato.
Tomer Ben David,

Risposte:


1189

Preferisci la composizione rispetto all'eredità in quanto è più malleabile / facile da modificare in seguito, ma non usa un approccio componi sempre. Con la composizione, è facile cambiare il comportamento al volo con l'iniezione / setter delle dipendenze. L'ereditarietà è più rigida poiché la maggior parte delle lingue non consente di derivare da più di un tipo. Quindi l'oca è più o meno cotta una volta derivato dal tipo A.

Il mio test dell'acido per quanto sopra è:

  • TypeB vuole esporre l'interfaccia completa (tutti i metodi pubblici non meno) di TypeA in modo tale che TypeB possa essere utilizzato dove TypeA è previsto? Indica l' ereditarietà .

    • es. Un biplano Cessna esporrà l'interfaccia completa di un aereo, se non di più. In modo che si adatti per derivare da aereo.
  • TypeB vuole solo parte / parte del comportamento esposto da TypeA? Indica la necessità di composizione.

    • es. Un Uccello può aver bisogno solo del comportamento al volo di un Aeroplano. In questo caso, ha senso estrarlo come interfaccia / classe / entrambi e renderlo un membro di entrambe le classi.

Aggiornamento: Sono appena tornato alla mia risposta e sembra ora che sia incompleto senza una menzione specifica del Principio di sostituzione Liskov di Barbara Liskov come test per "Dovrei ereditare da questo tipo?"


81
Il secondo esempio è uscito direttamente dal libro Head First Design Patterns ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) :) Consiglio vivamente questo libro a chiunque stia cercando su Google questa domanda.
Jeshurun,

4
È molto chiaro, ma potrebbe mancare qualcosa: "TypeB vuole esporre l'interfaccia completa (tutti i metodi pubblici non meno) di TypeA in modo che TypeB possa essere usato dove TypeA è previsto?" Ma cosa succede se questo è vero e TypeB espone anche l'interfaccia completa di TypeC? E se TypeC non è stato ancora modellato?
Tristan,

4
Alludi a quello che penso dovrebbe essere il test più semplice: "Questo oggetto dovrebbe essere utilizzabile dal codice che prevede oggetti del (quale sarebbe) il tipo di base". Se la risposta è sì, l'oggetto deve ereditare. Se no, allora probabilmente non dovrebbe. Se avessi i miei druther, le lingue fornirebbero una parola chiave per riferirsi a "questa classe", e fornirebbero un modo per definire una classe che dovrebbe comportarsi proprio come un'altra classe, ma non essere sostituibile per essa (tale classe avrebbe tutto "questo classe "riferimenti sostituiti con se stesso).
supercat

22
@Alexey - il punto è "Posso passare un biplano Cessna a tutti i clienti che si aspettano un aereo senza sorprenderli?". Se sì, allora è probabile che tu voglia ereditare.
Gishu,

9
In realtà faccio fatica a pensare a tutti gli esempi in cui l'ereditarietà sarebbe stata la mia risposta, trovo spesso aggregazione, composizione e interfacce che portano a soluzioni più eleganti. Molti degli esempi sopra potrebbero forse essere meglio spiegati usando quegli approcci ...
Stuart Wakefield,

415

Pensa al contenimento come a una relazione. Un'auto "ha un" motore, una persona "ha un" nome, ecc.

Pensa all'eredità come a è una relazione. Un'auto "è un" veicolo, una persona "è un" mammifero, ecc.

Non mi merito per questo approccio. L'ho preso direttamente dalla seconda edizione del codice completo di Steve McConnell , sezione 6.3 .


107
Questo non è sempre un approccio perfetto, è semplicemente una buona linea guida. il principio di sostituzione di Liskov è molto più preciso (fallisce meno).
Bill K,

41
"La mia macchina ha un veicolo." Se lo consideri come una frase separata, non in un contesto di programmazione, non ha assolutamente senso. E questo è il punto centrale di questa tecnica. Se sembra imbarazzante, probabilmente è sbagliato.
Nick Zalutskiy,

36
@Nick Certo, ma "My Car has a VehicleBehavior" ha più senso (immagino che la tua classe "Vehicle" possa essere chiamata "VehicleBehavior"). Quindi non puoi basare la tua decisione su "ha un" vs "è un" confronto, devi usare LSP, o commetterai errori
Tristan,

35
Invece di "è un" pensare a "si comporta come". L'ereditarietà riguarda l'ereditarietà del comportamento, non solo la semantica.
ybakos,

4
@ybakos "Behaves like" può essere realizzato tramite interfacce senza la necessità di ereditarietà. Da Wikipedia : "Un'implementazione della composizione sull'ereditarietà inizia in genere con la creazione di varie interfacce che rappresentano i comportamenti che il sistema deve esibire ... Pertanto, i comportamenti di sistema sono realizzati senza ereditarietà".
DavidRR

210

Se capisci la differenza, è più facile da spiegare.

Codice di procedura

Un esempio di ciò è PHP senza l'uso di classi (in particolare prima di PHP5). Tutta la logica è codificata in un insieme di funzioni. È possibile includere altri file contenenti funzioni di supporto e così via e condurre la propria logica aziendale passando i dati nelle funzioni. Questo può essere molto difficile da gestire man mano che l'applicazione cresce. PHP5 cerca di rimediare offrendo un design più orientato agli oggetti.

Eredità

Questo incoraggia l'uso delle lezioni. L'ereditarietà è uno dei tre principi del design OO (eredità, polimorfismo, incapsulamento).

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

Questa è eredità al lavoro. Il Dipendente "è una" Persona o eredita dalla Persona. Tutte le relazioni di ereditarietà sono relazioni "is-a". Il Dipendente ombreggia anche la proprietà Titolo da Persona, ovvero Dipendente. Il titolo restituirà il Titolo per il Dipendente e non per la Persona.

Composizione

La composizione è preferita rispetto all'eredità. Per dirla in parole povere avresti:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

La composizione è in genere "ha una" o "usa una" relazione. Qui la classe Employee ha una persona. Non eredita da Person ma ottiene invece l'oggetto Person passato ad esso, motivo per cui "ha una" Person.

Composizione su eredità

Ora supponiamo che tu voglia creare un tipo Manager in modo da finire con:

class Manager : Person, Employee {
   ...
}

Questo esempio funzionerà bene, tuttavia, cosa succede se sia la persona che il dipendente dichiarano entrambi Title? Manager.Title deve restituire "Manager of Operations" o "Mr."? Sotto composizione questa ambiguità viene gestita meglio:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

L'oggetto Manager è composto come un dipendente e una persona. Il comportamento del titolo è preso dal dipendente. Questa composizione esplicita rimuove ambiguità tra le altre cose e incontrerai meno bug.


6
Per eredità: non c'è ambiguità. Stai implementando la classe Manager in base ai requisiti. Quindi restituiresti "Manager of Operations" se questo è ciò che i tuoi requisiti specificati, altrimenti useresti semplicemente l'implementazione della classe base. Inoltre potresti rendere Person una classe astratta e quindi assicurarti che le classi downstream implementino una proprietà Title.
Raj Rao,

68
È importante ricordare che si potrebbe dire "Composizione per eredità", ma ciò non significa "Composizione per sempre in eredità". "È un" significa ereditarietà e porta al riutilizzo del codice. Il dipendente è una persona (il dipendente non ha una persona).
Raj Rao,

36
L'esempio è confuso. Il dipendente è una persona, quindi dovrebbe usare l'eredità. Non dovresti usare la composizione per questo esempio, perché è una relazione errata nel modello di dominio, anche se tecnicamente puoi dichiararla nel codice.
Michael Freidgeim,

15
Non sono d'accordo con questo esempio. Un dipendente è-a persona, che è un caso da manuale di uso corretto dell'eredità. Penso anche che il "problema" della ridefinizione del campo del titolo non abbia senso. Il fatto che Employee.Title ombre Person.Title è un segno di scarsa programmazione. Dopo tutto, sono "Mr." e "Manager of Operations" si riferisce davvero allo stesso aspetto di una persona (lettere minuscole)? Vorrei rinominare Employee.Title, e quindi essere in grado di fare riferimento agli attributi Title e JobTitle di un Employee, entrambi i quali hanno senso nella vita reale. Inoltre, non c'è motivo per Manager (continua ...)
Radon Rosborough,

9
(... continua) ereditare da Persona e Dipendente - dopo tutto, il Dipendente eredita già da Persona. In modelli più complessi, in cui una persona potrebbe essere un Manager e un Agent, è vero che l'ereditarietà multipla può essere utilizzata (con attenzione!), Ma sarebbe preferibile in molti ambienti avere una classe di ruolo astratta da cui Manager (contiene Employees gestisce) e eredita l'Agente (contiene Contratti e altre informazioni). Quindi, un dipendente è una persona che ha più ruoli. Pertanto, sia la composizione che l'ereditarietà vengono utilizzate correttamente.
Radon Rosborough

142

Con tutti i vantaggi innegabili forniti dall'eredità, ecco alcuni dei suoi svantaggi.

Svantaggi dell'ereditarietà:

  1. Non è possibile modificare l'implementazione ereditata dalle superclasse in fase di esecuzione (ovviamente perché l'ereditarietà è definita al momento della compilazione).
  2. L'ereditarietà espone una sottoclasse ai dettagli dell'implementazione della sua classe genitore, ecco perché spesso si dice che l'ereditarietà rompe l'incapsulamento (in un certo senso è necessario concentrarsi solo sulle interfacce e non sull'implementazione, quindi non è sempre preferibile riutilizzare la sottoclasse).
  3. Lo stretto accoppiamento fornito dall'ereditarietà rende l'implementazione di una sottoclasse molto legata all'implementazione di una superclasse secondo cui qualsiasi cambiamento nell'implementazione padre costringerà la sottoclasse a cambiare.
  4. Un riutilizzo eccessivo da parte di una sottoclasse può rendere lo stack ereditario molto profondo e anche molto confuso.

D'altra parte, la composizione degli oggetti viene definita in fase di esecuzione tramite oggetti che acquisiscono riferimenti ad altri oggetti. In tal caso, questi oggetti non saranno mai in grado di raggiungere reciprocamente i dati protetti (nessuna interruzione dell'incapsulamento) e saranno costretti a rispettare l'interfaccia reciproca. E anche in questo caso, le dipendenze per l'implementazione saranno molto inferiori rispetto al caso dell'ereditarietà.


5
Questa è una delle risposte migliori, secondo me - aggiungerò a ciò che provare a ripensare i tuoi problemi in termini di composizione, secondo la mia esperienza, tende a condurre a classi più piccole, più semplici, più autosufficienti e più riutilizzabili , con un ambito di responsabilità più chiaro, più piccolo e più mirato. Spesso questo significa che c'è meno bisogno di cose come l'iniezione di dipendenza o il derisione (nei test) poiché i componenti più piccoli sono generalmente in grado di resistere da soli. Solo la mia esperienza.
YMMV

3
L'ultimo paragrafo di questo post ha fatto davvero clic per me. Grazie.
Salx,

87

Un'altra ragione, molto pragmatica, di preferire la composizione rispetto all'eredità ha a che fare con il modello di dominio e mapparlo su un database relazionale. È davvero difficile mappare l'ereditarietà al modello SQL (si finisce con tutti i tipi di soluzioni temporanee, come la creazione di colonne che non vengono sempre utilizzate, l'utilizzo delle viste, ecc.). Alcuni ORML cercano di affrontarlo, ma si complicano sempre rapidamente. La composizione può essere facilmente modellata attraverso una relazione di chiave esterna tra due tabelle, ma l'ereditarietà è molto più difficile.


81

Mentre in poche parole concordo con "Preferire la composizione rispetto all'eredità", molto spesso per me sembra "preferire le patate alla coca-cola". Ci sono luoghi per l'eredità e luoghi per la composizione. Devi capire la differenza, quindi questa domanda scomparirà. Ciò che significa veramente per me è "se hai intenzione di usare l'eredità - ripensaci, è probabile che tu abbia bisogno di composizione".

Dovresti preferire le patate alla coca cola quando vuoi mangiare e la coca cola alle patate quando vuoi bere.

La creazione di una sottoclasse dovrebbe significare più di un semplice modo per chiamare i metodi di superclasse. Dovresti usare l'ereditarietà quando la sottoclasse "è-una" superclasse sia strutturalmente che funzionalmente, quando può essere usata come superclasse e la userai. Se non è così, non è eredità, ma qualcos'altro. La composizione è quando i tuoi oggetti sono costituiti da un altro o hanno una relazione con essi.

Quindi per me sembra che se qualcuno non sa se ha bisogno di ereditarietà o composizione, il vero problema è che non sa se vuole bere o mangiare. Pensa di più al tuo dominio problematico, capiscilo meglio.


5
Lo strumento giusto per il lavoro giusto. Un martello può essere migliore nel battere le cose di una chiave inglese, ma ciò non significa che si dovrebbe vedere una chiave come "un martello inferiore". L'ereditarietà può essere utile quando le cose aggiunte alla sottoclasse sono necessarie affinché l'oggetto si comporti come un oggetto superclasse. Ad esempio, considera una classe base InternalCombustionEnginecon una classe derivata GasolineEngine. Quest'ultimo aggiunge cose come le candele, che mancano alla classe base, ma l'uso della cosa InternalCombustionEnginefarà sì che le candele vengano utilizzate.
supercat,

61

L'ereditarietà è piuttosto allettante, specialmente proveniente dalla terra procedurale e spesso sembra ingannevolmente elegante. Voglio dire, tutto quello che devo fare è aggiungere un po 'di funzionalità ad un'altra classe, giusto? Bene, uno dei problemi è quello

l'eredità è probabilmente la peggior forma di accoppiamento che si possa avere

La classe base interrompe l'incapsulamento esponendo i dettagli dell'implementazione alle sottoclassi sotto forma di membri protetti. Questo rende il tuo sistema rigido e fragile. Il difetto più tragico è tuttavia che la nuova sottoclasse porta con sé tutto il bagaglio e l'opinione della catena dell'eredità.

L'articolo, Inheritance is Evil: The Epic Fail di DataAnnotationsModelBinder , ne illustra un esempio in C #. Mostra l'uso dell'ereditarietà quando la composizione avrebbe dovuto essere usata e come potrebbe essere rifattorizzata.


L'ereditarietà non è buona o cattiva, è semplicemente un caso speciale di Composizione. Dove, infatti, la sottoclasse sta implementando una funzionalità simile alla superclasse. Se la sottoclasse proposta non viene reimplementata ma utilizza semplicemente la funzionalità della superclasse, è stata utilizzata l'ereditarietà in modo errato. Questo è l'errore del programmatore, non una riflessione sull'ereditarietà.
iPherian,

42

In Java o C #, un oggetto non può cambiare il suo tipo dopo che è stato istanziato.

Quindi, se il tuo oggetto deve apparire come un oggetto diverso o comportarsi in modo diverso a seconda dello stato o delle condizioni di un oggetto, usa Composizione : fai riferimento a Modelli di progettazione di stato e strategia .

Se l'oggetto deve essere dello stesso tipo, utilizzare Ereditarietà o implementare le interfacce.


10
+1 Ho scoperto sempre meno che l'eredità funziona nella maggior parte delle situazioni. Preferisco di gran lunga le interfacce condivise / ereditate e la composizione degli oggetti .... o si chiama aggregazione? Non chiedermelo, ho una laurea EE !!
Kenny,

Credo che questo sia lo scenario più comune in cui si applica la "composizione sull'eredità" poiché entrambi potrebbero adattarsi in teoria. Ad esempio, in un sistema di marketing potresti avere il concetto di a Client. Quindi, un nuovo concetto di un PreferredClientpop-up in seguito. Dovrebbe PreferredClientereditare Client? Un client preferito "è un" client dopotutto, no? Beh, non così in fretta ... come hai detto, gli oggetti non possono cambiare la loro classe in fase di esecuzione. Come modelleresti l' client.makePreferred()operazione? Forse la risposta sta nell'usare la composizione con un concetto mancante, Accountforse?
plalx,

Invece di avere diversi tipi di Clientclassi, forse ce n'è solo uno che incapsula il concetto di un Accountche potrebbe essere un StandardAccounto un PreferredAccount...
Plalx,

40

Non ho trovato una risposta soddisfacente qui, quindi ne ho scritta una nuova.

Per capire perché " preferiamo la composizione rispetto all'eredità", dobbiamo prima recuperare l'assunto omesso in questo linguaggio abbreviato.

Esistono due vantaggi dell'ereditarietà: sottotipizzazione e sottoclasse

  1. Sottotipare significa conformarsi a una firma di tipo (interfaccia), ovvero un insieme di API, e si può scavalcare parte della firma per ottenere il polimorfismo del sottotipo.

  2. Sottoclasse significa riutilizzo implicito delle implementazioni del metodo.

Con i due vantaggi derivano due scopi diversi per l'ereditarietà: orientato al sottotipo e orientato al riutilizzo del codice.

Se il riutilizzo del codice è l' unico scopo, la sottoclasse può dare uno in più di quello di cui ha bisogno, cioè alcuni metodi pubblici della classe genitore non hanno molto senso per la classe figlio. In questo caso, invece di privilegiare la composizione rispetto all'eredità, la composizione è richiesta la . Questo è anche il punto da cui deriva l'idea "is-a" vs. "has-a".

Quindi solo quando si propone il sottotipo, cioè per usare la nuova classe in seguito in modo polimorfico, affrontiamo il problema della scelta dell'eredità o della composizione. Questo è il presupposto che viene omesso nel linguaggio abbreviato in discussione.

Sottotipo è conforme a una firma di tipo, questo significa che la composizione deve sempre esporre non meno quantità di API del tipo. Ora iniziano i trade off:

  1. L'ereditarietà fornisce un riutilizzo semplice del codice se non sovrascritto, mentre la composizione deve ricodificare ogni API, anche se è solo un semplice lavoro di delega.

  2. L'ereditarietà fornisce una ricorsione aperta diretta attraverso il sito polimorfico interno this, vale a dire invocare il metodo di override (o persino digitare ) in un'altra funzione membro, pubblica o privata (sebbene scoraggiata ). La ricorsione aperta può essere simulata tramite la composizione , ma richiede uno sforzo supplementare e potrebbe non essere sempre praticabile (?). Questa risposta a una domanda duplicata parla di qualcosa di simile.

  3. L'ereditarietà espone membri protetti . Ciò interrompe l'incapsulamento della classe genitore e, se utilizzato dalla sottoclasse, viene introdotta un'altra dipendenza tra il figlio e il suo genitore.

  4. La composizione ha il vantaggio dell'inversione del controllo e la sua dipendenza può essere iniettata in modo dinamico, come mostrato nel modello del decoratore e nel modello del proxy .

  5. La composizione ha il vantaggio di una programmazione orientata al combinatore , vale a dire lavorare in modo simile al modello composito .

  6. La composizione segue immediatamente la programmazione di un'interfaccia .

  7. La composizione ha il vantaggio di una semplice eredità multipla .

Tenendo presenti i compromessi sopra menzionati, preferiamo quindi la composizione rispetto all'eredità. Tuttavia, per le classi strettamente correlate, vale a dire quando il riutilizzo implicito del codice apporta davvero benefici o si desidera il potere magico della ricorsione aperta, l'ereditarietà sarà la scelta.


34

Personalmente ho imparato a preferire sempre la composizione rispetto all'eredità. Non esiste alcun problema programmatico che puoi risolvere con l'eredità che non puoi risolvere con la composizione; sebbene in alcuni casi potrebbe essere necessario utilizzare interfacce (Java) o protocolli (Obj-C). Dato che C ++ non sa nulla di simile, dovrai usare classi base astratte, il che significa che non puoi liberarti completamente dell'eredità in C ++.

La composizione è spesso più logica, fornisce una migliore astrazione, una migliore incapsulamento, un migliore riutilizzo del codice (specialmente in progetti molto grandi) ed è meno probabile che rompa qualsiasi cosa a distanza solo perché hai apportato una modifica isolata in qualsiasi punto del codice. Inoltre, facilita il rispetto del " Principio di responsabilità singola ", che è spesso sintetizzato come " Non dovrebbe esserci più di un motivo per cambiare una classe ", e significa che ogni classe esiste per uno scopo specifico e dovrebbe hanno solo metodi che sono direttamente correlati al suo scopo. Inoltre, avere un albero ereditario molto superficiale rende molto più semplice mantenere la visione d'insieme anche quando il progetto inizia a diventare molto grande. Molte persone pensano che l'eredità rappresenti il ​​nostro mondo realeabbastanza bene, ma non è la verità. Il mondo reale usa molta più composizione che eredità. Praticamente ogni oggetto del mondo reale che puoi tenere in mano è stato composto da altri oggetti del mondo reale più piccoli.

Ci sono aspetti negativi della composizione, però. Se salti del tutto l'eredità e ti concentri solo sulla composizione, noterai che spesso devi scrivere un paio di righe di codice extra che non erano necessarie se avessi usato l'ereditarietà. A volte sei anche costretto a ripetere te stesso e questo viola il principio DRY(DRY = Non ripetere te stesso). Anche la composizione richiede spesso una delega e un metodo chiama semplicemente un altro metodo di un altro oggetto senza nessun altro codice che circonda questa chiamata. Tali "doppie chiamate di metodo" (che possono facilmente estendersi a chiamate di metodo triple o quadruple e anche più lontane) hanno prestazioni molto peggiori dell'eredità, in cui erediti semplicemente un metodo del tuo genitore. Chiamare un metodo ereditato può essere altrettanto veloce di chiamarne uno non ereditato, oppure può essere leggermente più lento, ma di solito è ancora più veloce di due chiamate di metodo consecutive.

Potresti aver notato che la maggior parte delle lingue OO non consente l'ereditarietà multipla. Mentre ci sono un paio di casi in cui l'ereditarietà multipla può davvero comprarti qualcosa, ma quelli sono piuttosto eccezioni rispetto alla regola. Ogni volta che ti imbatti in una situazione in cui pensi che "l'ereditarietà multipla sia una caratteristica davvero interessante per risolvere questo problema", di solito sei nel punto in cui dovresti riconsiderare l'ereditarietà, dal momento che anche potrebbe richiedere un paio di righe di codice extra , una soluzione basata sulla composizione si rivelerà di solito molto più elegante, flessibile e a prova di futuro.

L'ereditarietà è davvero una caratteristica interessante, ma temo che sia stata abusata negli ultimi due anni. Le persone hanno trattato l'eredità come l'unico martello che può inchiodare tutto, indipendentemente dal fatto che fosse effettivamente un chiodo, una vite o forse qualcosa di completamente diverso.


"Molte persone pensano che l'eredità rappresenti piuttosto bene il nostro mondo reale, ma non è la verità." Tanto questo! Contrariamente a quasi tutti i tutorial di programmazione nel mondo di sempre, modellare oggetti del mondo reale come catene di eredità è probabilmente una cattiva idea a lungo termine. Dovresti usare l'eredità solo quando c'è una relazione incredibilmente ovvia, innata, semplice. Like TextFileis a File.
neonblitzer

25

La mia regola generale: prima di usare l'ereditarietà, considera se la composizione ha più senso.

Motivo: la sottoclasse di solito significa più complessità e connessione, vale a dire più difficile da modificare, mantenere e ridimensionare senza commettere errori.

Una risposta molto più completa e concreta di Tim Boudreau di Sun:

I problemi comuni all'uso dell'eredità secondo me sono:

  • Gli atti innocenti possono avere risultati inaspettati - L'esempio classico di ciò è rappresentato dalle chiamate a metodi sostituibili dal costruttore della superclasse, prima che i campi dell'istanza delle sottoclassi siano stati inizializzati. In un mondo perfetto, nessuno lo farebbe mai. Questo non è un mondo perfetto.
  • Offre tentazioni perverse ai sottoclassatori di fare ipotesi sull'ordine delle chiamate di metodo e simili - tali presupposti tendono a non essere stabili se la superclasse può evolversi nel tempo. Vedi anche l'analogia con il mio tostapane e caffettiera .
  • Le lezioni diventano più pesanti - non sai necessariamente quale lavoro sta facendo la tua superclasse nel suo costruttore, o quanta memoria userà. Quindi costruire un innocuo potenziale oggetto leggero può essere molto più costoso di quanto si pensi, e questo può cambiare nel tempo se la superclasse si evolve
  • Incoraggia un'esplosione di sottoclassi . Il caricamento delle classi costa tempo, più classi costa la memoria. Questo potrebbe non essere un problema fino a quando non hai a che fare con un'app sulla scala di NetBeans, ma lì abbiamo avuto problemi reali, ad esempio quando i menu sono lenti perché la prima visualizzazione di un menu ha innescato un caricamento di classe massiccio. Abbiamo risolto il problema passando a una sintassi più dichiarativa e ad altre tecniche, ma che ha richiesto anche tempi di riparazione.
  • Rende più difficile cambiare le cose in seguito - se hai reso pubblico un corso, lo scambio della superclasse romperà le sottoclassi - è una scelta con cui, una volta reso pubblico il codice, sei sposato. Quindi se non stai alterando la reale funzionalità della tua superclasse, avrai molta più libertà di cambiare le cose in seguito se lo usi, piuttosto che estendere ciò di cui hai bisogno. Prendiamo, ad esempio, la sottoclasse di JPanel - questo di solito è sbagliato; e se la sottoclasse è pubblica da qualche parte, non avrai mai la possibilità di rivisitare quella decisione. Se si accede come getThePanel () di JComponent, è ancora possibile farlo (suggerimento: esporre i modelli per i componenti all'interno come API).
  • Le gerarchie di oggetti non si ridimensionano (o renderle ridimensionabili in seguito è molto più difficile della pianificazione in anticipo) : questo è il classico problema "troppi livelli". Analizzerò qui di seguito e in che modo il modello AskTheOracle può risolverlo (sebbene possa offendere i puristi di OOP).

...

La mia opinione su cosa fare, se si consente l'eredità, che si può prendere con un granello di sale è:

  • Non esporre mai campi, tranne le costanti
  • I metodi devono essere astratti o definitivi
  • Non chiamare metodi dal costruttore della superclasse

...

tutto ciò vale meno per i piccoli progetti che per quelli di grandi dimensioni e meno per le lezioni private rispetto a quelle pubbliche


25

Perché preferire la composizione rispetto all'eredità?

Vedi altre risposte.

Quando puoi usare l'eredità?

Si dice spesso che una classe Barpuò ereditare una classe Fooquando è vera la seguente frase:

  1. un bar è un pazzo

Sfortunatamente, il test sopra riportato da solo non è affidabile. Utilizzare invece quanto segue:

  1. un bar è un foo, AND
  2. i bar possono fare tutto ciò che i foos possono fare.

Il primo test assicura che tutti i getter di abbianoFoo un senso in Bar(= proprietà condivise), mentre il secondo test si assicura che tutti i setter di abbianoFoo un senso in Bar(= funzionalità condivisa).

Esempio 1: Cane -> Animale

Un cane è un animale E i cani possono fare tutto ciò che gli animali possono fare (come respirare, morire, ecc.). Pertanto, la classe Dog può ereditare la classe Animal.

Esempio 2: Cerchio - / -> Ellisse

Un cerchio è un'ellisse MA i cerchi non possono fare tutto ciò che gli ellissi possono fare. Ad esempio, i cerchi non possono allungarsi, mentre le ellissi possono farlo. Pertanto, la classe Circle non può ereditare la classe Ellipse.

Questo è chiamato il problema Circle-Ellipse , che in realtà non è un problema, solo una chiara prova che il primo test da solo non è sufficiente per concludere che l'ereditarietà è possibile. In particolare, questo esempio evidenzia che le classi derivate dovrebbero estendere la funzionalità delle classi base, non limitarla mai . Altrimenti, la classe base non potrebbe essere utilizzata polimorficamente.

Quando dovresti usare l'eredità?

Anche se puoi usare l'eredità non significa che dovresti : usare la composizione è sempre un'opzione. L'ereditarietà è un potente strumento che consente il riutilizzo implicito del codice e l'invio dinamico, ma presenta alcuni svantaggi, motivo per cui la composizione è spesso preferita. I compromessi tra eredità e composizione non sono evidenti, e secondo me sono meglio spiegati nella risposta di LCN .

Come regola generale, tendo a scegliere l'ereditarietà rispetto alla composizione quando si prevede che l'uso polimorfico sia molto comune, nel qual caso il potere dell'invio dinamico può portare a un'API molto più leggibile ed elegante. Ad esempio, avere una classe polimorfica Widgetnei framework GUI o una classe polimorfica Nodenelle librerie XML consente di avere un'API che è molto più leggibile e intuitiva da usare rispetto a quella che avresti con una soluzione basata esclusivamente sulla composizione.

Principio di sostituzione di Liskov

Proprio per questo, un altro metodo utilizzato per determinare se l'ereditarietà è possibile è chiamato Principio di sostituzione di Liskov :

Le funzioni che utilizzano puntatori o riferimenti a classi di base devono essere in grado di utilizzare oggetti di classi derivate senza saperlo

In sostanza, ciò significa che l'ereditarietà è possibile se la classe base può essere usata polimorficamente, il che credo sia equivalente al nostro test "una barra è un foo e le barre possono fare tutto ciò che i foo possono fare".


Gli scenari ellittica e rettangolo quadrato sono scarsi esempi. Le sottoclassi sono invariabilmente più complesse della loro superclasse, quindi il problema è inventato. Questo problema viene risolto invertendo la relazione. Un'ellisse deriva da un cerchio e un rettangolo deriva da un quadrato. È estremamente sciocco usare la composizione in questi scenari.
Fuzzy Logic,

@FuzzyLogic D'accordo, ma in effetti il ​​mio post non sostiene mai l'uso della composizione in questo caso. Ho solo detto che il problema dell'ellisse circolare è un ottimo esempio del perché "is-a" non è un buon test da solo per concludere che Circle dovrebbe derivare da Ellipse. Una volta concluso che effettivamente, Circle non dovrebbe derivare da Ellipse a causa della violazione di LSP, quindi le possibili opzioni sono di invertire la relazione, o utilizzare la composizione, o utilizzare le classi modello o utilizzare un design più complesso che coinvolge classi aggiuntive o funzioni di supporto, ecc ... e la decisione ovviamente dovrebbe essere presa caso per caso.
Boris Dalstein,

1
@FuzzyLogic E se sei curioso di sapere cosa vorrei sostenere per il caso specifico di Circle-Ellipse: suggerirei di non implementare la classe Circle. Il problema con l'inversione della relazione è che viola anche LSP: immagina la funzione computeArea(Circle* c) { return pi * square(c->radius()); }. È ovviamente rotto se superato un'ellisse (cosa significa anche raggio ()?). Un'ellisse non è un cerchio e come tale non dovrebbe derivare dal cerchio.
Boris Dalstein,

computeArea(Circle *c) { return pi * width * height / 4.0; }Adesso è generico.
Fuzzy Logic,

2
@FuzzyLogic Non sono d'accordo: ti rendi conto che ciò significa che la classe Circle ha anticipato l'esistenza della classe derivata Ellipse, e quindi fornito width()e height()? E se ora un utente della libreria decidesse di creare un'altra classe chiamata "EggShape"? Dovrebbe anche derivare da "Cerchio"? Ovviamente no. Una forma d'uovo non è un cerchio, e neanche un'ellisse è un cerchio, quindi nessuno dovrebbe derivare dal cerchio poiché rompe LSP. I metodi che eseguono operazioni su una classe Circle * fanno forti assunzioni su cosa sia un cerchio e infrangere questi presupposti porterà quasi sicuramente a bug.
Boris Dalstein,

19

L'ereditarietà è molto potente, ma non puoi forzarla (vedi: il problema dell'ellisse circolare ). Se davvero non puoi essere completamente sicuro di una vera relazione del sottotipo "is-a", allora è meglio andare con la composizione.


15

L'ereditarietà crea una forte relazione tra una sottoclasse e una superclasse; la sottoclasse deve essere a conoscenza dei dettagli di implementazione della superclasse. Creare la superclasse è molto più difficile, quando devi pensare a come può essere esteso. Devi documentare attentamente gli invarianti di classe e dichiarare quali altri metodi sostituibili usano internamente.

L'ereditarietà è talvolta utile, se la gerarchia rappresenta davvero una relazione tra due persone. Si riferisce al principio aperto-chiuso, che afferma che le classi dovrebbero essere chiuse per modifica ma aperte all'estensione. In questo modo puoi avere il polimorfismo; avere un metodo generico che si occupa del super-tipo e dei suoi metodi, ma tramite l'invio dinamico viene invocato il metodo della sottoclasse. Questo è flessibile e aiuta a creare un riferimento indiretto, che è essenziale nel software (per saperne di meno sui dettagli di implementazione).

L'ereditarietà è facilmente abusata, tuttavia, e crea ulteriore complessità, con forti dipendenze tra le classi. Anche capire cosa succede durante l'esecuzione di un programma diventa piuttosto difficile a causa dei livelli e della selezione dinamica delle chiamate al metodo.

Suggerirei di usare la composizione come predefinita. È più modulare e offre il vantaggio dell'associazione tardiva (è possibile modificare dinamicamente il componente). Inoltre è più facile testare le cose separatamente. E se hai bisogno di usare un metodo di una classe, non sei obbligato ad avere una certa forma (principio di sostituzione di Liskov).


3
Vale la pena notare che l'eredità non è l'unico modo per raggiungere il polimorfismo. Il motivo decorativo fornisce l'aspetto del polimorfismo attraverso la composizione.
BitMask777,

1
@ BitMask777: il polimorfismo dei sottotipi è solo un tipo di polimorfismo, un altro sarebbe polimorfismo parametrico, non è necessario ereditare per quello. Ancora più importante: quando si parla di eredità, si intende l'eredità di classe; .ie puoi avere un polimorfismo di sottotipo avendo un'interfaccia comune per più classi e non hai problemi di ereditarietà.
egaga,

2
@engaga: ho interpretato il tuo commento Inheritance is sometimes useful... That way you can have polymorphismcome un legame diretto con i concetti di eredità e polimorfismo (sottotipizzazione assunta in base al contesto). Il mio commento aveva lo scopo di sottolineare ciò che chiarisci nel tuo commento: che l'eredità non è l'unico modo per implementare il polimorfismo, e in realtà non è necessariamente il fattore determinante quando si decide tra composizione ed eredità.
BitMask777

15

Supponiamo che un aereo abbia solo due parti: un motore e le ali.
Quindi ci sono due modi per progettare una classe di aeromobili.

Class Aircraft extends Engine{
  var wings;
}

Ora il tuo aereo può iniziare con le ali fisse
e cambiarle in ali rotanti al volo. È essenzialmente
un motore con le ali. E se volessi cambiare anche
il motore al volo?

O la classe base Engineespone un mutatore per modificarne le
proprietà, oppure riprogetto Aircraftcome:

Class Aircraft {
  var wings;
  var engine;
}

Ora posso sostituire anche il mio motore al volo.


Il tuo post evidenzia un punto che non avevo considerato prima: per continuare la tua analogia di oggetti meccanici con più parti, su qualcosa come un'arma da fuoco, c'è generalmente una parte contrassegnata con un numero seriale, il cui numero seriale è considerato essere quella dell'arma nel suo insieme (per una pistola, sarebbe tipicamente la cornice). Uno può sostituire tutte le altre parti e avere ancora la stessa arma da fuoco, ma se il telaio si rompe e deve essere sostituito, il risultato dell'assemblaggio di un nuovo telaio con tutte le altre parti della pistola originale sarebbe una nuova pistola. Nota che ...
supercat,

... il fatto che più parti di una pistola possano avere dei numeri di serie contrassegnati su di esse non significa che una pistola può avere più identità. Solo il numero di serie sul telaio identifica la pistola; il numero di serie su qualsiasi altra parte identifica con quale pistola quelle parti sono state fabbricate per essere assemblate, che potrebbe non essere la pistola su cui sono assemblate in un dato momento.
supercat


7

Quando vuoi "copiare" / esporre l'API della classe base, usi l'ereditarietà. Quando si desidera solo "copiare" la funzionalità, utilizzare la delega.

Un esempio di questo: vuoi creare uno Stack da un Elenco. Stack ha solo pop, push e peek. Non dovresti usare l'ereditarietà dato che non vuoi push_back, push_front, removeAt, e al.-tipo di funzionalità in uno Stack.


7

Questi due modi possono convivere bene e supportarsi a vicenda.

La composizione è semplicemente modulare: crei un'interfaccia simile alla classe genitore, crei un nuovo oggetto e deleghi le chiamate ad essa. Se questi oggetti non devono conoscersi, la composizione è abbastanza sicura e facile da usare. Ci sono così tante possibilità qui.

Tuttavia, se per qualche motivo la classe genitore deve accedere alle funzioni fornite dalla "classe figlio" per programmatori inesperti, può sembrare un ottimo posto per usare l'ereditarietà. La classe genitore può semplicemente chiamare il proprio astratto "foo ()" che viene sovrascritto dalla sottoclasse e quindi può dare il valore alla base astratta.

Sembra una buona idea, ma in molti casi è meglio dare alla classe un oggetto che implementa foo () (o addirittura impostare il valore fornito foo () manualmente) piuttosto che ereditare la nuova classe da una classe base che richiede la funzione foo () da specificare.

Perché?

Perché l'eredità è un modo scadente di spostare le informazioni .

La composizione ha un vero vantaggio qui: la relazione può essere invertita: la "classe genitore" o "lavoratore astratto" può aggregare qualsiasi oggetto "figlio" specifico che implementa una determinata interfaccia + qualsiasi figlio può essere impostato all'interno di qualsiasi altro tipo di genitore, che accetta è tipo . E può esserci un numero qualsiasi di oggetti, ad esempio MergeSort o QuickSort potrebbero ordinare qualsiasi elenco di oggetti implementando un'interfaccia di confronto astratta. O per dirla in altro modo: qualsiasi gruppo di oggetti che implementano "foo ()" e altri gruppi di oggetti che possono usare oggetti che hanno "foo ()" possono giocare insieme.

Posso pensare a tre ragioni reali per l'utilizzo dell'ereditarietà:

  1. Hai molte classi con la stessa interfaccia e vuoi risparmiare tempo a scriverle
  2. Devi usare la stessa classe base per ogni oggetto
  3. È necessario modificare le variabili private, che in ogni caso non possono essere pubbliche

Se questi sono veri, allora è probabilmente necessario usare l'ereditarietà.

Non c'è niente di male nell'usare la ragione 1, è molto buona avere un'interfaccia solida sui tuoi oggetti. Questo può essere fatto usando la composizione o con l'ereditarietà, nessun problema - se questa interfaccia è semplice e non cambia. Di solito l'eredità è abbastanza efficace qui.

Se il motivo è il numero 2, diventa un po 'complicato. Hai davvero solo bisogno di usare la stessa classe base? In generale, il solo utilizzo della stessa classe base non è abbastanza buono, ma potrebbe essere un requisito del framework, una considerazione di progettazione che non può essere evitata.

Tuttavia, se si desidera utilizzare le variabili private, il caso 3, è possibile che si verifichino problemi. Se consideri le variabili globali non sicure, allora dovresti considerare l'uso dell'ereditarietà per ottenere anche l'accesso alle variabili private . Intendiamoci, le variabili globali non sono tutte COSÌ cattive: i database sono essenzialmente un grande insieme di variabili globali. Ma se riesci a gestirlo, allora va abbastanza bene.


7

Per rispondere a questa domanda da una prospettiva diversa per i programmatori più recenti:

L'ereditarietà viene spesso insegnata presto quando apprendiamo la programmazione orientata agli oggetti, quindi è vista come una soluzione facile a un problema comune.

Ho tre classi che hanno tutte bisogno di alcune funzionalità comuni. Quindi, se scrivo una classe base e ne ho ereditati tutti, allora avranno tutti quella funzionalità e avrò bisogno di mantenerlo solo una volta.

Sembra fantastico, ma in pratica non funziona quasi mai, mai, per una delle diverse ragioni:

  • Scopriamo che ci sono alcune altre funzioni che vogliamo che le nostre classi abbiano. Se il modo in cui aggiungiamo funzionalità alle classi è attraverso l'ereditarietà, dobbiamo decidere: la aggiungiamo alla classe base esistente, anche se non tutte le classi che ereditano da essa hanno bisogno di quella funzionalità? Creiamo un'altra classe di base? Ma che dire delle classi che ereditano già dall'altra classe base?
  • Scopriamo che per una sola delle classi che eredita dalla nostra classe base vogliamo che la classe base si comporti in modo leggermente diverso. Quindi ora torniamo indietro e armeggiamo con la nostra classe base, magari aggiungendo alcuni metodi virtuali, o peggio ancora, un codice che dice: "Se ho ereditato il tipo A, fai questo, ma se sono ereditato di tipo B, fallo ". È un male per molte ragioni. Uno è che ogni volta che cambiamo la classe base, cambiamo effettivamente ogni classe ereditata. Quindi stiamo davvero cambiando classe A, B, C e D perché abbiamo bisogno di un comportamento leggermente diverso nella classe A. Per quanto pensiamo di essere, potremmo rompere una di quelle classi per ragioni che non hanno nulla a che fare con quelle classi.
  • Potremmo sapere perché abbiamo deciso di ereditare tutte queste classi l'una dall'altra, ma potrebbe non avere (probabilmente non avrà senso) qualcun altro che deve mantenere il nostro codice. Potremmo costringerli a una scelta difficile: faccio qualcosa di veramente brutto e disordinato per fare il cambiamento di cui ho bisogno (vedi il precedente punto elenco) o riscrivo solo un mucchio di questo.

Alla fine, leghiamo il nostro codice in alcuni nodi difficili e non ne traggiamo alcun beneficio, tranne per il fatto che possiamo dire "Bene, ho imparato a conoscere l'eredità e ora l'ho usato." Questo non vuole essere condiscendente perché l'abbiamo fatto tutti. Ma l'abbiamo fatto tutti perché nessuno ci ha detto di non farlo.

Non appena qualcuno mi ha spiegato "favorire la composizione rispetto all'ereditarietà", ho ripensato ogni volta che ho provato a condividere la funzionalità tra le classi usando l'ereditarietà e mi sono reso conto che la maggior parte delle volte non funzionava davvero bene.

L'antidoto è il principio di responsabilità singola . Pensalo come un vincolo. La mia classe deve fare una cosa. Io devo essere in grado di dare un nome che descrive in qualche modo che una cosa che fa la mia classe. (Ci sono eccezioni a tutto, ma a volte le regole assolute sono migliori quando stiamo imparando.) Ne consegue che non posso scrivere una classe base chiamata ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. Qualunque funzionalità distinta di cui ho bisogno deve essere nella sua stessa classe, e quindi altre classi che necessitano di tale funzionalità possono dipendere da quella classe, non ereditarne.

A rischio di semplificazione eccessiva, questa è composizione: comporre più classi per lavorare insieme. E una volta che prendiamo questa abitudine scopriamo che è molto più flessibile, gestibile e testabile rispetto all'uso dell'ereditarietà.


Il fatto che le classi non possano usare più classi di base non è una scarsa riflessione sull'ereditarietà ma piuttosto una scarsa riflessione sulla mancanza di capacità di un particolare linguaggio.
iPherian,

Nel tempo trascorso da quando ho scritto questa risposta, ho letto questo post di "Zio Bob" che affronta quella mancanza di capacità. Non ho mai usato un linguaggio che consenta l'ereditarietà multipla. Ma guardando indietro, la domanda è taggata "linguaggio agnostico" e la mia risposta assume C #. Devo ampliare i miei orizzonti.
Scott Hannen,

6

A parte una / ha delle considerazioni, bisogna anche considerare la "profondità" dell'eredità che deve attraversare il proprio oggetto. Qualunque cosa oltre i cinque o sei livelli di eredità profondi potrebbe causare inaspettati problemi di fusione e boxing / unboxing, e in quei casi potrebbe essere saggio comporre l'oggetto.


6

Quando hai una relazione tra due classi (ad esempio il cane è un cane), vai per eredità.

D'altra parte, quando hai una relazione aggettivo o una relazione aggettivo tra due classi (lo studente ha dei corsi) o (corsi di studio degli insegnanti), hai scelto la composizione.


Hai detto eredità ed eredità. non intendi eredità e composizione?
trevorKirkby il

No, non lo fai. Puoi anche definire un'interfaccia canina e lasciare che ogni cane la implementi e finirai con più codice SOLID.
markus,

5

Un modo semplice per dare un senso a questo sarebbe che l'ereditarietà dovrebbe essere usata quando hai bisogno di un oggetto della tua classe per avere la stessa interfaccia della sua classe genitore, in modo che possa essere trattato come un oggetto della classe genitore (upcasting) . Inoltre, le chiamate di funzione su un oggetto classe derivato rimarrebbero le stesse ovunque nel codice, ma il metodo specifico da chiamare sarebbe determinato in fase di esecuzione (ovvero l' implementazione di basso livello differisce, l' interfaccia di alto livello rimane la stessa).

La composizione dovrebbe essere usata quando non è necessario che la nuova classe abbia la stessa interfaccia, ovvero si desideri nascondere alcuni aspetti dell'implementazione della classe di cui l'utente di quella classe non ha bisogno. Quindi la composizione è più in grado di supportare l' incapsulamento (cioè nascondere l'implementazione) mentre l'ereditarietà ha lo scopo di supportare l' astrazione (ovvero fornire una rappresentazione semplificata di qualcosa, in questo caso la stessa interfaccia per una gamma di tipi con interni diversi).


+1 per la menzione dell'interfaccia. Uso spesso questo approccio per nascondere le classi esistenti e rendere la mia nuova classe propriamente testabile prendendo in giro l'oggetto usato per la composizione. Ciò richiede invece che il proprietario del nuovo oggetto gli passi la classe padre candidata.
Kell,


4

Concordo con @Pavel, quando dice che ci sono posti per la composizione e ci sono posti per l'eredità.

Penso che l'ereditarietà dovrebbe essere usata se la tua risposta è affermativa a una di queste domande.

  • La tua classe fa parte di una struttura che beneficia del polimorfismo? Ad esempio, se avevi una classe Shape, che dichiara un metodo chiamato draw (), allora abbiamo chiaramente bisogno che le classi Circle e Square siano sottoclassi di Shape, in modo che le loro classi client dipendano da Shape e non da sottoclassi specifiche.
  • La tua classe deve riutilizzare le interazioni di alto livello definite in un'altra classe? Il modello di progettazione del metodo modello sarebbe impossibile da implementare senza ereditarietà. Credo che tutti i framework estensibili utilizzino questo modello.

Tuttavia, se la tua intenzione è puramente quella di riutilizzare il codice, molto probabilmente la composizione è una scelta di design migliore.


4

L'ereditarietà è un macanismo molto potente per il riutilizzo del codice. Ma deve essere usato correttamente. Direi che l'ereditarietà viene utilizzata correttamente se la sottoclasse è anche un sottotipo della classe genitore. Come accennato in precedenza, il principio di sostituzione di Liskov è il punto chiave qui.

La sottoclasse non è la stessa del sottotipo. Puoi creare sottoclassi che non sono sottotipi (e questo è quando dovresti usare la composizione). Per capire cos'è un sottotipo, iniziamo a dare una spiegazione di che tipo è.

Quando diciamo che il numero 5 è di tipo intero, affermiamo che 5 appartiene a un insieme di valori possibili (ad esempio, vedere i possibili valori per i tipi primitivi Java). Stiamo anche affermando che esiste un insieme valido di metodi che posso eseguire sul valore come addizione e sottrazione. E infine stiamo affermando che ci sono un insieme di proprietà che sono sempre soddisfatte, ad esempio, se aggiungo i valori 3 e 5, otterrò 8 di conseguenza.

Per fare un altro esempio, pensa ai tipi di dati astratti, Set di numeri interi e Elenco di numeri interi, i valori che possono contenere sono limitati a numeri interi. Entrambi supportano una serie di metodi, come add (newValue) e size (). Entrambi hanno proprietà diverse (classe invariante), Sets non consente duplicati mentre List consente duplicati (ovviamente ci sono altre proprietà che entrambi soddisfano).

Il sottotipo è anche un tipo, che ha una relazione con un altro tipo, chiamato tipo parent (o supertipo). Il sottotipo deve soddisfare le caratteristiche (valori, metodi e proprietà) del tipo parent. La relazione significa che in qualsiasi contesto in cui è previsto il supertipo, può essere sostituibile da un sottotipo, senza influire sul comportamento dell'esecuzione. Andiamo a vedere un po 'di codice per esemplificare quello che sto dicendo. Supponiamo che io scriva un elenco di numeri interi (in una sorta di pseudo linguaggio):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Quindi, scrivo il set di numeri interi come sottoclasse dell'elenco di numeri interi:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

La nostra serie Set di numeri interi è una sottoclasse di Elenco di numeri interi, ma non è un sottotipo, poiché non soddisfa tutte le funzionalità della classe Elenco. I valori e la firma dei metodi sono soddisfatti ma le proprietà non lo sono. Il comportamento del metodo add (Integer) è stato chiaramente modificato, non mantenendo le proprietà del tipo parent. Pensa dal punto di vista del cliente delle tue lezioni. Potrebbero ricevere un set di numeri interi in cui è previsto un elenco di numeri interi. Il client potrebbe voler aggiungere un valore e ottenere quel valore aggiunto all'elenco anche se quel valore esiste già nell'elenco. Ma lei non otterrà quel comportamento se il valore esiste. Una grande sorpresa per lei!

Questo è un classico esempio di uso improprio dell'eredità. Usa la composizione in questo caso.

(un frammento di: usa l'ereditarietà correttamente ).


3

Una regola empirica che ho sentito è che l'ereditarietà dovrebbe essere usata quando è una relazione e una composizione "is-a" quando è una "has-a". Anche con questo, penso che dovresti sempre inclinarti verso la composizione perché elimina molta complessità.


2

Composizione v / s L'ereditarietà è un argomento ampio. Non esiste una risposta reale a ciò che è meglio, poiché penso che tutto dipenda dal design del sistema.

Generalmente il tipo di relazione tra oggetto fornisce informazioni migliori per sceglierne uno.

Se il tipo di relazione è relazione "IS-A", l'ereditarietà è un approccio migliore. altrimenti il ​​tipo di relazione è relazione "HAS-A", quindi la composizione si avvicinerà meglio.

Dipende totalmente dalla relazione tra entità.


2

Anche se la composizione è preferita, vorrei evidenziare i vantaggi dell'ereditarietà e i contro della composizione .

Pro dell'ereditarietà:

  1. Stabilisce una relazione logica " IS A" . Se auto e camion sono due tipi di veicoli (classe base), la classe figlio è una classe base.

    vale a dire

    L'auto è un veicolo

    Il camion è un veicolo

  2. Con l'ereditarietà, è possibile definire / modificare / estendere una capacità

    1. La classe base non prevede alcuna implementazione e la sottoclasse deve sovrascrivere il metodo completo (astratto) => È possibile implementare un contratto
    2. La classe base fornisce un'implementazione predefinita e la sottoclasse può modificare il comportamento => È possibile ridefinire il contratto
    3. La sottoclasse aggiunge l'estensione all'implementazione della classe base chiamando super.methodName () come prima istruzione => È possibile estendere un contratto
    4. La classe base definisce la struttura dell'algoritmo e la sottoclasse sostituirà una parte dell'algoritmo => È possibile implementare Template_method senza modifiche nello scheletro della classe base

Contro di composizione:

  1. In eredità, la sottoclasse può invocare direttamente il metodo della classe base anche se non sta implementando il metodo della classe base a causa della relazione IS A. Se usi la composizione, devi aggiungere metodi nella classe contenitore per esporre l'API della classe contenuta

ad es. se l' automobile contiene un veicolo e se devi ottenere il prezzo dell'auto , che è stato definito nel veicolo , il tuo codice sarà così

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

Penso che non risponda alla domanda
almanegra

È possibile rivedere la domanda OP. Ho affrontato: quali compromessi ci sono per ciascun approccio?
Ravindra babu,

Come hai detto, parli solo di "pro di ereditarietà e contro di composizione", e non dei compromessi per ogni approccio o dei casi in cui dovresti usarli l'uno sull'altro
almanegra

vantaggi e svantaggi forniscono un compromesso poiché i vantaggi dell'eredità sono contro la composizione e contro la composizione sono vantaggi dell'eredità.
Ravindra babu,

1

Come molte persone hanno detto, inizierò con il controllo - se esiste una relazione "is-a". Se esiste di solito controllo quanto segue:

Se la classe base può essere istanziata. Cioè, se la classe base può essere non astratta. Se può essere non astratto, di solito preferisco la composizione

Ad es. 1. Il contabile è un dipendente. Ma non userò l'eredità perché un oggetto Employee può essere istanziato.

Ad esempio 2. Il libro è un oggetto di vendita. Un oggetto di vendita non può essere istanziato: è un concetto astratto. Quindi userò l'ereditacne. SellingItem è una classe base astratta (o interfaccia in C #)

Cosa ne pensi di questo approccio?

Inoltre, supporto la risposta di @anon in Perché usare l'ereditarietà?

Il motivo principale per l'utilizzo dell'ereditarietà non è come una forma di composizione - è così che puoi ottenere un comportamento polimorfico. Se non hai bisogno del polimorfismo, probabilmente non dovresti usare l'ereditarietà.

@MatthieuM. dice in /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448

Il problema con l'ereditarietà è che può essere utilizzato per due scopi ortogonali:

interfaccia (per polimorfismo)

implementazione (per riutilizzo del codice)

RIFERIMENTO

  1. Quale design di classe è migliore?
  2. Ereditarietà vs. aggregazione

1
Non sono sicuro del motivo per cui "la classe base è astratta?" figure nella discussione .. LSP: tutte le funzioni che operano su Dogs funzioneranno se vengono passati oggetti Poodle? Se sì, allora Poodle può essere sostituito con Dog e quindi può ereditare da Dog.
Gishu

@Gishu Grazie. Esaminerò sicuramente LSP. Ma prima, puoi per favore fornire un "esempio in cui l'ereditarietà è corretta in cui la classe base non può essere astratta". Ciò che penso sia, l'ereditarietà è applicabile solo se la classe base è astratta. Se è necessario creare un'istanza della classe base separatamente, non utilizzare l'ereditarietà. Cioè, anche se ragioniere è un dipendente, non utilizzare l'eredità.
LCJ,

1
di recente ho letto WCF. Un esempio nel framework .net è SynchronizationContext (base + può essere istanziato) quali code funzionano su un thread ThreadPool. Le derivazioni includono WinFormsSyncContext (coda sul thread dell'interfaccia utente) e DispatcherSyncContext (coda sul dispatcher WPF)
Gishu

@Gishu Grazie. Tuttavia, sarebbe più utile se fosse possibile fornire uno scenario basato su dominio bancario, dominio delle risorse umane, dominio al dettaglio o qualsiasi altro dominio popolare.
LCJ,

1
Scusate. Non ho familiarità con quei domini. Un altro esempio se quello precedente era troppo ottuso è la classe Control in Winforms / WPF. Il controllo base / generico può essere istanziato. Le derivazioni includono Listbox, Textbox, ecc. Ora che ci penso, il modello Decorator Design è un buon esempio di IMHO e anche utile. Il decoratore deriva dall'oggetto non astratto che vuole avvolgere / decorare.
Gishu,

1

Vedo che nessuno ha menzionato il problema dei diamanti , che potrebbe sorgere con l'eredità.

A colpo d'occhio, se le classi B e C ereditano A ed entrambe ignorano il metodo X e una quarta classe D, ereditano sia da B che da C, e non sostituiscono X, quale implementazione di XD dovrebbe usare?

Wikipedia offre una bella panoramica dell'argomento discusso in questa domanda.


1
D eredita B e C non A. In tal caso, utilizzerebbe l'implementazione di X che è in classe A.
Fabricio

1
@fabricio: grazie, ho modificato il testo. A proposito, tale scenario non può verificarsi in lingue che non consentono l'ereditarietà di più classi, ho ragione?
Veverke,

si hai ragione .. e non ho mai lavorato con uno che consenta l'ereditarietà multipla (come nell'esempio nel problema dei diamanti) ..
fabricio
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.