9.1 Inversione delle dipendenze

Ricorda, una volta abbiamo detto che in un'applicazione server non puoi semplicemente creare flussi tramite new Thread().start()? Solo il contenitore dovrebbe creare thread. Ora svilupperemo ulteriormente questa idea.

Tutti gli oggetti dovrebbero anche essere creati solo dal contenitore . Ovviamente non stiamo parlando di tutti gli oggetti, ma piuttosto dei cosiddetti oggetti business. Sono anche spesso indicati come bidoni. Le gambe di questo approccio crescono dal quinto principio di SOLID, che richiede l'eliminazione delle classi e il passaggio alle interfacce:

  • I moduli di livello superiore non dovrebbero dipendere da moduli di livello inferiore. Sia quelli che altri dovrebbero dipendere da astrazioni.
  • Le astrazioni non dovrebbero dipendere dai dettagli. L'implementazione deve dipendere dall'astrazione.

I moduli non dovrebbero contenere riferimenti a implementazioni specifiche e tutte le dipendenze e le interazioni tra di essi dovrebbero essere costruite esclusivamente sulla base di astrazioni (ovvero interfacce). L'essenza stessa di questa regola può essere scritta in una frase: tutte le dipendenze devono essere sotto forma di interfacce .

Nonostante la sua natura fondamentale e l'apparente semplicità, questa regola viene violata più spesso. Vale a dire, ogni volta che utilizziamo l'operatore new nel codice del programma/modulo e creiamo un nuovo oggetto di un tipo specifico, quindi, invece di dipendere dall'interfaccia, si forma la dipendenza dall'implementazione.

È chiaro che questo non può essere evitato e gli oggetti devono essere creati da qualche parte. Ma, per lo meno, è necessario ridurre al minimo il numero di punti in cui ciò viene fatto e in cui le classi sono esplicitamente specificate, nonché localizzare e isolare tali luoghi in modo che non siano sparsi nel codice del programma.

Un'ottima soluzione è la folle idea di concentrare la creazione di nuovi oggetti all'interno di oggetti e moduli specializzati: fabbriche, localizzatori di servizi, contenitori IoC.

In un certo senso, tale decisione segue il principio della scelta singola, che afferma: "Ogni volta che un sistema software deve supportare molte alternative, il loro elenco completo dovrebbe essere noto solo a un modulo del sistema" .

Pertanto, se in futuro sarà necessario aggiungere nuove opzioni (o nuove implementazioni, come nel caso della creazione di nuovi oggetti che stiamo considerando), allora sarà sufficiente aggiornare solo il modulo che contiene queste informazioni e tutti gli altri moduli rimarranno inalterati e potranno continuare il loro lavoro come al solito.

Esempio 1

new ArrayList Invece di scrivere qualcosa come , avrebbe senso List.new()che JDK ti fornisse la corretta implementazione di una foglia: ArrayList, LinkedList o anche ConcurrentList.

Ad esempio, il compilatore vede che ci sono chiamate all'oggetto da thread diversi e vi inserisce un'implementazione thread-safe. O troppi inserimenti nel mezzo del foglio, quindi l'implementazione sarà basata su LinkedList.

Esempio 2

Questo è già successo con le specie, per esempio. Quando è stata l'ultima volta che hai scritto un algoritmo di ordinamento per ordinare una raccolta? Invece ora tutti usano il metodo Collections.sort(), e gli elementi della collezione devono supportare l'interfaccia Comparable (comparable).

Se sort()si passa al metodo una raccolta di meno di 10 elementi, è del tutto possibile ordinarla con un ordinamento a bolle (Bubble sort) e non con Quicksort.

Esempio 3

Il compilatore sta già osservando come concateni le stringhe e sostituirà il tuo codice con StringBuilder.append().

9.2 L'inversione delle dipendenze nella pratica

Ora il più interessante: pensiamo a come possiamo combinare teoria e pratica. In che modo i moduli possono creare e ricevere correttamente le loro "dipendenze" e non violare l'inversione delle dipendenze?

Per fare ciò, durante la progettazione di un modulo, devi decidere tu stesso:

  • cosa fa il modulo, quale funzione svolge;
  • quindi il modulo necessita dal suo ambiente, cioè con quali oggetti/moduli dovrà fare i conti;
  • E come lo avrà?

Per rispettare i principi di Dependency Inversion, devi assolutamente decidere quali oggetti esterni utilizza il tuo modulo e come otterrà i riferimenti ad essi.

Ed ecco le seguenti opzioni:

  • il modulo stesso crea oggetti;
  • il modulo prende gli oggetti dal contenitore;
  • il modulo non ha idea della provenienza degli oggetti.

Il problema è che per creare un oggetto è necessario chiamare un costruttore di un tipo specifico e, di conseguenza, il modulo dipenderà non dall'interfaccia, ma dall'implementazione specifica. Ma se non vogliamo che gli oggetti vengano creati in modo esplicito nel codice del modulo, allora possiamo usare il metodo Factory pattern .

"La linea di fondo è che invece di istanziare direttamente un oggetto tramite new, forniamo alla classe client un'interfaccia per creare oggetti. Poiché tale interfaccia può sempre essere sovrascritta con il design giusto, otteniamo una certa flessibilità quando utilizziamo moduli di basso livello nei moduli di alto livello" .

Nei casi in cui è necessario creare gruppi o famiglie di oggetti correlati, viene utilizzata una fabbrica astratta invece di un metodo di fabbrica .

9.3 Utilizzo del localizzatore di servizi

Il modulo prende gli oggetti necessari da chi li ha già. Si presuppone che il sistema disponga di un repository di oggetti, in cui i moduli possono "mettere" i propri oggetti e "prendere" oggetti dal repository.

Questo approccio è implementato dal pattern Service Locator , la cui idea principale è che il programma abbia un oggetto che sappia come ottenere tutte le dipendenze (servizi) che potrebbero essere richieste.

La principale differenza rispetto alle fabbriche è che Service Locator non crea oggetti, ma in realtà contiene già oggetti istanziati (o sa dove / come ottenerli, e se li crea, solo una volta alla prima chiamata). La fabbrica ad ogni chiamata crea un nuovo oggetto di cui ottieni la piena proprietà e puoi farci quello che vuoi.

Importante ! Il service locator produce riferimenti agli stessi oggetti già esistenti . Pertanto, devi stare molto attento con gli oggetti emessi dal Service Locator, poiché qualcun altro può usarli contemporaneamente a te.

Gli oggetti nel Service Locator possono essere aggiunti direttamente tramite il file di configurazione, e in effetti in qualsiasi modo conveniente per il programmatore. Lo stesso Service Locator può essere una classe statica con un set di metodi statici, un singleton o un'interfaccia e può essere passato alle classi richieste tramite un costruttore o un metodo.

Il Service Locator è talvolta chiamato anti-pattern ed è sconsigliato (perché crea connessioni implicite e dà solo l'apparenza di un buon design). Puoi leggere di più da Mark Seaman:

9.4 Iniezione di dipendenze

Il modulo non si preoccupa affatto delle dipendenze di "estrazione". Determina solo ciò di cui ha bisogno per funzionare e tutte le dipendenze necessarie vengono fornite (introdotte) dall'esterno da qualcun altro.

Questo è ciò che viene chiamato - Dependency Injection. In genere, le dipendenze richieste vengono passate come parametri del costruttore (Injection del costruttore) o tramite metodi di classe (Injection del setter).

Questo approccio inverte il processo di creazione delle dipendenze: invece del modulo stesso, la creazione delle dipendenze è controllata da qualcuno dall'esterno. Il modulo dall'emettitore attivo di oggetti diventa passivo: non è lui che crea, ma altri creano per lui.

Questo cambio di direzione è chiamato l'inversione del controllo o il principio di Hollywood: "Non chiamarci, ti chiamiamo noi".

Questa è la soluzione più flessibile, dando ai moduli la massima autonomia . Possiamo dire che solo implementa pienamente il "Principio di responsabilità unica": il modulo dovrebbe essere completamente concentrato sul fare bene il proprio lavoro e non preoccuparsi di nient'altro.

Fornire al modulo tutto il necessario per il lavoro è un'attività separata, che dovrebbe essere gestita dallo "specialista" appropriato (di solito un determinato contenitore, un contenitore IoC, è responsabile della gestione delle dipendenze e della loro implementazione).

Qui infatti tutto è come nella vita: in un'azienda ben organizzata i programmatori programmano, e le scrivanie, i computer e tutto ciò che serve per lavorare vengono comprati e forniti dal capoufficio. Oppure, se usi la metafora del programma come costruttore, il modulo non dovrebbe pensare ai fili, qualcun altro è coinvolto nell'assemblare il costruttore e non le parti stesse.

Non sarebbe un'esagerazione affermare che l'uso di interfacce per descrivere le dipendenze tra i moduli (Dependency Inversion) + la corretta creazione e iniezione di queste dipendenze (principalmente Dependency Injection) sono tecniche chiave per il disaccoppiamento .

Servono come base su cui poggiano l'accoppiamento libero del codice, la sua flessibilità, la resistenza ai cambiamenti, il riutilizzo e senza il quale tutte le altre tecniche hanno poco senso. Questa è la base dell'accoppiamento libero e della buona architettura.

Il principio di Inversion of Control (insieme a Dependency Injection e Service Locator) è discusso in dettaglio da Martin Fowler. Ci sono traduzioni di entrambi i suoi articoli: "Inversion of Control Containers and the Dependency Injection pattern" e "Inversion of Control" .