Introduzione
Un caso pratico in cui ereditarietà e polimorfismo risultano molto utili è la gestione delle commissioni di vendita. In un’applicazione commerciale, infatti, la commissione può essere calcolata in modi diversi a seconda del tipo di venditore: ad esempio una percentuale sul fatturato, una tariffa fissa per ogni ordine oppure una formula a scaglioni.
Senza il polimorfismo, potremmo creare una lunga serie di istruzioni if o switch per distinguere ogni modalità di calcolo. Questo approccio funziona inizialmente, ma diventa difficile da mantenere quando si aggiungono nuove regole.
In questo tutorial realizzeremo un piccolo motore di calcolo basato su una classe astratta comune e su più classi figlie. Il codice chiamante utilizzerà tutti gli oggetti nello stesso modo, senza conoscere i dettagli interni di ciascuna strategia.
Codice completo
<?php
declare(strict_types=1);
/**
* Classe astratta di base.
* Ogni strategia di commissione dovrà implementare
* il metodo calcolaCommissione().
*/
abstract class RegolaCommissione
{
public function __construct(
protected string $nome
) {
}
public function getNome(): string
{
return $this->nome;
}
/**
* Metodo astratto: la formula concreta
* sarà definita dalle classi figlie.
*/
abstract public function calcolaCommissione(float $fatturato): float;
}
/**
* Commissione calcolata come percentuale fissa.
*/
class CommissionePercentuale extends RegolaCommissione
{
public function __construct(
string $nome,
private float $percentuale
) {
parent::__construct($nome);
}
public function calcolaCommissione(float $fatturato): float
{
return $fatturato * ($this->percentuale / 100);
}
}
/**
* Commissione fissa per ogni periodo,
* indipendentemente dal fatturato.
*/
class CommissioneFissa extends RegolaCommissione
{
public function __construct(
string $nome,
private float $importo
) {
parent::__construct($nome);
}
public function calcolaCommissione(float $fatturato): float
{
// Il fatturato viene ricevuto per rispettare
// il contratto della classe base, ma non viene usato.
return $this->importo;
}
}
/**
* Commissione a scaglioni:
* - 3% fino a 10.000 euro
* - 5% sulla parte eccedente
*/
class CommissioneA加aglioni extends RegolaCommissione
{
public function __construct(
string $nome,
private float $soglia,
private float $percentualeBase,
private float $percentualeEccedenza
) {
parent::__construct($nome);
}
public function calcolaCommissione(float $fatturato): float
{
if ($fatturato <= $this->soglia) {
return $fatturato * ($this->percentualeBase / 100);
}
$commissioneBase = $this->soglia
* ($this->percentualeBase / 100);
$eccedenza = $fatturato - $this->soglia;
$commissioneEccedenza = $eccedenza
* ($this->percentualeEccedenza / 100);
return $commissioneBase + $commissioneEccedenza;
}
}
/**
* Il servizio lavora con la classe astratta.
* Non deve sapere quale implementazione concreta
* gli è stata passata.
*/
class CalcolatoreCommissioni
{
/**
* @param RegolaCommissione[] $regole
*/
public function generaRiepilogo(
array $regole,
float $fatturato
): void {
foreach ($regole as $regola) {
$commissione = $regola->calcolaCommissione($fatturato);
echo $regola->getNome() . ´: ´
. number_format($commissione, 2, ´,´, ´.´)
. " euron";
}
}
}
$regole = [
new CommissionePercentuale(
´Venditore senior´,
8.0
),
new CommissioneFissa(
´Collaboratore esterno´,
500.0
),
new CommissioneA加aglioni(
´Venditore con bonus progressivo´,
10000.0,
3.0,
5.0
),
];
$fatturatoMensile = 18000.0;
$calcolatore = new CalcolatoreCommissioni();
$calcolatore->generaRiepilogo(
$regole,
$fatturatoMensile
); Nel codice è presente un carattere non ASCII nel nome della classe. In un progetto reale è preferibile usare nomi composti esclusivamente da caratteri ASCII. La dichiarazione corretta della classe è quindi:
class CommissioneAScaglioni extends RegolaCommissione
{
public function __construct(
string $nome,
private float $soglia,
private float $percentualeBase,
private float $percentualeEccedenza
) {
parent::__construct($nome);
}
public function calcolaCommissione(float $fatturato): float
{
if ($fatturato <= $this->soglia) {
return $fatturato * ($this->percentualeBase / 100);
}
$commissioneBase = $this->soglia
* ($this->percentualeBase / 100);
$eccedenza = $fatturato - $this->soglia;
return $commissioneBase
+ ($eccedenza * ($this->percentualeEccedenza / 100));
}
} Spiegazione
La classe astratta
RegolaCommissione rappresenta il modello comune per tutte le regole. Non viene istanziata direttamente, perché descrive un concetto generale. Il metodo calcolaCommissione() è dichiarato astratto: ogni sottoclasse deve necessariamente fornire una propria implementazione.
La proprietà nome e il metodo getNome() sono invece condivisi da tutte le classi figlie. La chiamata a parent::__construct() permette di inizializzare la parte ereditata dalla classe base.
L’ereditarietà
Le classi CommissionePercentuale, CommissioneFissa e CommissioneAScaglioni ereditano da RegolaCommissione. Ognuna riutilizza la struttura comune, ma introduce i propri dati e la propria formula.
Per esempio, la commissione percentuale utilizza una semplice moltiplicazione, mentre quella a scaglioni separa il fatturato in una parte base e in una parte eccedente.
Il polimorfismo
Il polimorfismo è visibile nel metodo generaRiepilogo(). Il parametro $regole contiene oggetti di classi concrete diverse, ma tutti possono essere trattati come istanze di RegolaCommissione.
Durante il ciclo, PHP esegue automaticamente la versione corretta di calcolaCommissione(). Se l’oggetto è una CommissioneFissa, viene usata la formula fissa; se è una CommissionePercentuale, viene usata la percentuale. Il servizio non contiene condizioni specifiche per riconoscere il tipo dell’oggetto.
Con un fatturato di 18.000 euro, il risultato sarà simile al seguente:
Venditore senior: 1.440,00 euro
Collaboratore esterno: 500,00 euro
Venditore con bonus progressivo: 640,00 euro Best practice
- Programmare verso un’astrazione: il calcolatore riceve
RegolaCommissioneinvece delle singole classi concrete. - Applicare il principio Open/Closed: per aggiungere una nuova regola è sufficiente creare una nuova sottoclasse, senza modificare il servizio esistente.
- Usare tipi dichiarati:
declare(strict_types=1), proprietà tipizzate e valori restituiti rendono il codice più prevedibile. - Validare i dati: in un’applicazione reale bisogna impedire percentuali negative, soglie inferiori a zero e fatturati non validi.
- Separare le responsabilità: le classi calcolano le commissioni, mentre il servizio si occupa soltanto di elaborare e presentare il riepilogo.
- Evitare controlli sul tipo: costruire molti
instanceofnel servizio ridurrebbe i vantaggi del polimorfismo. - Scrivere test automatici: ogni regola dovrebbe essere verificata con fatturati inferiori, uguali e superiori alle soglie.
Riepilogo
L’ereditarietà consente di definire una struttura comune nella classe astratta RegolaCommissione, mentre il polimorfismo permette di usare strategie diverse attraverso la stessa interfaccia pubblica.
Questo approccio è particolarmente utile quando un’applicazione deve gestire regole variabili: commissioni, tariffe, calcoli fiscali, bonus o politiche aziendali. Il codice diventa più estendibile, leggibile e semplice da testare. Per introdurre una nuova modalità, come una commissione basata sul numero di contratti, sarà sufficiente creare una nuova classe che implementi calcolaCommissione().
