Perché preferire la composizione rispetto all'eredità? Quali sono i compromessi per ciascun approccio? Quando dovresti scegliere l'eredità rispetto alla composizione?
Perché preferire la composizione rispetto all'eredità? Quali sono i compromessi per ciascun approccio? Quando dovresti scegliere l'eredità rispetto alla composizione?
Risposte:
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à .
TypeB vuole solo parte / parte del comportamento esposto da TypeA? Indica la necessità di composizione.
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?"
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 .
Se capisci la differenza, è più facile da spiegare.
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.
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.
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.
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.
Con tutti i vantaggi innegabili forniti dall'eredità, ecco alcuni dei suoi svantaggi.
Svantaggi dell'ereditarietà:
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à.
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.
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.
InternalCombustionEngine
con una classe derivata GasolineEngine
. Quest'ultimo aggiunge cose come le candele, che mancano alla classe base, ma l'uso della cosa InternalCombustionEngine
farà sì che le candele vengano utilizzate.
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
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.
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.
Client
. Quindi, un nuovo concetto di un PreferredClient
pop-up in seguito. Dovrebbe PreferredClient
ereditare 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, Account
forse?
Client
classi, forse ce n'è solo uno che incapsula il concetto di un Account
che potrebbe essere un StandardAccount
o un PreferredAccount
...
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
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.
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:
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.
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.
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.
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 .
La composizione ha il vantaggio di una programmazione orientata al combinatore , vale a dire lavorare in modo simile al modello composito .
La composizione segue immediatamente la programmazione di un'interfaccia .
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.
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.
TextFile
is a File
.
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
Vedi altre risposte.
Si dice spesso che una classe Bar
può ereditare una classe Foo
quando è vera la seguente frase:
- un bar è un pazzo
Sfortunatamente, il test sopra riportato da solo non è affidabile. Utilizzare invece quanto segue:
- un bar è un foo, AND
- 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.
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 Widget
nei framework GUI o una classe polimorfica Node
nelle 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.
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".
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.
computeArea(Circle *c) { return pi * width * height / 4.0; }
Adesso è generico.
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.
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.
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).
Inheritance is sometimes useful... That way you can have polymorphism
come 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à.
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 Engine
espone un mutatore per modificarne le
proprietà, oppure riprogetto Aircraft
come:
Class Aircraft {
var wings;
var engine;
}
Ora posso sostituire anche il mio motore al volo.
Devi dare un'occhiata al principio di sostituzione di Liskov nei principi SOLIDI di design di classe dello zio Bob . :)
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.
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à:
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.
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:
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à.
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.
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.
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).
Il sottotipo è appropriato e più potente in cui è possibile elencare gli invarianti , altrimenti utilizzare la composizione della funzione per estensibilità.
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.
Tuttavia, se la tua intenzione è puramente quella di riutilizzare il codice, molto probabilmente la composizione è una scelta di design migliore.
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 ).
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à.
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à.
Anche se la composizione è preferita, vorrei evidenziare i vantaggi dell'ereditarietà e i contro della composizione .
Pro dell'ereditarietà:
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
Con l'ereditarietà, è possibile definire / modificare / estendere una capacità
Contro di composizione:
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();
}
}
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
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.