Ereditarietà rispetto all’aggregazione

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

  1. Ereditarietà: estendere le funzionalità di una class creando una sottoclass. Sostituisci i membri della superclass nelle sottoclassi per fornire nuove funzionalità. Rendi i metodi astratti / virtuali per forzare le sottoclassi in “fill-in-the-blanks” quando la superclass vuole una particolare interfaccia ma è agnostica sulla sua implementazione.

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

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

Vedo questo dibattito venire su base regolare, ma non penso 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.

Non è 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 class è più o meno come la class originale. Usa l’ereditarietà. La nuova class è ora una sottoclass della class originale.
  • Se la nuova class deve avere la class originale. Utilizzare l’aggregazione. La nuova class ha ora la class originale come membro.

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

  • Se abbiamo usato l’ereditarietà (o intendiamo usarlo), ma usiamo solo parte dell’interfaccia, o siamo costretti a sovrascrivere molte funzionalità per mantenere la correlazione logica. Poi abbiamo un cattivo odore che indica che dovevamo usare l’aggregazione.
  • Se abbiamo usato l’aggregazione (o intendiamo usarlo) ma scopriamo che abbiamo bisogno di copiare quasi tutte le funzionalità. Allora abbiamo un odore che indica la direzione dell’eredità.

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

Un’altra possibilità, nel caso in cui abbiamo una class che ha bisogno di parte della funzionalità della class originale, è quella di dividere la class originale in una class radice e una class secondaria. E lascia che la nuova class erediti dalla class radice. Ma dovresti prenderti cura di questo, non creare una separazione illogica.

Aggiungiamo un esempio. Abbiamo una class ‘Cane’ con metodi: ‘Eat’, ‘Walk’, ‘Bark’, ‘Play’.

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

Ora abbiamo bisogno di una class ‘Cat’, che ha bisogno di ‘Eat’, ‘Walk’, ‘Purr’ e ‘Play’. Quindi, prima prova a estenderlo da un cane.

 class Cat is Dog Purr; end; 

Guarda, va bene, ma aspetta. Questo gatto può abbaiare (gli amanti dei gatti mi uccideranno per quello). E un gatto che abbaia viola i principi dell’universo. Quindi abbiamo bisogno di sovrascrivere 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, questo è bello. Questo gatto non abbaia più, nemmeno silenzioso. Ma ha ancora un cane interno che vuole uscire. Quindi proviamo con 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 gatti e cani sono allo stesso livello. Possiamo persino introdurre altri animali domestici per estendere il modello. A meno che non sia un pesce o qualcosa che non cammina. In quel caso dobbiamo nuovamente refactoring. Ma è qualcosa per un’altra volta.

All’inizio del GOF affermano

Favorisci la composizione dell’object rispetto all’eredità della class.

Questo è ulteriormente discusso qui

La differenza viene in genere espressa come differenza tra “è un” e “ha un”. L’ereditarietà, la relazione “è una”, si riassume bene nel Principio di sostituzione di Liskov . L’aggregazione, la relazione “ha una”, è proprio questo: mostra che l’object aggregante ha uno degli oggetti aggregati.

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

Ecco il mio argomento più comune:

In qualsiasi sistema orientato agli oggetti, ci sono due parti per ogni class:

  1. La sua interfaccia : la “faccia pubblica” dell’object. Questo è l’insieme delle funzionalità che annuncia al resto del mondo. In molte lingue, il set è ben definito in una “class”. Di solito queste sono le firme del metodo dell’object, anche se variano un po ‘per lingua.

  2. La sua implementazione : il lavoro “dietro le quinte” che l’object fa per soddisfare la sua interfaccia e fornire funzionalità. Questo è in genere il codice e i dati dei membri dell’object.

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

Quando una sottoclass eredita da una sottoclass, generalmente eredita sia l’implementazione che l’interfaccia. Questo, a sua volta, significa che sei costretto ad accettare entrambi come vincoli sulla tua class.

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

Quindi, ogni volta che sviluppo software orientato agli oggetti, preferisco quasi sempre l’aggregazione sull’ereditarietà.

Ho dato una risposta a “Is a” vs “Has a”: qual è il migliore? .

Fondamentalmente sono d’accordo con altre persone: usa l’ereditarietà solo se la tua class derivata è veramente il tipo che stai estendendo, non solo perché contiene gli stessi dati. Ricorda che l’ereditarietà significa che la sottoclass acquisisce sia i metodi che i dati.

Ha senso che la tua class derivata abbia tutti i metodi della superclass? O ti stai solo tranquillamente promettendo che quei metodi dovrebbero essere ignorati nella class derivata? O ti ritrovi a superare i metodi della superclass, rendendoli non operativi così nessuno li chiama inavvertitamente? O dare suggerimenti al tuo strumento di generazione di documenti API per omettere il metodo dal documento?

Quelli sono indizi forti che l’aggregazione è la scelta migliore in quel caso.

Vedo molte risposte “is-a vs. ha-a, sono concettualmente diverse” su questa e le domande correlate.

L’unica cosa che ho trovato nella mia esperienza è che il tentativo di determinare se una relazione è “è-un” o “ha-un” è destinato a fallire. Anche se ora puoi determinare correttamente la determinazione per gli oggetti, cambiando i requisiti significa che probabilmente a un certo punto ti sbaglierai in futuro.

Un’altra cosa che ho trovato è che è molto difficile convertire dall’ereditarietà all’aggregazione una volta che c’è un sacco di codice scritto attorno a una gerarchia dell’ereditarietà. Passare da una superclass a un’interfaccia significa cambiare quasi ogni sottoclass nel sistema.

E, come ho già detto in questo post, l’aggregazione tende ad essere meno flessibile dell’ereditarietà.

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

  1. La tua scelta sarà probabilmente sbagliata ad un certo punto
  2. Cambiare questa scelta è difficile una volta che ce l’hai fatta.
  3. L’ereditarietà tende ad essere una scelta peggiore in quanto è più vincolante.

Quindi, tendo a scegliere l’aggregazione – anche quando sembra esserci una forte – una relazione.

La domanda è normalmente formulata come Composition vs. Ereditarietà , ed è stata chiesta qui prima.

Volevo rendere questo un commento sulla domanda originale, ma 300 morsi di carattere [; <).

Penso che dobbiamo stare attenti. In primo luogo, ci sono più aromi rispetto ai due esempi piuttosto specifici fatti nella domanda.

Inoltre, suggerisco che è utile non confondere l’objective con lo strumento. Si vuole essere certi che la tecnica o la metodologia scelta supporti il ​​raggiungimento dell’objective primario, ma non è molto utile la discussione fuori dal contesto che la tecnica è la migliore. Aiuta a conoscere le insidie ​​dei diversi approcci insieme ai loro chiari punti dolci.

Ad esempio, cosa sei in grado di realizzare, cosa hai a disposizione per iniziare e quali sono i vincoli?

Stai creando un framework di componenti, anche uno speciale? Le interfacce sono separabili dalle implementazioni nel sistema di programmazione o sono eseguite da una pratica che utilizza un diverso tipo di tecnologia? Puoi separare la struttura ereditaria delle interfacce (se esiste) dalla struttura di ereditarietà delle classi che le implementano? È importante hide la struttura di class di un’implementazione dal codice che si basa sulle interfacce fornite dall’implementazione? Esistono più implementazioni per essere utilizzabili allo stesso tempo o la variazione è più eccessiva a causa di manutenzione e miglioramento? Questo e altri aspetti devono essere considerati prima di fissarti su uno strumento o una metodologia.

Infine, è così importante bloccare le distinzioni nell’astrazione e come la si pensa (come in is-a versus ha-a) alle diverse caratteristiche della tecnologia OO? Forse sì, se mantiene la struttura concettuale coerente e gestibile per te e gli altri. Ma è saggio non essere schiavizzato da questo e dalle contorsioni che potresti finire per fare. Forse è meglio arretrare di un livello e non essere così rigido (ma lasciare una buona narrazione in modo che altri possano dire cosa succede). [Cerco ciò che rende spiegabile una particolare parte di un programma, ma a volte vado per eleganza quando c’è una vittoria più grande. Non sempre la migliore idea.]

Sono un purista dell’interfaccia e sono attratto dal tipo di problemi e approcci in cui il purismo dell’interfaccia è appropriato, sia che si costruisca una struttura Java o che si organizzino alcune implementazioni COM. Ciò non lo rende adatto a 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.)

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

  1. le relazioni is-a (ereditarietà) si verificano meno frequentemente delle relazioni has-a (composizione).
  2. L’ereditarietà è più difficile da ottenere, anche quando è appropriato utilizzarla, quindi è necessario prendere la dovuta diligenza perché può interrompere l’incapsulamento, incoraggiare l’accoppiamento stretto esponendo l’implementazione e così via.

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

Anche se, naturalmente, non avrebbe senso avere una class Shape ‘having-a’ Point e una Square. Qui l’eredità è dovuta.

Le persone tendono a pensare prima all’ereditarietà quando cercano di progettare qualcosa di estensibile, cioè ciò che è sbagliato.

Tratterò la parte where-these-apply-apply. Ecco un esempio di entrambi, in uno scenario di gioco. Supponiamo, ci sia un gioco che ha diversi tipi di soldati. Ogni soldato può avere uno zaino che può contenere cose diverse.

Ereditarietà qui? C’è un berretto marino, verde e un cecchino. Questi sono tipi di soldati. Quindi, c’è un soldato di class base con Marine, Beret verde e Sniper come classi derivate

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

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

Il vantaggio non è immediatamente visibile. Per vedere il vantaggio è necessario attendere la successiva richiesta di modifica imprevista. Quindi, nella maggior parte dei casi, quelli che si attengono alla generalità non hanno successo se confrontati con quelli che hanno abbracciato la composizione (tranne un caso ovvio menzionato in seguito). Quindi la regola. Da un punto di vista dell’apprendimento se è ansible implementare con successo un’iniezione di dipendenza, allora si dovrebbe sapere quale privilegiare e quando. La regola ti aiuta anche a prendere una decisione; se non si è sicuri, selezionare la composizione.

Riepilogo: Composizione: l’accoppiamento si riduce semplicemente aggiungendo alcune cose più piccole che si inseriscono in qualcosa di più grande, e l’object più grande richiama solo l’object più piccolo indietro. Generazione: da un punto di vista dell’API che definisce che un metodo può essere sovrascritto è un impegno più forte della 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 invece che da una grande class

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

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