Injeção de dependência
Dica
Esse conteúdo é um trecho do livro eletrônico, Padrões de Aplicativo Empresarial Usando .NETMAUI, disponível em .NET Docs ou em PDF para download gratuito que pode ser lido off-line.
Normalmente, um construtor de classe é invocado ao instanciar um objeto e todos os valores necessários pelo objeto são passados como argumentos para o construtor. Este é um exemplo de injeção de dependência conhecida como injeção de construtor. As dependências de que o objeto precisa são injetadas no construtor.
Ao especificar dependências como tipos de interface, a injeção de dependência permite desacoplar os tipos concretos do código que depende desses tipos. Geralmente, ele usa um contêiner que contém uma lista de registros e mapeamentos entre interfaces e tipos abstratos e os tipos concretos que implementam ou estendem esses tipos.
Há também outros tipos de injeção de dependência, como injeção de setter de propriedade e injeção de chamada de método, mas eles são menos comumente vistos. Portanto, este capítulo se concentrará apenas na execução da injeção de construtor com um contêiner de injeção de dependência.
Introdução à injeção de dependência
A injeção de dependência é uma versão especializada do padrão de IoC (Inversão de Controle), em que a preocupação que está sendo invertida é o processo de obtenção da dependência necessária. Com a injeção de dependência, outra classe é responsável por injetar dependências em um objeto em runtime. O exemplo de código a seguir mostra como a classe ProfileViewModel
é estruturada ao usar a injeção de dependência:
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
}
O construtor ProfileViewModel
recebe várias instâncias de objeto de interface como argumentos injetados por outra classe. A única dependência na classe ProfileViewModel
é nos tipos de interface. Portanto, a classe ProfileViewModel
não tem nenhum conhecimento da classe responsável por instanciar os objetos de interface. A classe responsável por instanciar os objetos de interface e inseri-los na ProfileViewModel
classe é conhecida como contêiner de injeção de dependência.
Os contêineres de injeção de dependência reduzem o acoplamento entre objetos fornecendo uma instalação para instanciar instâncias de classe e gerenciar seu tempo de vida com base na configuração do contêiner. Durante a criação do objeto, o contêiner injeta todas as dependências que o objeto requer nele. Se essas dependências ainda não tiverem sido criadas, o contêiner criará e resolverá suas dependências primeiro.
Há várias vantagens em usar um contêiner de injeção de dependência:
- Um contêiner remove a necessidade de uma classe localizar suas dependências e gerenciar seus tempos de vida.
- Um contêiner permite o mapeamento de dependências implementadas sem afetar a classe.
- Um contêiner facilita a capacidade de teste permitindo que as dependências sejam simuladas.
- Um contêiner aumenta a capacidade de manutenção, permitindo que novas classes sejam facilmente adicionadas ao aplicativo.
No contexto de um aplicativo .NET MAUI que usa MVVM, um contêiner de injeção de dependência normalmente será usado para registrar e resolver exibições, registrar e resolver modelos de exibição e para registrar serviços e injetá-los em modelos de exibição.
Há muitos contêineres de injeção de dependência disponíveis no .NET; o aplicativo multiplataforma eShop usa Microsoft.Extensions.DependencyInjection
para gerenciar a instanciação de exibições, modelos de exibição e classes de serviço no aplicativo. Microsoft.Extensions.DependencyInjection
facilita a criação de aplicativos acoplados de forma flexível e fornece todos os recursos comumente encontrados em contêineres de injeção de dependência, incluindo métodos para registrar mapeamentos de tipo e instâncias de objeto, resolver objetos, gerenciar tempos de vida de objetos e injetar objetos dependentes em construtores de objetos que ele resolve. Para mais informações sobre Microsoft.Extensions.DependencyInjection
confira Injeção de dependência no .NET.
No .NET MAUI, a classe MauiProgram
chamará o método CreateMauiApp
para criar um objeto MauiAppBuilder
. O objeto MauiAppBuilder
tem uma propriedade Services
do tipo IServiceCollection
, que fornece um local para registrar nossos componentes, como exibições, modelos de exibição e serviços para injeção de dependência. Todos os componentes registrados com a propriedade Services
serão fornecidos ao contêiner de injeção de dependência quando o método MauiAppBuilder.Build
for chamado.
Em runtime, o contêiner deve saber qual implementação dos serviços está sendo solicitada para instanciá-los para os objetos solicitados. No aplicativo multiplataforma eShop, as interfaces IAppEnvironmentService
, IDialogService
, INavigationService
e ISettingsService
precisam ser resolvidas antes de criar uma instância de um objeto ProfileViewModel
. Isso envolve o contêiner executando as seguintes ações:
- Decidindo como instanciar um objeto que implementa a interface. Isso é conhecido como registro.
- Instanciando o objeto que implementa a interface necessária e o objeto
ProfileViewModel
. Isso é conhecido como resolução.
Por fim, o aplicativo terminará de usar o objeto ProfileViewModel
e ele ficará disponível para coleta de lixo. Neste ponto, o coletor de lixo deverá descartar quaisquer implementações de interface de curta duração se outras classes não compartilharem a mesma instância.
Registro
Antes que as dependências possam ser injetadas em um objeto, os tipos das dependências devem primeiro ser registrados com o contêiner. Registrar um tipo envolve passar ao contêiner uma interface e um tipo concreto que implementa a interface.
Há duas maneiras de registrar tipos e objetos no contêiner por meio do código:
- Registre um tipo ou mapeamento com o contêiner. Isso é conhecido como registro transitório. Quando necessário, o contêiner criará uma instância do tipo especificado.
- Registre um objeto existente no contêiner como um singleton. Quando necessário, o contêiner retornará uma referência ao objeto existente.
Observação
Os contêineres de injeção de dependência nem sempre são adequados. A injeção de dependência introduz complexidade e requisitos adicionais que podem não ser apropriados ou úteis para aplicativos pequenos. Se uma classe não tiver dependências ou não for uma dependência para outros tipos, talvez não faça sentido colocá-la no contêiner. Além disso, se uma classe tiver um único conjunto de dependências que sejam integrais ao tipo e nunca forem alteradas, talvez não faça sentido colocá-la no contêiner.
O registro de tipos que exigem injeção de dependência deve ser executado em um único método em um aplicativo. Esse método deve ser invocado no início do ciclo de vida do aplicativo para garantir que ele esteja ciente das dependências entre suas classes. O aplicativo multiplataforma eShop executa esse método MauiProgram.CreateMauiApp
. O exemplo de código a seguir mostra como o aplicativo multiplataforma eShop declara o CreateMauiApp
na classe MauiProgram
:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
=> MauiApp.CreateBuilder()
.UseMauiApp<App>()
// Omitted for brevity
.RegisterAppServices()
.RegisterViewModels()
.RegisterViews()
.Build();
}
O método MauiApp.CreateBuilder
cria um objeto MauiAppBuilder
que podemos usar para registrar nossas dependências. Muitas dependências no aplicativo multiplataforma eShop precisam ser registradas, portanto, os métodos de extensão RegisterAppServices
, RegisterViewModels
e RegisterViews
foram criados para ajudar a fornecer um fluxo de trabalho de registro organizado e de fácil manutenção. O código a seguir mostra o 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;
}
Esse método recebe uma instância de MauiAppBuilder
e podemos usar a propriedade Services
para registrar nossos modelos de exibição. Dependendo das necessidades do aplicativo, talvez seja necessário adicionar serviços com tempos de vida diferentes. A tabela a seguir fornece informações sobre quando você pode querer escolher esses diferentes tempos de vida de registro:
Método | Descrição |
---|---|
AddSingleton<T> |
Criará uma única instância do objeto que permanecerá durante o tempo de vida do aplicativo. |
AddTransient<T> |
Criará uma nova instância do objeto quando solicitado durante a resolução. Objetos transitórios não têm um tempo de vida predefinido, mas normalmente seguirão o tempo de vida do host. |
Observação
Os modelos de exibição não herdam de uma interface, portanto, só precisam do tipo concreto fornecido aos métodos AddSingleton<T>
e AddTransient<T>
.
O CatalogViewModel
é usado próximo à raiz do aplicativo e deve estar sempre disponível, portanto, registrá-lo com AddSingleton<T>
é benéfico. Outros modelos de exibição, como CheckoutViewModel
e OrderDetailViewModel
navegam até ele conforme a situação ou são usados posteriormente no aplicativo. Suponha que você saiba que tem um componente que nem sempre pode ser usado. Nesse caso, se for memória, uso intensivo computacional ou exigir dados just-in-time, ele poderá ser um candidato melhor para o registro AddTransient<T>
.
Outra maneira comum de adicionar serviços é usando os métodos AddSingleton<TService, TImplementation>
e AddTransient<TService, TImplementation>
. Esses métodos têm dois tipos de entrada: a definição de interface e a implementação concreta. Esse tipo de registro é melhor para casos em que você está implementando serviços com base em interfaces. No exemplo de código abaixo, registramos nossa interface ISettingsService
usando a implementação SettingsService
:
public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder mauiAppBuilder)
{
mauiAppBuilder.Services.AddSingleton<ISettingsService, SettingsService>();
// Omitted for brevity...
}
Depois que todos os serviços forem registrados, o método MauiAppBuilder.Build
deverá ser chamado para criar nosso MauiApp
e preencher nosso contêiner de injeção de dependência com todos os serviços registrados.
Importante
Depois que o método Build
tiver sido chamado, o contêiner de injeção de dependência será imutável e não poderá mais ser atualizado ou modificado. Verifique se todos os serviços necessários em seu aplicativo foram registrados antes de você chamar Build
.
Resolução
Depois que um tipo é registrado, ele pode ser resolvido ou injetado como uma dependência. Quando um tipo está sendo resolvido e o contêiner precisa criar uma nova instância, ele injeta todas as dependências na instância.
Geralmente, quando um tipo é resolvido, uma das três coisas acontece:
- Se o tipo não tiver sido registrado, o contêiner gerará uma exceção.
- Se o tipo tiver sido registrado como singleton, o contêiner retornará a instância singleton. Se essa for a primeira vez que o tipo for chamado, o contêiner o criará se necessário e manterá uma referência a ele.
- Se o tipo tiver sido registrado como transitório, o contêiner retornará uma nova instância e não manterá uma referência a ela.
O .NET MAUI oferece várias maneiras de resolver componentes registrados com base em suas necessidades. A maneira mais direta de obter acesso ao contêiner de injeção de dependência é de um Element
usando o Handler.MauiContext.Services
. Um exemplo disso é mostrado abaixo:
var settingsService = this.Handler.MauiContext.Services.GetServices<ISettingsService>();
Isso poderá ser útil se você precisar resolver um serviço de dentro de um Element
ou de fora do construtor de seu Element
.
Cuidado
Há a possibilidade de que a propriedade Handler
de seu Element
possa ser nula, portanto, lembre-se de que talvez seja necessário lidar com essas situações. Para obter mais informações, confira Ciclo de vida do manipulador no Centro de Documentação da Microsoft.
Se estiver usando o controle Shell
para .NET MAUI, ele chamará implicitamente o contêiner de injeção de dependência para criar nossos objetos durante a navegação. Ao configurar nosso controle Shell
, o método Routing.RegisterRoute
vinculará um caminho de rota a um View
, conforme mostrado no exemplo abaixo:
Routing.RegisterRoute("Filter", typeof(FiltersView));
Durante a navegação do Shell
, ele procurará registros do FiltersView
e, se algum for encontrado, ele criará essa exibição e injetará dependências no construtor. Conforme mostrado no exemplo de código abaixo, o CatalogViewModel
será injetado no FiltersView
:
namespace eShop.Views;
public partial class FiltersView : ContentPage
{
public FiltersView(CatalogViewModel viewModel)
{
BindingContext = viewModel;
InitializeComponent();
}
}
Dica
O contêiner de injeção de dependência é ótimo para criar instâncias de modelo de exibição. Se um modelo de exibição tiver dependências, ele manipulará a criação e a injeção de todos os serviços necessários. Apenas certifique-se de registrar seus modelos de exibição e quaisquer dependências que eles possam ter com o método CreateMauiApp
na classe MauiProgram
.
Resumo
A injeção de dependência permite o desacoplamento de tipos concretos do código que depende desses tipos. Normalmente, ele usa um contêiner que contém uma lista de registros e mapeamentos entre interfaces e tipos abstratos e os tipos concretos que implementam ou estendem esses tipos.
Microsoft.Extensions.DependencyInjection
facilita a criação de aplicativos acoplados de forma flexível e fornece todos os recursos comumente encontrados em contêineres de injeção de dependência, incluindo métodos para registrar mapeamentos de tipo e instâncias de objeto, resolver objetos, gerenciar tempos de vida de objetos e injetar objetos dependentes em construtores de objetos que ele resolve.