Il problema: azioni che vivono solo nel momento
Un controller riceve una richiesta POST e chiama direttamente $order->cancel(). Funziona. Ma cosa succede quando servono requisiti aggiuntivi? "Vogliamo un log di chi ha cancellato l'ordine e quando." Aggiungi una riga di logging. "Vogliamo poter annullare la cancellazione." Adesso serve memorizzare lo stato precedente. "Vogliamo che le cancellazioni passino per un'approvazione." Serve una coda. "Vogliamo poter ripetere l'operazione se fallisce." Serve un sistema di retry.
Ogni requisito aggiuntivo aggiunge complessita al controller perche l'azione — cancellare un ordine — vive solo nel flusso di esecuzione: non e un oggetto che puoi manipolare, accodare, serializzare o annullare. Il Command Pattern risolve questo problema trasformando l'azione in un oggetto con un proprio stato, una propria identita e un ciclo di vita.
Cos'e il Command Pattern: definizione formale
Il Gang of Four definisce il Command come un pattern comportamentale che "incapsula una richiesta come oggetto, permettendo di parametrizzare i client con richieste diverse, accodare o loggare le richieste, e supportare operazioni annullabili". La struttura prevede cinque attori:
- Command: l'interfaccia con il metodo
execute() - ConcreteCommand: implementa
execute()con la logica specifica, mantenendo un riferimento al Receiver - Receiver: l'oggetto che sa effettivamente come eseguire l'operazione
- Invoker: chiede al Command di eseguire la richiesta, senza sapere cosa fa
- Client: crea il ConcreteCommand e lo associa al Receiver
Esempio teorico: un sistema di ordini con undo
Immagina un gestionale dove gli operatori gestiscono ordini. Ogni azione e un Command:
CancelOrderCommand: riceve l'ordine nel costruttore. Il metodoexecute()salva lo stato corrente ($this->previousStatus = $order->getStatus()), poi chiama$order->cancel(). Il metodoundo()ripristina lo stato precedente:$order->setStatus($this->previousStatus).ShipOrderCommand: segna l'ordine come spedito, registra la data di spedizione. L'undo()annulla la spedizione e ripristina lo stato precedente.RefundOrderCommand: esegue il rimborso. L'undo()annulla il rimborso (se ancora possibile).ChangeAddressCommand: aggiorna l'indirizzo di spedizione. L'undo()ripristina l'indirizzo precedente.
L'Invoker mantiene una history stack: ogni Command eseguito viene pushato nello stack. L'operazione "annulla" fa pop dall'stack e chiama undo() sull'ultimo Command. L'operazione "ripeti" fa la stessa cosa con uno stack di redo. Il sistema di undo/redo e generico e funziona con qualsiasi Command, senza conoscerne il tipo concreto.
Command Pattern nella CLI
In Soft PHP MVC, i comandi CLI sono un'applicazione naturale del Command Pattern. Ogni comando implementa una classe con un metodo handle(Input, Output): int. L'attributo #[CliCommand] dichiara il nome e la descrizione. Il Kernel (l'Invoker) riceve il nome del comando dalla linea di comando, cerca il ConcreteCommand corrispondente, e chiama handle(). Il Kernel non sa cosa fa il comando: sa solo che ha un metodo handle() e restituisce un exit code.
Questa struttura permette di aggiungere comandi senza toccare il Kernel: basta creare una nuova classe con l'attributo #[CliCommand] e l'auto-discovery la trova. Il Command Pattern e il motivo per cui 26 comandi diversi — da migrate:fresh a key:generate — condividono la stessa infrastruttura senza conflitti.
Esempio teorico: job queue asincrona
Il Command Pattern e alla base di ogni sistema di code (queue). Un job e un Command serializzato: contiene tutti i dati necessari per eseguire l'operazione, ma l'esecuzione e differita nel tempo. Il flusso e:
- Il controller crea il Command:
$command = new SendInvoiceEmail($orderId, $customerEmail) - Il dispatcher (Invoker) serializza il Command e lo mette in coda (Redis, database, RabbitMQ)
- Un worker legge il Command dalla coda, lo deserializza e chiama
execute() - Se l'esecuzione fallisce, il Command puo essere rimesso in coda per un retry
- Se fallisce N volte, viene spostato in una dead letter queue per analisi manuale
Il fatto che l'azione sia un oggetto serializzabile rende possibile tutto questo: accodare, ritardare, ritentare, loggare, monitorare. Un'azione che vive solo come chiamata di metodo non puo essere accodata o ritentata — un Command si.
Command vs Strategy: la differenza
Command e Strategy sono entrambi pattern comportamentali con una struttura simile (un oggetto con un metodo da invocare), ma l'intento e diverso:
- Strategy: rappresenta come fare qualcosa. E intercambiabile: puoi cambiare l'algoritmo a runtime. L'accento e sull'alternativa.
- Command: rappresenta cosa fare. E un'azione reificata con il proprio stato e ciclo di vita. L'accento e sull'operazione.
Uno Strategy cambia il comportamento di un contesto. Un Command e un'azione che puoi manipolare come dato: accodare, serializzare, annullare, ripetere. La differenza e sottile ma fondamentale per scegliere il pattern giusto.
Quando usare il Command Pattern
- Usa il Command quando vuoi supportare undo/redo: ogni Command salva lo stato precedente e sa come ripristinarlo
- Usa il Command quando le azioni devono essere accodate, ritardate o eseguite in batch
- Usa il Command quando vuoi loggare ogni azione con i suoi parametri per auditing o debugging
- Usa il Command quando azioni diverse devono passare per la stessa pipeline (validazione, autorizzazione, logging)
- Non usare il Command per operazioni semplici e immediate dove la complessita aggiuntiva non e giustificata
- Non usare il Command se non hai bisogno di nessuna delle funzionalita che il pattern offre: l'astrazione senza beneficio e solo burocrazia
Il Command Pattern trasforma le azioni da verbi effimeri a sostantivi permanenti. Questa trasformazione apre possibilita che prima non esistevano: undo, queue, replay, auditing. E uno dei pattern con il miglior rapporto tra complessita introdotta e funzionalita abilitate.