Come usare l'inversione del controllo in C #

Sia l'inversione del controllo che l'inserimento delle dipendenze consentono di interrompere le dipendenze tra i componenti dell'applicazione e semplificarne il test e la manutenzione. Tuttavia, l'inversione del controllo e l'iniezione di dipendenza non sono la stessa cosa: ci sono sottili differenze tra i due.

In questo articolo esamineremo l'inversione del pattern di controllo e capiremo in che modo differisce dall'inserimento di dipendenze con esempi di codice pertinenti in C #.

Per lavorare con gli esempi di codice forniti in questo articolo, dovresti avere Visual Studio 2019 installato nel tuo sistema. Se non hai già una copia, puoi scaricare Visual Studio 2019 qui. 

Crea un progetto di applicazione console in Visual Studio

Prima di tutto, creiamo un progetto di applicazione console .NET Core in Visual Studio. Supponendo che Visual Studio 2019 sia installato nel sistema, seguire i passaggi descritti di seguito per creare un nuovo progetto di applicazione console .NET Core in Visual Studio.

  1. Avvia l'IDE di Visual Studio.
  2. Fai clic su "Crea nuovo progetto".
  3. Nella finestra "Crea nuovo progetto", seleziona "App Console (.NET Core)" dall'elenco dei modelli visualizzati.
  4. Fare clic su Avanti. 
  5. Nella finestra "Configura il tuo nuovo progetto" mostrata di seguito, specifica il nome e la posizione per il nuovo progetto.
  6. Fare clic su Crea. 

Questo creerà un nuovo progetto di applicazione console .NET Core in Visual Studio 2019. Useremo questo progetto per esplorare l'inversione del controllo nelle sezioni successive di questo articolo.

Cos'è l'inversione di controllo?

Inversion of control (IoC) è un modello di progettazione in cui il flusso di controllo di un programma viene invertito. È possibile sfruttare l'inversione del pattern di controllo per disaccoppiare i componenti dell'applicazione, scambiare le implementazioni delle dipendenze, simulare le dipendenze e rendere l'applicazione modulare e testabile.

L'inserimento delle dipendenze è un sottoinsieme del principio di inversione del controllo. In altre parole, l'inserimento delle dipendenze è solo un modo per implementare l'inversione del controllo. È inoltre possibile implementare l'inversione del controllo utilizzando eventi, delegati, modello di modello, metodo di fabbrica o localizzatore di servizi, ad esempio.

L'inversione del modello di progettazione del controllo afferma che gli oggetti non devono creare oggetti da cui dipendono per eseguire alcune attività. Invece, dovrebbero ottenere quegli oggetti da un servizio esterno o da un contenitore. L'idea è analoga al principio di Hollywood che dice: "Non chiamarci, ti chiameremo". Ad esempio, invece dell'applicazione che chiama i metodi in un framework, il framework chiamerà l'implementazione che è stata fornita dall'applicazione. 

Esempio di inversione del controllo in C #

Si supponga di creare un'applicazione di elaborazione degli ordini e di voler implementare la registrazione. Per semplicità, supponiamo che la destinazione del log sia un file di testo. Seleziona il progetto dell'applicazione console appena creato nella finestra Esplora soluzioni e crea due file, denominati ProductService.cs e FileLogger.cs.

    Public class ProductService

    {

        privato in sola lettura FileLogger _fileLogger = nuovo FileLogger ();

        public void Log (stringa messaggio)

        {

            _fileLogger.Log (messaggio);

        }

    }

    FileLogger di classe pubblica

    {

        public void Log (stringa messaggio)

        {

            Console.WriteLine ("Metodo Inside Log di FileLogger.");

            LogToFile (messaggio);

        }

        void privato LogToFile (messaggio di stringa)

        {

            Console.WriteLine ("Metodo: LogToFile, Testo: {0}", messaggio);

        }

    }

L'implementazione mostrata nello snippet di codice precedente è corretta ma è presente una limitazione. Sei vincolato a registrare i dati solo in un file di testo. Non è possibile in alcun modo registrare i dati su altre origini dati o destinazioni di registro diverse.

Un'implementazione inflessibile della registrazione

E se volessi registrare i dati in una tabella di database? L'implementazione esistente non lo supporterebbe e saresti costretto a cambiare l'implementazione. È possibile modificare l'implementazione della classe FileLogger o creare una nuova classe, ad esempio DatabaseLogger.

    DatabaseLogger della classe pubblica

    {

        public void Log (stringa messaggio)

        {

            Console.WriteLine ("Metodo Inside Log di DatabaseLogger.");

            LogToDatabase (messaggio);

        }

        private void LogToDatabase (messaggio di stringa)

        {

            Console.WriteLine ("Metodo: LogToDatabase, Testo: {0}", messaggio);

        }

    }

Potresti persino creare un'istanza della classe DatabaseLogger all'interno della classe ProductService come mostrato nello snippet di codice di seguito.

Public class ProductService

    {

        privato in sola lettura FileLogger _fileLogger = nuovo FileLogger ();

        privato in sola lettura DatabaseLogger _databaseLogger =

         new DatabaseLogger ();

        public void LogToFile (messaggio di stringa)

        {

            _fileLogger.Log (messaggio);

        }

        public void LogToDatabase (messaggio di stringa)

        {

            _fileLogger.Log (messaggio);

        }

    }

Tuttavia, anche se questo funzionerebbe, cosa succederebbe se avessi bisogno di registrare i dati della tua applicazione su EventLog? Il tuo design non è flessibile e sarai costretto a cambiare la classe ProductService ogni volta che devi accedere a una nuova destinazione di log. Questo non è solo ingombrante, ma ti renderà anche estremamente difficile gestire la classe ProductService nel tempo.

Aggiungi flessibilità con un'interfaccia 

La soluzione a questo problema è utilizzare un'interfaccia che le classi di logger concrete implementerebbero. Il frammento di codice seguente mostra un'interfaccia chiamata ILogger. Questa interfaccia sarebbe implementata dalle due classi concrete FileLogger e DatabaseLogger.

interfaccia pubblica ILogger

{

    void Log (messaggio di stringa);

}

Di seguito vengono fornite le versioni aggiornate delle classi FileLogger e DatabaseLogger.

FileLogger di classe pubblica: ILogger

    {

        public void Log (stringa messaggio)

        {

            Console.WriteLine ("Metodo Inside Log di FileLogger.");

            LogToFile (messaggio);

        }

        void privato LogToFile (messaggio di stringa)

        {

            Console.WriteLine ("Metodo: LogToFile, Testo: {0}", messaggio);

        }

    }

Classe pubblica DatabaseLogger: ILogger

    {

        public void Log (stringa messaggio)

        {

            Console.WriteLine ("Metodo Inside Log di DatabaseLogger.");

            LogToDatabase (messaggio);

        }

        private void LogToDatabase (messaggio di stringa)

        {

            Console.WriteLine ("Metodo: LogToDatabase, Testo: {0}", messaggio);

        }

    }

È ora possibile utilizzare o modificare l'implementazione concreta dell'interfaccia ILogger ogni volta che è necessario. Il frammento di codice seguente mostra la classe ProductService con un'implementazione del metodo Log.

Public class ProductService

    {

        public void Log (stringa messaggio)

        {

            Logger ILogger = nuovo FileLogger ();

            logger.Log (messaggio);

        }

    }

Fin qui tutto bene. Tuttavia, cosa succede se si desidera utilizzare DatabaseLogger al posto di FileLogger nel metodo Log della classe ProductService? È possibile modificare l'implementazione del metodo Log nella classe ProductService per soddisfare il requisito, ma ciò non rende la progettazione flessibile. Rendiamo ora il design più flessibile utilizzando l'inversione del controllo e l'inserimento delle dipendenze.

Invertire il controllo utilizzando l'inserimento delle dipendenze

Il frammento di codice seguente illustra come sfruttare l'inserimento delle dipendenze per passare un'istanza di una classe logger concreta utilizzando l'inserimento del costruttore.

Public class ProductService

    {

        privato in sola lettura ILogger _logger;

        public ProductService (ILogger logger)

        {

            _logger = logger;

        }

        public void Log (stringa messaggio)

        {

            _logger.Log (messaggio);

        }

    }

Infine, vediamo come passare un'implementazione dell'interfaccia ILogger alla classe ProductService. Il frammento di codice seguente mostra come creare un'istanza della classe FileLogger e usare l'inserimento del costruttore per passare la dipendenza.

static void Main (string [] args)

{

    Logger ILogger = nuovo FileLogger ();

    ProductService productService = nuovo ProductService (logger);

    productService.Log ("Hello World!");

}

In tal modo, abbiamo invertito il controllo. La classe ProductService non è più responsabile della creazione di un'istanza di un'implementazione dell'interfaccia ILogger o della decisione sull'implementazione dell'interfaccia ILogger da utilizzare.

L'inversione del controllo e l'inserimento delle dipendenze ti aiutano con la creazione automatica di istanze e la gestione del ciclo di vita dei tuoi oggetti. ASP.NET Core include una semplice inversione incorporata del contenitore dei controlli con un set limitato di funzionalità. Puoi utilizzare questo contenitore IoC integrato se le tue esigenze sono semplici o utilizzare un contenitore di terze parti se desideri sfruttare funzionalità aggiuntive.

Puoi leggere ulteriori informazioni su come lavorare con l'inversione del controllo e l'inserimento delle dipendenze in ASP.NET Core nel mio post precedente qui.