Introduzione
Il Design Pattern Facade, o Facciata, è un pattern strutturale che offre un’interfaccia semplice per utilizzare un insieme di classi più complesse. Invece di obbligare il codice client a conoscere ogni dettaglio interno di un sottosistema, la Facade espone pochi metodi chiari e coordina le operazioni necessarie.
Questo pattern è particolarmente utile quando un’applicazione deve gestire workflow composti da più passaggi. Un esempio realistico è la pubblicazione di un ordine: occorre verificare i dati del cliente, controllare il magazzino, calcolare il totale, effettuare il pagamento e infine inviare una notifica. Senza una Facade, il controller potrebbe contenere molte dipendenze e una quantità eccessiva di logica applicativa.
La Facade non elimina le classi del sottosistema e non impedisce di utilizzarle direttamente quando necessario. Il suo scopo è fornire un punto di accesso più semplice per gli scenari più comuni, migliorando leggibilità, manutenzione e testabilità del codice.
Codice completo
Nel seguente esempio costruiremo una Facade per completare un ordine online. Le classi interne rappresentano servizi indipendenti: gestione del magazzino, pagamento e invio delle notifiche.
<?php
declare(strict_types=1);
/**
* Servizio che controlla e aggiorna la disponibilità dei prodotti.
*/
final class InventoryService
{
/**
* @param array<string, int> $stock
*/
public function __construct(
private array $stock
) {
}
public function isAvailable(string $productId, int $quantity): bool
{
return ($this->stock[$productId] ?? 0) >= $quantity;
}
public function reserve(string $productId, int $quantity): void
{
if (!$this->isAvailable($productId, $quantity)) {
throw new RuntimeException(
"Prodotto non disponibile: {$productId}"
);
}
$this->stock[$productId] -= $quantity;
}
}
/**
* Servizio responsabile dei pagamenti.
*/
final class PaymentService
{
public function charge(string $customerEmail, float $amount): string
{
if ($amount <= 0) {
throw new InvalidArgumentException(
´L’importo deve essere maggiore di zero.´
);
}
// In un progetto reale qui verrebbe chiamato un provider esterno.
return ´PAY-´ . strtoupper(bin2hex(random_bytes(4)));
}
}
/**
* Servizio per le comunicazioni al cliente.
*/
final class NotificationService
{
public function sendOrderConfirmation(
string $customerEmail,
string $orderId,
string $paymentId
): void {
echo "Email inviata a {$customerEmail}. ";
echo "Ordine {$orderId}, pagamento {$paymentId}." . PHP_EOL;
}
}
/**
* Facade: nasconde il workflow complesso al codice client.
*/
final class OrderFacade
{
public function __construct(
private InventoryService $inventory,
private PaymentService $payment,
private NotificationService $notification
) {
}
/**
* @param array<string, mixed> $order
*/
public function placeOrder(array $order): string
{
$productId = (string) $order[´product_id´];
$quantity = (int) $order[´quantity´];
$email = (string) $order[´customer_email´];
$amount = (float) $order[´amount´];
$orderId = (string) $order[´order_id´];
if (!$this->inventory->isAvailable($productId, $quantity)) {
throw new RuntimeException(
´Impossibile completare l’ordine: prodotto esaurito.´
);
}
// La prenotazione avviene prima del pagamento.
$this->inventory->reserve($productId, $quantity);
$paymentId = $this->payment->charge($email, $amount);
$this->notification->sendOrderConfirmation(
$email,
$orderId,
$paymentId
);
return $orderId;
}
}
// Codice client: conosce soltanto la Facade.
$inventory = new InventoryService([
´PHP-BOOK´ => 10,
]);
$payment = new PaymentService();
$notification = new NotificationService();
$orderFacade = new OrderFacade(
$inventory,
$payment,
$notification
);
$orderId = $orderFacade->placeOrder([
´order_id´ => ´ORD-1001´,
´product_id´ => ´PHP-BOOK´,
´quantity´ => 2,
´customer_email´ => ´[email protected]´,
´amount´ => 39.90,
]);
echo "Ordine completato: {$orderId}" . PHP_EOL; Spiegazione
Il sottosistema
Le classi InventoryService, PaymentService e NotificationService svolgono responsabilità diverse. Ognuna potrebbe essere utilizzata singolarmente, ma per completare un ordine è necessario coordinarle in una sequenza precisa.
Il controller o lo script principale non dovrebbe occuparsi direttamente di tutti questi passaggi. Se lo facesse, diventerebbe strettamente legato ai dettagli implementativi dei servizi. Inoltre, ogni modifica al processo di acquisto richiederebbe probabilmente interventi in più punti dell’applicazione.
Il ruolo della Facade
OrderFacade espone il metodo placeOrder(). Questo metodo rappresenta un’operazione significativa per il dominio: inserire un ordine. Al suo interno la Facade:
- legge i dati essenziali dell’ordine;
- verifica la disponibilità del prodotto;
- riserva la quantità richiesta;
- esegue il pagamento;
- invia la conferma al cliente;
- restituisce l’identificativo dell’ordine.
Il codice client non deve quindi conoscere l’ordine corretto delle operazioni. Questa è la principale utilità del pattern: centralizzare l’orchestrazione senza esporre inutilmente la complessità.
Facade e responsabilità
Una Facade non dovrebbe diventare un contenitore di ogni regola dell’applicazione. Deve coordinare i servizi, mentre le singole regole specifiche restano nelle classi competenti. Per esempio, la disponibilità del prodotto appartiene a InventoryService, mentre il pagamento appartiene a PaymentService.
In un’applicazione reale sarebbe inoltre opportuno usare oggetti dedicati, come OrderData o un DTO, invece di un array generico. L’array rende l’esempio breve, ma non protegge dagli errori di battitura nelle chiavi.
Best practice
- Mantenere una Facade focalizzata: una Facade dovrebbe rappresentare un’area funzionale coerente, come ordini, report o autenticazione.
- Usare tipi espliciti: dichiarare tipi per parametri, proprietà e valori restituiti rende il contratto più chiaro e riduce gli errori.
- Iniettare le dipendenze: i servizi devono essere forniti al costruttore, così la Facade rimane facilmente sostituibile e testabile.
- Non creare dipendenze globali: evitare chiamate statiche onnipresenti e istanze globali, perché rendono i test più difficili.
- Gestire gli errori: se un passaggio fallisce, valutare transazioni, rollback o procedure di compensazione. Nel caso di un pagamento fallito dopo la prenotazione, il prodotto dovrebbe eventualmente essere nuovamente reso disponibile.
- Non nascondere troppo: la Facade deve semplificare l’uso comune, ma non deve impedire l’accesso diretto ai servizi quando un caso avanzato lo richiede.
- Testare il workflow: i test dovrebbero verificare che i servizi vengano chiamati nell’ordine corretto e che gli errori siano gestiti adeguatamente.
Un’evoluzione naturale dell’esempio consiste nell’introdurre interfacce come PaymentGatewayInterface e NotificationSenderInterface. In questo modo è possibile usare implementazioni reali in produzione e mock o stub nei test automatici.
Riepilogo
Il Design Pattern Facade fornisce un punto di accesso semplice a un sistema composto da più classi. In PHP è utile per ridurre la complessità nei controller, centralizzare workflow applicativi e separare il codice client dai dettagli del sottosistema.
Nell’esempio, OrderFacade coordina inventario, pagamento e notifiche attraverso un unico metodo: placeOrder(). Le responsabilità rimangono distribuite tra servizi specializzati, mentre la Facade gestisce il flusso complessivo.
Il pattern è una buona scelta quando il codice client conosce troppi dettagli o deve ripetere sempre la stessa sequenza di operazioni. Non è però una soluzione per accorpare indiscriminatamente tutta la logica in una classe gigantesca: una Facade efficace deve essere specifica, leggibile e con responsabilità ben definite.
