Domande frequenti sulla DI (FAQ)
- DI è un altro nome per IoC?
- Cos'è un Service Locator?
- Quando è meglio non usare la DI?
- L'uso della DI ha degli svantaggi?
- Come si riscrive con la DI un'applicazione datata?
- Perché si preferisce la composizione all'ereditarietà?
- Nette DI Container si può usare fuori da Nette?
- Perché la configurazione è in file NEON?
- L'analisi dei file NEON rallenta l'applicazione?
- Come accedo nella mia classe ai parametri del file di configurazione?
- Nette supporta l'interfaccia Container di PSR-11?
- Cosa significano i termini container, compiler, definition ecc.?
DI è un altro nome per IoC?
L'Inversion of Control (IoC) è un principio che descrive il flusso di controllo di un programma: è il vostro codice
a chiamare codice esterno, oppure è il codice esterno (per esempio un framework) a chiamare il vostro? IoC è un concetto ampio,
che comprende gli eventi, il cosiddetto principio hollywoodiano e altri aspetti.
Rientrano in questo concetto anche le factory, di cui si parla nella Regola n. 3: lasciate
fare alla factory, che rappresentano un'inversione dell'operatore new.
La dependency injection (DI) si concentra su come gli oggetti ottengono le proprie dipendenze (cioè gli altri oggetti con cui devono lavorare). È un design pattern che sostiene di passare esplicitamente le dipendenze agli oggetti, invece di far sì che siano gli oggetti a crearle o a cercarle.
La DI si può quindi considerare una forma specifica di IoC. Non tutte le forme di IoC, però, favoriscono un codice pulito. Sono per esempio antipattern le tecniche che si affidano allo stato globale o al pattern Service Locator.
Cos'è un Service Locator?
È un approccio alternativo alla dependency injection. Consiste in un oggetto centrale (il locator) in cui sono registrati tutti i servizi (le dipendenze) disponibili. Quando un oggetto ha bisogno di una dipendenza, la chiede al Service Locator.
Rispetto alla DI, però, manca di trasparenza. Le dipendenze sono nascoste nel codice dell'oggetto (nelle chiamate al locator) invece di essere esplicite nella sua API (nel costruttore o nei metodi), e per capire i collegamenti bisogna esaminare il codice. Anche i test sono più complessi, perché non potete semplicemente passare dipendenze fittizie istanziando un oggetto: spesso dovete manipolare il Service Locator stesso. Inoltre il Service Locator introduce una dipendenza superflua: gli oggetti diventano accoppiati al locator, a differenza della DI, dove idealmente gli oggetti non sanno nulla del container.
Quando è meglio non usare la DI?
Non si conoscono svantaggi significativi nell'uso corretto del design pattern della dependency injection. Al contrario, ottenere le dipendenze da posizioni globalmente accessibili (come proprietà statiche o singleton) porta a numerose complicazioni, così come l'uso di un Service Locator. Usare la DI è quindi in generale sempre consigliabile. Non è un dogma: semplicemente non si è affermata nessuna alternativa migliore per gestire le dipendenze in modo pulito.
Esistono però situazioni specifiche e limitate in cui accedere agli oggetti globalmente può essere accettabile. Per esempio durante il debugging, quando dovete scaricare il valore di una variabile, misurare il tempo di esecuzione o registrare un messaggio in un punto preciso. In questi casi, che riguardano azioni temporanee destinate a essere rimosse in seguito dal codice, usare un dumper, un timer o un logger globalmente accessibile può essere legittimo. Questi strumenti non fanno parte della progettazione di fondo dell'applicazione.
L'uso della DI ha degli svantaggi?
L'uso della dependency injection porta svantaggi, come più codice da scrivere o prestazioni ridotte? Cosa perdiamo quando iniziamo a scrivere codice conforme alla DI?
La DI in sé ha un impatto trascurabile sulle prestazioni in fase di esecuzione o sull'uso della memoria. Le prestazioni del container DI possono contare, ma Nette DI compila il container in semplice codice PHP, con un sovraccarico praticamente nullo durante l'esecuzione dell'applicazione.
Scrivendo codice secondo i principi della DI, spesso dovete creare costruttori che accettano dipendenze. Se in passato poteva sembrare noioso, gli IDE moderni e funzionalità come la constructor property promotion di PHP 8 lo rendono velocissimo. Le factory spesso possono essere generate automaticamente da Nette DI, riducendo ulteriormente il codice ripetitivo. D'altra parte, eliminate la necessità di scrivere singleton e accessor statici.
Nel complesso, un'applicazione ben progettata che usa la DI non è di norma né molto più breve né molto più lunga di una che si affida ai singleton o all'accesso globale. Il codice relativo alla creazione e al collegamento delle dipendenze si sposta semplicemente dalle singole classi a posizioni dedicate: la configurazione del container DI e le factory.
Come si riscrive con la DI un'applicazione datata?
Migrare un'applicazione datata verso la dependency injection può essere un processo impegnativo, soprattutto per applicazioni grandi e complesse. È importante affrontare questo processo in modo sistematico.
- Passando alla dependency injection è importante che tutti i membri del team comprendano i principi e le pratiche usate.
- Analizzate per prima cosa l'applicazione esistente per individuarne i componenti chiave e le loro dipendenze. Fate un piano di quali parti verranno rifattorizzate e in quale ordine.
- Implementate un container DI oppure, meglio ancora, usate una libreria esistente come Nette DI.
- Rifattorizzate gradualmente le parti dell'applicazione perché usino la dependency injection. Questo può comportare la modifica dei costruttori o dei metodi perché accettino le dipendenze come parametri.
- Aggiornate il codice in cui gli oggetti vengono istanziati, perché li ottenga dal container o usi le factory fornite dal container.
Ricordate che passare alla dependency injection è un investimento nella qualità del codice e nella manutenibilità a lungo termine dell'applicazione. Anche se può essere impegnativo apportare queste modifiche, il risultato dovrebbe essere un codice più pulito, più modulare e facilmente testabile, pronto per future estensioni e manutenzioni.
Perché si preferisce la composizione all'ereditarietà?
Per il riuso del codice si preferisce di norma la composizione all'ereditarietà, perché porta a un accoppiamento più lasco. Con la composizione è meno probabile incontrare problemi in cui il cambiamento di una classe base rompe le sottoclassi che ne dipendono. Un esempio tipico è la situazione nota come inferno dei costruttori.
Nette DI Container si può usare fuori da Nette?
Assolutamente sì. Nette DI Container fa parte di Nette, ma è progettato come libreria autonoma, utilizzabile indipendentemente dalle altre parti del framework. Basta installarlo con Composer, creare un file di configurazione che definisca i vostri servizi e poi usare qualche riga di codice PHP per creare il container DI. E potete subito iniziare a sfruttare la dependency injection nei vostri progetti.
Il capitolo su Nette DI Container descrive un caso d'uso concreto con esempi di codice.
Perché la configurazione è in file NEON?
NEON è un linguaggio di configurazione semplice e facilmente leggibile, sviluppato all'interno di Nette per configurare applicazioni, servizi e le loro dipendenze. Rispetto a JSON o YAML offre a questo scopo possibilità molto più intuitive e flessibili. In NEON potete descrivere in modo naturale definizioni di servizi e relazioni che sarebbe difficile o impossibile esprimere con altrettanta chiarezza in JSON o YAML.
L'analisi dei file NEON rallenta l'applicazione?
Benché i file NEON vengano analizzati molto rapidamente, la velocità della loro analisi è in gran parte irrilevante in produzione. Il motivo è che i file di configurazione vengono analizzati una sola volta, alla prima esecuzione dell'applicazione (o quando cambiano). Dopo l'analisi viene generato il codice del container DI, messo in cache (salvato su disco), e a ogni richiesta successiva viene eseguito questo codice PHP compilato, senza bisogno di ulteriori analisi.
Così funziona in un ambiente di produzione. Durante lo sviluppo i file NEON vengono analizzati ogni volta che il loro contenuto cambia, garantendo allo sviluppatore un container DI sempre aggiornato. Come detto, l'analisi in sé è velocissima.
Come accedo nella mia classe ai parametri del file di configurazione?
Tenete presente la Regola n. 1: fatevele passare. Se una classe ha bisogno di un'informazione presente nel file di configurazione, non cercate di capire come la classe possa procurarsela. Limitatevi a chiederla, per esempio tramite il costruttore della classe. E poi fornite quel valore nel file di configurazione.
In questo esempio %myParameter% è un segnaposto per il valore del parametro myParameter, che verrà
passato al costruttore di MyClass:
# config.neon
parameters:
myParameter: Some value
services:
- MyClass(%myParameter%)
Se volete passare più parametri o usare l'autowiring, conviene racchiudere i parametri in un oggetto.
Nette supporta l'interfaccia Container di PSR-11?
Nette DI Container non supporta direttamente PSR-11. Se però vi serve interoperabilità tra Nette DI Container e librerie o framework che si aspettano la Container Interface di PSR-11, potete creare un semplice adattatore che faccia da ponte tra Nette DI Container e PSR-11.
Cosa significano i termini container, compiler, definition ecc.?
Un breve vocabolario delle parole che ricorrono di continuo intorno a Nette DI, per lo più quando si scrivono estensioni:
- Container – l'oggetto compilato (
Nette\DI\Container) che crea i servizi su richiesta e li conserva in fase di esecuzione. Viene generato una sola volta come codice PHP ottimizzato. - Compiler – il meccanismo che trasforma i file di configurazione e le estensioni nella classe di quel container.
- ContainerBuilder – il modello modificabile del container, usato durante la compilazione; contiene le definizioni dei servizi prima che esista un servizio reale. Vedi Creare estensioni.
- Servizio – un oggetto gestito dal container, di norma creato una sola volta e condiviso (un singleton): una connessione al database, un mailer, un logger.
- Definizione – la ricetta di un servizio: il suo tipo, come crearlo e cosa fare dopo. Nette trasforma le definizioni nei metodi factory del container; ne esistono di vari tipi (vedi tipi di definizione).
- Tipo – la classe o l'interfaccia di un servizio, usata dall'autowiring per abbinare i servizi ai punti che li richiedono.
- Autowiring – il passaggio automatico dei servizi ai costruttori e ai metodi in base al loro tipo, così che non dobbiate collegare le dipendenze a mano.
- Tag – un'etichetta attaccata a una definizione (facoltativamente con un valore); un'estensione può poi trovare con
findByTag()tutti i servizi che la portano. - Setup – chiamate aggiuntive eseguite su un servizio subito dopo la sua creazione: chiamate di metodi o assegnazioni
di proprietà, aggiunte con
addSetup(). - Alias – un nome alternativo per un servizio esistente.