Introduzione
Un caso pratico in cui ereditarietà e polimorfismo risultano molto utili è il calcolo dei costi di spedizione. Un negozio online può offrire diversi metodi di consegna: spedizione standard, consegna espressa e ritiro presso un punto convenzionato.
Ogni metodo applica regole differenti, ma tutti condividono lo stesso obiettivo: calcolare un costo partendo da alcune informazioni, come peso, distanza e valore dell’ordine. Invece di inserire molti if o switch nello stesso metodo, possiamo definire una classe astratta comune e lasciare che ogni sottoclasse implementi il proprio algoritmo.
Il codice che utilizza le spedizioni non dovrà conoscere i dettagli delle singole strategie. Sarà sufficiente lavorare con il tipo astratto MetodoSpedizione. Questo è il cuore del polimorfismo: lo stesso metodo può comportarsi in modo diverso a seconda dell’oggetto concreto ricevuto.
Codice completo
<?php
declare(strict_types=1);
/**
* Classe base per ogni metodo di spedizione.
*/
abstract class MetodoSpedizione
{
public function __construct(
protected string $nome
) {
}
public function getNome(): string
{
return $this->nome;
}
/**
* Ogni sottoclasse deve definire il proprio calcolo.
*/
abstract public function calcolaCosto(
float $peso,
float $distanza,
float $valoreOrdine
): float;
/**
* Metodo condiviso da tutte le sottoclassi.
*/
protected function verificaDati(
float $peso,
float $distanza,
float $valoreOrdine
): void {
if ($peso <= 0) {
throw new InvalidArgumentException(
´Il peso deve essere maggiore di zero.´
);
}
if ($distanza < 0) {
throw new InvalidArgumentException(
´La distanza non può essere negativa.´
);
}
if ($valoreOrdine < 0) {
throw new InvalidArgumentException(
´Il valore dell’ordine non può essere negativo.´
);
}
}
}
class SpedizioneStandard extends MetodoSpedizione
{
public function __construct()
{
parent::__construct(´Spedizione standard´);
}
public function calcolaCosto(
float $peso,
float $distanza,
float $valoreOrdine
): float {
$this->verificaDati($peso, $distanza, $valoreOrdine);
$costoBase = 5.00;
$costoPeso = $peso * 1.20;
$costoDistanza = $distanza * 0.02;
return round($costoBase + $costoPeso + $costoDistanza, 2);
}
}
class SpedizioneExpress extends MetodoSpedizione
{
public function __construct()
{
parent::__construct(´Spedizione express´);
}
public function calcolaCosto(
float $peso,
float $distanza,
float $valoreOrdine
): float {
$this->verificaDati($peso, $distanza, $valoreOrdine);
$costoBase = 12.00;
$costoPeso = $peso * 2.00;
$costoDistanza = $distanza * 0.04;
// Supplemento per ordini di valore elevato.
$supplementoPriorita = $valoreOrdine > 100 ? 3.00 : 0.00;
return round(
$costoBase + $costoPeso + $costoDistanza + $supplementoPriorita,
2
);
}
}
class RitiroPuntoConvenzionato extends MetodoSpedizione
{
public function __construct()
{
parent::__construct(´Ritiro presso punto convenzionato´);
}
public function calcolaCosto(
float $peso,
float $distanza,
float $valoreOrdine
): float {
$this->verificaDati($peso, $distanza, $valoreOrdine);
// Il ritiro ha un costo fisso e non dipende dalla distanza.
$costo = 2.50;
// Per ordini di almeno 50 euro il ritiro è gratuito.
if ($valoreOrdine >= 50) {
$costo = 0.00;
}
return $costo;
}
}
/**
* Servizio che utilizza il polimorfismo.
*/
class CalcolatoreSpedizione
{
/**
* @param MetodoSpedizione[] $metodi
*/
public function confrontaCosti(
array $metodi,
float $peso,
float $distanza,
float $valoreOrdine
): void {
foreach ($metodi as $metodo) {
$costo = $metodo->calcolaCosto(
$peso,
$distanza,
$valoreOrdine
);
echo $metodo->getNome() . ´: ´
. number_format($costo, 2, ´,´, ´.´)
. " euron";
}
}
}
$metodi = [
new SpedizioneStandard(),
new SpedizioneExpress(),
new RitiroPuntoConvenzionato(),
];
$calcolatore = new CalcolatoreSpedizione();
$calcolatore->confrontaCosti(
$metodi,
peso: 3.5,
distanza: 120,
valoreOrdine: 85
); Spiegazione
La classe astratta
MetodoSpedizione è una classe astratta. Non può essere istanziata direttamente, perché rappresenta un concetto generale e non un metodo di consegna specifico. Contiene però elementi comuni, come il nome della spedizione e la validazione dei dati.
Il metodo calcolaCosto() è dichiarato astratto. Questo obbliga ogni classe figlia a fornire una propria implementazione. In questo modo PHP garantisce che tutti i metodi di spedizione siano in grado di calcolare un costo.
L’ereditarietà
Le classi SpedizioneStandard, SpedizioneExpress e RitiroPuntoConvenzionato estendono la classe base usando la parola chiave extends. Ricevono automaticamente il metodo getNome() e il metodo protetto verificaDati().
Il costruttore delle sottoclassi richiama parent::__construct(), così il nome viene inizializzato nella classe genitore. La logica specifica del prezzo rimane invece all’interno della rispettiva sottoclasse.
Il polimorfismo
Nel metodo confrontaCosti() il parametro è un array di oggetti compatibili con MetodoSpedizione. Il ciclo invoca sempre:
$metodo->calcolaCosto()
Tuttavia, PHP esegue automaticamente l’implementazione corretta: quella standard per SpedizioneStandard, quella express per SpedizioneExpress e quella del punto convenzionato per la terza classe.
Il calcolatore non contiene condizioni come “se il metodo è express”. Questo rende il codice più leggibile e facilita l’aggiunta di nuove opzioni.
Best practice
- Usare una classe astratta per il comportamento comune: validazioni e proprietà condivise non devono essere duplicate.
- Mantenere una responsabilità chiara: ogni classe deve occuparsi del proprio algoritmo di tariffazione.
- Tipizzare parametri e valori restituiti: dichiarare
float,arrayevoidriduce gli errori. - Validare gli input: peso, distanza e valore dell’ordine devono essere controllati prima dei calcoli.
- Preferire il polimorfismo agli switch complessi: nuove strategie possono essere aggiunte senza modificare il calcolatore.
- Usare valori monetari con attenzione: per applicazioni finanziarie importanti è spesso preferibile lavorare in centesimi interi o adottare una libreria specifica per la gestione decimale.
- Separare dominio e presentazione: in un’applicazione reale il servizio dovrebbe restituire dati, mentre la formattazione in euro dovrebbe essere gestita dal livello di presentazione.
Riepilogo
L’ereditarietà consente di definire una struttura comune per più metodi di spedizione. La classe astratta stabilisce quali operazioni devono essere disponibili, mentre le classi figlie implementano regole differenti.
Il polimorfismo permette al calcolatore di utilizzare oggetti diversi attraverso lo stesso tipo astratto. Grazie a questo approccio, il codice principale non dipende dai dettagli delle singole tariffe ed è più semplice da estendere, testare e mantenere.
Per aggiungere, ad esempio, una nuova classe SpedizioneInternazionale, sarebbe sufficiente estendere MetodoSpedizione e implementare calcolaCosto(). Il metodo confrontaCosti() potrebbe utilizzarla senza alcuna modifica.
