Introduzione
Quando si inizia a usare la programmazione orientata agli oggetti in PHP, si tende spesso a concentrarsi su classi, proprietà e metodi. Tuttavia, uno dei vantaggi più potenti dell’OOP è la possibilità di costruire gerarchie di classi riutilizzabili, usando ereditarietà e polimorfismo.
Questi due concetti ti permettono di modellare meglio il dominio applicativo, ridurre la duplicazione del codice e scrivere componenti più facili da estendere. In pratica, puoi creare una classe base con il comportamento comune e poi specializzarla in classi figlie che aggiungono o modificano funzionalità specifiche.
In questo tutorial vedremo un caso realistico: la gestione di diversi metodi di pagamento in un e-commerce. È un esempio utile perché mostra bene quando conviene usare una classe base e quando invece serve una classe specializzata.
Codice completo
<?php
declare(strict_types=1);
/**
* Classe base astratta per tutti i pagamenti.
* Contiene dati e comportamento comune.
*/
abstract class PaymentMethod
{
public function __construct(
protected string $holderName,
protected float $amount
) {}
public function getSummary(): string
{
return $this->holderName . ´ deve pagare ´ . number_format($this->amount, 2, ´,´, ´.´) . ´ EUR´;
}
// Ogni metodo di pagamento deve implementare la sua logica di esecuzione.
abstract public function pay(): string;
}
/**
* Pagamento con carta di credito.
*/
class CreditCardPayment extends PaymentMethod
{
public function __construct(
string $holderName,
float $amount,
private string $maskedCardNumber
) {
parent::__construct($holderName, $amount);
}
public function pay(): string
{
return "Pagamento con carta autorizzato per {$this->holderName}, carta {$this->maskedCardNumber}. Importo: " .
number_format($this->amount, 2, ´,´, ´.´) . " EUR";
}
}
/**
* Pagamento con PayPal.
*/
class PayPalPayment extends PaymentMethod
{
public function __construct(
string $holderName,
float $amount,
private string $email
) {
parent::__construct($holderName, $amount);
}
public function pay(): string
{
return "Pagamento PayPal eseguito per {$this->holderName} ({$this->email}). Importo: " .
number_format($this->amount, 2, ´,´, ´.´) . " EUR";
}
}
/**
* Pagamento tramite bonifico bancario.
*/
class BankTransferPayment extends PaymentMethod
{
public function __construct(
string $holderName,
float $amount,
private string $iban
) {
parent::__construct($holderName, $amount);
}
public function pay(): string
{
return "Bonifico avviato per {$this->holderName}, IBAN {$this->iban}. Importo: " .
number_format($this->amount, 2, ´,´, ´.´) . " EUR";
}
}
/**
* Servizio che utilizza il polimorfismo.
*/
class PaymentProcessor
{
/**
* Accetta qualsiasi oggetto che estende PaymentMethod.
*/
public function process(PaymentMethod $paymentMethod): void
{
echo $paymentMethod->getSummary() . PHP_EOL;
echo $paymentMethod->pay() . PHP_EOL;
}
}
// Esempio d´uso
$payments = [
new CreditCardPayment(´Mario Rossi´, 120.50, ´**** **** **** 1234´),
new PayPalPayment(´Lucia Bianchi´, 89.90, ´[email protected]´),
new BankTransferPayment(´Andrea Verdi´, 240.00, ´IT60X0542811101000000123456´),
];
$processor = new PaymentProcessor();
foreach ($payments as $payment) {
$processor->process($payment);
echo str_repeat(´-´, 50) . PHP_EOL;
} Spiegazione
Il codice mostra un esempio tipico di ereditarietà in PHP. La classe PaymentMethod è la classe base astratta: rappresenta il concetto generale di pagamento, ma non può essere istanziata direttamente. Questo è corretto, perché un “metodo di pagamento” generico non è sufficiente per eseguire un’operazione concreta.
1. La classe base astratta
La classe PaymentMethod contiene:
- proprietà comuni: nome del titolare e importo;
- un metodo concreto getSummary(), utile a tutte le varianti;
- un metodo astratto pay(), che obbliga le classi figlie a definire la propria logica.
Questo approccio riduce la duplicazione: non serve riscrivere ogni volta la gestione dell’importo o del nome del cliente.
2. Le classi figlie specializzate
Le classi CreditCardPayment, PayPalPayment e BankTransferPayment estendono la classe base e aggiungono i dati specifici del loro contesto:
- la carta mascherata per il pagamento con carta;
- l’email per PayPal;
- l’IBAN per il bonifico.
Ogni classe implementa il metodo pay() in modo diverso. Questo è il cuore del polimorfismo: oggetti diversi rispondono allo stesso messaggio, ma con comportamenti specifici.
3. Il processore di pagamenti
La classe PaymentProcessor riceve un parametro tipizzato come PaymentMethod. Questo è molto importante: il processore non deve conoscere il tipo concreto del pagamento, gli basta sapere che esiste un oggetto compatibile con la classe base.
Quando chiamiamo $paymentMethod->pay(), PHP esegue automaticamente il metodo corretto in base all’oggetto reale passato. Questo rende il codice flessibile e aperto all’estensione.
4. Vantaggi pratici
- meno duplicazione del codice comune;
- estendibilità: puoi aggiungere un nuovo metodo di pagamento senza modificare il processore;
- manutenzione più semplice: le responsabilità sono separate;
- codice più leggibile: ogni classe ha un ruolo preciso.
Best practice
Usare ereditarietà e polimorfismo in PHP è utile, ma va fatto con criterio. Ecco alcune buone pratiche da seguire.
- Non abusare dell’ereditarietà: usa una classe base solo quando esiste davvero un rapporto “è un tipo di”. Se il legame è debole, valuta composizione o servizi separati.
- Preferisci classi astratte quando serve una base comune: se vuoi condividere codice e imporre metodi obbligatori, una classe astratta è spesso la scelta migliore.
- Usa type hint e tipi di ritorno: rendono il codice più sicuro e comprensibile.
- Evita logiche troppo complesse nella classe base: la classe padre deve offrire supporto, non diventare un “mostro” difficile da mantenere.
- Non forzare le classi figlie a ereditare comportamenti inutili: se una sottoclasse non ha senso in un certo contesto, probabilmente la gerarchia è sbagliata.
- Rendi i metodi astratti davvero necessari: un metodo astratto deve rappresentare un comportamento che ogni variante deve implementare in modo proprio.
Un errore comune è costruire gerarchie troppo profonde. In PHP, come in altri linguaggi, una catena di ereditarietà lunga spesso complica il debug e rende difficile capire dove sia definita una certa logica. Meglio una struttura semplice e ben documentata.
Un altro consiglio importante riguarda la visibilità delle proprietà. Nell’esempio abbiamo usato protected per i dati comuni, così le classi figlie possono accedervi senza esporli all’esterno. Questo mantiene un buon equilibrio tra riuso e incapsulamento.
Riepilogo
Ereditarietà e polimorfismo sono due pilastri dell’OOP in PHP. Con una classe base astratta puoi definire il comportamento condiviso, mentre le classi figlie implementano i dettagli specifici. Il risultato è un codice più pulito, modulare e facile da estendere.
Nell’esempio dei pagamenti abbiamo visto come un processore possa lavorare con oggetti diversi senza conoscere la loro implementazione interna. Questo è uno dei vantaggi più concreti del polimorfismo: scrivere codice che dipende da astrazioni, non da dettagli.
Se vuoi progettare applicazioni PHP solide, impara a riconoscere i casi in cui una gerarchia di classi è davvero utile. Quando il modello è corretto, il codice diventa più naturale da leggere e più semplice da evolvere.
Approfondisci con risorse ufficiali
- Manuale PHP - Classi e oggetti: documentazione ufficiale per la base dell’OOP.
- Manuale PHP - Classi astratte: utile per approfondire l’uso dei metodi astratti.
- Manuale PHP - Ereditarietà: per capire come funzionano classi padre e figlie.
- Manuale PHP - Type declarations: per migliorare robustezza e chiarezza del codice.
- Manuale PHP - Promoted properties: comodo per semplificare i costruttori nelle classi moderne.
