PHP OOP: applicare il Pattern Strategy per rendere il codice flessibile

by theArchitect
SHARE
PHP OOP: applicare il Pattern Strategy per rendere il codice flessibile
© Guida-HTML5.it

Introduzione

Quando un’applicazione deve eseguire la stessa operazione in modi diversi, è comune inserire molti if o switch nello stesso metodo. Questa soluzione può funzionare all’inizio, ma tende a diventare difficile da leggere e da modificare. Ogni nuovo comportamento richiede infatti di intervenire su una classe già esistente, aumentando il rischio di introdurre errori.

Il Pattern Strategy risolve questo problema incapsulando ogni algoritmo o comportamento in una classe separata. Una classe principale utilizza una strategia senza conoscere i dettagli della sua implementazione. In questo modo è possibile sostituire il comportamento durante l’esecuzione del programma.

In questo tutorial realizzeremo un esempio pratico in PHP: un sistema per calcolare il costo di una spedizione. Il costo potrà essere calcolato usando diverse strategie, come spedizione standard, espressa o gratuita. Il codice sarà organizzato tramite un’interfaccia, alcune classi concrete e un oggetto di contesto.

Codice completo

<?php

declare(strict_types=1);

/**
 * Contratto comune per tutte le strategie di spedizione.
 */
interface StrategiaSpedizione
{
    /**
     * Restituisce il costo della spedizione.
     */
    public function calcolaCosto(float $peso, float $totaleOrdine): float;
}

/**
 * Spedizione standard:
 * costo fisso più una quota basata sul peso.
 */
final class SpedizioneStandard implements StrategiaSpedizione
{
    public function calcolaCosto(float $peso, float $totaleOrdine): float
    {
        return 5.00 + ($peso * 1.20);
    }
}

/**
 * Spedizione espressa:
 * più costosa, ma più veloce.
 */
final class SpedizioneEspressa implements StrategiaSpedizione
{
    public function calcolaCosto(float $peso, float $totaleOrdine): float
    {
        return 12.00 + ($peso * 2.50);
    }
}

/**
 * Spedizione gratuita per ordini sopra una soglia.
 */
final class SpedizioneGratuita implements StrategiaSpedizione
{
    public function calcolaCosto(float $peso, float $totaleOrdine): float
    {
        if ($totaleOrdine >= 80.00) {
            return 0.00;
        }

        // Se la soglia non è raggiunta, applichiamo
        // un costo standard ridotto.
        return 3.50;
    }
}

/**
 * Contesto che utilizza una strategia.
 * Non conosce i dettagli dei singoli algoritmi.
 */
final class CalcolatoreSpedizione
{
    private StrategiaSpedizione $strategia;

    public function __construct(StrategiaSpedizione $strategia)
    {
        $this->strategia = $strategia;
    }

    /**
     * Permette di cambiare strategia anche dopo
     * la creazione dell´oggetto.
     */
    public function impostaStrategia(StrategiaSpedizione $strategia): void
    {
        $this->strategia = $strategia;
    }

    public function calcola(float $peso, float $totaleOrdine): float
    {
        if ($peso <= 0) {
            throw new InvalidArgumentException(
                ´Il peso deve essere maggiore di zero.´
            );
        }

        if ($totaleOrdine < 0) {
            throw new InvalidArgumentException(
                ´Il totale dell’ordine non può essere negativo.´
            );
        }

        return $this->strategia->calcolaCosto($peso, $totaleOrdine);
    }
}

// Esempio di utilizzo
$calcolatore = new CalcolatoreSpedizione(
    new SpedizioneStandard()
);

$costo = $calcolatore->calcola(4.5, 55.00);

echo ´Costo standard: € ´ . number_format($costo, 2, ´,´, ´.´) . PHP_EOL;

// Cambiamo algoritmo senza modificare CalcolatoreSpedizione
$calcolatore->impostaStrategia(new SpedizioneEspressa());

$costo = $calcolatore->calcola(4.5, 55.00);

echo ´Costo espresso: € ´ . number_format($costo, 2, ´,´, ´.´) . PHP_EOL;

$calcolatore->impostaStrategia(new SpedizioneGratuita());

$costo = $calcolatore->calcola(4.5, 100.00);

echo ´Costo con promozione: € ´ . number_format($costo, 2, ´,´, ´.´) . PHP_EOL;

Spiegazione

1. L’interfaccia della strategia

StrategiaSpedizione definisce il contratto che ogni algoritmo deve rispettare. Tutte le strategie devono quindi implementare il metodo calcolaCosto(), ricevendo il peso del pacco e il totale dell’ordine.

L’interfaccia non contiene la logica concreta. Stabilisce soltanto quali operazioni sono disponibili. Questo permette a CalcolatoreSpedizione di lavorare con qualsiasi strategia compatibile.

2. Le strategie concrete

Le classi SpedizioneStandard, SpedizioneEspressa e SpedizioneGratuita rappresentano algoritmi diversi. Ognuna contiene esclusivamente la logica relativa al proprio calcolo.

Questa separazione è importante perché evita di concentrare regole differenti in una sola classe. Se domani fosse necessario aggiungere una spedizione internazionale, basterebbe creare una nuova classe che implementa StrategiaSpedizione.

3. Il contesto

CalcolatoreSpedizione è il contesto del pattern. Riceve una strategia nel costruttore e la utilizza nel metodo calcola(). Non sa se il calcolo è standard, espresso o promozionale: delega semplicemente il lavoro all’oggetto corretto.

Il metodo impostaStrategia() consente di cambiare comportamento durante l’esecuzione. Questo è utile, ad esempio, quando un utente seleziona un metodo di consegna da un modulo web.

4. Perché usare final?

Le classi concrete sono dichiarate final perché non sono progettate per essere estese tramite ereditarietà. Il loro comportamento è definito dall’interfaccia e può essere sostituito creando un’altra strategia. In questo caso la composizione risulta più chiara dell’ereditarietà.

Best practice

  • Mantieni ogni strategia focalizzata: una classe dovrebbe occuparsi di un solo algoritmo o di una sola regola di business.
  • Usa nomi espliciti: nomi come SpedizioneEspressa comunicano immediatamente lo scopo della classe.
  • Definisci contratti stabili: l’interfaccia dovrebbe contenere solo i metodi necessari a tutte le strategie.
  • Valida gli input nel punto corretto: il contesto può controllare dati comuni, come peso e totale dell’ordine, prima di delegare il calcolo.
  • Evita strategie con troppe responsabilità: se una strategia gestisce anche database, email e formattazione, è probabilmente troppo complessa.
  • Preferisci il cambio di strategia alla proliferazione di condizioni: aggiungere una classe è spesso più sicuro che modificare un lungo switch.
  • Scrivi test indipendenti: ogni strategia può essere testata separatamente con pochi casi significativi.

Il Pattern Strategy è particolarmente utile per sconti, metodi di pagamento, esportazione di documenti, sistemi di autenticazione e regole di tariffazione. Non conviene però usarlo per ogni piccola variazione: se esistono soltanto due righe di logica stabile, una classe separata potrebbe aggiungere complessità inutile.

Riepilogo

Il Pattern Strategy permette di separare algoritmi alternativi in classi indipendenti. Un’interfaccia definisce il contratto, le strategie concrete implementano i comportamenti e una classe di contesto utilizza la strategia senza conoscerne i dettagli.

Il risultato è un codice più modulare, testabile ed estendibile. Per aggiungere una nuova modalità di spedizione non è necessario modificare il calcolatore esistente: si crea una nuova classe compatibile con l’interfaccia. Questo riduce le condizioni condizionali e rende più semplice applicare il principio Open/Closed, secondo cui le classi dovrebbero essere aperte all’estensione ma chiuse alla modifica.

Approfondisci con risorse ufficiali

SHARE