Cómo usar la inversión de control en C #

Tanto la inversión de control como la inyección de dependencias le permiten romper las dependencias entre los componentes de su aplicación y hacer que su aplicación sea más fácil de probar y mantener. Sin embargo, la inversión de control y la inyección de dependencia no son lo mismo: existen diferencias sutiles entre los dos.

En este artículo, examinaremos la inversión del patrón de control y entenderemos en qué se diferencia de la inyección de dependencia con ejemplos de código relevantes en C #.

Para trabajar con los ejemplos de código proporcionados en este artículo, debe tener Visual Studio 2019 instalado en su sistema. Si aún no tiene una copia, puede descargar Visual Studio 2019 aquí. 

Crear un proyecto de aplicación de consola en Visual Studio

En primer lugar, creemos un proyecto de aplicación de consola .NET Core en Visual Studio. Suponiendo que Visual Studio 2019 esté instalado en su sistema, siga los pasos que se describen a continuación para crear un nuevo proyecto de aplicación de consola .NET Core en Visual Studio.

  1. Inicie el IDE de Visual Studio.
  2. Haga clic en "Crear nuevo proyecto".
  3. En la ventana "Crear nuevo proyecto", seleccione "Aplicación de consola (.NET Core)" en la lista de plantillas que se muestran.
  4. Haga clic en Siguiente. 
  5. En la ventana "Configure su nuevo proyecto" que se muestra a continuación, especifique el nombre y la ubicación del nuevo proyecto.
  6. Haga clic en Crear. 

Esto creará un nuevo proyecto de aplicación de consola .NET Core en Visual Studio 2019. Usaremos este proyecto para explorar la inversión de control en las siguientes secciones de este artículo.

¿Qué es la inversión de control?

La inversión de control (IoC) es un patrón de diseño en el que se invierte el flujo de control de un programa. Puede aprovechar la inversión del patrón de control para desacoplar los componentes de su aplicación, intercambiar implementaciones de dependencia, simular dependencias y hacer que su aplicación sea modular y comprobable.

La inyección de dependencia es un subconjunto del principio de inversión del control. En otras palabras, la inyección de dependencia es solo una forma de implementar la inversión de control. También puede implementar la inversión de control utilizando eventos, delegados, patrón de plantilla, método de fábrica o localizador de servicios, por ejemplo.

La inversión del patrón de diseño de control establece que los objetos no deben crear objetos de los que dependan para realizar alguna actividad. En cambio, deberían obtener esos objetos de un servicio externo o un contenedor. La idea es análoga al principio de Hollywood que dice: "No nos llames, te llamaremos". Como ejemplo, en lugar de que la aplicación llame a los métodos en un marco, el marco llamaría a la implementación proporcionada por la aplicación. 

Ejemplo de inversión de control en C #

Suponga que está creando una aplicación de procesamiento de pedidos y le gustaría implementar el registro. En aras de la simplicidad, supongamos que el destino del registro es un archivo de texto. Seleccione el proyecto de la aplicación de consola que acaba de crear en la ventana del Explorador de soluciones y cree dos archivos, denominados ProductService.cs y FileLogger.cs.

    ProductService de clase pública

    {

        FileLogger privado de solo lectura _fileLogger = new FileLogger ();

        registro de vacío público (mensaje de cadena)

        {

            _fileLogger.Log (mensaje);

        }

    }

    FileLogger de clase pública

    {

        registro de vacío público (mensaje de cadena)

        {

            Console.WriteLine ("Método de registro interno de FileLogger.");

            LogToFile (mensaje);

        }

        Private void LogToFile (mensaje de cadena)

        {

            Console.WriteLine ("Método: LogToFile, Texto: {0}", mensaje);

        }

    }

La implementación que se muestra en el fragmento de código anterior es correcta, pero existe una limitación. Está limitado a registrar datos solo en un archivo de texto. De ninguna manera puede registrar datos en otras fuentes de datos o diferentes destinos de registro.

Una implementación inflexible de la tala

¿Y si quisiera registrar datos en una tabla de base de datos? La implementación existente no respaldaría esto y se vería obligado a cambiar la implementación. Puede cambiar la implementación de la clase FileLogger o puede crear una nueva clase, por ejemplo, DatabaseLogger.

    DatabaseLogger de clase pública

    {

        registro de vacío público (mensaje de cadena)

        {

            Console.WriteLine ("Método de registro interno de DatabaseLogger.");

            LogToDatabase (mensaje);

        }

        Private void LogToDatabase (mensaje de cadena)

        {

            Console.WriteLine ("Método: LogToDatabase, Texto: {0}", mensaje);

        }

    }

Incluso puede crear una instancia de la clase DatabaseLogger dentro de la clase ProductService como se muestra en el fragmento de código a continuación.

ProductService de clase pública

    {

        FileLogger privado de solo lectura _fileLogger = new FileLogger ();

        DatabaseLogger privado de solo lectura _databaseLogger =

         nuevo DatabaseLogger ();

        public void LogToFile (mensaje de cadena)

        {

            _fileLogger.Log (mensaje);

        }

        public void LogToDatabase (mensaje de cadena)

        {

            _fileLogger.Log (mensaje);

        }

    }

Sin embargo, aunque esto funcionaría, ¿qué pasaría si necesitara registrar los datos de su aplicación en EventLog? Su diseño no es flexible y se verá obligado a cambiar la clase ProductService cada vez que necesite iniciar sesión en un nuevo destino de registro. Esto no solo es engorroso, sino que también le resultará extremadamente difícil administrar la clase ProductService con el tiempo.

Agregue flexibilidad con una interfaz 

La solución a este problema es utilizar una interfaz que implementarían las clases de registradores concretos. El siguiente fragmento de código muestra una interfaz llamada ILogger. Esta interfaz sería implementada por las dos clases concretas FileLogger y DatabaseLogger.

interfaz pública ILogger

{

    Void Log (mensaje de cadena);

}

Las versiones actualizadas de las clases FileLogger y DatabaseLogger se proporcionan a continuación.

FileLogger de clase pública: ILogger

    {

        registro de vacío público (mensaje de cadena)

        {

            Console.WriteLine ("Método de registro interno de FileLogger.");

            LogToFile (mensaje);

        }

        Private void LogToFile (mensaje de cadena)

        {

            Console.WriteLine ("Método: LogToFile, Texto: {0}", mensaje);

        }

    }

DatabaseLogger de clase pública: ILogger

    {

        registro de vacío público (mensaje de cadena)

        {

            Console.WriteLine ("Método de registro interno de DatabaseLogger.");

            LogToDatabase (mensaje);

        }

        Private void LogToDatabase (mensaje de cadena)

        {

            Console.WriteLine ("Método: LogToDatabase, Texto: {0}", mensaje);

        }

    }

Ahora puede usar o cambiar la implementación concreta de la interfaz ILogger siempre que sea necesario. El siguiente fragmento de código muestra la clase ProductService con una implementación del método Log.

ProductService de clase pública

    {

        registro de vacío público (mensaje de cadena)

        {

            ILogger logger = nuevo FileLogger ();

            logger.Log (mensaje);

        }

    }

Hasta aquí todo bien. Sin embargo, ¿qué sucede si desea utilizar DatabaseLogger en lugar de FileLogger en el método Log de la clase ProductService? Puede cambiar la implementación del método Log en la clase ProductService para cumplir con el requisito, pero eso no hace que el diseño sea flexible. Hagamos ahora el diseño más flexible mediante la inversión de control y la inyección de dependencia.

Invertir el control mediante inyección de dependencia

El siguiente fragmento de código ilustra cómo puede aprovechar la inyección de dependencia para pasar una instancia de una clase de registrador concreta mediante la inyección de constructor.

ProductService de clase pública

    {

        ILogger _logger privado de solo lectura;

        ProductService público (registrador ILogger)

        {

            _logger = registrador;

        }

        registro de vacío público (mensaje de cadena)

        {

            _logger.Log (mensaje);

        }

    }

Por último, veamos cómo podemos pasar una implementación de la interfaz ILogger a la clase ProductService. El siguiente fragmento de código muestra cómo puede crear una instancia de la clase FileLogger y usar la inyección del constructor para pasar la dependencia.

static void Main (cadena [] argumentos)

{

    ILogger logger = nuevo FileLogger ();

    ProductService productService = new ProductService (registrador);

    productService.Log ("¡Hola mundo!");

}

Al hacerlo, hemos invertido el control. La clase ProductService ya no es responsable de crear una instancia de una implementación de la interfaz ILogger o incluso de decidir qué implementación de la interfaz ILogger debe usarse.

La inversión de control y la inyección de dependencia lo ayudan con la creación automática de instancias y la administración del ciclo de vida de sus objetos. ASP.NET Core incluye un contenedor de inversión de control simple e integrado con un conjunto limitado de características. Puede utilizar este contenedor de IoC integrado si sus necesidades son simples o utilizar un contenedor de terceros si desea aprovechar funciones adicionales.

Puede leer más sobre cómo trabajar con inversión de control e inyección de dependencia en ASP.NET Core en mi publicación anterior aquí.