Decorator Pattern: aggiungere comportamento senza toccare il codice esistente — articolo

> Decorator Pattern: aggiungere comportamento senza toccare il codice esistente

Pillar article sul Decorator Pattern: come wrappare oggetti per estenderne le funzionalita senza modificare le classi originali, con esempi pratici in PHP.

Luigi Iadicola
~5 min lettura
#Middleware #Architettura #Design Pattern #SOLID
Decorator Pattern: aggiungere comportamento senza toccare il codice esistente
Decorator Pattern: aggiungere comportamento senza toccare il codice esistente

Il problema: estendere senza ereditare

Immagina di avere un servizio che invia notifiche via email. Funziona perfettamente. Poi arriva la richiesta: "vogliamo anche un log di ogni notifica inviata". La tentazione immediata e modificare la classe esistente, aggiungendo il logging dentro il metodo send(). Ma questo viola il principio di responsabilita singola: la classe che invia email non dovrebbe occuparsi di logging. E se domani servisse anche il caching? E la validazione? Ogni nuova responsabilita si accumulera nella stessa classe fino a renderla ingestibile.

L'ereditarieta sembra una soluzione: LoggingEmailNotifier extends EmailNotifier. Ma l'ereditarieta crea una gerarchia rigida. Se vuoi logging + caching, serve una nuova sottoclasse. Logging + caching + rate limiting? Un'altra ancora. Il numero di combinazioni esplode esponenzialmente. Questo problema ha un nome preciso nella letteratura del software: class explosion, e il Decorator Pattern e la risposta elegante.

Cos'e il Decorator Pattern: definizione formale

Il Gang of Four definisce il Decorator come un pattern strutturale che "attacca responsabilita aggiuntive a un oggetto in modo dinamico, fornendo un'alternativa flessibile alla sottoclasse per estendere le funzionalita". Il concetto e semplice: il decorator implementa la stessa interfaccia dell'oggetto che wrappa, delega le chiamate all'oggetto originale, e aggiunge il proprio comportamento prima, dopo, o attorno alla delega.

La struttura prevede quattro attori: il Component (l'interfaccia comune), il ConcreteComponent (l'implementazione base), il Decorator (la classe astratta che mantiene un riferimento al Component wrappato) e i ConcreteDecorator (le implementazioni che aggiungono comportamento specifico). Il client non distingue tra l'oggetto originale e quello decorato: entrambi rispettano lo stesso contratto.

Esempio teorico: un sistema di notifiche componibile

Consideriamo un'interfaccia NotifierInterface con un metodo send(string $message): void. L'implementazione base EmailNotifier invia l'email. Poi creiamo i decorator:

  • LoggingDecorator: riceve un NotifierInterface nel costruttore, chiama $this->notifier->send($message) e poi logga l'invio. Il logging e aggiunto senza toccare EmailNotifier.
  • RateLimitDecorator: prima di delegare la chiamata, controlla se il limite di invii e stato raggiunto. Se si, lancia un'eccezione. Se no, delega e incrementa il contatore.
  • RetryDecorator: wrappa la chiamata in un try/catch e ritenta fino a N volte in caso di fallimento. Aggiunge resilienza senza che il notifier base sappia nulla di retry.
  • EncryptionDecorator: prima di delegare, cripta il messaggio. Il notifier base riceve un messaggio gia criptato senza saperlo.

La composizione avviene a runtime: $notifier = new LoggingDecorator(new RateLimitDecorator(new RetryDecorator(new EmailNotifier()))). L'ordine conta: il logging avvolge il rate limiting che avvolge il retry che avvolge l'invio. Puoi ricombinare questi decorator in qualsiasi ordine senza creare nuove classi.

Perche la composizione batte l'ereditarieta

Con 4 decorator e la necessita di combinarne qualsiasi sottoinsieme, l'ereditarieta richiederebbe 15 sottoclassi (tutte le combinazioni possibili). Con il Decorator ne servono 4: una per responsabilita. Ogni nuovo decorator si compone con tutti gli esistenti senza modificare nulla. Questa e la potenza della composizione rispetto all'ereditarieta, uno dei principi fondamentali della programmazione ad oggetti moderna.

Decorator nel mondo reale: middleware HTTP

Se hai lavorato con framework PHP moderni, hai gia usato il Decorator Pattern senza saperlo. I middleware HTTP sono decorator: ogni middleware riceve la request, puo modificarla, delega al middleware successivo (l'oggetto wrappato), e puo modificare la response al ritorno. La pipeline di middleware e una catena di decorator dove ogni anello aggiunge un comportamento: autenticazione, CORS, rate limiting, logging.

In Soft PHP MVC, il MiddlewarePipeline funziona esattamente cosi. Il CsrfMiddleware decora la request aggiungendo la verifica del token. Il RateLimitMiddleware decora aggiungendo il controllo delle richieste per IP. Il CorsMiddleware decora la response aggiungendo gli header necessari. Nessuno di questi middleware conosce gli altri: ognuno fa una cosa sola e la fa bene.

Decorator Pattern e stream I/O

Un altro esempio classico e l'I/O. Immagina un'interfaccia StreamInterface con metodi read() e write(). L'implementazione base FileStream legge e scrive su file. Un BufferedStream wrappa qualsiasi stream aggiungendo un buffer in memoria per ridurre le operazioni disco. Un CompressedStream wrappa qualsiasi stream aggiungendo compressione gzip al volo. Un EncryptedStream aggiunge crittografia.

La composizione new EncryptedStream(new CompressedStream(new BufferedStream(new FileStream('data.bin')))) crea uno stream che scrive su file, con buffer, compressione e crittografia, senza che nessuna di queste classi conosca le altre. Ogni layer e indipendente, testabile, e riutilizzabile in contesti diversi.

Quando usare il Decorator

  • Usa il Decorator quando vuoi aggiungere responsabilita a oggetti singoli senza influenzare altri oggetti della stessa classe
  • Usa il Decorator quando l'ereditarieta porta a un'esplosione di sottoclassi per coprire tutte le combinazioni
  • Usa il Decorator quando il comportamento aggiuntivo deve essere componibile e l'ordine puo variare
  • Non usare il Decorator se il comportamento aggiuntivo e sempre lo stesso e non cambia mai: in quel caso l'ereditarieta semplice e sufficiente
  • Non usare il Decorator se la catena diventa troppo profonda e il debugging diventa impossibile: a quel punto considera il middleware pattern con una pipeline esplicita

Decorator e principi SOLID

Il Decorator e uno dei pattern che meglio incarnano i principi SOLID. Rispetta il Single Responsibility Principle perche ogni decorator ha una sola ragione di cambiare. Rispetta l'Open/Closed Principle perche estende il comportamento senza modificare il codice esistente. Rispetta il Liskov Substitution Principle perche ogni decorator e sostituibile al component originale. Rispetta il Dependency Inversion Principle perche tutti dipendono dall'astrazione (l'interfaccia), non dalle implementazioni concrete.

Pochi pattern riescono a soddisfare quattro principi SOLID contemporaneamente. Il Decorator ci riesce naturalmente, senza forzature. Questo spiega perche e uno dei pattern piu diffusi e piu utili nel software professionale.

altri articoli