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

138

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

Inoltre, non vedo mai una spiegazione reale per perché l'ereditarietà è cattiva e la composizione è buona, il che mi rende ancora più sospettoso. Dovrebbe essere accettato solo per fede? La sostituzione e il polimorfismo di Liskov hanno ben noti e chiari vantaggi, e IMO comprende l'intero punto di utilizzo della programmazione orientata agli oggetti, e nessuno mai spiega perché dovrebbero essere scartati a favore della composizione.

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

    
posta Mason Wheeler 05.04.2011 - 00:54
fonte

14 risposte

131

Anche se penso di aver sentito discussioni sulla composizione-contro-eredità molto prima di GoF, non riesco a puntare il dito su una fonte specifica. Potrebbe essere stato Booch comunque.

< rant >

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

  1. viene introdotto con una spiegazione dettagliata e un argomento di una fonte ben rispettata che conia la frase come promemoria della complessa discussione originale
  2. è condiviso per un po 'con un conoscente ammiccamento da parte del club da parte di alcuni in-the-know, generalmente quando commenta errori n00b
  3. presto viene ripetuto senza pensieri da migliaia e migliaia di persone che non hanno mai letto la spiegazione, ma ama usarlo come scusa per non pensare e come un modo economico e facile per sentirsi superiori agli altri
  4. alla fine, nessuna quantità di debunking ragionevole può arginare la "marea meme" - e il paradigma degenera in religione e dogma.

Il meme, originariamente inteso a portare i n00bs all'illuminazione, è ora usato come un club per colpire i loro incoscienti.

La composizione e l'ereditarietà sono cose molto diverse e non devono essere confuse l'una con l'altra. Se è vero che la composizione può essere usata per simulare l'eredità con molto lavoro extra , ciò non rende l'ereditarietà un cittadino di seconda classe, né rende la composizione il figlio preferito. Il fatto che molti n00bs provano a usare l'ereditarietà come una scorciatoia non invalida il meccanismo, e quasi tutti gli n00bs apprendono dall'errore e quindi migliorano.

Prego PENSA dei tuoi progetti e smetti di lanciare slogan.

< / rant >

    
risposta data 19.04.2012 - 03:55
fonte
72

L'esperienza.

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

L'ereditarietà è principalmente uno strumento polimorfico, ma alcune persone, molto più a rischio e pericolo, tentano di usarlo come modo di riutilizzare / condividere il codice. La logica è "bene se eredito quindi 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'ereditarietà - beh semplicemente perché molto spesso la relazione tra le classi non è polimorfica. Esiste semplicemente per aiutare a ricordare alle persone di non rispondere alle istanze del ginocchio ereditando.

    
risposta data 05.04.2011 - 06:40
fonte
41

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

Il problema principale con l'ereditarietà è che si tratta di una white-box. Per definizione, è necessario conoscere i dettagli di implementazione della classe da cui si eredita. Con la composizione, invece, ti interessa solo l'interfaccia pubblica della classe che stai componendo.

Dal libro GoF:

Inheritance exposes a subclass to details of its parent's implementation, it's often said that 'inheritance breaks encapsulation'

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

    
risposta data 05.04.2011 - 01:08
fonte
26

Per rispondere a una parte della tua domanda, credo che questo concetto sia apparso per la prima volta nei Modelli di design: elementi di riutilizzabile orientato agli oggetti Software , pubblicato per la prima volta nel 1994. La frase appare nella parte superiore della pagina 20, nell'introduzione:

Favor object composition over inheritance

Presentano questa affermazione con un breve confronto dell'ereditarietà con la composizione. Non dicono "non usare mai l'ereditarietà".

    
risposta data 05.04.2011 - 01:07
fonte
22

"La 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à." / p>

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

Questo significa solo che l'ereditarietà deve essere gestita con cura perché ha un costo, non che non è utile. In realtà, "La composizione sull'ereditarietà" spesso finisce per essere "Composizione + ereditarietà sull'eredità" poiché spesso si desidera che la dipendenza composta sia una superclasse astratta piuttosto che una sottoclasse concreta. Ti consente di passare da diverse implementazioni concrete della tua dipendenza in fase di runtime.

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

Un esempio (metaforico) potrebbe essere:

"Ho una classe Snake e voglio includere come parte di quella classe cosa succede quando i morsi di Snake. Sarei tentato di far sì che Snake erediti una classe BiterAnimal che abbia il metodo Bite () e sovrascriva quel metodo per riflettono il morso velenoso, ma Composition over Ereditance mi avverte che dovrei provare a usare invece la composizione ... Nel mio caso, questo potrebbe tradurre in Snake un membro Bite. La classe Bite potrebbe essere astratta (o un'interfaccia) con diverse sottoclassi. Questo mi permetterebbe cose piacevoli come avere sottoclassi VenomousBite e DryBite ed essere in grado di cambiare morso sulla stessa istanza Snake di quando il serpente cresce di età.Inoltre, gestire tutti gli effetti di un morso nella sua classe separata potrebbe consentirmi di riutilizzarlo in quella classe Frost, perché morde il gelo ma non è un BiterAnimal, e così via ... "

    
risposta data 06.04.2011 - 17:15
fonte
22

Alcuni possibili argomenti per la composizione:

La composizione è leggermente più indipendente dalla lingua e dal framework L'ereditarietà e ciò che impone / richiede / abilita differiscono tra le lingue in termini di ciò a cui la sub / superclasse ha accesso e quali implicazioni sulla performance possono avere i metodi virtuali ecc. La composizione è abbastanza semplice e richiede pochissimo supporto linguistico e quindi implementazioni attraverso diverse piattaforme / framework puoi condividere più facilmente i modelli di composizione.

Composition è un modo molto semplice e tattile per costruire oggetti L'ereditarietà è relativamente facile da comprendere, ma non è altrettanto facilmente dimostrata nella vita reale. Molti oggetti nella vita reale possono essere suddivisi in parti e composti. Supponiamo che una bicicletta possa essere costruita usando due ruote, un telaio, un sedile, una catena, ecc. Facilmente spiegato dalla 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 esempio di ereditarietà molto buono, ma il punto rimane lo stesso). Persino l'ereditarietà delle parole (almeno della maggior parte degli inglesi che mi aspetterei) invoca automaticamente un significato sulla falsariga di "Qualcosa tramandato da un parente defunto" che ha qualche correlazione con il suo significato nel software, ma si adatta ancora solo vagamente. p>

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

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

Disclaimer:
Ovviamente non è questo chiaro / strada 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 quando si preferisce la composizione.

    
risposta data 07.02.2013 - 21:00
fonte
10

Forse hai appena notato che la gente lo diceva negli ultimi mesi, ma è stato conosciuto da buoni programmatori per molto più tempo. Certamente lo sto dicendo, se del caso, per circa un decennio.

Il punto del concetto è che c'è un grande overhead concettuale per l'ereditarietà. Quando si utilizza l'ereditarietà, ogni singola chiamata al metodo ha una spedizione implicita in essa. Se si dispone di alberi di ereditarietà profonda, o di dispacciamento multiplo, o (ancora peggio) entrambi, allora capire dove il particolare metodo invierà in una particolare chiamata può diventare un PITA reale. Rende il ragionamento corretto sul codice più complesso e rende più difficile il debug.

Lasciatemi fare un semplice esempio per illustrare. Supponiamo che in profondità in un albero di ereditarietà, qualcuno abbia chiamato un metodo foo . Poi arriva qualcun altro e aggiunge foo nella parte superiore dell'albero, ma facendo qualcosa di diverso. (Questo caso è più comune con l'ereditarietà multipla.) Ora quella persona che lavora alla classe radice ha infranto l'oscura classe figlia 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 cima non penserebbe di testare la classe figlio, ei test per la classe figlio non pensano di testare i nuovi metodi creati nella parte superiore . (Ammettiamo che ci sono modi per scrivere test unitari che cattureranno questo, ma ci sono anche casi in cui non è possibile scrivere facilmente i test in questo modo.)

Per contro, quando si utilizza la composizione, ad ogni chiamata è generalmente più chiaro a cosa si sta inviando la chiamata. (OK, se stai usando l'inversione di controllo, ad esempio con l'iniezione di dipendenza, allora capire dove va la chiamata può anche diventare problematico, ma di solito è più semplice capirlo.) Questo rende più facile ragionare su di esso. Come bonus, la composizione risulta nell'avere metodi separati l'uno dall'altro. L'esempio sopra non dovrebbe accadere lì perché la classe figlio si sposterebbe verso un componente oscuro e non c'è mai una domanda sul fatto che la chiamata a foo sia stata pensata per il componente oscuro o l'oggetto principale.

Ora hai perfettamente ragione sul fatto che l'ereditarietà e la composizione sono due strumenti molto diversi che servono due diversi tipi di cose. Sicuramente l'ereditarietà porta il sovraccarico concettuale, ma quando è lo strumento giusto per il lavoro, comporta un sovraccarico concettuale inferiore rispetto al tentativo di non usarlo e fare a mano quello che fa per te. Nessuno che sa cosa stanno facendo direbbe che non si dovrebbe mai usare l'ereditarietà. Ma assicurati che sia la cosa giusta da fare.

Sfortunatamente molti sviluppatori apprendono del software orientato agli oggetti, imparano l'ereditarietà e poi escono per usare la loro nuova ascia il più spesso possibile. Il che significa che cercano di usare l'ereditarietà laddove la composizione era lo strumento giusto. Speriamo che imparino meglio nel tempo, ma spesso questo non accade fino a dopo aver rimosso alcuni arti, ecc. Dicendoli in anticipo che è una cattiva idea, tende ad accelerare il processo di apprendimento e ridurre gli infortuni.

    
risposta data 05.04.2011 - 01:30
fonte
8

È una reazione all'osservazione che i principianti OO tendono ad usare l'ereditarietà quando non ne hanno bisogno. L'ereditarietà non è certamente una brutta cosa, ma può essere abusata. Se una classe ha solo bisogno di funzionalità da un'altra, allora probabilmente la composizione funzionerà. In altre circostanze, l'ereditarietà 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 usare una base avrebbe 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 sono più propensi a richiedere modifiche all'altra.

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

Pertanto, se la composizione o l'ereditarietà faranno efficacemente il lavoro, scegli la composizione, perché è l'accoppiamento più basso. Se la composizione non farà il lavoro in modo efficace, e l'ereditarietà, scegli l'ereditarietà, perché devi.

    
risposta data 05.04.2011 - 16:55
fonte
8

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

IMHO, Si tratta del fatto che la maggior parte dei programmatori non eredita realmente l'ereditarietà e finisce per esagerare, in parte come risultato di come viene insegnato questo concetto. Questo concetto esiste come un modo per cercare di dissuaderli dal strafare, invece di concentrarsi sull'insegnare loro come farlo bene.

Chiunque abbia trascorso un periodo di insegnamento o mentoring sa che questo è ciò che spesso accade, specialmente con i nuovi sviluppatori che hanno esperienza in altri paradigmi:

Questi sviluppatori inizialmente ritengono che l'ereditarietà sia questo concetto spaventoso. Così finiscono per creare tipi con sovrapposizioni di interfacce (ad esempio, lo stesso comportamento specificato senza sottotipizzazione comune) e con globali per l'implementazione di elementi di funzionalità comuni.

Quindi (spesso come conseguenza di un insegnamento troppo zelante), vengono a conoscenza dei vantaggi dell'ereditarietà, ma viene spesso insegnata come la soluzione più generica da riutilizzare. Finiscono con la percezione che qualsiasi comportamento condiviso debba essere condiviso attraverso l'ereditarietà. Questo perché l'attenzione è spesso sull'ereditarietà dell'implementazione piuttosto che sulla sottotipizzazione.

Nell'80% dei casi è abbastanza buono. Ma l'altro 20% è dove inizia il problema. Per evitare di riscrivere e per assicurarsi che abbiano 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" è un buon esempio di questo.

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

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

    
risposta data 05.04.2011 - 17:26
fonte
8

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

Lo spero.

Il problema con l'ereditarietà è 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:

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

E qui diventa il problema.

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

Inoltre, la mancanza di facilità per fare altrimenti contribuisce a renderlo ampiamente utilizzato: francamente, pur sapendo questo, erediteresti da un'interfaccia e incorporerai la classe base per composizione, delegando la maggior parte delle chiamate ai metodi? Sarebbe molto meglio, tuttavia, verrai addirittura avvisato se all'improvviso un nuovo metodo si apre nell'interfaccia e dovrebbe scegliere, consapevolmente, come implementarlo.

Come contrappunto, Haskell consente solo di usare il Principio di Liskov quando "deriva" da pure interfacce (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 valore 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.

    
risposta data 06.04.2011 - 19:14
fonte
7

La semplice risposta è: l'ereditarietà ha un maggiore accoppiamento rispetto alla composizione. Con due opzioni, di qualità altrimenti equivalenti, scegli quella meno accoppiata.

    
risposta data 05.04.2011 - 22:27
fonte
7

Penso che questo tipo di consiglio sia come dire "preferisco guidare al volo". 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 alla periferia, i consigli che davvero, davvero hanno bisogno di sentire è che non hanno bisogno di volare, e infatti, volare lo renderà semplicemente più complicato a lungo termine, anche se sembra freddo / efficiente / facile a breve termine. Mentre quando do devi volare, in genere dovrebbe essere ovvio.

Allo stesso modo, l'ereditarietà 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 presupporre che hai bisogno di ereditarietà quando non lo fai, allora non hai bisogno del consiglio di "preferire la composizione". Ma molte persone fanno e fanno hanno bisogno di questo consiglio.

Dovrebbe essere implicito che quando hai veramente bisogno di ereditarietà, è ovvio, e dovresti usarlo allora.

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

    
risposta data 26.11.2012 - 18:58
fonte
2

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

Quando guardi una classe, sta facendo più di quanto dovrebbe assolutamente ( SRP )? È la duplicazione della funzionalità inutilmente ( DRY ), oppure è eccessivamente interessata alle proprietà o ai metodi di altre classi ( Invidia caratteristica ) ?. Se la classe sta violando tutti questi concetti (e possibilmente di più), sta tentando di essere una classe di Dio . Si tratta di una serie di problemi che possono verificarsi durante la progettazione di software, nessuno dei quali è necessariamente un problema di ereditarietà, ma che può creare rapidamente grosse difficoltà e dipendenze fragili in cui è stato applicato anche il polimorfismo.

La radice del problema rischia di essere meno una mancanza di comprensione dell'ereditarietà e più una scelta sbagliata in termini di design, o forse non riconoscere "odori di codice" relativi a classi che non aderiscono al Principio di responsabilità singola . Polymorphism e Liskov Sostituzione non devono essere scartati a favore della composizione. Il polimorfismo stesso può essere applicato senza fare affidamento sull'eredità, tutti concetti del tutto complementari. Se applicato pensosamente. Il trucco è mirare a mantenere il codice semplice e pulito e a non soccombere per essere eccessivamente preoccupati del numero di classi che è necessario creare per creare un solido design di sistema.

Nella misura in cui il problema di favorire la composizione sull'ereditarietà, questo è solo un altro caso dell'applicazione ponderata degli elementi di progettazione che ha più senso in termini di risoluzione del problema. Se non hai bisogno di per ereditare il comportamento, allora probabilmente non dovresti farlo perché la composizione ti aiuterà a evitare le incompatibilità e gli importanti sforzi di refactoring in seguito. Se invece trovi che stai ripetendo un sacco di codice tale che tutta la duplicazione è centrata su un gruppo di classi simili, allora potrebbe essere che la creazione di un antenato comune aiuterebbe a ridurre il numero di chiamate e classi identiche potrebbe essere necessario ripetere tra ogni classe. Quindi, stai favorendo la composizione, eppure non stai assumendo che l'ereditarietà non sia mai applicabile.

    
risposta data 19.04.2012 - 05:29
fonte
-1

La citazione attuale viene, 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 è stata specificamente progettata e documentata per l'estensione. Egli sostiene, come altri hanno notato, che l'ereditarietà rompe l'incapsulamento perché una sottoclasse dipende dai dettagli di implementazione della sua superclasse. Questo porta, dice, alla fragilità.

Anche se non ne parla, mi sembra che l'ereditarietà singola in Java significhi che la composizione ti offre molta più flessibilità dell'ereditarietà.

    
risposta data 05.04.2011 - 23:03
fonte

Leggi altre domande sui tag