Domanda

Cos’è il principio di inversione delle dipendenze e perché è importante?

È stato utile?

Soluzione

Dai un'occhiata a questo documento: Il principio di inversione delle dipendenze.

In sostanza dice:

  • I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello.Entrambi dovrebbero dipendere da astrazioni.
  • Le astrazioni non dovrebbero mai dipendere dai dettagli.I dettagli dovrebbero dipendere dalle astrazioni.

Quanto al motivo per cui è importante, in breve:i cambiamenti sono rischiosi e, dipendendo da un concetto invece che da un'implementazione, si riduce la necessità di cambiamenti nei siti di chiamata.

In effetti, il DIP riduce l'accoppiamento tra diversi pezzi di codice.L'idea è che, sebbene esistano molti modi per implementare, ad esempio, una struttura di registrazione, il modo in cui la utilizzeresti dovrebbe essere relativamente stabile nel tempo.Se riesci a estrarre un'interfaccia che rappresenti il ​​concetto di logging, questa interfaccia dovrebbe essere molto più stabile nel tempo rispetto alla sua implementazione e i siti di chiamata dovrebbero essere molto meno influenzati dalle modifiche che potresti apportare mantenendo o estendendo quel meccanismo di logging.

Facendo dipendere l'implementazione anche da un'interfaccia, hai la possibilità di scegliere in fase di esecuzione quale implementazione è più adatta al tuo particolare ambiente.A seconda dei casi, anche questo può essere interessante.

Altri suggerimenti

I libri Agile Software Development, Principles, Patterns, and Practices e Agile Principles, Patterns, and Practices in C# sono le migliori risorse per comprendere appieno gli obiettivi originali e le motivazioni alla base del principio di inversione delle dipendenze.Anche l'articolo "Il principio di inversione delle dipendenze" è una buona risorsa, ma poiché si tratta di una versione condensata di una bozza che alla fine è entrata nei libri precedentemente menzionati, tralascia alcune importanti discussioni sul concetto di a proprietà del pacchetto e dell'interfaccia che sono fondamentali per distinguere questo principio dal consiglio più generale di "programmare per un'interfaccia, non per un'implementazione" che si trova nel libro Design Patterns (Gamma, et.al).

Per fornire una sintesi, il Principio di Inversione di Dipendenza riguarda principalmente retromarcia la direzione convenzionale delle dipendenze dai componenti di "livello superiore" ai componenti di "livello inferiore" in modo tale che i componenti di "livello inferiore" dipendano dalle interfacce posseduto dai componenti di “livello superiore”.(Nota:Il componente di "livello superiore" qui si riferisce al componente che richiede dipendenze/servizi esterni, non necessariamente la sua posizione concettuale all'interno di un'architettura a più livelli.) In tal modo, l'accoppiamento non è ridotto così com'è spostato da componenti teoricamente meno preziosi a componenti teoricamente più preziosi.

Ciò si ottiene progettando componenti le cui dipendenze esterne sono espresse in termini di un'interfaccia per la quale il consumatore del componente deve fornire un'implementazione.In altre parole, le interfacce definite esprimono ciò di cui ha bisogno il componente, non il modo in cui utilizzi il componente (ad es."INeedSomething", non "IDoSomething").

Ciò a cui il Principio di Inversione delle Dipendenze non si riferisce è la semplice pratica di astrarre le dipendenze attraverso l'uso di interfacce (ad es.MyService → [ILogger ⇐ Logger]).Sebbene ciò disaccoppia un componente dallo specifico dettaglio di implementazione della dipendenza, non inverte la relazione tra consumatore e dipendenza (ad es.[MyService → IMyServiceLogger] ⇐ Registratore.

L'importanza del principio di inversione delle dipendenze può essere ridotta al singolo obiettivo di poter riutilizzare componenti software che si basano su dipendenze esterne per una parte della loro funzionalità (registrazione, convalida, ecc.)

All’interno di questo obiettivo generale del riutilizzo, possiamo delineare due sottotipi di riutilizzo:

  1. Utilizzo di un componente software all'interno di più applicazioni con implementazioni di sottodipendenze (ad es.Hai sviluppato un contenitore DI e desideri fornire la registrazione, ma non vuoi accoppiare il tuo contenitore a un logger specifico in modo tale che tutti coloro che utilizzano il tuo contenitore debbano utilizzare anche la libreria di registrazione scelta).

  2. Utilizzo di componenti software in un contesto in evoluzione (es.Hai sviluppato componenti di logica aziendale che rimangono gli stessi in più versioni di un'applicazione in cui i dettagli di implementazione stanno evolvendo).

Con il primo caso di riutilizzo di componenti su più applicazioni, ad esempio con una libreria di infrastruttura, l'obiettivo è fornire un'infrastruttura di base necessaria ai tuoi consumatori senza accoppiare i tuoi consumatori a dipendenze secondarie della tua libreria poiché assumere dipendenze da tali dipendenze richiede il tuo anche i consumatori richiedono le stesse dipendenze.Ciò può essere problematico quando gli utenti della tua biblioteca scelgono di utilizzare una biblioteca diversa per le stesse esigenze infrastrutturali (ad es.NLog vs.log4net) o se scelgono di utilizzare una versione successiva della libreria richiesta che non sia retrocompatibile con la versione richiesta dalla tua libreria.

Con il secondo caso di riutilizzo dei componenti della logica aziendale (ad es."componenti di livello superiore"), l'obiettivo è isolare l'implementazione del dominio principale della tua applicazione dalle mutevoli esigenze dei dettagli di implementazione (ad es.modifica/aggiornamento delle librerie di persistenza, delle librerie di messaggistica, delle strategie di crittografia, ecc.).Idealmente, la modifica dei dettagli di implementazione di un'applicazione non dovrebbe interrompere i componenti che incapsulano la logica aziendale dell'applicazione.

Nota:Alcuni potrebbero opporsi alla descrizione di questo secondo caso come riutilizzo effettivo, ragionando che componenti come quelli di logica aziendale utilizzati all'interno di una singola applicazione in evoluzione rappresentano solo un singolo utilizzo.L'idea qui, tuttavia, è che ogni modifica ai dettagli di implementazione dell'applicazione crea un nuovo contesto e quindi un caso d'uso diverso, sebbene gli obiettivi finali possano essere distinti in isolamento e isolamento.portabilità.

Sebbene seguire il principio di inversione di dipendenza in questo secondo caso possa offrire qualche vantaggio, va notato che il suo valore applicato a linguaggi moderni come Java e C# è molto ridotto, forse al punto da essere irrilevante.Come discusso in precedenza, il DIP prevede la separazione completa dei dettagli di implementazione in pacchetti separati.Nel caso di un'applicazione in evoluzione, tuttavia, il semplice utilizzo di interfacce definite in termini di dominio aziendale eviterà la necessità di modificare componenti di livello superiore a causa delle mutevoli esigenze dei componenti di dettaglio dell'implementazione, anche se i dettagli di implementazione in definitiva all'interno dello stesso pacchetto.Questa parte del principio riflette aspetti pertinenti al linguaggio in esame al momento della codificazione del principio (ad es.C++) che non sono rilevanti per i linguaggi più recenti.Detto questo, l’importanza del principio di inversione delle dipendenze risiede principalmente nello sviluppo di componenti/librerie software riutilizzabili.

È possibile trovare una discussione più lunga su questo principio in relazione al semplice utilizzo delle interfacce, dell'inserimento delle dipendenze e del modello di interfaccia separata Qui.Inoltre, è possibile trovare una discussione su come il principio si riferisce ai linguaggi tipizzati dinamicamente come JavaScript Qui.

Quando progettiamo applicazioni software possiamo considerare classi di basso livello le classi che implementano operazioni basilari e primarie (accesso al disco, protocolli di rete,...) e classi di alto livello le classi che incapsulano logiche complesse (flussi di business, ...).

Gli ultimi si affidano alle classi di basso livello.Un modo naturale per implementare tali strutture sarebbe quello di scrivere classi di basso livello e, una volta ottenute, scrivere classi complesse di alto livello.Poiché le classi di alto livello sono definite in termini di altre, questo sembra il modo logico per farlo.Ma questo non è un design flessibile.Cosa succede se dobbiamo sostituire una classe di basso livello?

Il principio di inversione di dipendenza afferma che:

  • I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello.Entrambi dovrebbero dipendere da astrazioni.
  • Le astrazioni non dovrebbero dipendere dai dettagli.I dettagli dovrebbero dipendere dalle astrazioni.

Questo principio cerca di "invertire" la nozione convenzionale secondo cui i moduli di alto livello nel software dovrebbero dipendere dai moduli di livello inferiore.Qui i moduli di alto livello possiedono l'astrazione (ad esempio, decidendo i metodi dell'interfaccia) che sono implementati dai moduli di livello inferiore.Rendendo così i moduli di livello inferiore dipendenti da moduli di livello superiore.

Per me, il Principio di Inversione di Dipendenza, come descritto nel articolo ufficiale, è in realtà un tentativo fuorviante di aumentare la riusabilità di moduli che sono intrinsecamente meno riutilizzabili, nonché un modo per risolvere un problema nel linguaggio C++.

Il problema in C++ è che i file header in genere contengono dichiarazioni di campi e metodi privati.Pertanto, se un modulo C++ di alto livello include il file di intestazione per un modulo di basso livello, dipenderà dall'effettivo implementazione dettagli di quel modulo.E questo, ovviamente, non è una bella cosa.Ma questo non è un problema nelle lingue più moderne comunemente usate oggi.

I moduli di alto livello sono intrinsecamente meno riutilizzabili dei moduli di basso livello perché i primi sono normalmente più specifici dell'applicazione/contesto rispetto ai secondi.Ad esempio, un componente che implementa una schermata dell'interfaccia utente è di altissimo livello e anche molto (completamente?) specifico per l'applicazione.Cercare di riutilizzare un componente di questo tipo in un'applicazione diversa è controproducente e può solo portare a un'ingegneria eccessiva.

Quindi, la creazione di un'astrazione separata allo stesso livello di un componente A che dipende da un componente B (che non dipende da A) può essere effettuata solo se il componente A sarà davvero utile per il riutilizzo in applicazioni o contesti diversi.Se così non fosse, applicare il DIP sarebbe una cattiva progettazione.

L'inversione delle dipendenze ben applicata offre flessibilità e stabilità a livello dell'intera architettura della tua applicazione.Permetterà alla tua applicazione di evolversi in modo più sicuro e stabile.

Architettura tradizionale a strati

Tradizionalmente un'interfaccia utente con architettura a più livelli dipendeva dal livello aziendale e questo a sua volta dipendeva dal livello di accesso ai dati.

http://xurxodev.com/content/images/2016/02/Traditional-Layered.png

Devi comprendere il livello, il pacchetto o la libreria.Vediamo come sarebbe il codice.

Avremmo una libreria o un pacchetto per il livello di accesso ai dati.

// DataAccessLayer.dll
public class ProductDAO {

}

E un'altra logica aziendale del livello di libreria o pacchetto che dipende dal livello di accesso ai dati.

// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO { 
    private ProductDAO productDAO;
}

Architettura a più livelli con inversione delle dipendenze

L'inversione delle dipendenze indica quanto segue:

I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello.Entrambi dovrebbero dipendere da astrazioni.

Le astrazioni non dovrebbero dipendere dai dettagli.I dettagli dovrebbero dipendere dalle astrazioni.

Quali sono i moduli di alto livello e di basso livello?Pensando ai moduli come librerie o pacchetti, i moduli di alto livello sarebbero quelli che tradizionalmente hanno dipendenze e di basso livello da cui dipendono.

In altre parole, il livello alto del modulo sarebbe dove viene invocata l'azione e il livello basso dove viene eseguita l'azione.

Una conclusione ragionevole da trarre da questo principio è che non dovrebbe esserci dipendenza tra le concrezioni, ma deve esserci una dipendenza da un'astrazione.Ma secondo l’approccio che adottiamo potremmo applicare erroneamente la dipendenza dagli investimenti, ma si tratta di un’astrazione.

Immaginiamo di adattare il nostro codice come segue:

Avremmo una libreria o un pacchetto per il livello di accesso ai dati che definisce l'astrazione.

// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{

}

E un'altra logica aziendale del livello di libreria o pacchetto che dipende dal livello di accesso ai dati.

// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO { 
    private IProductDAO productDAO;
}

Sebbene dipendiamo da un'astrazione, la dipendenza tra business e accesso ai dati rimane la stessa.

http://xurxodev.com/content/images/2016/02/Traditional-Layered.png

Per ottenere l'inversione delle dipendenze, l'interfaccia di persistenza deve essere definita nel modulo o pacchetto in cui si trova questa logica o dominio di alto livello e non nel modulo di basso livello.

Per prima cosa definiamo cos'è lo strato del dominio e l'astrazione della sua comunicazione viene definita persistenza.

// Domain.dll
public interface IProductRepository;

using DataAccessLayer;
public class ProductBO { 
    private IProductRepository productRepository;
}

Dopo che il livello di persistenza dipende dal dominio, ora si può invertire se viene definita una dipendenza.

// Persistence.dll
public class ProductDAO : IProductRepository{

}

http://xurxodev.com/content/images/2016/02/Dependency-Inversion-Layers.png

Approfondimento del principio

È importante assimilare bene il concetto, approfondendone finalità e benefici.Se rimaniamo meccanicamente e impariamo a memorizzare i casi tipici, non saremo in grado di identificare dove possiamo applicare il principio di dipendenza.

Ma perché invertiamo una dipendenza?Qual è l’obiettivo principale al di là degli esempi specifici?

Così comunemente consente alle cose più stabili, che non dipendono da cose meno stabili, di cambiare più frequentemente.

È più semplice modificare il tipo di persistenza, ovvero il database o la tecnologia per accedere allo stesso database, rispetto alla logica del dominio o alle azioni progettate per comunicare con la persistenza.Per questo motivo, la dipendenza è invertita poiché è più semplice modificare la persistenza se si verifica questo cambiamento.In questo modo non dovremo cambiare dominio.Il livello del dominio è il più stabile di tutti, motivo per cui non dovrebbe dipendere da nulla.

Ma non esiste solo questo esempio di repository.Esistono molti scenari in cui si applica questo principio ed esistono architetture basate su questo principio.

Architetture

Esistono architetture in cui l'inversione delle dipendenze è fondamentale per la sua definizione.In tutti i domini è il più importante e sono le astrazioni che indicheranno il protocollo di comunicazione tra il dominio e il resto dei pacchetti o delle librerie definite.

Architettura pulita

In Architettura pulita il dominio si trova al centro e se si guarda nella direzione delle frecce che indicano la dipendenza, è chiaro quali sono gli strati più importanti e stabili.Gli strati esterni sono considerati strumenti instabili, quindi evita di dipendere da essi.

Architettura esagonale

Succede lo stesso con l'architettura esagonale, dove il dominio si trova anche nella parte centrale e le porte sono astrazioni di comunicazione dal domino verso l'esterno.Anche in questo caso è evidente che il dominio è il più stabile e la dipendenza tradizionale è invertita.

Fondamentalmente dice:

La classe dovrebbe dipendere da astrazioni (ad esempio interfaccia, classi astratte), non da dettagli specifici (implementazioni).

Un modo molto più chiaro per affermare il principio di inversione di dipendenza è:

I tuoi moduli che incapsulano la logica aziendale complessa non dovrebbero dipendere direttamente da altri moduli che incapsulano la logica aziendale.Dovrebbero invece dipendere solo da interfacce con dati semplici.

Cioè, invece di implementare la tua classe Logic come fanno di solito le persone:

class Dependency { ... }
class Logic {
    private Dependency dep;
    int doSomething() {
        // Business logic using dep here
    }
}

dovresti fare qualcosa del tipo:

class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
    private Dependency dep;
    ...
}
class Logic {
    int doSomething(Data data) {
        // compute something with data
    }
}

Data E DataFromDependency dovrebbe vivere nello stesso modulo di Logic, non con Dependency.

Perché farlo?

  1. I due moduli di logica aziendale sono ora disaccoppiati.Quando Dependency cambiamenti, non è necessario cambiare Logic.
  2. Capire cosa Logic fa è un compito molto più semplice:funziona solo su quello che sembra un ADT.
  3. Logic ora possono essere testati più facilmente.Ora puoi istanziare direttamente Data con dati falsi e trasmetterli.Non sono necessarie simulazioni o complesse impalcature di prova.

Buone risposte e buoni esempi sono già stati forniti da altri qui.

La ragione IMMERSIONE è importante perché garantisce il principio OO della "progettazione liberamente accoppiata".

Gli oggetti nel tuo software NON dovrebbero entrare in una gerarchia in cui alcuni oggetti sono quelli di livello superiore, dipendenti da oggetti di basso livello.Le modifiche negli oggetti di basso livello si trasmetteranno quindi agli oggetti di livello superiore, rendendo il software molto fragile al cambiamento.

Vuoi che i tuoi oggetti di "livello superiore" siano molto stabili e non fragili al cambiamento, quindi devi invertire le dipendenze.

Inversione di controllo (IoC) è un modello di progettazione in cui un oggetto riceve la sua dipendenza da un framework esterno, anziché chiedere a un framework la sua dipendenza.

Esempio di pseudocodice utilizzando la ricerca tradizionale:

class Service {
    Database database;
    init() {
        database = FrameworkSingleton.getService("database");
    }
}

Codice simile utilizzando IoC:

class Service {
    Database database;
    init(database) {
        this.database = database;
    }
}

I vantaggi dell’IoC sono:

  • Non hai dipendenza da un quadro centrale, quindi questo può essere cambiato se lo si desidera.
  • Poiché gli oggetti vengono creati per iniezione, preferibilmente utilizzando interfacce, è facile creare test unitari che sostituiscono le dipendenze con versioni finte.
  • Disaccoppiamento del codice.

Lo scopo dell'inversione delle dipendenze è rendere il software riutilizzabile.

L'idea è che invece di due pezzi di codice che dipendono l'uno dall'altro, si basano su un'interfaccia astratta.Quindi puoi riutilizzare uno dei due pezzi senza l'altro.

Il modo più comune per raggiungere questo obiettivo è attraverso un contenitore di inversione del controllo (IoC) come Spring in Java.In questo modello, le proprietà degli oggetti vengono impostate tramite una configurazione XML invece che gli oggetti escano e trovino la loro dipendenza.

Immagina questo pseudocodice...

public class MyClass
{
  public Service myService = ServiceLocator.service;
}

MyClass dipende direttamente sia dalla classe Service che dalla classe ServiceLocator.Sono necessari entrambi se si desidera utilizzarlo in un'altra applicazione.Ora immagina questo...

public class MyClass
{
  public IService myService;
}

Ora MyClass si basa su una singola interfaccia, l'interfaccia IService.Lasciamo che sia il contenitore IoC a impostare effettivamente il valore di quella variabile.

Quindi ora MyClass può essere facilmente riutilizzato in altri progetti, senza portare con sé la dipendenza di quelle altre due classi.

Ancora meglio, non è necessario trascinare le dipendenze di MyService e le dipendenze di tali dipendenze e il...beh, hai capito.

Inversione dei contenitori di controllo e modello di inserimento delle dipendenze di Martin Fowler è anch'esso una buona lettura.ho trovato Testare i modelli di progettazione un libro fantastico per la mia prima incursione nell'apprendimento del DI e di altri modelli.

Inversione di dipendenza:Dipendi dalle astrazioni, non dalle concrezioni.

Inversione di controllo:Principale vs Astrazione e come il Principale sia il collante dei sistemi.

DIP and IoC

Questi sono alcuni buoni post che parlano di questo:

https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/

https://coderstower.com/2019/04/02/main-and-abstraction-the-deactivated-peers/

https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/

Lo dice il Principio di Inversione di Dipendenza (DIP).

i) I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello.Entrambi dovrebbero dipendere da astrazioni.

ii) Le astrazioni non dovrebbero mai dipendere dai dettagli.I dettagli dovrebbero dipendere dalle astrazioni.

Esempio:

    public interface ICustomer
    {
        string GetCustomerNameById(int id);
    }

    public class Customer : ICustomer
    {
        //ctor
        public Customer(){}

        public string GetCustomerNameById(int id)
        {
            return "Dummy Customer Name";
        }
    }

    public class CustomerFactory
    {
        public static ICustomer GetCustomerData()
        {
            return new Customer();
        }
    }

    public class CustomerBLL
    {
        ICustomer _customer;
        public CustomerBLL()
        {
            _customer = CustomerFactory.GetCustomerData();
        }

        public string GetCustomerNameById(int id)
        {
            return _customer.GetCustomerNameById(id);
        }
    }

    public class Program
    {
        static void Main()
        {
            CustomerBLL customerBLL = new CustomerBLL();
            int customerId = 25;
            string customerName = customerBLL.GetCustomerNameById(customerId);


            Console.WriteLine(customerName);
            Console.ReadKey();
        }
    }

Nota:La classe dovrebbe dipendere da astrazioni come l'interfaccia o classi astratte, non da dettagli specifici (implementazione dell'interfaccia).

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top