Compartir a través de


Inserción de dependencia

Sugerencia

Este contenido es un extracto del libro electrónico "Patrones de aplicaciones empresariales con .NET MAUI", disponible en Documentación de .NET o como un PDF descargable y gratuito que se puede leer sin conexión.

Miniatura de la portada del libro electrónico

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 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 establecedor 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:

private readonly ISettingsService _settingsService;
private readonly IAppEnvironmentService _appEnvironmentService;

public ProfileViewModel(
    IAppEnvironmentService appEnvironmentService,
    IDialogService dialogService, 
    INavigationService navigationService, 
    ISettingsService settingsService)
    : base(dialogService, navigationService, settingsService)
{
    _appEnvironmentService = appEnvironmentService;
    _settingsService = settingsService;

    // Omitted for brevity
}

El constructor ProfileViewModel recibe varias instancias de objeto de interfaz como argumentos insertados por otra clase. La única dependencia de la clase ProfileViewModel es de los tipos de interfaz. Por lo tanto, la clase ProfileViewModel no tiene ningún conocimiento de la clase responsable de crear instancias de los objetos de interfaz. La clase responsable de crear instancias de los objetos de interfaz 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 las dependencias que el objeto requiere. Si esas dependencias aún no se han creado, el contenedor crea y resuelve primero sus dependencias.

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 MAUI de .NET que usa MVVM, normalmente se usará un contenedor de inserción de dependencias para registrar y resolver vistas, registrar y resolver modelos de vista, y para registrar servicios e insertarlos en modelos de vista.

Hay muchos contenedores de inserción de dependencias disponibles en .NET; la aplicación multiplataforma eShop usa Microsoft.Extensions.DependencyInjection para administrar la creación de instancias de vistas, modelos de vista y clases de servicio en la aplicación. Microsoft.Extensions.DependencyInjection facilita la creación de aplicaciones de acoplamiento flexible y proporciona todas las características que se encuentran normalmente en contenedores de inserción de dependencias, incluidos métodos para registrar asignaciones de tipos e instancias de objeto, resolver objetos, administrar duraciones de objetos e insertar objetos dependientes en constructores de objetos que resuelve. Para más información sobre Microsoft.Extensions.DependencyInjection, vea Inserción de dependencias en .NET.

En .NET MAUI, la clase MauiProgram llamará al método CreateMauiApp para crear un objeto MauiAppBuilder. El objeto MauiAppBuilder tiene una propiedad Services de tipo IServiceCollection, que proporciona una ubicación para registrar nuestros componentes, como vistas, modelos de vista y servicios para la inserción de dependencias. Los componentes registrados con la propiedad Services se proporcionarán al contenedor de inserción de dependencias cuando se llame al método MauiAppBuilder.Build.

En tiempo de ejecución, el contenedor debe saber qué implementación de los servicios se solicita para crear instancias de ellos para los objetos solicitados. En la aplicación multiplataforma eShop, las interfaces IAppEnvironmentService, IDialogService , INavigationService y ISettingsService deben resolverse para poder crear instancias de un objeto ProfileViewModel. Esto implica que el contenedor realice las siguientes acciones:

  • Decidir cómo crear una instancia de un objeto que implementa la interfaz. Esto se conoce como registro.
  • Crear instancias del objeto que implementa la interfaz necesaria 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 las implementaciones de interfaz de corta duración si otras clases no comparten la misma instancia.

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 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. Esto se conoce como registro transitorio. 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.

Nota:

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 requieren inserción de dependencias debe realizarse en un único método de una aplicación. Este método se debe invocar al principio del ciclo de vida de la aplicación para asegurarse de que conoce las dependencias entre sus clases. La aplicación multiplataforma eShop ejecuta este método MauiProgram.CreateMauiApp. En el siguiente ejemplo de código se muestra cómo la aplicación multiplataforma eShop declara CreateMauiApp en la clase MauiProgram:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
        => MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            // Omitted for brevity            
            .RegisterAppServices()
            .RegisterViewModels()
            .RegisterViews()
            .Build();
}

El método MauiApp.CreateBuilder crea un objeto MauiAppBuilder que podemos usar para registrar nuestras dependencias. Deben registrarse muchas dependencias de la aplicación multiplataforma eShop, por lo que los métodos de extensión RegisterAppServices, RegisterViewModels y RegisterViews se crearon para ayudar a proporcionar un flujo de trabajo de registro organizado y fácil de mantener. En el código siguiente se muestra el método RegisterViewModels:

public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddSingleton<ViewModels.MainViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.LoginViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.BasketViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.CatalogViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.ProfileViewModel>();

    mauiAppBuilder.Services.AddTransient<ViewModels.CheckoutViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.OrderDetailViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.SettingsViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.CampaignViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.CampaignDetailsViewModel>();

    return mauiAppBuilder;
}

Este método recibe una instancia de MauiAppBuilder, y podemos usar la propiedad Services para registrar los modelos de vista. En función de las necesidades de la aplicación, es posible que tenga que agregar servicios con diferentes duraciones. En la tabla siguiente se proporciona información sobre cuándo puede elegir estas diferentes duraciones de registro:

Method Descripción
AddSingleton<T> Creará una única instancia del objeto que permanecerá durante la vigencia de la aplicación.
AddTransient<T> Creará una instancia del objeto cuando se solicite durante la resolución. Los objetos transitorios no tienen una duración predefinida, pero normalmente durarán lo mismo que su host.

Nota:

Los modelos de vista no heredan de una interfaz, por lo que solo necesitan su tipo concreto proporcionado a los métodos AddSingleton<T> y AddTransient<T>.

CatalogViewModel se usa cerca de la raíz de la aplicación y siempre debe estar disponible, por lo que es beneficioso registrarlo en AddSingleton<T>. Se navega a otros modelos de vista, como CheckoutViewModel y OrderDetailViewModel, en función de la situación, o bien se usan más tarde en la aplicación. Supongamos que sabe que tiene un componente que puede no usarse siempre. En ese caso, si requiere un uso intensivo de la memoria o de cálculo o requiere datos Just-In-Time, puede ser un mejor candidato para el registro de AddTransient<T>.

Otra manera común de agregar servicios es usar los métodos AddSingleton<TService, TImplementation> y AddTransient<TService, TImplementation>. Estos métodos usan dos tipos de entrada: la definición de interfaz y la implementación concreta. Este tipo de registro es más conveniente para los casos en los que se implementan servicios basados en interfaces. En el ejemplo de código siguiente, registramos nuestra interfaz ISettingsService mediante la implementación de SettingsService:

public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddSingleton<ISettingsService, SettingsService>();
    // Omitted for brevity...
}

Una vez registrados todos los servicios, se debe llamar al método MauiAppBuilder.Build para crear nuestro MauiApp y rellenar nuestro contenedor de inserción de dependencias con todos los servicios registrados.

Importante

Una vez que se ha llamado al método Build, el contenedor de inserción de dependencias es inmutable y ya no se puede actualizar ni modificar. Asegúrese de que todos los servicios que necesite dentro de la aplicación se hayan registrado antes de llamar a Build.

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 transitorio, el contenedor devuelve una nueva instancia y no mantiene una referencia a él.

.NET MAUI ofrece varias maneras de resolver los componentes registrados en función de sus necesidades. La manera más directa de acceder al contenedor de inserción de dependencias es a partir de un elemento Element mediante Handler.MauiContext.Services. A continuación se muestra un ejemplo:

var settingsService = this.Handler.MauiContext.Services.GetServices<ISettingsService>();

Esto puede resultar útil si necesita resolver un servicio desde dentro de Element o desde fuera del constructor de Element.

Precaución

Existe la posibilidad de que la propiedad Handler de su Element sea null, por lo que debe tener en cuenta que es posible que tenga que controlar esas situaciones. Para obtener más información, consulte Ciclo de vida del controlador en el Centro de documentación de Microsoft.

Si usa el control Shell para .NET MAUI, llamará implícitamente al contenedor de inserción de dependencias para crear los objetos durante la navegación. Al configurar nuestro control Shell, el método Routing.RegisterRoute vinculará una ruta de acceso a View como se muestra en el ejemplo siguiente:

Routing.RegisterRoute("Filter", typeof(FiltersView));

Durante la navegación por Shell, buscará registros de FiltersView y, si se encuentra alguno, creará esa vista e insertará las dependencias en el constructor. Como se muestra en el ejemplo de código siguiente, CatalogViewModel se insertará en FiltersView:

namespace eShop.Views;

public partial class FiltersView : ContentPage
{
    public FiltersView(CatalogViewModel viewModel)
    {
        BindingContext = viewModel;

        InitializeComponent();
    }
}

Sugerencia

El contenedor de inserción de dependencias es ideal para crear instancias de modelo de vista. Si un modelo de vista tiene dependencias, controlará la creación e inserción de todos los servicios necesarios. Solo tiene que asegurarse de registrar los modelos de vista y las dependencias que puedan tener con el método CreateMauiApp en la clase MauiProgram.

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.

Microsoft.Extensions.DependencyInjection facilita la creación de aplicaciones de acoplamiento flexible y proporciona todas las características que se encuentran normalmente en contenedores de inserción de dependencias, incluidos métodos para registrar asignaciones de tipos e instancias de objeto, resolver objetos, administrar duraciones de objetos e insertar objetos dependientes en los constructores de objetos que resuelve.