Il problema: oggetti pesanti che non servono subito
Hai un modello Article con una relazione comments(). La pagina del blog elenca 20 articoli con titolo, data e autore. Nessuno ha ancora cliccato su un articolo per vederne i commenti, eppure il codice carica tutte le relazioni in anticipo, eseguendo 21 query (una per gli articoli e una per i commenti di ciascuno). E il classico N+1 problem, ma la radice e piu profonda: il problema e che l'oggetto Article espone direttamente i commenti, e chi lo usa non ha modo di decidere quando caricarli.
Il Proxy Pattern risolve questo problema interponendo un oggetto surrogato (il proxy) tra il client e l'oggetto reale. Il proxy implementa la stessa interfaccia dell'oggetto reale, ma controlla quando e come l'oggetto reale viene creato o acceduto. Il client non sa di parlare con un proxy — per lui e lo stesso oggetto di sempre.
Cos'e il Proxy Pattern: definizione e varianti
Il Gang of Four definisce il Proxy come un pattern strutturale che "fornisce un surrogato o segnaposto per un altro oggetto per controllarne l'accesso". La struttura e simile al Decorator — entrambi wrappano un oggetto con la stessa interfaccia — ma l'intento e diverso: il Decorator aggiunge comportamento, il Proxy controlla l'accesso.
Esistono diverse varianti del Proxy, ognuna con uno scopo specifico:
- Virtual Proxy (Lazy Loading): ritarda la creazione dell'oggetto reale fino al primo utilizzo. Utile per oggetti pesanti che potrebbero non servire mai.
- Protection Proxy: verifica i permessi prima di delegare la chiamata. L'oggetto reale non sa nulla di autorizzazione — il proxy la gestisce esternamente.
- Cache Proxy: memorizza il risultato dell'oggetto reale e lo restituisce direttamente alle chiamate successive, evitando ricalcoli costosi.
- Remote Proxy: rappresenta un oggetto che vive su un altro server o processo. Il client chiama metodi locali, il proxy traduce in chiamate di rete.
- Logging Proxy: registra ogni chiamata all'oggetto reale per debugging o auditing senza che il codice di business contenga logica di logging.
Esempio teorico: lazy loading delle relazioni ORM
Consideriamo un ORM che carica un Article dal database. L'articolo ha una proprietà $comments che dovrebbe contenere un array di oggetti Comment. Con il caricamento eager, tutti i commenti vengono caricati immediatamente. Con il Proxy Pattern, la proprietà contiene inizialmente un CommentCollectionProxy.
Il CommentCollectionProxy implementa la stessa interfaccia di una collection (es. Countable, IteratorAggregate, ArrayAccess). Internamente mantiene un flag $loaded = false e un riferimento al query necessario per caricare i dati. Quando qualcuno chiama count() o itera la collection, il proxy esegue la query, memorizza il risultato e delega la chiamata alla collection reale. Alle chiamate successive il dato e gia in memoria.
Il vantaggio: N+1 diventa 1+0
Se la pagina del blog mostra solo titolo e data, nessun proxy viene mai risolto: zero query per i commenti. Se l'utente apre un articolo specifico, il proxy di quell'articolo risolve i commenti con una singola query. Il pattern trasforma l'N+1 problem in un "1 + solo quello che serve", senza che il codice del controller cambi di una riga.
Esempio teorico: Protection Proxy per le API
Immagina un servizio ReportService con un metodo generateFinancialReport(). Questo metodo puo essere chiamato solo da utenti con ruolo "admin" o "finance". Invece di aggiungere il controllo dei permessi dentro il service (violando SRP), crei un AuthorizedReportServiceProxy:
- Il proxy riceve il
ReportServicereale e unAuthorizationCheckernel costruttore - Prima di delegare
generateFinancialReport(), verifica che l'utente corrente abbia il permessoreport.financial.generate - Se il permesso manca, lancia una
ForbiddenExceptionsenza mai chiamare il service reale - Il
ReportServiceresta pulito: genera report, punto. Non sa nulla di permessi.
Esempio teorico: Cache Proxy per chiamate API esterne
Un WeatherService chiama un'API esterna per ottenere le previsioni meteo. Ogni chiamata costa 200ms di latenza. Un CachedWeatherProxy wrappa il service: alla prima chiamata delega al service reale e salva il risultato in cache con un TTL di 30 minuti. Alle chiamate successive restituisce il dato dalla cache in meno di 1ms. Il controller non sa se sta ricevendo dati freschi o cachati — e non deve saperlo.
Proxy vs Decorator: la differenza sottile
Proxy e Decorator hanno la stessa struttura: un wrapper con la stessa interfaccia dell'oggetto wrappato. La differenza e nell'intento:
- Decorator: aggiunge comportamento nuovo. Il focus e sull'estensione delle funzionalita.
- Proxy: controlla l'accesso all'oggetto esistente. Il focus e su quando, come e se l'oggetto viene usato.
In pratica: un LoggingDecorator aggiunge logging come funzionalita extra. Un LazyProxy controlla quando l'oggetto viene creato. Un ProtectionProxy controlla se l'oggetto viene acceduto. La distinzione e concettuale, non strutturale — ed e importante per comunicare l'intento del codice a chi lo legge.
Quando usare il Proxy
- Lazy Loading: quando creare l'oggetto e costoso e potrebbe non servire (relazioni ORM, connessioni, file pesanti)
- Access Control: quando vuoi separare la logica di autorizzazione dalla logica di business
- Caching: quando il risultato e costoso da calcolare ma stabile nel tempo
- Remote Access: quando l'oggetto reale vive su un altro server e vuoi nascondere la complessita di rete
- Non usare il Proxy se l'oggetto e leggero e viene sempre usato: il proxy aggiunge indirezione senza beneficio
- Non usare il Proxy se la trasparenza diventa un problema: a volte e meglio rendere esplicito che un'operazione e lazy o cachata
Il Proxy Pattern e uno dei pattern piu invisibili — quando funziona bene, nessuno sa che c'e. E questa e la sua forza: il codice di business resta pulito, e il controllo dell'accesso, del caching o del caricamento e gestito in un unico punto, testabile e modificabile indipendentemente.