Il problema: classi che creano le proprie dipendenze
Una classe OrderService ha bisogno di un OrderRepository per accedere ai dati e di un Mailer per inviare conferme. L'approccio più istintivo e crearli internamente: $this->repository = new PgsqlOrderRepository() e $this->mailer = new SmtpMailer(). Sembra semplice, ma ha conseguenze profonde.
Primo: OrderService e accoppiato al cemento. Sa che il repository e PostgreSQL e il mailer e SMTP. Se vuoi usare MySQL o un mock per i test, devi modificare OrderService. Secondo: non puoi testare OrderService senza un database e un server SMTP reali. Terzo: se PgsqlOrderRepository a sua volta crea le proprie dipendenze, hai una catena di creazione che nessuno controlla centralmente. Ogni classe e responsabile di crearsi il proprio mondo — e questo rende il sistema rigido, fragile e difficile da testare.
Cos'e la Dependency Injection
La Dependency Injection (DI) e un principio semplice: una classe non crea le proprie dipendenze, le riceve dall'esterno. Invece di $this->repository = new PgsqlOrderRepository(), il costruttore dichiara: public function __construct(private readonly OrderRepositoryInterface $repository, private readonly MailerInterface $mailer). Chi crea OrderService decide quale implementazione passare.
La DI non e un framework, non e un container, non e una libreria. E un principio di design: le dipendenze vengono iniettate, non create internamente. Esistono tre forme:
- Constructor Injection: le dipendenze vengono passate nel costruttore. E la forma preferita perché rende le dipendenze esplicite e immutabili: la classe non può esistere senza le sue dipendenze.
- Setter Injection: le dipendenze vengono passate tramite metodi setter. Utile per dipendenze opzionali, ma rende possibile l'uso della classe in uno stato incompleto.
- Method Injection: la dipendenza viene passata come parametro del metodo che la usa. Utile quando la dipendenza varia ad ogni chiamata.
Esempio teorico: un servizio ordini testabile
Senza DI, testare OrderService richiede un database reale con dati di test e un server SMTP (o un mock complesso). Con DI, il test crea implementazioni fake:
-
InMemoryOrderRepository: implementaOrderRepositoryInterfacecon un array PHP.save()aggiunge all'array.findById()cerca nell'array. Zero database, zero latenza. -
SpyMailer: implementaMailerInterfaceregistrando ogni email in un array interno. Il test verifica:$this->assertCount(1, $mailer->getSentEmails()). Zero SMTP, zero email reali.
Il test diventa: $service = new OrderService(new InMemoryOrderRepository(), new SpyMailer()); $service->cancel($orderId); $this->assertTrue(...). E un test unitario puro: testa la logica di cancellazione senza dipendenze esterne, senza database, senza rete. Gira in millisecondi.
Il test dice la verità sul design
Se una classe e difficile da testare, quasi sempre e perché le dipendenze non sono iniettate. Il test e il primo "utente" della classe, e se l'utente fa fatica, il design ha un problema. La DI non e un requisito per il testing — e un requisito per il buon design, e il testing e la prova empirica.
Il Container: automazione della DI
Con la DI manuale, chi crea OrderService deve conoscere tutte le sue dipendenze e le dipendenze delle dipendenze. In un'applicazione con 50 classi, questa catena diventa lunga e ripetitiva. Il DI Container automatizza la risoluzione:
- Registri le associazioni:
OrderRepositoryInterface→PgsqlOrderRepository - Il container legge il costruttore di
OrderServicevia Reflection - Vede che serve un
OrderRepositoryInterface, lo risolve aPgsqlOrderRepository - Crea ricorsivamente tutte le dipendenze necessarie
- Restituisce un
OrderServicecompletamente assemblato
Il container e un lusso, non una necessita. Per progetti piccoli, la DI manuale (creare gli oggetti nel bootstrap) e perfettamente valida. Per progetti con decine di servizi e dipendenze profonde, il container elimina il boilerplate di assemblaggio.
DI e il Dependency Inversion Principle
La Dependency Injection e la tecnica. Il Dependency Inversion Principle (la D di SOLID) e il principio: "i moduli di alto livello non dovrebbero dipendere da moduli di basso livello. Entrambi dovrebbero dipendere da astrazioni." La DI e il meccanismo pratico per realizzare questo principio.
Senza DI, OrderService (alto livello) dipende da PgsqlOrderRepository (basso livello). Con DI, OrderService dipende da OrderRepositoryInterface (astrazione), e PgsqlOrderRepository implementa quell'astrazione. La direzione della dipendenza si e invertita: il modulo di basso livello dipende dall'astrazione definita dal modulo di alto livello, non viceversa.
Errori comuni con la DI
- Service Locator: iniettare il container stesso e usarlo per risolvere le dipendenze a runtime. Nasconde le dipendenze reali e rende il codice opaco. Le dipendenze devono essere nel costruttore, non risolte a runtime.
- Troppe dipendenze: se un costruttore ha 8 parametri, il problema non e la DI — e che la classe ha troppe responsabilità. La DI rende visibile il problema, non lo causa.
-
Interfacce inutili: creare un'interfaccia per ogni classe "perché la DI lo richiede" e cargo cult. L'interfaccia serve quando ci sono (o ci saranno realisticamente) multiple implementazioni. Una
UserRepositoryInterfacecon una sola implementazione e burocrazia — a meno che non serva per il testing, e allora ha un motivo concreto. - Auto-wiring magico: affidarsi ciecamente alla risoluzione automatica del container rende invisibili le dipendenze. E comodo ma va bilanciato con la leggibilità.
Quando usare la Dependency Injection
- Sempre per le dipendenze di servizio (repository, mailer, logger, cache): sono le dipendenze che cambiano tra ambienti (produzione, test, staging)
- Sempre quando vuoi che la classe sia testabile con mock o fake
-
Non necessaria per value objects e DTO: creare un
new Money(100, 'EUR')internamente e perfettamente valido - Non necessaria per factory di oggetti semplici: non serve iniettare una factory se la creazione e banale
La Dependency Injection non e un pattern opzionale da adottare quando si diventa esperti: e il fondamento che rende possibili tutti gli altri pattern. Strategy, Repository, Observer, Decorator — tutti funzionano perché le dipendenze sono iniettate, non create internamente. Senza DI, i pattern diventano rigidi e non sostituibili. Con DI, diventano componenti intercambiabili di un sistema flessibile.
Top comments (0)