DEV Community

Cover image for Dependency Injection: il pattern che rende tutto il resto possibile
Dev-Iadicola
Dev-Iadicola

Posted on • Originally published at iadicola.it

Dependency Injection: il pattern che rende tutto il resto possibile

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: implementa OrderRepositoryInterface con un array PHP. save() aggiunge all'array. findById() cerca nell'array. Zero database, zero latenza.
  • SpyMailer: implementa MailerInterface registrando 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: OrderRepositoryInterfacePgsqlOrderRepository
  • Il container legge il costruttore di OrderService via Reflection
  • Vede che serve un OrderRepositoryInterface, lo risolve a PgsqlOrderRepository
  • Crea ricorsivamente tutte le dipendenze necessarie
  • Restituisce un OrderService completamente 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 UserRepositoryInterface con 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.


👉 Leggi l'articolo completo su iadicola.it

Top comments (0)