Inserción de dependencias

Nota:

Este libro electrónico se publicó en la primavera de 2017 y no se ha actualizado desde entonces. Mucho contenido del libro que sigue siendo valioso, pero algunos de los materiales están obsoletos.

Normalmente, se invoca un constructor de clase al crear una instancia de un objeto, y los valores que el objeto necesita se pasan como argumentos al constructor. Este es un ejemplo de inserción de dependencias conocida en concreto como inserción de constructores. Las dependencias que necesita el objeto se insertan en el constructor.

Al especificar dependencias como tipos de interfaz, la inserción de dependencias permite desacoplar los tipos concretos del código que depende de estos tipos. Por lo general, usa un contenedor que contiene una lista de registros y asignaciones entre interfaces y tipos abstractos, y los tipos concretos que implementan o extienden estos tipos.

También hay otros tipos de inserción de dependencias, como la inserción de establecedores de propiedades y la inserción de llamadas de método, pero son menos frecuentes. Por lo tanto, este capítulo se centrará únicamente en la realización de la inserción de constructores con un contenedor de inserción de dependencias.

Introducción a la inserción de dependencias

La inserción de dependencias es una versión especializada del patrón de inversión de control (IoC), donde la preocupación que se invierte es el proceso de obtener la dependencia necesaria. Con la inserción de dependencias, otra clase es responsable de insertar dependencias en un objeto en tiempo de ejecución. En el ejemplo de código siguiente se muestra cómo se estructura la clase ProfileViewModel al usar la inserción de dependencias:

public class ProfileViewModel : ViewModelBase  
{  
    private IOrderService _orderService;  

    public ProfileViewModel(IOrderService orderService)  
    {  
        _orderService = orderService;  
    }  
    ...  
}

El constructor ProfileViewModel recibe una instancia de IOrderService como argumento, insertada por otra clase. La única dependencia de la clase ProfileViewModel es del tipo de interfaz. Por tanto, la clase ProfileViewModel no tiene ningún conocimiento de la clase responsable de crear instancias del objeto IOrderService. La clase responsable de crear instancias del objeto IOrderService y de insertarlas en la clase ProfileViewModel se conoce como contenedor de inserción de dependencias.

Los contenedores de inserción de dependencias reducen el acoplamiento entre objetos proporcionando un recurso para crear instancias de clase y administrar su duración en función de la configuración del contenedor. Durante la creación de objetos, el contenedor inserta en el objeto las as dependencias que necesita. Si esas dependencias aún no se han creado, el contenedor crea y resuelve primero sus dependencias.

Nota:

La inserción de dependencias también se puede implementar manualmente mediante generadores. Pero el uso de un contenedor proporciona funcionalidades adicionales, como la administración de la duración y el registro mediante el examen de ensamblados.

El uso de un contenedor de inserción de dependencias ofrece varias ventajas:

  • Un contenedor elimina la necesidad de que una clase localice sus dependencias y administre sus duraciones.
  • Un contenedor permite la asignación de dependencias implementadas sin afectar a la clase.
  • Un contenedor facilita la comprobación, ya que permite simular las dependencias.
  • Un contenedor aumenta la capacidad de mantenimiento al permitir que las nuevas clases se agreguen fácilmente a la aplicación.

En el contexto de una aplicación de Xamarin.Forms que usa MVVM, normalmente se usará un contenedor de inserción de dependencias para registrar y resolver modelos y para registrar servicios e insertarlos en modelos de vista.

Hay muchos contenedores de inserción de dependencias disponibles; la aplicación móvil eShopOnContainers usa TinyIoC para administrar la creación de instancias de clases de modelo de vista y servicio en la aplicación. Se ha elegido TinyIoC después de evaluar una serie de contenedores diferentes y ofrece un rendimiento superior en plataformas móviles en comparación con la mayoría de los contenedores conocidos. Facilita la creación de aplicaciones de acoplamiento flexible y proporciona todas las características que se encuentran normalmente en los contenedores de inserción de dependencias, incluidos métodos para registrar asignaciones de tipos, resolver objetos, administrar duraciones de objetos e insertar objetos dependientes en constructores de objetos que resuelve. Para más información sobre TinyIoC, vea TinyIoC en github.com.

En TinyIoC, el tipo TinyIoCContainer proporciona el contenedor de inserción de dependencias. En la figura 3-1 se muestran las dependencias al usar este contenedor, que crea una instancia de un objeto IOrderService y la inserta en la clase ProfileViewModel.

Dependencies example when using dependency injection

Figura 3-1: Dependencias al usar la inserción de dependencias

En tiempo de ejecución, el contenedor debe saber de qué implementación de la interfaz IOrderService debe crear una instancia, para poder crear instancias de un objeto ProfileViewModel. Esto implica:

  • El contenedor decide cómo crear una instancia de un objeto que implementa la interfaz IOrderService. Esto se conoce como registro.
  • El contenedor crear instancias del objeto que implementa la interfaz IOrderService y el objeto ProfileViewModel. Esto se conoce como resolución.

Al final, la aplicación terminará usando el objeto ProfileViewModel y estará disponible para la recolección de elementos no utilizados. En este momento, el recolector de elementos no utilizados debe eliminar la instancia de IOrderService si otras clases no la comparten.

Sugerencia

Escriba código independiente del contenedor. Intente siempre escribir código independiente del contenedor para desacoplar la aplicación del contenedor de dependencias específico que se usa.

Registro

Para que las dependencias se puedan insertar en un objeto, los tipos de las dependencias deben registrarse primero en el contenedor. El registro de un tipo normalmente implica pasar al contenedor una interfaz y un tipo concreto que implementa la interfaz.

Hay dos maneras de registrar tipos y objetos en el contenedor mediante código:

  • Registre un tipo o una asignación en el contenedor. Cuando sea necesario, el contenedor compilará una instancia del tipo especificado.
  • Registre un objeto existente en el contenedor como singleton. Cuando sea necesario, el contenedor devolverá una referencia al objeto existente.

Sugerencia

Los contenedores de inserción de dependencias no siempre son adecuados. La inserción de dependencias presenta una complejidad adicional y requisitos que podrían no ser adecuados o útiles para aplicaciones pequeñas. Si una clase no tiene dependencias o no es una dependencia para otros tipos, es posible que no tenga sentido colocarla en el contenedor. Además, si una clase tiene un único conjunto de dependencias que son integrales del tipo y nunca cambiará, es posible que no tenga sentido colocarla en el contenedor.

El registro de tipos que necesitan inserción de dependencias se debe realizar en un único método de una aplicación y este método se debe invocar al principio del ciclo de vida de la aplicación para asegurarse de que la aplicación sea consciente de las dependencias entre sus clases. En la aplicación móvil eShopOnContainers, esto se realiza mediante la clase ViewModelLocator, que compila el objeto TinyIoCContainer y es la única clase de la aplicación que contiene una referencia a ese objeto. En el siguiente ejemplo de código se muestra cómo la aplicación móvil eShopOnContainers declara el objeto TinyIoCContainer en la clase ViewModelLocator:

private static TinyIoCContainer _container;

Los tipos se registran en el constructor ViewModelLocator. Para ello, primero se crea una instancia de TinyIoCContainer, que se muestra en el ejemplo de código siguiente:

_container = new TinyIoCContainer();

Después, los tipos se registran con el objeto TinyIoCContainer y en el ejemplo de código siguiente se muestra la forma más común de registro de tipos:

_container.Register<IRequestProvider, RequestProvider>();

El método Register que se muestra aquí asigna un tipo de interfaz a un tipo concreto. De manera predeterminada, cada registro de interfaz se configura como singleton para que cada objeto dependiente reciba la misma instancia compartida. Por tanto, solo existirá una instancia de RequestProvider en el contenedor, que es compartida por objetos que necesitan una inserción de IRequestProvider mediante un constructor.

Los tipos concretos también se pueden registrar directamente sin una asignación desde un tipo de interfaz, como se muestra en el ejemplo de código siguiente:

_container.Register<ProfileViewModel>();

De manera predeterminada, cada registro de clase concreto se configura como una instancia múltiple para que cada objeto dependiente reciba una nueva instancia. Por tanto, cuando se resuelve ProfileViewModel, se creará una instancia y el contenedor insertará sus dependencias necesarias.

Solución

Una vez registrado un tipo, se puede resolver o insertar como una dependencia. Cuando se resuelve un tipo y el contenedor debe crear una instancia, inserta todas las dependencias en la instancia.

Por lo general, cuando se resuelve un tipo, sucede una de estas tres cosas:

  1. Si el tipo no se ha registrado, el contenedor produce una excepción.
  2. Si el tipo se ha registrado como singleton, el contenedor devuelve la instancia singleton. Si es la primera vez que se llama al tipo, el contenedor lo crea si es necesario y mantiene una referencia a él.
  3. Si el tipo se ha registrado como singleton, el contenedor devuelve una nueva instancia y no mantiene una referencia a ella.

En el ejemplo de código siguiente se muestra cómo se puede resolver el tipo RequestProvider que se ha registrado antes con TinyIoC:

var requestProvider = _container.Resolve<IRequestProvider>();

En este ejemplo, se le pide a TinyIoC que resuelva el tipo concreto del tipo IRequestProvider, junto con las dependencias. Normalmente, se llama al método Resolve cuando se necesita una instancia de un tipo específico. Para obtener información sobre cómo controlar la duración de los objetos resueltos, vea Administración de la duración de los objetos resueltos.

En el ejemplo de código siguiente se muestra cómo la aplicación móvil eShopOnContainers crea instancias de los tipos de modelo de vista y sus dependencias:

var viewModel = _container.Resolve(viewModelType);

En este ejemplo, se le pide a TinyIoC que resuelva el tipo de modelo de vista para un modelo de vista solicitado y el contenedor también resolverá las dependencias. Al resolver el tipo ProfileViewModel, las dependencias que se van a resolver son un objeto ISettingsService y un objeto IOrderService. Como se han usado los registros de interfaz al registrar las clases SettingsService y OrderService, TinyIoC devuelve instancias singleton para las clases SettingsService y OrderService, y después las pasa al constructor de la clase ProfileViewModel. Para más información sobre cómo la aplicación móvil eShopOnContainers construye modelos de vista y los asocia a vistas, vea Creación automática de un modelo de vista con un localizador de modelos de vista.

Nota:

El registro y la resolución de tipos con un contenedor supone un costo de rendimiento, que los contenedores usan reflexión para crear todos los tipos, especialmente si se reconstruyen las dependencias para cada navegación de página en la aplicación. Si hay muchas dependencias, o estas son muy amplias, el costo de la creación puede aumentar significativamente.

Administración de la duración de los objetos resueltos

Después de registrar un tipo mediante un registro de clase concreto, el comportamiento predeterminado de TinyIoC es crear una instancia del tipo registrado cada vez que se resuelve el tipo o cuando el mecanismo de dependencia inserta instancias en otras clases. En este escenario, el contenedor no contiene una referencia al objeto resuelto. Pero al registrar un tipo mediante el registro de interfaz, el comportamiento predeterminado de TinyIoC es administrar la duración del objeto como singleton. Por tanto, la instancia permanece en el ámbito mientras el contenedor está en el ámbito y se elimina cuando el contenedor sale del ámbito y se recopila como elemento no utilizado, o bien cuando el código elimina explícitamente el contenedor.

El comportamiento de registro de TinyIoC predeterminado se puede invalidar mediante los métodos AsSingleton y AsMultiInstance de la API fluida. Por ejemplo, el método AsSingleton se puede usar con el método Register para que el contenedor cree o devuelva una instancia singleton de un tipo al llamar al método Resolve. En el ejemplo de código siguiente se muestra cómo se le indica a TinyIoC que cree una instancia singleton de la clase LoginViewModel:

_container.Register<LoginViewModel>().AsSingleton();

La primera vez que se resuelve el tipo LoginViewModel, el contenedor crea un objeto LoginViewModel y mantiene una referencia a él. En las resoluciones posteriores de LoginViewModel, el contenedor devuelve una referencia al objeto LoginViewModel que se ha creado antes.

Nota:

Los tipos registrados como singleton se eliminan cuando se elimina el contenedor.

Resumen

La inserción de dependencias permite desacoplar tipos concretos del código que depende de estos tipos. Normalmente usa un contenedor que contiene una lista de registros y asignaciones entre interfaces y tipos abstractos, y los tipos concretos que implementan o extienden estos tipos.

TinyIoC es un contenedor ligero que ofrece un rendimiento superior en plataformas móviles en comparación con la mayoría de los contenedores conocidos. Facilita la creación de aplicaciones de acoplamiento flexible y proporciona todas las características que se encuentran normalmente en los contenedores de inserción de dependencias, incluidos métodos para registrar asignaciones de tipos, resolver objetos, administrar duraciones de objetos e insertar objetos dependientes en constructores de objetos que resuelve.