Strategy Pattern: quando il comportamento diventa intercambiabile — articolo

> Strategy Pattern: quando il comportamento diventa intercambiabile

Pillar article sullo Strategy Pattern applicato nel framework: driver di cache, grammar SQL multi-database e il principio Open/Closed nella pratica.

Luigi Iadicola
~6 min lettura
#PHP 8.4 #ORM #Architettura #Design Pattern
Strategy Pattern: quando il comportamento diventa intercambiabile
Strategy Pattern: quando il comportamento diventa intercambiabile

Il problema: un if che cresce all'infinito

Ogni sviluppatore PHP ha incontrato quel momento: un metodo con un if/elseif che distingue tra MySQL, PostgreSQL, SQLite e MariaDB per generare la sintassi corretta. All'inizio sono quattro righe, poi diventano quaranta, poi quattrocento. Ogni nuovo driver richiede di toccare lo stesso metodo, violando il principio Open/Closed in modo sempre piu evidente. Il risultato e un metodo che nessuno vuole toccare, un metodo che tutti temono di rompere.

Lo Strategy Pattern propone una soluzione elegante: invece di decidere dentro il metodo quale comportamento adottare, si delega la decisione fuori. Ogni strategia — MysqlSchemaGrammar, PostgresSchemaGrammar, SqliteSchemaGrammar — implementa la stessa interfaccia ma con la propria logica. Il contesto non sa quale grammar sta usando, e non deve saperlo. Questa ignoranza deliberata e il cuore del pattern.

Cos'e lo Strategy Pattern: definizione e struttura

Il Gang of Four definisce lo Strategy come un modo per "definire una famiglia di algoritmi, incapsularli, e renderli intercambiabili". La struttura prevede tre attori: il Context, che mantiene un riferimento alla strategia corrente; l'interfaccia Strategy, che dichiara il contratto comune; e le ConcreteStrategy, che implementano l'algoritmo specifico. Il Context delega l'esecuzione alla strategia senza conoscerne il tipo concreto.

In termini PHP, il Context e una classe che riceve nel costruttore (o via setter) un oggetto che implementa un'interfaccia. I metodi del Context chiamano i metodi dell'interfaccia, e il polimorfismo fa il resto. Non serve nessuna magia: e OOP nella sua forma piu pura, interfacce e implementazioni. La potenza non sta nella complessita del meccanismo, ma nella disciplina che impone.

Le grammar SQL come strategie concrete

In Soft PHP MVC, il query builder accetta una grammar che traduce le operazioni astratte — select, where, join, orderBy — nella sintassi specifica del database. MysqlSchemaGrammar produce LIMIT ?, ? per la paginazione; PostgresSchemaGrammar produce LIMIT ? OFFSET ?. La differenza e nel dialetto, non nella logica. Ogni grammar conosce le peculiarita del proprio database: i tipi di dato supportati, la sintassi per le colonne auto-incrementali, il modo di gestire le migrazioni di schema.

Aggiungere MariaDB ha significato creare MariadbSchemaGrammar che estende MysqlSchemaGrammar, sovrascrivendo solo cio che diverge: la collation di default, il supporto a RETURNING, la gestione delle colonne JSON. Nessun file esistente e stato modificato — il pattern ha funzionato esattamente come prometteva. Questo e il principio Open/Closed nella pratica: aperto all'estensione, chiuso alla modifica.

Come funziona la selezione della grammar

La scelta della grammar avviene in fase di boot: il framework legge il driver configurato nel file .env, istanzia la grammar corrispondente tramite un Factory Method (altro design pattern che collabora con lo Strategy), e la inietta nel query builder. Da quel momento in poi, ogni query viene compilata dalla grammar corretta senza che il codice applicativo debba preoccuparsi di quale database e in uso. Il cambio di database diventa un cambio di configurazione, non un cambio di codice.

I driver di cache: stessa interfaccia, storie diverse

Il sistema di cache di Soft PHP MVC usa lo stesso approccio Strategy: FileCache, DatabaseCache, NullCache. Tutti implementano gli stessi metodi — get(), put(), forget(), flush() — ma ciascuno con la propria semantica di storage. Il FileCache serializza i dati su disco, il DatabaseCache li persiste in una tabella dedicata, il NullCache non fa assolutamente nulla.

Il NullCache e particolarmente istruttivo: una strategia che non fa nulla, utile nei test e in sviluppo dove la cache sarebbe solo d'intralcio. E un'applicazione del Null Object Pattern dentro lo Strategy — due pattern che si compongono naturalmente. Il codice applicativo non ha bisogno di verificare if ($cache !== null) prima di ogni chiamata: il NullCache risponde a ogni metodo con un no-op, mantenendo l'interfaccia uniforme.

Esempio concreto: il passaggio da FileCache a DatabaseCache

In produzione, il framework e passato da FileCache a DatabaseCache per migliorare le performance su un hosting con I/O disco lento. Il cambiamento ha richiesto esattamente una riga: il valore del parametro CACHE_DRIVER nel file di configurazione. Nessun controller modificato, nessun service riscritto, nessun test aggiornato. Il codice applicativo continuava a chiamare Cache::get('key') come prima — solo la strategia sottostante era cambiata.

Strategy e principi SOLID

Lo Strategy Pattern e una realizzazione concreta di almeno tre principi SOLID. Il Single Responsibility Principle: ogni strategia ha una sola ragione per cambiare (la logica specifica del suo algoritmo). L'Open/Closed Principle: il contesto e aperto all'estensione (nuove strategie) ma chiuso alla modifica. Il Dependency Inversion Principle: il contesto dipende dall'astrazione (l'interfaccia), non dalle implementazioni concrete.

Questa convergenza non e casuale. I design pattern e i principi SOLID sono due facce della stessa medaglia: i principi descrivono cosa vogliamo ottenere (basso accoppiamento, alta coesione, estensibilita), i pattern descrivono come ottenerlo. Lo Strategy e il "come" del principio Open/Closed applicato alla variazione di comportamento.

Quando usare Strategy e quando no

Lo Strategy Pattern non e la risposta a ogni if. Se hai due rami e non ne prevedi un terzo, un semplice condizionale e piu chiaro di una gerarchia di classi. Il pattern brilla quando: i comportamenti sono piu di due, crescono nel tempo, e il contesto non deve conoscere i dettagli dell'implementazione. Se ti ritrovi ad aggiungere un nuovo elseif ogni mese, probabilmente hai bisogno di una strategia.

Ecco i segnali che indicano la necessita di uno Strategy:

  • Un switch o if/elseif che cresce ad ogni release
  • Comportamenti alternativi che condividono la stessa firma ma divergono nell'implementazione
  • La necessita di testare ogni variante in isolamento
  • La possibilita che nuove varianti vengano aggiunte da sviluppatori diversi o plugin esterni

Al contrario, non usare Strategy quando i rami sono stabili e pochi, quando l'overhead della creazione di classi separate supera il beneficio, o quando la logica condizionale e semplice e localizzata in un solo punto.

La lezione piu profonda: il comportamento come dato

La lezione piu profonda dello Strategy e che il comportamento puo essere trattato come un dato: qualcosa che si passa, si configura, si sostituisce a runtime. Un metodo che riceve la sua strategia dall'esterno e un metodo libero — libero di essere testato con un mock, di essere esteso senza modifiche, di essere composto con strategie che non esistevano quando e stato scritto.

In PHP 8.4, con le closure e i first-class callable, lo Strategy diventa ancora piu leggero: non serve sempre una classe dedicata, a volte basta un callable che rispetta la firma attesa. Il pattern si adatta al linguaggio, non il contrario. E questa flessibilita — dalla classe formale al callable informale — e cio che rende lo Strategy il pattern piu versatile del catalogo GoF.

altri articoli