Salta al contenuto principale

03 - Ereditarietà e Composizione: Riutilizzo del Codice in OOP

Ereditarietà vs Composizione: Due Approcci al Riutilizzo del Codice L' ereditarietà e la composizione sono due meccanismi fondamentali per il riutilizzo...

Cosa ne pensi di questo articolo?

In Breve

Ereditarietà vs Composizione: Due Approcci al Riutilizzo del Codice L' ereditarietà e la composizione sono due meccanismi fondamentali per il riutilizzo...

Ereditarietà vs Composizione: Due Approcci al Riutilizzo del Codice

L'ereditarietà e la composizione sono due meccanismi fondamentali per il riutilizzo del codice nella programmazione orientata agli oggetti. Entrambi permettono di costruire software complesso partendo da componenti più semplici, ma con approcci e implicazioni molto diverse.

🎯 Cosa Imparerai

  • Quando usare ereditarietà e quando composizione
  • Differenze tra classi astratte e interfacce
  • Il principio "Composition over Inheritance"
  • Come evitare problemi comuni dell'ereditarietà

Ereditarietà: La Relazione "IS-A"

L'ereditarietà esprime una relazione "è un" (is-a). Un cane è un animale, un cerchio è una forma. La classe figlia eredita tutti i membri della classe padre e può estenderli o sovrascriverli.

TypeScript - Ereditarietà Base
class Vehicle {{ '{' }}
  protected speed: number = 0;

  constructor(protected brand: string) {{ '{' }}{{ '}' }}

  accelerate(amount: number): void {{ '{' }}
    this.speed += amount;
    console.log(`${{ '{' }}this.brand{{ '}' }} accelera a ${{ '{' }}this.speed{{ '}' }} km/h`);
  {{ '}' }}

  getInfo(): string {{ '{' }}
    return `${{ '{' }}this.brand{{ '}' }} - Velocità: ${{ '{' }}this.speed{{ '}' }} km/h`;
  {{ '}' }}
{{ '}' }}

class Car extends Vehicle {{ '{' }}
  private doors: number;

  constructor(brand: string, doors: number) {{ '{' }}
    super(brand); // Chiama il costruttore del padre
    this.doors = doors;
  {{ '}' }}

  // Estende la funzionalità del padre
  getInfo(): string {{ '{' }}
    return `${{ '{' }}super.getInfo(){{ '}' }} - Porte: ${{ '{' }}this.doors{{ '}' }}`;
  {{ '}' }}

  openTrunk(): void {{ '{' }}
    console.log("Bagagliaio aperto");
  {{ '}' }}
{{ '}' }}

const myCar = new Car("Tesla", 4);
myCar.accelerate(50);
console.log(myCar.getInfo());
// Output: Tesla - Velocità: 50 km/h - Porte: 4

Classi Astratte vs Interfacce

TypeScript offre sia classi astratte che interfacce per definire contratti e strutture riutilizzabili.

Classi Astratte:

  • Possono avere implementazioni
  • Supportano modificatori (private, protected)
  • Ereditarietà singola (extends una sola)
  • Usale per condividere codice comune

Interfacce:

  • Solo firme, nessuna implementazione
  • Tutti i membri sono public
  • Implementazione multipla (implements più interfacce)
  • Usale per definire contratti
TypeScript - Classi Astratte
abstract class Shape {{ '{' }}
  protected color: string;

  constructor(color: string) {{ '{' }}
    this.color = color;
  {{ '}' }}

  // Metodo concreto (con implementazione)
  getColor(): string {{ '{' }}
    return this.color;
  {{ '}' }}

  // Metodo astratto (da implementare nelle sottoclassi)
  abstract area(): number;
  abstract perimeter(): number;
{{ '}' }}

class Circle extends Shape {{ '{' }}
  constructor(private radius: number, color: string) {{ '{' }}
    super(color);
  {{ '}' }}

  area(): number {{ '{' }}
    return Math.PI * this.radius ** 2;
  {{ '}' }}

  perimeter(): number {{ '{' }}
    return 2 * Math.PI * this.radius;
  {{ '}' }}
{{ '}' }}

const circle = new Circle(5, "rosso");
console.log(circle.area()); // 78.54
console.log(circle.getColor()); // "rosso"
TypeScript - Interfacce Multiple
interface Flyable {{ '{' }}
  fly(): void;
  altitude: number;
{{ '}' }}

interface Swimmable {{ '{' }}
  swim(): void;
  depth: number;
{{ '}' }}

// Una classe può implementare più interfacce
class Duck implements Flyable, Swimmable {{ '{' }}
  altitude: number = 0;
  depth: number = 0;

  fly(): void {{ '{' }}
    this.altitude = 100;
    console.log(`Volo a ${{ '{' }}this.altitude{{ '}' }}m di altitudine`);
  {{ '}' }}

  swim(): void {{ '{' }}
    this.depth = 2;
    console.log(`Nuoto a ${{ '{' }}this.depth{{ '}' }}m di profondità`);
  {{ '}' }}
{{ '}' }}

const duck = new Duck();
duck.fly();   // "Volo a 100m di altitudine"
duck.swim();  // "Nuoto a 2m di profondità"

Composizione: La Relazione "HAS-A"

La composizione esprime una relazione "ha un" (has-a). Un'auto ha un motore, un computer ha una CPU. Invece di ereditare, si includono istanze di altre classi come membri.

TypeScript - Composizione
class Engine {{ '{' }}
  constructor(private horsepower: number) {{ '{' }}{{ '}' }}

  start(): void {{ '{' }}
    console.log(`Motore da ${{ '{' }}this.horsepower{{ '}' }}HP avviato`);
  {{ '}' }}

  getHorsepower(): number {{ '{' }}
    return this.horsepower;
  {{ '}' }}
{{ '}' }}

class Transmission {{ '{' }}
  constructor(private type: string) {{ '{' }}{{ '}' }}

  shift(gear: number): void {{ '{' }}
    console.log(`Cambio ${{ '{' }}this.type{{ '}' }}: marcia ${{ '{' }}gear{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

// Car "ha un" Engine e "ha una" Transmission
class CarComposed {{ '{' }}
  private engine: Engine;
  private transmission: Transmission;

  constructor(horsepower: number, transmissionType: string) {{ '{' }}
    this.engine = new Engine(horsepower);
    this.transmission = new Transmission(transmissionType);
  {{ '}' }}

  start(): void {{ '{' }}
    this.engine.start();
  {{ '}' }}

  drive(): void {{ '{' }}
    this.transmission.shift(1);
    console.log("Auto in movimento");
  {{ '}' }}

  getSpecs(): string {{ '{' }}
    return `${{ '{' }}this.engine.getHorsepower(){{ '}' }}HP`;
  {{ '}' }}
{{ '}' }}

const car = new CarComposed(200, "automatico");
car.start();  // "Motore da 200HP avviato"
car.drive();  // "Cambio automatico: marcia 1"
              // "Auto in movimento"

Composition Over Inheritance

Il principio "Composition over Inheritance" suggerisce di preferire la composizione all'ereditarietà quando possibile. Perché?

✅ Vantaggi della Composizione:

  • Flessibilità: Cambia comportamenti a runtime
  • Riutilizzo: Combina componenti in modi diversi
  • Testing: Testa componenti indipendentemente
  • Accoppiamento debole: Modifiche più sicure

⚠️ Problemi dell'Ereditarietà:

  • Gerarchia rigida difficile da modificare
  • Accoppiamento forte tra padre e figlio
  • Fragile Base Class Problem
  • Diamond Problem (ereditarietà multipla)

Quando Usare Ereditarietà

L'ereditarietà è appropriata quando:

  • C'è una chiara relazione "is-a" (Cane è un Animale)
  • La sottoclasse è una specializzazione vera della superclasse
  • Serve polimorfismo (trattare oggetti diversi con interfaccia comune)
  • La gerarchia è stabile e non cambierà frequentemente
Buon Uso dell'Ereditarietà
// Gerarchia chiara e stabile
abstract class Employee {{ '{' }}
  constructor(
    protected name: string,
    protected salary: number
  ) {{ '{' }}{{ '}' }}

  abstract calculateBonus(): number;

  getDetails(): string {{ '{' }}
    return `${{ '{' }}this.name{{ '}' }} - Stipendio: €${{ '{' }}this.salary{{ '}' }}`;
  {{ '}' }}
{{ '}' }}

class Manager extends Employee {{ '{' }}
  calculateBonus(): number {{ '{' }}
    return this.salary * 0.2; // 20% bonus
  {{ '}' }}
{{ '}' }}

class Developer extends Employee {{ '{' }}
  constructor(name: string, salary: number, private language: string) {{ '{' }}
    super(name, salary);
  {{ '}' }}

  calculateBonus(): number {{ '{' }}
    return this.salary * 0.15; // 15% bonus
  {{ '}' }}
{{ '}' }}

Quando Usare Composizione

La composizione è preferibile quando:

  • La relazione è "has-a" (Auto ha un Motore)
  • Serve riutilizzo flessibile di funzionalità
  • I comportamenti devono essere intercambiabili
  • La struttura può evolvere nel tempo
Composizione per Flessibilità
// Strategy pattern con composizione
interface PaymentStrategy {{ '{' }}
  pay(amount: number): void;
{{ '}' }}

class CreditCardPayment implements PaymentStrategy {{ '{' }}
  pay(amount: number): void {{ '{' }}
    console.log(`Pagamento di €${{ '{' }}amount{{ '}' }} con carta di credito`);
  {{ '}' }}
{{ '}' }}

class PayPalPayment implements PaymentStrategy {{ '{' }}
  pay(amount: number): void {{ '{' }}
    console.log(`Pagamento di €${{ '{' }}amount{{ '}' }} con PayPal`);
  {{ '}' }}
{{ '}' }}

class ShoppingCartComposed {{ '{' }}
  private items: number[] = [];
  private paymentStrategy!: PaymentStrategy;

  addItem(price: number): void {{ '{' }}
    this.items.push(price);
  {{ '}' }}

  // Cambia strategia a runtime!
  setPaymentStrategy(strategy: PaymentStrategy): void {{ '{' }}
    this.paymentStrategy = strategy;
  {{ '}' }}

  checkout(): void {{ '{' }}
    const total = this.items.reduce((sum, price) => sum + price, 0);
    this.paymentStrategy.pay(total);
  {{ '}' }}
{{ '}' }}

const cart = new ShoppingCartComposed();
cart.addItem(50);
cart.addItem(30);

cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(); // "Pagamento di €80 con carta di credito"

cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(); // "Pagamento di €80 con PayPal"

Conclusione

Ereditarietà e composizione non sono nemiche ma strumenti complementari. L'ereditarietà è potente per modellare gerarchie stabili con polimorfismo, mentre la composizione offre flessibilità e riutilizzo modulare. La regola pratica: inizia con la composizione, usa l'ereditarietà solo quando la relazione "is-a" è chiara e i benefici superano i costi.

🎯 Punti Chiave

  • Ereditarietà = "is-a", Composizione = "has-a"
  • Classi astratte per codice condiviso, interfacce per contratti
  • Composition over Inheritance per maggiore flessibilità
  • Usa ereditarietà per gerarchie stabili con polimorfismo chiaro

Domande frequenti

Qual è la differenza fondamentale tra ereditarietà e composizione nella programmazione orientata agli oggetti?

La principale differenza sta nel tipo di relazione che esprimono: l'ereditarietà rappresenta una relazione "è un" (is-a), mentre la composizione rappresenta una relazione "ha un" (has-a).

Quando è appropriato utilizzare l'ereditarietà?

L'ereditarietà è adatta quando si vuole modellare gerarchie stabili con polimorfismo e la relazione "is-a" è chiara.

Quali sono le caratteristiche della composizione rispetto all'ereditarietà?

La composizione offre flessibilità e riutilizzo modulare, in quanto si includono istanze di altre classi come membri invece di ereditarli.

Qual è il principio consigliato per scegliere tra ereditarietà e composizione?

Il principio "Composition over Inheritance" suggerisce di preferire la composizione all'ereditarietà quando possibile.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti