Da dove viene questo concetto di "favorire la composizione sull'eredità"?


143

Negli ultimi mesi, il mantra "favorire la composizione sull'eredità" sembra essere sorto dal nulla e diventare quasi una sorta di meme all'interno della comunità di programmazione. E ogni volta che lo vedo, sono un po 'sconcertato. È come se qualcuno avesse detto "favorire i trapani sui martelli". Nella mia esperienza, la composizione e l'eredità sono due strumenti diversi con casi d'uso diversi e trattarli come se fossero intercambiabili e l'uno era intrinsecamente superiore all'altro non ha senso.

Inoltre, non vedo mai una vera spiegazione del perché l' eredità è cattiva e la composizione è buona, il che mi rende più sospettoso. Dovrebbe essere semplicemente accettato sulla fede? La sostituzione e il polimorfismo di Liskov hanno benefici ben noti e chiari, e l'IMO comprende l'intero punto di usare la programmazione orientata agli oggetti, e nessuno spiega mai perché dovrebbero essere scartati a favore della composizione.

Qualcuno sa da dove viene questo concetto e qual è la logica alla base?


45
Come altri hanno sottolineato, è passato molto tempo - sono sorpreso che tu ne abbia sentito parlare. È intuitivo per chiunque abbia costruito grandi sistemi in lingue come Java per un certo periodo di tempo. È fondamentale per qualsiasi intervista che io abbia mai rilasciato e quando un candidato inizia a parlare di eredità, inizio a dubitare del loro livello di abilità e della quantità di esperienza. Ecco una buona introduzione al perché l'eredità è una soluzione fragile (ce ne sono molte altre): artima.com/lejava/articles/designprinciples4.html
Janx

6
@Janx: forse è tutto. Non costruisco grandi sistemi in lingue come Java; Li costruisco a Delfi e senza la sostituzione e il polimorfismo di Liskov non avremmo mai fatto nulla. Il suo modello a oggetti è per certi versi diverso da quello di Java o C ++, e molti dei problemi che questa massima sembra essere pensata per risolvere non esistono realmente, o sono molto meno problematici, in Delphi. Diverse prospettive da diversi punti di vista, immagino.
Mason Wheeler,

14
Ho trascorso diversi anni in una squadra costruendo sistemi relativamente grandi a Delfi e gli alberi ad alto patrimonio ereditato hanno sicuramente morso la nostra squadra e ci hanno causato un notevole dolore. Sospetto che la tua attenzione ai principi SOLID ti stia aiutando a evitare le aree problematiche, non il tuo uso di Delphi.
Bevan,

8
Ultimi mesi?!?
Jas,

6
IMHO questo concetto non è mai stato completamente adattato alla varietà di linguaggi che supportano sia l'ereditarietà dell'interfaccia (cioè sottotipizzazione con interfacce pure) sia l'ereditarietà dell'implementazione. Troppe persone seguono questo mantra e non usano abbastanza interfacce.
Uri,

Risposte:


136

Anche se penso di aver ascoltato discussioni sulla composizione contro l'ereditarietà molto prima di GoF, non posso mettere il dito su una fonte specifica. Potrebbe essere stato Booch comunque.

<Rant>

Ah, ma come molti mantra, questo è degenerato secondo le linee tipiche:

  1. viene introdotto con una spiegazione e un argomento dettagliati da una fonte rispettata che conia il slogan come promemoria della discussione complessa originale
  2. è condiviso con un ammiccamento di parte del club consapevole da alcuni in-the-know per un po ', generalmente quando si commenta errori n00b
  3. presto viene ripetutamente insensato da migliaia e migliaia che non leggono mai la spiegazione, ma amano usarla come una scusa per non pensare e come un modo economico e facile per sentirsi superiore agli altri
  4. alla fine, nessuna quantità ragionevole di debunking può arginare la marea del "meme" - e il paradigma degenera in religione e dogma.

Il meme, originariamente destinato a condurre n00bs verso l'illuminazione, è ora usato come un club per farli diventare inconsapevoli.

Composizione ed eredità sono cose molto diverse e non devono essere confuse tra loro. Sebbene sia vero che la composizione può essere utilizzata per simulare l'eredità con molto lavoro extra , ciò non rende l'eredità un cittadino di seconda classe, né rende la composizione il figlio prediletto. Il fatto che molti n00bs provino a utilizzare l'ereditarietà come scorciatoia non invalida il meccanismo e quasi tutti i n00bs imparano dall'errore e quindi migliorano.

Si prega di PENSARE circa i vostri disegni, e smettere di schizzando slogan.

</ Rant>


16
Booch sostiene che l'eredità dell'implementazione introduce un elevato accoppiamento tra le classi. D'altra parte, considera l'ereditarietà il concetto chiave che distingue OOP dalla programmazione procedurale.
Nemanja Trifunovic,

13
Dovrebbe esserci una parola per questo genere di cose. Rigurgito prematuro , forse?
Jörg W Mittag,

8
@Jörg: sai cosa accadrà a un termine come il rigurgito prematuro? Esattamente quello che è spiegato sopra. :) (A proposito, quando il rigurgito non è mai prematuro?)
Bjarke Freund-Hansen,

6
@Nemanja: giusto. La domanda è se questo accoppiamento è davvero negativo. Se le classi sono concettualmente fortemente accoppiate e concettualmente formano una relazione supertipo-sottotipo, e non potrebbero mai essere disaccoppiate concettualmente anche se formalmente disaccoppiate a livello linguistico, allora l'accoppiamento forte va bene.
dsimcha,

5
Il mantra è semplice, "solo perché puoi modellare il play-doh in modo che assomigli a qualsiasi cosa non significa che dovresti ricavare tutto dal play-doh". ;)
Evan Plaice,

73

Esperienza.

Come dici tu sono strumenti per diversi lavori, ma la frase è nata perché le persone non la usavano in quel modo.

L'ereditarietà è principalmente uno strumento polimorfico, ma alcune persone, con grande ritardo, tentano di usarlo come un modo per riutilizzare / condividere il codice. La logica è "bene se eredito allora ottengo tutti i metodi gratuitamente", ma ignorando il fatto che queste due classi potenzialmente non hanno alcuna relazione polimorfica.

Quindi perché preferire la composizione all'eredità - beh semplicemente perché il più delle volte la relazione tra classi non è polimorfica. Esiste semplicemente per aiutare a ricordare alle persone di non rispondere a scatti del ginocchio ereditando.


7
Quindi, in sostanza, non puoi dire "non dovresti usare OOP in primo luogo se non capisci la sostituzione di Liskov", quindi dici "favorisci la composizione sull'eredità" invece come un tentativo di limitare il danno fatto da incompetente programmatori informatici?
Mason Wheeler,

13
@Mason: Come ogni mantra, "favorire la composizione sull'ereditarietà" è rivolto ai programmatori principianti. Se sai già quando usare l'eredità e quando usare la composizione, è inutile ripetere un mantra del genere.
Dean Harding,

7
@Dean - Non sono sicuro che il principiante sia lo stesso di chi non comprende le migliori pratiche di ereditarietà. Penso che ci sia molto di più. La scarsa eredità è la causa di molti, molti mal di testa nel mio lavoro e non è il codice scritto da programmatori che sarebbero considerati "principianti".
Nicole,

6
@Renesis: La prima cosa a cui penso quando sento che è "alcune persone hanno dieci anni di esperienza e alcune persone hanno un anno di esperienza ripetuta dieci volte".
Mason Wheeler,

8
Stavo progettando un progetto abbastanza grande subito dopo il primo "Getting" OO e ci ho fatto molto affidamento. Anche se ha funzionato bene, ho trovato costantemente fragile il design - causando lievi irritazioni qua e là e talvolta trasformando alcuni refactor in una cagna completa. È un po 'difficile spiegare l'intera esperienza, ma la frase "Favorire la complicazione sull'ereditarietà" la descrive ESATTAMENTE. Nota che non dice né implica "Evita l'ereditarietà", ti dà semplicemente una piccola spinta quando la scelta non è ovvia.
Bill K,

43

Non è una nuova idea, credo che sia stata effettivamente introdotta nel libro dei modelli di design GoF, pubblicato nel 1994.

Il problema principale con l'ereditarietà è che è white-box. Per definizione, devi conoscere i dettagli di implementazione della classe da cui erediti. Con la composizione, d'altra parte, ti interessa solo l'interfaccia pubblica della classe che stai componendo.

Dal libro GoF:

L'ereditarietà espone una sottoclasse ai dettagli dell'implementazione del genitore, si dice spesso che "l'ereditarietà rompe l'incapsulamento"

L'articolo di Wikipedia sul libro GoF ha un'introduzione decente.


14
Non sono d'accordo con quello. Non è necessario conoscere i dettagli di implementazione della classe da cui si eredita; solo i membri pubblici e protetti esposti dalla classe. Se devi conoscere i dettagli dell'implementazione, allora tu o chiunque abbia scritto la classe base state facendo qualcosa di sbagliato, e se il difetto è nella classe base, la composizione non vi aiuterà a risolvere / aggirare.
Mason Wheeler,

18
Come puoi non essere d'accordo con qualcosa che non hai letto? C'è una pagina solida e mezzo di discussioni da GoF di cui stai ottenendo solo una piccola visione.
philosodad,

7
@pholosodad: non sono in disaccordo con qualcosa che non ho letto. Non sono d'accordo con ciò che ha scritto Dean, secondo cui "Per definizione, devi conoscere i dettagli di implementazione della classe da cui stai ereditando", che ho letto.
Mason Wheeler,

10
Quello che ho scritto è solo un riassunto di ciò che è descritto nel libro GoF. Potrei averlo formulato in modo un po 'forte (non è necessario conoscere tutti i dettagli di implementazione), ma questa è la ragione generale per cui il GoF afferma di favorire la composizione rispetto all'eredità.
Dean Harding,

2
Correggimi se sbaglio, ma lo vedo come "favorisci relazioni di classe esplicite (cioè interfacce) piuttosto che implicite (cioè eredità)". Il primo ti dice ciò di cui hai bisogno senza dirti come farlo. Quest'ultimo non solo ti dirà come farlo, ma ti farà pentire lungo la strada.
Evan Plaice,

26

Per rispondere a una parte della tua domanda, credo che questo concetto sia apparso per la prima volta nel libro GOF Design Patterns: Elements of Reusable Object-Oriented Software , pubblicato per la prima volta nel 1994. La frase appare all'inizio di pagina 20, nell'introduzione:

Favorisce la composizione dell'oggetto rispetto all'eredità

Prefanno questa affermazione con un breve confronto tra eredità e composizione. Non dicono "non usare mai l'eredità".


23

"Composizione sull'ereditarietà" è un modo breve (e apparentemente fuorviante) di dire "Quando senti che i dati (o il comportamento) di una classe dovrebbero essere incorporati in un'altra classe, considera sempre l'uso della composizione prima di applicare ciecamente l'ereditarietà".

Perché è vero? Perché l'ereditarietà crea un accoppiamento stretto, in fase di compilazione tra le 2 classi. La composizione al contrario è un accoppiamento libero, che tra l'altro consente una chiara separazione delle preoccupazioni, la possibilità di cambiare le dipendenze in fase di esecuzione e una più facile, più isolata testabilità delle dipendenze.

Ciò significa solo che l'eredità dovrebbe essere gestita con cura perché ha un costo, non che non sia utile. In realtà, "Composizione su eredità" spesso finisce per essere "Composizione + eredità su eredità" poiché spesso si desidera che la propria dipendenza composta sia una superclasse astratta anziché la sottoclasse stessa. Ti consente di passare tra diverse implementazioni concrete della tua dipendenza in fase di esecuzione.

Per questo motivo (tra gli altri), probabilmente vedrai l'ereditarietà usata più spesso sotto forma di implementazione dell'interfaccia o di classi astratte rispetto all'ereditarietà vaniglia.

Un esempio (metaforico) potrebbe essere:

"Ho una classe Snake e voglio includere come parte di quella classe cosa succede quando il Snake morde. Sarei tentato di avere il Snake ereditare una classe BiterAnimal che ha il metodo Bite () e sovrascrivere quel metodo per riflettere il morso velenoso Ma la composizione sull'ereditarietà mi avverte che dovrei provare a usare la composizione invece ... Nel mio caso, questo potrebbe tradursi nel serpente con un membro Bite. La classe Bite potrebbe essere astratta (o un'interfaccia) con diverse sottoclassi. cose carine come avere sottoclassi VenomousBite e DryBite e poter cambiare morso sulla stessa istanza di Snake man mano che il serpente cresce di età. Inoltre, gestire tutti gli effetti di un Bite nella sua classe separata potrebbe permettermi di riutilizzarlo in quella classe di Frost , perché il gelo morde ma non è un BiterAnimal e così via ... "


4
Ottimo esempio con il serpente. Recentemente ho incontrato un caso simile con le mie lezioni
Maksee

22

Alcuni possibili argomenti per la composizione:

La composizione è leggermente più
ereditaria di linguaggio / framework e ciò che impone / richiede / abilita differirà tra le lingue in termini di ciò a cui la sottoclasse / superclasse ha accesso e quali implicazioni sulle prestazioni potrebbe avere rispetto ai metodi virtuali ecc. La composizione è piuttosto semplice e richiede pochissimo supporto linguistico, e quindi implementazioni su piattaforme / framework diversi possono condividere più facilmente i modelli di composizione.

La composizione è un modo molto semplice e tattile di costruire oggetti
L'ereditarietà è relativamente facile da capire, ma ancora non così facilmente dimostrabile nella vita reale. Molti oggetti nella vita reale possono essere suddivisi in parti e composti. Supponiamo che una bicicletta possa essere costruita utilizzando due ruote, un telaio, un sedile, una catena, ecc. Spiegazione facile per composizione. Mentre in una metafora dell'eredità si potrebbe dire che una bicicletta estende un monociclo, un po 'fattibile ma ancora molto più lontano dal quadro reale rispetto alla composizione (ovviamente questo non è un ottimo esempio di eredità, ma il punto rimane lo stesso). Perfino la parola eredità (almeno della maggior parte dei parlanti di inglese americano che mi aspetterei) invoca automaticamente un significato lungo le linee "Qualcosa tramandato da un parente deceduto" che ha una certa correlazione con il suo significato nel software, ma si adatta solo vagamente.

La composizione è quasi sempre più flessibile
Usando la composizione puoi sempre scegliere di definire il tuo comportamento o semplicemente esporre quella parte delle parti composte. In questo modo non si affrontano le restrizioni che possono essere imposte da una gerarchia ereditaria (virtuale contro non virtuale ecc.)

Quindi, potrebbe essere perché la composizione è naturalmente una metafora più semplice che ha meno vincoli teorici dell'eredità. Inoltre, questi motivi particolari possono essere più evidenti durante la fase di progettazione, o forse sporgere quando si affrontano alcuni dei punti dolenti dell'eredità.

Disclaimer:
ovviamente non è questa strada chiara / a senso unico. Ogni progetto merita la valutazione di diversi modelli / strumenti. L'ereditarietà è ampiamente utilizzata, ha molti vantaggi e molte volte è più elegante della composizione. Questi sono solo alcuni dei possibili motivi che si potrebbero usare per favorire la composizione.


1
"Ovviamente non è così chiaro / strada a senso unico." Quando la composizione non è più (o ugualmente) flessibile dell'eredità? Direi che è una strada a senso unico. L'ereditarietà è solo zucchero sintattico per un caso speciale di composizione.
weberc2,

La composizione spesso non è flessibile quando si utilizza un linguaggio che implementa la composizione utilizzando interfacce esplicitamente implementate , poiché tali interfacce non possono evolversi in modo compatibile con le versioni precedenti nel tempo. #jmtcw
MikeSchinkel

11

Forse hai notato persone che lo hanno detto negli ultimi mesi, ma è stato conosciuto da buoni programmatori per molto più tempo. Lo dico certamente dove appropriato da circa un decennio.

Il punto del concetto è che esiste un grande sovraccarico concettuale dell'ereditarietà. Quando si utilizza l'ereditarietà, ogni singola chiamata di metodo ha una spedizione implicita in essa. Se hai alberi di eredità profondi, o spedizione multipla, o (anche peggio) entrambi, quindi capire dove verrà inviato il metodo particolare in una chiamata particolare può diventare una PITA reale. Rende più complesso il ragionamento corretto sul codice e rende più difficile il debug.

Vorrei fare un semplice esempio per illustrare. Supponiamo che nel profondo di un albero ereditario, qualcuno abbia chiamato un metodo foo. Poi arriva qualcun altro e aggiunge fooin cima all'albero, ma facendo qualcosa di diverso. (Questo caso è più comune con l'ereditarietà multipla.) Ora quella persona che lavora nella classe radice ha rotto l'oscura classe figlio e probabilmente non se ne rende conto. Potresti avere una copertura del 100% con i test unitari e non notare questa rottura perché la persona in alto non penserebbe di testare la classe figlio, e i test per la classe figlio non pensano di testare i nuovi metodi creati in alto . (Certamente ci sono modi per scrivere test unit che li cattureranno, ma ci sono anche casi in cui non puoi scrivere test in quel modo.)

Al contrario, quando si utilizza la composizione, ad ogni chiamata di solito è più chiaro a cosa si sta inviando la chiamata. (OK, se stai usando l'inversione del controllo, ad esempio con l'iniezione di dipendenza, anche capire dove va la chiamata può diventare problematico. Ma di solito è più semplice da capire.) Questo rende più facile il ragionamento. Come bonus, la composizione si traduce in metodi separati l'uno dall'altro. L'esempio sopra non dovrebbe accadere lì perché la classe figlio si sposterebbe verso qualche componente oscuro, e non c'è mai una domanda sul fatto che la chiamata a foofosse intesa per il componente oscuro o l'oggetto principale.

Ora hai perfettamente ragione che eredità e composizione sono due strumenti molto diversi che servono due diversi tipi di cose. Sicuramente l'eredità comporta un sovraccarico concettuale, ma quando è lo strumento giusto per il lavoro, comporta un sovraccarico meno concettuale rispetto al cercare di non usarlo e fare a mano ciò che fa per te. Nessuno che sa cosa stanno facendo direbbe che non dovresti mai usare l'eredità. Ma assicurati che sia la cosa giusta da fare.

Sfortunatamente molti sviluppatori conoscono il software orientato agli oggetti, imparano l'eredità e poi escono per usare la loro nuova ascia il più spesso possibile. Ciò significa che cercano di usare l'ereditarietà dove la composizione era lo strumento giusto. Speriamo che impareranno meglio nel tempo, ma spesso ciò non accade fino a dopo che alcuni arti sono stati rimossi, ecc. Dire loro che è una cattiva idea tende ad accelerare il processo di apprendimento e ridurre gli infortuni.


3
Va bene, suppongo che abbia senso dal punto di vista del C ++. È qualcosa a cui non ho mai pensato, perché non è un problema in Delphi, che è quello che uso la maggior parte delle volte. (Non c'è ereditarietà multipla e se si dispone di un metodo in una classe base e di un altro metodo con lo stesso nome in una classe derivata che non sovrascrive il metodo base, il compilatore emetterà un avviso, quindi non si finisce accidentalmente con questo tipo di problema.)
Mason Wheeler

1
@Mason: la versione di Ander di Object Pascal (alias Delphi) è superiore a C ++ e Java quando si tratta del fragile problema di eredità della classe base. A differenza di C ++ e Java, il sovraccarico di un metodo virtuale ereditato non è implicito.
bit-twiddler

2
@ bit-twiddler, quello che dici su C ++ e Java può essere detto su Smalltalk, Perl, Python, Ruby, JavaScript, Common Lisp, Objective-C e ogni altra lingua che ho imparato che fornisce qualsiasi forma di supporto OO. Detto questo, googling suggerisce che C # segue l'esempio di Object Pascal.
btilly

3
Questo perché Anders Hejlsberg ha disegnato il sapore Borland di Object Pascal (aka Delphi) e C #.
bit-twiddler,

8

È una reazione all'osservazione che i principianti di OO tendono a usare l'eredità quando non ne hanno bisogno. L'ereditarietà non è certamente una cosa negativa, ma può essere abusata. Se una classe ha solo bisogno di funzionalità da un'altra, probabilmente la composizione funzionerà. In altre circostanze, l'eredità funzionerà e la composizione no.

Ereditare da una classe implica molte cose. Implica che un Derivato è un tipo di Base (vedi il principio di Sostituzione di Liskov per i dettagli cruenti), in quanto ogni volta che usi una Base ha senso usare un Derivato. Fornisce l'accesso derivato ai membri protetti e alle funzioni membro di Base. È una relazione stretta, il che significa che ha un elevato accoppiamento e le modifiche a una hanno maggiori probabilità di richiedere modifiche all'altra.

L'accoppiamento è una cosa negativa. Rende i programmi più difficili da comprendere e modificare. A parità di altre condizioni, dovresti sempre selezionare l'opzione con meno accoppiamento.

Pertanto, se la composizione o l'eredità eseguiranno il lavoro in modo efficace, scegliere la composizione, poiché è un accoppiamento inferiore. Se la composizione non farà il lavoro in modo efficace e l'eredità lo farà, scegli l'eredità, perché devi farlo.


"In altre circostanze, l'eredità funzionerà e la composizione no". Quando? L'ereditarietà può essere modellata usando il polimorfismo della composizione e dell'interfaccia, quindi sarei sorpreso se ci sono circostanze per le quali la composizione non funzionerà.
weberc2,

8

Ecco i miei due centesimi (oltre a tutti i punti eccellenti già raccolti):

IMHO, Dipende dal fatto che la maggior parte dei programmatori non ottiene realmente l'eredità e finisce per esagerare, in parte a causa di come viene insegnato questo concetto. Questo concetto esiste come un modo per cercare di dissuaderli dall'esagerare, invece di concentrarsi sull'insegnare loro come farlo nel modo giusto.

Chiunque abbia trascorso del tempo a insegnare o tutorare sa che questo è ciò che accade spesso, soprattutto con i nuovi sviluppatori che hanno esperienza in altri paradigmi:

Questi sviluppatori inizialmente ritengono che l'ereditarietà sia questo concetto spaventoso. Quindi finiscono per creare tipi con sovrapposizioni di interfaccia (ad esempio, lo stesso comportamento specificato senza sottotipi comuni) e con globi per l'implementazione di funzionalità comuni.

Quindi (spesso come risultato dell'insegnamento troppo zelante), apprendono i benefici dell'eredità, ma spesso viene insegnato come la soluzione generale da riutilizzare. Finiscono con la percezione che qualsiasi comportamento condiviso debba essere condiviso attraverso l'eredità. Questo perché l'attenzione si concentra spesso sull'ereditarietà dell'implementazione piuttosto che sul sottotipo.

Nell'80% dei casi è abbastanza buono. Ma l'altro 20% è dove inizia il problema. Per evitare di riscrivere e per assicurarsi di aver approfittato della condivisione dell'implementazione, iniziano a progettare la loro gerarchia attorno all'implementazione prevista piuttosto che alle astrazioni sottostanti. Lo "Stack eredita da una lista doppiamente collegata" ne è un buon esempio.

La maggior parte degli insegnanti non insiste a introdurre il concetto di interfacce a questo punto, perché si tratta di un altro concetto o perché (in C ++) devi falsificarli usando classi astratte e eredità multiple che non vengono insegnate in questa fase. In Java, molti insegnanti non distinguono "nessuna eredità multipla" o "l'eredità multipla è malvagia" dall'importanza delle interfacce.

Tutto ciò è aggravato dal fatto che abbiamo imparato così tanto della bellezza di non dover scrivere codice superfluo con eredità di implementazione, che tonnellate di codice di delega semplice sembrano innaturali. Quindi la composizione sembra disordinata, motivo per cui abbiamo bisogno di queste regole empiriche per spingere i nuovi programmatori a usarli comunque (che esagerano anche).


Questa è una parte vera dell'educazione. Devi ottenere i corsi superiori per ottenere il resto del 20%, ma tu, come studente, hai appena insegnato i corsi di base e forse intermedi. Alla fine, ti senti ben istruito perché hai fatto bene i tuoi corsi (e semplicemente non lo vedi solo uno spillo sulla scala). È solo una parte della realtà e la nostra migliore scommessa è capire i suoi effetti collaterali, non attaccare i programmatori.
Indipendente

8

In uno dei commenti, Mason ha menzionato che un giorno avremmo parlato di Eredità considerata dannosa .

Lo spero.

Il problema con l'eredità è allo stesso tempo semplice e mortale, non rispetta l'idea che un concetto dovrebbe avere una funzionalità.

Nella maggior parte dei linguaggi OO, quando si eredita da una classe base:

  • ereditare dalla sua interfaccia
  • ereditare dalla sua implementazione (sia dati che metodi)

E qui diventa il problema.

Questo non è l'unico approccio, sebbene 00 lingue siano per lo più bloccate. Fortunatamente esistono interfacce / classi astratte in quelle.

Inoltre, la mancanza di facilità nel fare altrimenti contribuirebbe a renderlo ampiamente utilizzato: francamente, pur sapendo questo, erediteresti da un'interfaccia e incorporeresti la classe base per composizione, delegando la maggior parte delle chiamate al metodo? Sarebbe molto meglio, verrebbe anche avvertito se all'improvviso un nuovo metodo si apre nell'interfaccia e dovrebbe scegliere, consapevolmente, come implementarlo.

Come contrappunto, Haskellconsentire di utilizzare il Principio di Liskov solo quando "derivano" da interfacce pure (chiamate concetti) (1). Non puoi derivare da una classe esistente, solo la composizione ti consente di incorporare i suoi dati.

(1) i concetti possono fornire un default ragionevole per un'implementazione, ma poiché non hanno dati, questo default può essere definito solo in termini di altri metodi proposti dal concetto o in termini di costanti.


7

La semplice risposta è: l'eredità ha un accoppiamento maggiore della composizione. Date due opzioni, di qualità altrimenti equivalenti, scegliere quella che è meno accoppiata.


2
Ma questo è il punto. Sono Non "altrimenti equivalente." L'ereditarietà consente la sostituzione e il polimorfismo di Liskov, che sono il punto centrale dell'uso di OOP. La composizione no.
Mason Wheeler,

4
Il polimorfismo / LSP non sono "il punto" di OOP, sono una caratteristica. Esistono anche incapsulamenti, astrazioni, ecc. L'ereditarietà implica una relazione "è una", modelli di aggregazione e una relazione "ha una".
Steve,

@Steve: è possibile eseguire astrazioni e incapsulamenti in qualsiasi paradigma che supporti la creazione di strutture dati e procedure / funzioni. Ma il polimorfismo (in particolare il dispacciamento del metodo virtuale in questo contesto) e la sostituzione di Liskov sono unici per la programmazione orientata agli oggetti. Questo è ciò che intendevo con questo.
Mason Wheeler,

3
@ Mason: la linea guida è "favorire la composizione sull'eredità". Questo non significa in alcun modo non usare o addirittura evitare l'eredità. Tutto ciò dice che quando tutte le altre cose sono uguali, scegli la composizione. Ma se hai bisogno dell'eredità, usala!
Jeffrey Faust,

7

Penso che questo tipo di consiglio sia come dire "preferisci guidare per volare". Cioè, gli aerei hanno tutti i tipi di vantaggi rispetto alle automobili, ma ciò comporta una certa complessità. Quindi, se molte persone provano a volare dal centro della città alla periferia, il consiglio che hanno davvero, davvero bisogno di ascoltare è che non hanno bisogno di volare, e in effetti volare a lungo termine renderà tutto più complicato, anche se sembra freddo / efficiente / facile a breve termine. Mentre quando si fa bisogno di volare, è generalmente dovrebbe essere ovvio.

Allo stesso modo, l'eredità può fare cose che la composizione non può, ma dovresti usarla quando ne hai bisogno e non quando non lo fai. Quindi, se non sei mai tentato di presumere che hai bisogno di eredità quando non lo fai, allora non hai bisogno del consiglio di "preferire la composizione". Ma molte persone fanno , e lo fanno bisogno di quel consiglio.

Dovrebbe essere implicito che quando hai davvero bisogno dell'eredità, è ovvio, e dovresti usarlo allora.

Inoltre, la risposta di Steven Lowe. Davvero, proprio quello.


1
Mi piace la tua analogia
barrylloyd,

2

L'ereditarietà non è intrinsecamente negativa e la composizione non è intrinsecamente buona. Sono semplicemente strumenti che un programmatore OO può utilizzare per progettare software.

Quando guardi una classe, sta facendo più di quanto dovrebbe assolutamente ( SRP )? Duplica inutilmente le funzionalità ( DRY ) o è eccessivamente interessato alle proprietà o ai metodi di altre classi ( Feature Envy ) ?. Se la classe sta violando tutti questi concetti (e forse anche di più), sta tentando di essere una Classe divina . Questi sono una serie di problemi che possono verificarsi durante la progettazione di software, nessuno dei quali è necessariamente un problema di ereditarietà, ma che può rapidamente creare gravi mal di testa e fragili dipendenze in cui è stato applicato anche il polimorfismo.

La radice del problema è probabilmente la mancanza di comprensione dell'ereditarietà e una scelta più scadente in termini di progettazione o forse non riconoscendo "odori di codice" relativi a classi che non aderiscono al principio della responsabilità singola . Il polimorfismo e la sostituzione di Liskov non devono essere scartati a favore della composizione. Il polimorfismo stesso può essere applicato senza fare affidamento sull'eredità, sono tutti concetti abbastanza complementari. Se applicato meditatamente. Il trucco è mirare a mantenere il codice semplice e pulito e a non soccombere all'eccessiva preoccupazione per il numero di classi che è necessario creare per creare un solido sistema di progettazione.

Per quanto riguarda il problema di favorire la composizione rispetto all'eredità, questo è in realtà solo un altro caso dell'applicazione ponderata degli elementi di design che ha più senso in termini di risoluzione del problema. Se non hai bisogno di ereditare il comportamento, probabilmente non dovresti come composizione aiuterà ad evitare incompatibilità e importanti sforzi di refactoring in seguito. Se d'altra parte trovi che stai ripetendo molto codice in modo tale che tutta la duplicazione sia centrata su un gruppo di classi simili, allora potrebbe essere che la creazione di un antenato comune possa aiutare a ridurre il numero di chiamate e classi identiche potresti dover ripetere tra ogni lezione. Pertanto, stai favorendo la composizione, ma non stai assumendo che l'eredità non sia mai applicabile.


-1

La citazione reale proviene, credo, da "Effective Java" di Joshua Bloch, dove è il titolo di uno dei capitoli. Afferma che l'ereditarietà è sicura all'interno di un pacchetto e ovunque una classe sia stata appositamente progettata e documentata per l'estensione. Egli sostiene, come altri hanno notato, che l'eredità rompe l'incapsulamento perché una sottoclasse dipende dai dettagli di implementazione della sua superclasse. Ciò porta, dice, alla fragilità.

Anche se non lo menziona, mi sembra che la singola eredità in Java significhi che la composizione offre molta più flessibilità dell'eredità.

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.