Introduzione
In JavaScript, ereditarietà e polimorfismo sono due concetti fondamentali per organizzare il codice in modo flessibile, riusabile e facile da estendere. Invece di scrivere molte funzioni separate e scollegate, possiamo modellare il comportamento di oggetti diversi partendo da una base comune.
In questo tutorial useremo un caso pratico e molto utile: un sistema di gestione dei pagamenti. È un argomento diverso dai classici esempi su prodotti, utenti, veicoli o notifiche, ma perfetto per vedere come funzionano classi, ereditarietà e polimorfismo in un contesto reale.
L’obiettivo sarà creare una classe base PaymentMethod e alcune classi derivate come CreditCardPayment, PayPalPayment e BankTransferPayment. Ogni metodo di pagamento avrà un comportamento specifico, ma tutti condivideranno un’interfaccia comune.
Codice completo
class PaymentMethod {
constructor(owner) {
this.owner = owner;
}
// Metodo generico: ogni sottoclasse lo sovrascriverà
pay(amount) {
throw new Error("Il metodo pay() deve essere implementato nelle sottoclassi");
}
// Metodo comune a tutti i pagamenti
getReceipt(amount) {
return {
owner: this.owner,
amount,
timestamp: new Date().toISOString()
};
}
}
class CreditCardPayment extends PaymentMethod {
constructor(owner, cardLast4) {
super(owner);
this.cardLast4 = cardLast4;
}
pay(amount) {
if (amount <= 0) {
throw new Error("L´importo deve essere maggiore di zero");
}
return {
method: "credit_card",
status: "approved",
cardLast4: this.cardLast4,
receipt: this.getReceipt(amount)
};
}
}
class PayPalPayment extends PaymentMethod {
constructor(owner, email) {
super(owner);
this.email = email;
}
pay(amount) {
if (amount <= 0) {
throw new Error("L´importo deve essere maggiore di zero");
}
return {
method: "paypal",
status: "approved",
email: this.email,
receipt: this.getReceipt(amount)
};
}
}
class BankTransferPayment extends PaymentMethod {
constructor(owner, iban) {
super(owner);
this.iban = iban;
}
pay(amount) {
if (amount <= 0) {
throw new Error("L´importo deve essere maggiore di zero");
}
return {
method: "bank_transfer",
status: "pending",
iban: this.iban,
receipt: this.getReceipt(amount)
};
}
}
// Funzione che sfrutta il polimorfismo
function processPayment(paymentMethod, amount) {
const result = paymentMethod.pay(amount);
console.log("Pagamento processato:", result);
return result;
}
// Esempio di utilizzo
const cardPayment = new CreditCardPayment("Luca", "4242");
const paypalPayment = new PayPalPayment("Sara", "[email protected]");
const bankPayment = new BankTransferPayment("Marco", "IT60X0542811101000000123456");
processPayment(cardPayment, 120.50);
processPayment(paypalPayment, 49.99);
processPayment(bankPayment, 300);
// Dimostrazione del comportamento comune ereditato
console.log(cardPayment.getReceipt(120.50));
console.log(paypalPayment.getReceipt(49.99));
console.log(bankPayment.getReceipt(300)); Spiegazione
La classe PaymentMethod rappresenta il contratto comune. Contiene un costruttore che salva il nome del proprietario e un metodo getReceipt() condiviso da tutti i pagamenti. Questo metodo non cambia tra i vari tipi di pagamento, quindi ha senso definirlo una sola volta nella classe base.
Il metodo pay(), invece, è volutamente generico. Nella classe base lancia un errore, perché ogni metodo di pagamento deve implementare la propria logica. Questo è un approccio utile quando vuoi obbligare le sottoclassi a definire un comportamento specifico.
Ereditarietà
Le classi CreditCardPayment, PayPalPayment e BankTransferPayment estendono PaymentMethod. Grazie a extends, ereditano proprietà e metodi comuni. Con super() inizializzano correttamente la parte condivisa della classe padre.
Questo ci permette di evitare duplicazione. Per esempio, il campo owner e il metodo getReceipt() non devono essere riscritti in ogni sottoclasse.
Polimorfismo
Il polimorfismo significa che possiamo trattare oggetti diversi attraverso la stessa interfaccia. La funzione processPayment() riceve un oggetto generico paymentMethod e chiama pay(amount) senza sapere in anticipo se si tratta di una carta, PayPal o bonifico.
Il comportamento finale dipende dall’oggetto concreto passato alla funzione. Questo è il cuore del polimorfismo: stesso metodo, risultati diversi.
Perché questo esempio è pratico
In un’app reale, un sistema di pagamento potrebbe:
- validare importi e dati dell’utente;
- gestire metodi di pagamento differenti con la stessa logica di processo;
- generare ricevute uniformi;
- aggiungere nuovi metodi in futuro senza modificare il codice esistente.
Ad esempio, domani potresti introdurre ApplePayPayment o CryptoPayment senza cambiare la funzione processPayment(). Ti basterebbe creare una nuova classe che estende PaymentMethod e implementa pay().
Best practice
- Usa la classe base per il comportamento comune: proprietà condivise, metodi riutilizzabili e validazioni generiche vanno centralizzati.
- Definisci un contratto chiaro: se una sottoclasse deve implementare un metodo, fai in modo che la classe base segnali l’errore in modo esplicito.
- Evita ereditarietà inutile: non creare gerarchie troppo profonde se non portano reale valore. In JavaScript, spesso composizione ed ereditarietà vanno bilanciate.
- Rendi le funzioni generiche: come processPayment(), dovrebbero lavorare con il tipo base e non con i dettagli interni delle sottoclassi.
- Valida sempre gli input: controllare importi negativi o nulli evita errori logici difficili da diagnosticare.
- Usa nomi chiari: classi e metodi devono comunicare bene il loro ruolo, soprattutto in gerarchie orientate agli oggetti.
Riepilogo
In questo tutorial abbiamo visto come usare ereditarietà e polimorfismo in JavaScript partendo da un sistema di gestione pagamenti. La classe base PaymentMethod ha fornito una struttura comune, mentre le sottoclassi hanno definito comportamenti specifici.
Abbiamo anche visto come una funzione generica possa lavorare con oggetti diversi senza conoscere i dettagli di implementazione. Questo rende il codice più estendibile, più pulito e più facile da mantenere.
Il messaggio chiave è semplice: usa l’ereditarietà per condividere ciò che è comune, e il polimorfismo per trattare oggetti diversi in modo uniforme.
Approfondisci con risorse ufficiali
- MDN Web Docs - Classes: documentazione ufficiale sulle classi in JavaScript.
- MDN Web Docs - Inheritance and the prototype chain: approfondimento sulla catena dei prototipi.
- MDN Web Docs - super: uso corretto di super() nelle classi derivate.
- MDN Web Docs - Object-oriented JavaScript: panoramica completa sulla programmazione orientata agli oggetti in JavaScript.
Se vuoi fare un passo avanti, prova ad aggiungere un nuovo metodo di pagamento e a integrare una funzione di logging o di persistenza. È un ottimo esercizio per consolidare davvero ereditarietà e polimorfismo.
