Development

Strumenti, idee e visione per diventare uno sviluppatore consapevole, ambizioso e pronto a costruire il proprio futuro con coraggio e competenza.

Dettaglio di ingranaggi e componenti LEGO Technic della mia collezione personale
Foto reale dal mio garage: i miei LEGO Technic, compagni fedeli di relax e ottimi maestri di modularità.

Composition over inheritance: perché i LEGO vincono sulla saldatrice

Scritto da Marco Morello il 30 Dicembre 2025

💡 TL;DR: l'ereditarietà crea legami rigidi ("saldature") che si rompono quando i requisiti cambiano. La composizione permette di assemblare oggetti ("LEGO") combinando piccoli componenti indipendenti. In questo articolo vediamo perché (quasi) sempre conviene avvitare invece che saldare.


Se hai letto il mio utimo articolo sulle classi astratte e le interfacce, ti ricorderai che ci eravamo lasciati con una promessa: smontare il mito dell'ereditarietà come soluzione a tutti i mali.

Quando ho iniziato a studiare C#, l'ereditarietà mi sembrava la cosa più magica del mondo, quasi un superpotere.
"Wow! Posso creare una classe Animale, poi Mammifero, poi Cane, poi Bassotto ed eredito tutto il codice gratis!"

Sembrava il paradiso del risparmio di tempo. Da montatore meccanico quale sono nella vita reale, la vedevo come una catena di montaggio perfetta, dove ogni pezzo usciva già pronto, verniciato e assemblato. Pensavo: "Perché riscrivere codice se posso semplicemente ereditarlo?". Mi sentivo furbo: scrivevo una riga e ottenevo cento metodi.

Poi però, come succede sempre nei viaggi (sia in moto che nel codice), ho incontrato la realtà. E la realtà ha curve molto più strette, asfalto molto più scivoloso e buche più profonde di quanto dicano i manuali universitari.

Oggi parliamo di uno dei principi più importanti per un aspirante architetto software, uno di quelli che separa i junior dai senior: composition over inheritance (preferire la composizione all'ereditarietà).

O, come piace dire a me: perché è meglio assemblare componenti coi bulloni piuttosto che saldarli insieme in un blocco unico.


Il problema del "gorilla e la banana"

C'è una frase famosa di Joe Armstrong (il creatore di Erlang) che riassume perfettamente il problema dell'abuso dell'ereditarietà e che mi ha aperto gli occhi anni fa:

"Il problema dei linguaggi orientati agli oggetti è che si portano dietro tutto l'ambiente implicito. Volevi solo una banana, ma hai ottenuto un gorilla che regge la banana e l'intera giungla."

Cosa significa in pratica? Quando usi l'ereditarietà (class Figlio : Padre), stai creando un accoppiamento fortissimo, il più forte che esista in programmazione. È un vincolo "genetico". Il figlio si porta dietro tutto del padre: metodi pubblici, proprietà, campi privati, dipendenze dal database, logger e logiche di validazione che magari nemmeno ti servono o, peggio, che cozzano con quello che devi fare tu.

Immaginiamo di dover gestire dei personaggi per un videogioco (o dei robot in una linea di produzione, per restare in tema officina).

L'approccio "saldato" (ereditarietà)

Parti con una classe base Nemico. Ci metti dentro un po' di logica di movimento base (Muoviti()) e di attacco (Spara()). Poi crei NemicoCheCammina e NemicoCheVola.

Tutto sembra funzionare alla grande, il codice è pulito. Finché il tuo capo (o il cliente, che è sempre la variabile impazzita) non ti dice: "Ehi, ora voglio un nemico che vola E spara laser, ma che ogni tanto può atterrare e camminare. Ah, e voglio anche una mina vagante che esplode ma non si muove."

Tu vai nel panico. Dove lo attacchi in quella gerarchia rigida?

Per risolvere il problema della mina che non deve volare, finisci a fare questo orrore:

public override void Vola()
{
    throw new NotImplementedException("Le mine non volano!");
}

Questo è il campanello d'allarme definitivo. Se devi lanciare un'eccezione per "spegnere" una funzionalità ereditata, hai sbagliato astrazione. Hai violato il principio di sostituzione (Liskov): stai dicendo al compilatore che la mina è un nemico (che sa volare), ma a runtime menti e spacchi tutto.

⚠️ I pericoli subdoli: la "fragile base class"

L'ereditarietà rompe l'incapsulamento. Immagina di avere una classe base Motore e dieci classi figlie. Un giorno correggi un bug in Motore cambiando il calcolo della temperatura. Compili, tutto verde.

Sei mesi dopo scopri che MotoreDaCorsa (che non hai toccato!) è esploso perché dipendeva implicitamente dal vecchio calcolo sbagliato. Modificando il padre, hai rotto i figli. Nell'ereditarietà, le modifiche si propagano a cascata, spesso in modo distruttivo.


La soluzione: i LEGO (composizione)

Qui entra in gioco il mindset del montatore meccanico. Invece di costruire un pezzo unico fuso insieme, costruiamo un telaio vuoto e ci "montiamo" sopra i pezzi che ci servono, avvitandoli al momento del bisogno.

L'idea fondamentale è passare da una relazione IS-A (è un) a una relazione HAS-A (ha un).
Invece di dire: "Il bassotto E' un cane", diciamo: "Il robot HA un motore e HA un'arma".

Validazione autorevole: non è solo una mia opinione. Questo principio è la "Regola #1" del libro Design Patterns (Gang of Four), la bibbia della programmazione a oggetti. Leggi la definizione tecnica specifica (Wikipedia).

L'approccio "assemblato"

Creiamo componenti isolati, ognuno responsabile di una singola azione. Sono "scatole nere": non mi interessa come funzionano dentro, mi interessa solo cosa fanno.

// Definisco i "pezzi" del mio LEGO (i contratti)
public interface IMovimento { void Muoviti(); }
public interface IAttacco { void Attacca(); }

// Implementazione dei pezzi: piccoli, semplici e testabili
public class MotoreVolo : IMovimento
{
    public void Muoviti() => Console.WriteLine("Sto volando tra le nuvole!");
}

public class MotoreCingoli : IMovimento
{
    public void Muoviti() => Console.WriteLine("Avanzo lentamente sui cingoli.");
}

public class NessunMovimento : IMovimento // Ottimo per torrette fisse o mine!
{
    public void Muoviti() => Console.WriteLine("Sono immobile.");
}

public class CannoneLaser : IAttacco
{
    public void Attacca() => Console.WriteLine("PEW PEW! Laser sparato.");
}

Ora, il nostro Nemico non è più il nodo finale di una complessa gerarchia. È un delegatore. Non fa il lavoro, dice agli altri di farlo.

public class Nemico
{
    // Il nemico NON sa come ci si muove o come si attacca.
    // HA dei componenti che lo sanno fare.
    private readonly IMovimento _movimento;
    private readonly IAttacco _attacco;

    // COSTRUTTORE: Qui avviene la magia dell'assemblaggio.
    // Gli passo i componenti che voglio usare. Iniezione pura!
    public Nemico(IMovimento movimento, IAttacco attacco)
    {
        _movimento = movimento;
        _attacco = attacco;
    }

    public void EseguiAzione()
    {
        // Delega: "Ehi motore, pensaci tu!"
        _movimento.Muoviti();
        _attacco.Attacca();
    }
}

Perché questo approccio ti salva la vita

Immagina che il cliente torni: "Voglio un robot assurdo che si muove sui cingoli ma spara frecce, e una torretta fissa laser."

Con la composizione: non devi creare nessuna nuova classe. Ti limiti ad assemblare diversamente i pezzi che hai già.

// Robot cingolato con arco
var robotStrano = new Nemico(new MotoreCingoli(), new ArcoFrecce());
robotStrano.EseguiAzione();

// Torretta fissa con laser
var torretta = new Nemico(new NessunMovimento(), new CannoneLaser());
torretta.EseguiAzione();

I vantaggi reali che pagano l'affitto:

  1. Flessibilità estrema (runtime): con l'ereditarietà, un oggetto nasce e muore con quel tipo. Con la composizione, puoi tecnicamente cambiare il comportamento di un oggetto "a caldo".
  2. Testabilità (unit testing): come testo il CannoneLaser se è saldato in un Nemico gigante? Con la composizione, testo la classe CannoneLaser da sola. È come testare un carburatore sul banco di prova prima di montarlo sulla moto.
  3. Sicurezza: se modifico MotoreVolo, so che l'unico impatto sarà sul metodo Muoviti. Ognuno si fa i fatti suoi.

Ma allora... l'ereditarietà è da buttare? (Quando usare la saldatrice)

Assolutamente no! L'ereditarietà è uno dei tre pilastri sacri della OOP. Non sto dicendo di buttare la saldatrice dalla finestra dell'officina. Sto dicendo di non usarla per avvitare una lampadina.

Ecco i casi in cui la saldatura (ereditarietà) è strutturalmente migliore dell'assemblaggio:

1. Il principio "IS-A" è forte e stabile (identità)

Un esempio classico sono le entità del database. Quasi tutte le tabelle hanno un ID e una data di creazione. Non ha senso usare la composizione per l'ID ("L'utente HA un ID"? No, l'ID è parte della sua identità).

public abstract class BaseEntity
{
    public int Id { get; set; }
    public DateTime DataCreazione { get; set; } = DateTime.Now;
    public bool IsNew() => Id == 0;
}

public class Utente : BaseEntity 
{
    public string Nome { get; set; }
    // Eredita Id e DataCreazione automaticamente.
    // Qui la "saldatura" va bene perché un Utente NON smetterà mai di avere un ID.
}

2. Framework e "superpoteri" gratuiti

In C# lavorerai spesso con framework come ASP.NET Core. Qui l'ereditarietà è il modo standard per accedere alle funzionalità del sistema. Erediti da ControllerBase per ottenere gratuitamente tutta la complessità della gestione HTTP.

public class MioController : ControllerBase 
{
    [HttpGet]
    public IActionResult Get()
    {
        // Posso usare Ok(), NotFound() e accedere a this.User 
        // perché li ho ereditati dal framework.
        return Ok("Tutto ok!");
    }
}

La regola d'oro: usa l'ereditarietà per definire chi è l'oggetto (identità) o per riutilizzare infrastruttura stabile. Usa la composizione per definire cosa fa l'oggetto (comportamento).


Mettiti alla prova: saldatrice o cacciavite?

Hai capito la differenza? Prova a decidere quale strumento usare in questi scenari reali. Clicca sulla domanda per svelare la risposta dell'architetto.

La situazione: devi modellare i dipendenti. Tutti hanno nome, cognome, matricola e data di assunzione. Queste proprietà non cambieranno mai e sono l'identità base di ogni persona in azienda.


Soluzione: saldatrice (ereditarietà)

Perché? Qui c'è una forte relazione di identità ("È un"). Un Dipendente è una Persona. Non ha senso comporre l'identità base. Una classe base DipendenteBase è perfetta per evitare duplicazioni.

La situazione: il tuo personaggio può raccogliere oggetti che gli danno abilità temporanee: doppi salti, invisibilità o sparo rapido. Queste abilità possono essere combinate e perse se viene colpito.


Soluzione: LEGO (composizione)

Perché? Le abilità sono temporanee e intercambiabili. Se usassi l'ereditarietà (`PersonaggioInvisibile`, `PersonaggioDoppioSalto`) impazziresti a combinare tutto (`PersonaggioInvisibileConDoppioSalto`). Meglio avere una lista di componenti IAbilita da aggiungere e rimuovere a runtime.

La situazione: hai un servizio che deve inviare avvisi. Oggi mandiamo email, domani vorremo mandare SMS, e forse dopodomani notifiche push su Telegram. Vuoi poter cambiare il metodo di invio senza toccare la logica principale.


Soluzione: LEGO (composizione)

Perché? Questo è il classico strategy pattern. La tua classe principale non deve sapere come si invia (ereditarietà), ma deve avere un componente che sa inviare (INotificatore). Così puoi "svitare" l'invio email e "avvitare" l'invio SMS senza rompere nulla.

Conclusione: la cassetta degli attrezzi

L'ereditarietà è come un matrimonio vecchio stampo: finché morte non vi separi. La composizione è come un appuntamento: stiamo insieme finché funziona, se le esigenze cambiano, ci stringiamo la mano e amici come prima.

Da quando ho iniziato a vedere le mie classi non come statue di marmo da scolpire, ma come set di LEGO da assemblare, il mio codice è diventato più pulito, più facile da leggere e, soprattutto, molto più resistente alle modifiche.

Ti è piaciuto l'articolo? Hai mai creato una "classe mostro" impossibile da gestire che ti ha fatto rimpiangere di aver scelto di fare il programmatore? Discutiamone insieme! Scrivimi direttamente su LinkedIn per raccontarmi la tua esperienza o chiedermi un parere.

🔜 Prossimamente su "Il Viaggio del Programmatore"

Hai visto come nel costruttore abbiamo passato i componenti manualmente? In un progetto vero diventerebbe un incubo! Nel prossimo articolo vedremo come automatizzare questo assemblaggio con la dependency injection, lasciando che sia il sistema a fornirci i pezzi giusti al momento giusto.

Prepara il cacciavite!

← Torna indietro