Inheritance vs. Aggregation [chiuso]


151

Esistono due scuole di pensiero su come estendere, migliorare e riutilizzare al meglio il codice in un sistema orientato agli oggetti:

  1. Ereditarietà: estendi le funzionalità di una classe creando una sottoclasse. Sostituisci i membri della superclasse nelle sottoclassi per fornire nuove funzionalità. Rendi i metodi astratti / virtuali per forzare le sottoclassi a "riempire gli spazi vuoti" quando la superclasse vuole un'interfaccia particolare ma è agnostica riguardo alla sua implementazione.

  2. Aggregazione: crea nuove funzionalità prendendo altre classi e combinandole in una nuova classe. Collegare un'interfaccia comune a questa nuova classe per l'interoperabilità con altro codice.

Quali sono i vantaggi, i costi e le conseguenze di ciascuno? Ci sono altre alternative?

Vedo che questo dibattito si svolge regolarmente, ma non credo che sia stato ancora chiesto su Stack Overflow (anche se c'è qualche discussione correlata). C'è anche una sorprendente mancanza di buoni risultati di Google per questo.


9
Bella domanda, purtroppo non ho abbastanza tempo ora.
Toon Krijthe

4
Una buona risposta è migliore di una più veloce ... Guarderò la mia domanda, quindi ti voterò almeno :-P
Craig Walker,

Risposte:


181

Non è una questione di quale sia il migliore, ma di quando usare cosa.

Nei casi "normali" è sufficiente una semplice domanda per scoprire se abbiamo bisogno di ereditarietà o aggregazione.

  • Se La nuova classe è più o meno la classe originale. Usa l'ereditarietà. La nuova classe è ora una sottoclasse della classe originale.
  • Se la nuova classe deve avere la classe originale. Usa aggregazione. La nuova classe ha ora la classe originale come membro.

Tuttavia, c'è una grande area grigia. Quindi abbiamo bisogno di molti altri trucchi.

  • Se abbiamo usato l'ereditarietà (o abbiamo in programma di usarlo) ma utilizziamo solo parte dell'interfaccia o siamo costretti a sovrascrivere molte funzionalità per mantenere logica la correlazione. Quindi abbiamo un odore sgradevole che indica che abbiamo dovuto usare l'aggregazione.
  • Se abbiamo usato l'aggregazione (o abbiamo in programma di usarlo) ma scopriamo che dobbiamo copiare quasi tutte le funzionalità. Quindi abbiamo un odore che punta nella direzione dell'eredità.

Per farla breve. Dovremmo usare l'aggregazione se parte dell'interfaccia non viene utilizzata o deve essere cambiata per evitare una situazione illogica. Dobbiamo solo utilizzare l'ereditarietà, se abbiamo bisogno di quasi tutte le funzionalità senza grandi cambiamenti. E in caso di dubbio, utilizzare Aggregazione.

Un'altra possibilità per, nel caso in cui abbiamo una classe che ha bisogno di parte della funzionalità della classe originale, è quella di dividere la classe originale in una classe radice e in una sottoclasse. E lascia che la nuova classe erediti dalla classe radice. Ma dovresti occupartene, non creare una separazione illogica.

Consente di aggiungere un esempio. Abbiamo una classe "Cane" con metodi: "Mangia", "Cammina", "Abbaia", "Gioca".

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Ora abbiamo bisogno di una classe "Gatto", che ha bisogno di "Mangia", "Cammina", "Fusa" e "Gioca". Quindi prima prova ad estenderlo da un cane.

class Cat is Dog
  Purr; 
end;

Sembra, va bene, ma aspetta. Questo gatto può abbaiare (gli amanti dei gatti mi uccideranno per questo). E un gatto che abbaia viola i principi dell'universo. Quindi dobbiamo scavalcare il metodo Bark in modo che non faccia nulla.

class Cat is Dog
  Purr; 
  Bark = null;
end;

Ok, funziona, ma ha un cattivo odore. Quindi proviamo un'aggregazione:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

Ok, è carino Questo gatto non abbaia più, nemmeno silenzioso. Ma ha ancora un cane interno che vuole uscire. Quindi proviamo la soluzione numero tre:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

Questo è molto più pulito. Nessun cane interno. E cani e gatti sono allo stesso livello. Possiamo anche introdurre altri animali domestici per estendere il modello. A meno che non sia un pesce o qualcosa che non cammina. In tal caso, dobbiamo nuovamente rifattorizzare. Ma questo è qualcosa per un'altra volta.


5
Il "riutilizzo di quasi tutte le funzionalità di una classe" è l'unica volta in cui preferisco l'ereditarietà. Quello che mi piacerebbe davvero è un linguaggio che ha la capacità di dire facilmente "delegare a questo oggetto aggregato per questi metodi specifici"; questo è il migliore dei due mondi.
Craig Walker,

Non sono sicuro di altre lingue, ma Delphi ha un meccanismo che consente ai membri di implementare parte dell'interfaccia.
Toon Krijthe

2
L'obiettivo C utilizza protocolli che ti consentono di decidere se la tua funzione è obbligatoria o facoltativa.
cantfindaname88,

1
Ottima risposta con un esempio pratico. Progetterei la classe Pet con un metodo chiamato makeSounde lascerei che ogni sottoclasse implementasse il proprio tipo di suono. Questo aiuterà quindi nelle situazioni in cui hai molti animali domestici e lo fai for_each pet in the list of pets, pet.makeSound.
Blongho,

38

All'inizio del GOF affermano

Favorisce la composizione dell'oggetto rispetto all'eredità di classe.

Questo è ulteriormente discusso qui


27

La differenza viene in genere espressa come differenza tra "è un" e "ha un". L'ereditarietà, la relazione "è un", è riassunta bene nel Principio di sostituzione di Liskov . L'aggregazione, la relazione "ha un", è proprio questo: mostra che l'oggetto aggregante ha uno degli oggetti aggregati.

Esistono anche ulteriori distinzioni: l'ereditarietà privata in C ++ indica che una relazione "è implementata in termini di", che può anche essere modellata dall'aggregazione di oggetti membri (non esposti).


Inoltre, la differenza è riassunta bene nella domanda a cui dovresti rispondere. Vorrei che le persone leggessero prima le domande prima di iniziare a tradurre. Promuovi ciecamente schemi di design per diventare un membro del club d'élite?
Val

Questo approccio non mi piace, riguarda più la composizione
Alireza Rahmani Khalili,

15

Ecco il mio argomento più comune:

In qualsiasi sistema orientato agli oggetti, ci sono due parti in ogni classe:

  1. La sua interfaccia : la "faccia pubblica" dell'oggetto. Questo è l'insieme di funzionalità che annuncia al resto del mondo. In molte lingue, l'insieme è ben definito in una "classe". Di solito queste sono le firme dei metodi dell'oggetto, anche se varia leggermente in base alla lingua.

  2. La sua implementazione : il lavoro "dietro le quinte" che l'oggetto fa per soddisfare la sua interfaccia e fornire funzionalità. In genere si tratta del codice e dei dati dei membri dell'oggetto.

Uno dei principi fondamentali di OOP è che l'implementazione è incapsulata (ovvero: nascosta) all'interno della classe; l'unica cosa che gli estranei dovrebbero vedere è l'interfaccia.

Quando una sottoclasse eredita da una sottoclasse, in genere eredita sia l'implementazione che l'interfaccia. Questo, a sua volta, significa che sei costretto ad accettare entrambi come vincoli per la tua classe.

Con l'aggregazione, puoi scegliere l'implementazione o l'interfaccia o entrambi, ma non sei obbligato a farlo. La funzionalità di un oggetto è lasciata all'oggetto stesso. Può rimandare ad altri oggetti come preferisce, ma alla fine è responsabile di se stesso. Nella mia esperienza, questo porta a un sistema più flessibile: uno che è più facile da modificare.

Quindi, ogni volta che sto sviluppando software orientato agli oggetti, quasi sempre preferisco l'aggregazione rispetto all'eredità.


Penserei che tu usi una classe astratta per definire un'interfaccia e quindi usi l'ereditarietà diretta per ogni classe di implementazione concreta (rendendola piuttosto superficiale). Qualsiasi aggregazione è sotto la concreta attuazione.
orcmid

Se hai bisogno di interfacce aggiuntive rispetto a quella, restituiscile tramite metodi sull'interfaccia dell'interfaccia astratta di livello principale, risciacqua ripetizione.
orcmid

Pensi che l'aggregazione sia migliore in questo scenario? Un libro è un oggetto di vendita, un disco digitale è un articolo Selliing codereview.stackexchange.com/questions/14077/…
LCJ

11

Ho dato una risposta a "È un" vs "Ha un": quale è meglio? .

Fondamentalmente sono d'accordo con altre persone: usa l'ereditarietà solo se la tua classe derivata è veramente il tipo che stai estendendo, non semplicemente perché contiene gli stessi dati. Ricorda che l'ereditarietà indica che la sottoclasse ottiene sia i metodi che i dati.

Ha senso che la tua classe derivata abbia tutti i metodi della superclasse? O ti prometti tranquillamente che quei metodi dovrebbero essere ignorati nella classe derivata? O ti trovi a scavalcare i metodi dalla superclasse, rendendoli non operativi quindi nessuno li chiama inavvertitamente? O dando suggerimenti al tuo strumento di generazione di documenti API per omettere il metodo dal documento?

Questi sono forti indizi sul fatto che l'aggregazione sia la scelta migliore in quel caso.


6

Vedo molte risposte "is-a vs. has-a; sono concettualmente diverse" su questo e sulle relative domande.

L'unica cosa che ho trovato nella mia esperienza è che cercare di determinare se una relazione è "is-a" o "has-a" fallirà. Anche se ora puoi prendere correttamente quella determinazione per gli oggetti, cambiare i requisiti significa che probabilmente ti sbaglierai ad un certo punto in futuro.

Un'altra cosa che ho scoperto è che è molto difficile convertire da eredità a aggregazione una volta che c'è molto codice scritto attorno a una gerarchia di ereditarietà. Passare da una superclasse a un'interfaccia significa cambiare quasi tutte le sottoclassi del sistema.

E, come ho già detto altrove in questo post, l'aggregazione tende ad essere meno flessibile dell'eredità.

Quindi, hai una tempesta perfetta di argomenti contro l'eredità ogni volta che devi scegliere l'uno o l'altro:

  1. La tua scelta sarà probabilmente quella sbagliata ad un certo punto
  2. Cambiare quella scelta è difficile dopo averlo fatto.
  3. L'ereditarietà tende ad essere una scelta peggiore in quanto è più vincolante.

Quindi, tendo a scegliere l'aggregazione, anche quando sembra esserci una relazione is-forte.


L'evoluzione dei requisiti significa che il tuo progetto OO sarà inevitabilmente sbagliato. Eredità vs. aggregazione è solo la punta di quell'iceberg. Non puoi progettare tutti i futuri possibili, quindi segui l'idea di XP: risolvi i requisiti di oggi e accetta che potresti dover fare il refactoring.
Bill Karwin,

Mi piace questa linea di indagine e domande. Preoccupato per il blurrung è però -a, has-a e usi-a. Comincio con le interfacce (insieme all'identificazione delle astrazioni implementate a cui voglio fare interfacce). Il livello aggiuntivo di riferimento indiretto è inestimabile. Non seguire la tua conclusione di aggregazione.
orcmid

Questo è praticamente il mio punto però. Sia l'ereditarietà che l'aggregazione sono metodi per risolvere il problema. Uno di questi metodi comporta tutti i tipi di penalità quando devi risolvere i problemi domani.
Craig Walker,

Nella mia mente, aggregazione + interfacce sono l'alternativa all'ereditarietà / sottoclasse; non puoi fare il polimorfismo basato sulla sola aggregazione.
Craig Walker,

3

La domanda è normalmente formulata come Composizione vs. Ereditarietà , ed è stata posta qui prima.


Giusto tu sei ... divertente che SO non l'abbia data come una delle domande correlate.
Craig Walker,

2
Quindi la ricerca fa ancora schifo
Vinko Vrsalovic,

Concordato. Ecco perché non l'ho chiuso. Questo e molte persone avevano già risposto qui.
Bill the Lizard,

1
SO ha bisogno di una funzione per inserire 2 domande (e le relative risposte) in 1
Craig Walker,

3

Volevo fare questo un commento sulla domanda originale, ma 300 caratteri morde [; <).

Penso che dobbiamo stare attenti. Innanzitutto, ci sono più sapori rispetto ai due esempi piuttosto specifici fatti nella domanda.

Inoltre, suggerisco che è utile non confondere l'obiettivo con lo strumento. Uno vuole assicurarsi che la tecnica o la metodologia prescelte supportino il raggiungimento dell'obiettivo primario, ma non discuto fuori dal contesto quale tecnica sia la migliore discussione sia molto utile. Aiuta a conoscere le insidie ​​dei diversi approcci insieme ai loro chiari punti dolci.

Ad esempio, cosa hai intenzione di realizzare, con cosa hai a disposizione per iniziare e quali sono i vincoli?

Stai creando un framework di componenti, anche per scopi speciali? Le interfacce sono separabili dalle implementazioni nel sistema di programmazione o sono realizzate da una pratica che utilizza un diverso tipo di tecnologia? Puoi separare la struttura ereditaria delle interfacce (se presente) dalla struttura ereditaria delle classi che le implementano? È importante nascondere la struttura di classe di un'implementazione dal codice che si basa sulle interfacce fornite dall'implementazione? Esistono più implementazioni da utilizzare contemporaneamente o la variazione è più nel tempo come conseguenza della manutenzione e del miglioramento? Questo e altro deve essere considerato prima di fissare uno strumento o una metodologia.

Infine, è così importante bloccare le distinzioni nell'astrazione e come la pensi (come in is-a vers-has-a) a diverse funzionalità della tecnologia OO? Forse è così, se mantiene la struttura concettuale coerente e gestibile per te e gli altri. Ma è saggio non lasciarti schiavizzare da ciò e dalle contorsioni che potresti finire. Forse è meglio tornare indietro di un livello e non essere così rigidi (ma lasciare una buona narrazione in modo che gli altri possano dire cosa succede). [Cerco ciò che rende spiegabile una parte particolare di un programma, ma a volte vado per eleganza quando c'è una vittoria più grande. Non sempre la migliore idea.]

Sono un purista di interfaccia e sono attratto dai tipi di problemi e approcci in cui il purismo di interfaccia è appropriato, sia che si tratti di costruire un framework Java o di organizzare alcune implementazioni COM. Ciò non lo rende appropriato per tutto, nemmeno vicino a tutto, anche se lo giuro. (Ho un paio di progetti che sembrano fornire seri contro-esempi contro il purismo dell'interfaccia, quindi sarà interessante vedere come riesco a farcela.)


2

Tratterò la parte dove questi potrebbero applicare. Ecco un esempio di entrambi, in uno scenario di gioco. Supponiamo che esista un gioco con diversi tipi di soldati. Ogni soldato può avere uno zaino che può contenere cose diverse.

Eredità qui? C'è un berretto verde marino e un cecchino. Questi sono tipi di soldati. Quindi, c'è un soldato della classe base con Marine, Green Beret & Sniper come classi derivate

Aggregazione qui? Lo zaino può contenere granate, pistole (diversi tipi), coltello, medikit, ecc. Un soldato può essere equipaggiato con uno di questi in qualsiasi momento, inoltre può avere un giubbotto antiproiettile che funge da armatura quando viene attaccato e il suo l'infortunio diminuisce fino a una certa percentuale. La classe soldato contiene un oggetto della classe giubbotto antiproiettile e la classe zaino che contiene riferimenti a questi oggetti.


2

Penso che non sia uno o / o dibattito. È solo questo:

  1. Le relazioni is-a (ereditarietà) si verificano meno spesso delle relazioni has-a (composizione).
  2. L'ereditarietà è più difficile da correggere, anche quando è appropriato usarla, quindi è necessario prendere la dovuta diligenza perché può rompere l'incapsulamento, incoraggiare un accoppiamento stretto esponendo l'implementazione e così via.

Entrambi hanno il loro posto, ma l'eredità è più rischiosa.

Anche se ovviamente non avrebbe senso avere una classe Shape "avendo-a" classi Point e a Square. Qui l'eredità è dovuta.

Le persone tendono a pensare prima all'eredità quando provano a progettare qualcosa di estensibile, ecco cosa c'è che non va.


1

Il favore si verifica quando entrambi i candidati si qualificano. A e B sono opzioni e tu preferisci A. Il motivo è che la composizione offre più possibilità di estensione / flessibilità rispetto alla generalizzazione. Questa estensione / flessibilità si riferisce principalmente alla flessibilità di runtime / dinamica.

Il vantaggio non è immediatamente visibile. Per vedere il vantaggio è necessario attendere la successiva richiesta di modifica imprevista. Quindi, nella maggior parte dei casi, quelli attaccati alla generlalizzazione falliscono rispetto a quelli che abbracciavano la composizione (tranne un caso ovvio menzionato più avanti). Da qui la regola. Da un punto di vista dell'apprendimento se è possibile implementare correttamente un'iniezione di dipendenza, è necessario sapere quale favorire e quando. La regola ti aiuta anche a prendere una decisione; se non sei sicuro seleziona la composizione.

Riepilogo: Composizione: l'accoppiamento si riduce semplicemente avendo alcune cose più piccole da inserire in qualcosa di più grande, e l'oggetto più grande richiama semplicemente l'oggetto più piccolo. Generazione: dal punto di vista dell'API la definizione di un metodo che può essere sovrascritto è un impegno più forte rispetto alla definizione di un metodo che può essere chiamato. (pochissime occasioni in cui vince la generalizzazione). E non dimenticare mai che con la composizione usi anche l'ereditarietà, da un'interfaccia anziché da una grande classe


0

Entrambi gli approcci vengono utilizzati per risolvere diversi problemi. Non è sempre necessario aggregare più di due o più classi quando si eredita da una classe.

A volte devi aggregare una singola classe perché quella classe è sigillata o ha membri altrimenti non virtuali che devi intercettare in modo da creare un livello proxy che ovviamente non è valido in termini di eredità ma fintanto che la classe che stai inoltrando ha un'interfaccia a cui puoi iscriverti e può funzionare abbastanza bene.

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.