Cos'è la Dependency Injection?

Questo capitolo vi introdurrà alle pratiche di programmazione di base che dovreste seguire quando scrivete qualsiasi applicazione. Si tratta delle basi necessarie per scrivere codice pulito, comprensibile e manutenibile.

Se adotterete queste regole e le seguirete, Nette vi supporterà in ogni passo. Si occuperà dei compiti di routine per voi e vi fornirà la massima comodità, così potrete concentrarvi sulla logica stessa.

I principi che vi mostreremo qui sono piuttosto semplici. Non c'è nulla di cui preoccuparsi.

Ricordi il tuo primo programma?

Non sappiamo in quale linguaggio lo hai scritto, ma se fosse stato PHP, probabilmente sarebbe stato simile a questo:

function soucet(float $a, float $b): float
{
	return $a + $b;
}

echo soucet(23, 1); // stampa 24

Poche righe di codice banali, ma contengono così tanti concetti chiave. Che esistono le variabili. Che il codice è diviso in unità più piccole, come le funzioni. Che passiamo loro argomenti di input e restituiscono risultati. Mancano solo le condizioni e i cicli.

Il fatto che passiamo dati di input a una funzione e questa restituisca un risultato è un concetto perfettamente comprensibile, utilizzato anche in altri campi, come la matematica.

Una funzione ha la sua firma, che consiste nel suo nome, un elenco di parametri e i loro tipi, e infine il tipo di valore di ritorno. Come utenti, ci interessa la firma; di solito non abbiamo bisogno di sapere nulla dell'implementazione interna.

Ora immagina che la firma della funzione fosse così:

function soucet(float $x): float

Una somma con un solo parametro? Strano… E che ne dici di questo?

function soucet(): float

Questo è davvero molto strano, vero? Come si usa la funzione?

echo soucet(); // cosa stamperà?

Guardando un codice del genere, saremmo confusi. Non solo un principiante non lo capirebbe, ma nemmeno un programmatore esperto capirebbe un codice del genere.

Ti stai chiedendo come sarebbe effettivamente una funzione del genere all'interno? Dove prenderebbe gli addendi? Probabilmente se li procurerebbe in qualche modo da sola, forse così:

function soucet(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

Nel corpo della funzione, abbiamo scoperto dipendenze nascoste verso altre funzioni globali o metodi statici. Per scoprire da dove provengono effettivamente gli addendi, dobbiamo indagare ulteriormente.

Non da questa parte!

Il design che abbiamo appena mostrato è l'essenza di molte caratteristiche negative:

  • la firma della funzione fingeva di non aver bisogno di addendi, il che ci confondeva
  • non sappiamo affatto come far sommare alla funzione altri due numeri
  • abbiamo dovuto guardare nel codice per scoprire dove prendeva gli addendi
  • abbiamo scoperto dipendenze nascoste
  • per una comprensione completa, è necessario esaminare anche queste dipendenze

Ed è compito della funzione di somma procurarsi gli input? Ovviamente no. La sua responsabilità è solo la somma stessa.

Non vogliamo incontrare codice del genere, e certamente non vogliamo scriverlo. La correzione è semplice: tornare alle basi e usare semplicemente i parametri:

function soucet(float $a, float $b): float
{
	return $a + $b;
}

Regola n. 1: fatti passare le dipendenze

La regola più importante è: tutti i dati di cui le funzioni o le classi hanno bisogno devono essere passati loro.

Invece di inventare modi nascosti attraverso i quali potrebbero ottenerli da soli, passa semplicemente i parametri. Risparmierai tempo necessario per inventare percorsi nascosti, che sicuramente non miglioreranno il tuo codice.

Se seguirai sempre e ovunque questa regola, sarai sulla strada per un codice senza dipendenze nascoste. Verso un codice comprensibile non solo per l'autore, ma anche per chiunque lo leggerà dopo di lui. Dove tutto è comprensibile dalle firme delle funzioni e delle classi e non c'è bisogno di cercare segreti nascosti nell'implementazione.

Questa tecnica è tecnicamente chiamata dependency injection. E questi dati sono chiamati dipendenze. In realtà, si tratta semplicemente di passare parametri, niente di più.

Per favore, non confondete la dependency injection, che è un design pattern, con il “dependency injection container”, che è invece uno strumento, cioè qualcosa di diametralmente diverso. Ci occuperemo dei container più avanti.

Dalle funzioni alle classi

E come si relaziona questo con le classi? Una classe è un'unità più complessa di una semplice funzione, ma la regola n. 1 si applica pienamente anche qui. Ci sono solo più opzioni per passare gli argomenti. Ad esempio, in modo abbastanza simile al caso di una funzione:

class Matematika
{
	public function soucet(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Matematika;
echo $math->soucet(23, 1); // 24

Oppure usando altri metodi, o direttamente il costruttore:

class Soucet
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function spocti(): float
	{
		return $this->a + $this->b;
	}

}

$soucet = new Soucet(23, 1);
echo $soucet->spocti(); // 24

Entrambi gli esempi sono pienamente conformi alla dependency injection.

Esempi reali

Nel mondo reale, non scriverai classi per sommare numeri. Passiamo a esempi pratici.

Abbiamo una classe Article che rappresenta un articolo di blog:

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		// salviamo l'articolo nel database
	}
}

e l'utilizzo sarà il seguente:

$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();

Il metodo save() salva l'articolo in una tabella del database. Implementarlo usando Nette Database sarebbe un gioco da ragazzi, se non fosse per un intoppo: dove prende Article la connessione al database, cioè l'oggetto della classe Nette\Database\Connection?

Sembra che abbiamo molte opzioni. Può prenderla da qualche variabile statica. O ereditare da una classe che fornisce la connessione al database. O utilizzare il cosiddetto singleton. O le cosiddette facades, che vengono utilizzate in Laravel:

use Illuminate\Support\Facades\DB;

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}

Fantastico, abbiamo risolto il problema.

O no?

Ricordiamo la Regola n. 1: fatti passare le dipendenze: tutte le dipendenze di cui la classe ha bisogno devono essere passate ad essa. Perché se violiamo la regola, abbiamo intrapreso la strada verso un codice sporco pieno di dipendenze nascoste, incomprensibilità, e il risultato sarà un'applicazione che sarà doloroso mantenere e sviluppare.

L'utente della classe Article non ha idea di dove il metodo save() salvi l'articolo. In una tabella del database? In quale, quella di produzione o di test? E come si può cambiare?

L'utente deve guardare come è implementato il metodo save() e trova l'uso del metodo DB::insert(). Quindi deve indagare ulteriormente su come questo metodo ottiene la connessione al database. E le dipendenze nascoste possono formare una catena piuttosto lunga.

Nel codice pulito e ben progettato, non ci sono mai dipendenze nascoste, facades di Laravel o variabili statiche. Nel codice pulito e ben progettato, si passano argomenti:

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Ancora più pratico, come vedremo più avanti, sarà tramite il costruttore:

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Se sei un programmatore esperto, potresti pensare che Article non dovrebbe affatto avere un metodo save(), dovrebbe rappresentare puramente un componente dati e il salvataggio dovrebbe essere gestito da un repository separato. Questo ha senso. Ma ci porterebbe molto lontano dall'argomento, che è la dependency injection, e dallo sforzo di fornire esempi semplici.

Se scrivi una classe che richiede, ad esempio, un database per funzionare, non inventare da dove ottenerlo, ma fattelo passare. Ad esempio, come parametro del costruttore o di un altro metodo. Riconosci le dipendenze. Riconoscile nell'API della tua classe. Otterrai un codice comprensibile e prevedibile.

E che ne dici di questa classe, che registra i messaggi di errore:

class Logger
{
	public function log(string $message)
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}

Cosa ne pensi, abbiamo rispettato la Regola n. 1: fatti passare le dipendenze?

Non l'abbiamo rispettata.

L'informazione chiave, cioè la directory con il file di log, la classe se la procura da sola da una costante.

Guarda l'esempio di utilizzo:

$logger = new Logger;
$logger->log('La temperatura è 23 °C');
$logger->log('La temperatura è 10 °C');

Senza conoscere l'implementazione, saresti in grado di rispondere alla domanda su dove vengono scritti i messaggi? Ti verrebbe in mente che per funzionare è necessaria l'esistenza della costante LOG_DIR? E saresti in grado di creare una seconda istanza che scriva altrove? Certamente no.

Correggiamo la classe:

class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

La classe è ora molto più comprensibile, configurabile e quindi più utile.

$logger = new Logger('/percorso/al/log.txt');
$logger->log('La temperatura è 15 °C');

Ma questo non mi interessa!

„Quando creo un oggetto Article e chiamo save(), non voglio occuparmi del database, voglio semplicemente che venga salvato in quello che ho impostato nella configurazione.“

„Quando uso Logger, voglio semplicemente che il messaggio venga scritto, e non voglio preoccuparmi di dove. Che venga utilizzata l'impostazione globale.“

Queste sono osservazioni corrette.

Come esempio, mostreremo una classe che invia newsletter e registra come è andata:

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Le email sono state inviate');

		} catch (Exception $e) {
			$logger->log('Si è verificato un errore durante l\'invio');
			throw $e;
		}
	}
}

Il Logger migliorato, che non utilizza più la costante LOG_DIR, richiede nel costruttore di specificare il percorso del file. Come risolvere questo problema? La classe NewsletterDistributor non si preoccupa affatto di dove vengono scritti i messaggi, vuole solo scriverli.

La soluzione è di nuovo la Regola n. 1: fatti passare le dipendenze: tutti i dati di cui la classe ha bisogno, glieli passiamo.

Quindi significa che passiamo il percorso del log tramite il costruttore, che poi usiamo quando creiamo l'oggetto Logger?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ NON COSÌ!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

Non così! Il percorso infatti non appartiene ai dati di cui la classe NewsletterDistributor ha bisogno; questi li necessita Logger. Percepisci la differenza? La classe NewsletterDistributor ha bisogno del logger come tale. Quindi glielo passiamo:

class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Le email sono state inviate');

		} catch (Exception $e) {
			$this->logger->log('Si è verificato un errore durante l\'invio');
			throw $e;
		}
	}
}

Ora è chiaro dalle firme della classe NewsletterDistributor che parte della sua funzionalità è anche il logging. E il compito di sostituire il logger con un altro, ad esempio per i test, è del tutto banale. Inoltre, se il costruttore della classe Logger dovesse cambiare, ciò non avrebbe alcun impatto sulla nostra classe.

Regola n. 2: prendi ciò che è tuo

Non lasciarti confondere e non farti passare le dipendenze delle tue dipendenze. Fatti passare solo le tue dipendenze.

Grazie a ciò, il codice che utilizza altri oggetti sarà completamente indipendente dalle modifiche ai loro costruttori. La sua API sarà più veritiera. E soprattutto, sarà banale sostituire queste dipendenze con altre.

Nuovo membro della famiglia

Nel team di sviluppo è stata presa la decisione di creare un secondo logger, che scrive nel database. Creeremo quindi la classe DatabaseLogger. Quindi abbiamo due classi, Logger e DatabaseLogger, una scrive su file, l'altra nel database… non ti sembra ci sia qualcosa di strano in questa denominazione? Non sarebbe meglio rinominare Logger in FileLogger? Certamente sì.

Ma lo faremo in modo intelligente. Sotto il nome originale, creeremo un'interfaccia:

interface Logger
{
	function log(string $message): void;
}

… che entrambi i logger implementeranno:

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

E grazie a ciò, non sarà necessario cambiare nulla nel resto del codice dove viene utilizzato il logger. Ad esempio, il costruttore della classe NewsletterDistributor sarà ancora soddisfatto del fatto che come parametro richiede Logger. E starà solo a noi decidere quale istanza passargli.

Per questo motivo non diamo mai ai nomi delle interfacce il suffisso Interface o il prefisso I. Altrimenti non sarebbe possibile sviluppare il codice in modo così elegante.

Houston, abbiamo un problema

Mentre in tutta l'applicazione possiamo accontentarci di una singola istanza del logger, sia esso basato su file o database, e semplicemente passarlo ovunque si registri qualcosa, la situazione è completamente diversa nel caso della classe Article. Le sue istanze, infatti, le creiamo secondo necessità, anche più volte. Come gestire la dipendenza dal database nel suo costruttore?

Come esempio può servire un controller che, dopo l'invio di un form, deve salvare l'articolo nel database:

class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Una possibile soluzione si offre direttamente: ci facciamo passare l'oggetto database tramite il costruttore in EditController e usiamo $article = new Article($this->db).

Proprio come nel caso precedente con Logger e il percorso del file, questa non è la procedura corretta. Il database non è una dipendenza di EditController, ma di Article. Passare il database va quindi contro la Regola n. 2: prendi ciò che è tuo. Se il costruttore della classe Article cambia (viene aggiunto un nuovo parametro), sarà necessario modificare anche il codice in tutti i punti in cui viene creata un'istanza. Ufff.

Houston, cosa suggerisci?

Regola n. 3: lascia fare alla factory

Eliminando le dipendenze nascoste e passando tutte le dipendenze come argomenti, abbiamo ottenuto classi più configurabili e flessibili. E quindi abbiamo bisogno di qualcos'altro che crei e configuri per noi queste classi più flessibili. Lo chiameremo factory.

La regola è: se una classe ha dipendenze, lascia la creazione delle sue istanze a una factory.

Le factory sono un sostituto più intelligente dell'operatore new nel mondo della dependency injection.

Per favore, non confondete con il design pattern factory method, che descrive un modo specifico di utilizzare le factory e non è correlato a questo argomento.

Factory

Una factory è un metodo o una classe che produce e configura oggetti. La classe che produce Article la chiameremo ArticleFactory e potrebbe assomigliare ad esempio a questo:

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Il suo utilizzo nel controller sarà il seguente:

class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// lasciamo che la factory crei l'oggetto
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Se in questo momento la firma del costruttore della classe Article cambia, l'unica parte del codice che deve reagire è la factory stessa ArticleFactory. Tutto il resto del codice che lavora con gli oggetti Article, come ad esempio EditController, non ne sarà minimamente influenzato.

Forse ora ti stai chiedendo se ci siamo davvero aiutati. La quantità di codice è aumentata e tutto inizia a sembrare sospettosamente complicato.

Non preoccuparti, tra poco arriveremo al Nette DI container. E quello ha molti assi nella manica che semplificheranno enormemente la costruzione di applicazioni che utilizzano la dependency injection. Ad esempio, invece della classe ArticleFactory, basterà scrivere una semplice interfaccia:

interface ArticleFactory
{
	function create(): Article;
}

Ma stiamo anticipando, resisti ancora un po' :-)

All'inizio di questo capitolo, abbiamo promesso di mostrarvi un metodo per progettare codice pulito. Basta alle classi

  1. passare le dipendenze di cui hanno bisogno
  2. e al contrario non passare ciò di cui non hanno direttamente bisogno
  3. e che gli oggetti con dipendenze si producono meglio nelle factory

Potrebbe non sembrare così a prima vista, ma queste tre regole hanno conseguenze di vasta portata. Portano a una visione radicalmente diversa della progettazione del codice. Ne vale la pena? I programmatori che hanno abbandonato le vecchie abitudini e hanno iniziato a usare costantemente la dependency injection considerano questo passo un momento fondamentale nella loro vita professionale. Si è aperto loro il mondo delle applicazioni chiare e manutenibili.

Ma cosa succede se il codice non utilizza costantemente la dependency injection? Cosa succede se è basato su metodi statici o singleton? Porta a qualche problema? Sì, e molto fondamentali.

versione: 3.x