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.
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
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"
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.
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
// 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
// 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.

Commenti
Caricamento commenti...
Accedi per commentare