Descomplicando o Mundo da Programação - Um Guia sobre Dependency Injection, ciclo de vida Singleton, Scoped e Transient em .NET.

brunopizol

Bruno Pizol Camargo

Posted on February 23, 2024

Descomplicando o Mundo da Programação - Um Guia sobre Dependency Injection, ciclo de vida Singleton, Scoped e Transient em .NET.

Você durante sua caminhada pelo vasto universo da programação, provavelmente já esbarrou em termos como "Dependency Injection", "Singleton", "Scoped" e "Transient". Calma, não precisa entrar em pânico! Neste artigo, vamos desbravar esses conceitos sem firulas e numa linguagem que até a máquina de café vai entender. Prepare-se para uma jornada no mundo do desenvolvimento de software, onde vamos desvendar os segredos por trás dessas palavras que parecem complicadas, mas são mais amigas do que você imagina. Vamos nessa! 🚀

O que é dependency Injection?

Dependency Injection é um padrão de design no desenvolvimento de software, no qual as dependências de uma classe são fornecidas de fora, em vez de serem criadas dentro da própria classe. Esse método promove a flexibilidade, manutenibilidade e testabilidade do código.

Imagina que você está construindo um jogo de videogame:

No jogo, você tem diferentes personagens, monstros e armas, certo? Agora, imagine que cada um desses personagens e objetos precisa de coisas para funcionar, como energia, habilidades especiais e até mesmo uma espada mágica.

Em vez de construir todas essas coisas dentro de cada personagem ou objeto, o que pode ficar bagunçado, a Dependency Injection é como chamar alguém (um "injetor") para entregar essas coisas quando cada personagem ou objeto precisa delas.

Resumindo:

Dependency Injection é como ter um assistente (ou "injetor") que organiza e fornece tudo o que seus personagens e objetos precisam para funcionar no jogo, para que você possa se concentrar em jogar sem se preocupar muito com os detalhes complicados de como cada coisa é feita. É uma maneira de tornar as coisas mais fáceis e organizadas no mundo da criação de jogos (ou qualquer outro tipo de software)!

Image description

Exemplo Prático de Dependency Injection:

Imagine que você está desenvolvendo uma aplicação web e precisa de um serviço de log para registrar eventos importantes. Utilizando Dependency Injection, você pode criar uma interface ILogService e implementações específicas para diferentes ciclos de vida:

public interface ILogService
{
    void Log(string message);
}

public class SingletonLogService : ILogService
{
    public void Log(string message)
    {
        // Implementação do log para Singleton
    }
}

public class ScopedLogService : ILogService
{
    public void Log(string message)
    {
        // Implementação do log para Scoped
    }
}

public class TransientLogService : ILogService
{
    public void Log(string message)
    {
        // Implementação do log para Transient
    }
}

Enter fullscreen mode Exit fullscreen mode

No momento do registro do serviço na injeção de dependência, você pode escolher o ciclo de vida desejado:

// Registro do serviço Singleton
services.AddSingleton<ILogService, SingletonLogService>();

// Registro do serviço Scoped
services.AddScoped<ILogService, ScopedLogService>();

// Registro do serviço Transient
services.AddTransient<ILogService, TransientLogService>();

Enter fullscreen mode Exit fullscreen mode

Assim, ao longo do desenvolvimento, você pode escolher o ciclo de vida mais apropriado para o serviço de log, dependendo dos requisitos de sua aplicação.

Vantagens da Dependency Injection:

  • Desacoplamento: O uso de DI reduz o acoplamento entre os componentes da aplicação, tornando o código mais modular e fácil de entender.
  • Facilidade de Testes: Injetar dependências torna mais fácil substituir implementações reais por versões de teste (mocks) durante os testes unitários.
  • Flexibilidade: Permite alterar o comportamento de componentes da aplicação sem modificar o código fonte, facilitando a manutenção e a evolução do sistema.
  • Reutilização de Componentes: Componentes podem ser reutilizados em diferentes contextos, uma vez que as dependências podem ser facilmente trocadas.

Ciclo de vida Singleton

Singleton é um padrão de design de software que garante a existência de apenas uma instância de uma classe em todo o programa e fornece um ponto global de acesso a essa instância. Em outras palavras, ele assegura que, independentemente de quantas vezes seja necessário, haverá apenas uma única ocorrência da classe durante a execução do programa.

Imagine que você tem uma caixa especial onde guarda todas as suas coisas mais importantes, tipo um cofre só seu. Agora, o Singleton é um pouco como ter um superpoder que faz com que só exista uma caixa dessas no mundo todo. Ou seja, não importa quantas vezes você pedir, sempre vai ser a mesma caixa.

Resumindo

Em termos de programação, o Singleton é um jeito de garantir que, não importa quantas vezes você precise usar algo (como uma caixa especial para guardar informações), só vai existir uma versão dessa "caixa" durante todo o programa. Isso é útil quando você quer ter certeza de que certas informações importantes são sempre as mesmas, não importa onde você esteja no código. É tipo ter um superpoder de organização para manter tudo no lugar certo!

Image description

Padrão singleton vs ciclo de vida Singleton

O termo "singleton" pode ser usado de duas maneiras relacionadas, mas diferentes, dependendo do contexto: o padrão de design Singleton e o ciclo de vida Singleton usado em injeção de dependência.

  1. Padrão de Design Singleton:

    • Definição: O padrão de design Singleton é um padrão de criação que garante que uma classe tenha apenas uma instância e fornece um ponto global de acesso a essa instância.
    • Características Principais:
      • Um construtor privado para evitar instancias externas.
      • Uma propriedade ou método estático que retorna a única instância da classe.
      • A instância é criada apenas quando necessário e, uma vez criada, é reutilizada.
  2. Ciclo de Vida Singleton (Injeção de Dependência):

    • Definição: No contexto da injeção de dependência (DI), o ciclo de vida Singleton refere-se à duração da existência de uma instância de um serviço específico gerenciado pelo contêiner de DI.
    • Características Principais:
      • Uma única instância do serviço é criada e compartilhada por todas as dependências que solicitam esse serviço.
      • A instância persiste durante a vida do contêiner de injeção de dependência.

Diferenças:

  • O padrão de design Singleton é uma abordagem mais ampla que se aplica a qualquer classe, independentemente de ser ou não gerenciada por um contêiner de injeção de dependência.
  • O ciclo de vida Singleton em injeção de dependência é específico para o contexto de um contêiner de DI e se refere à maneira como as instâncias de serviços são gerenciadas e compartilhadas entre dependências.

Nesse texto estamos abordando somente o ciclo de vida Singleton, porém você pode encontrar sobre o padrão singleton nesse artigo.

Benefícios:

  • Eficiência: Reduz a sobrecarga de criação de instâncias, pois a mesma instância é compartilhada.
  • Manutenção de Estado: Útil para serviços que precisam manter um estado global, como configuração ou armazenamento em cache.
  • Cache de Dados: Útil para serviços que precisam armazenar em cache dados que podem ser compartilhados por toda a aplicação.

Malefícios:

  • Memória: Pode levar a um aumento no uso de memória, pois a instância persiste durante toda a vida da aplicação.
  • Concorrência: Deve ser cuidadosamente projetado para ser thread-safe, já que a mesma instância é compartilhada entre threads.

Exemplo:
Imagine um serviço de configuração global que é usado por diferentes partes da aplicação:

public class AppConfigurationService
{
    public string AppName { get; set; }
    // Outras propriedades e métodos de configuração
}

// Registro do serviço como Singleton
services.AddSingleton<AppConfigurationService>();

Enter fullscreen mode Exit fullscreen mode

Exemplo:
Vamos considerar um serviço de cache simples usando o ciclo de vida Singleton:

public class CacheService
{
    private Dictionary<string, object> _cache = new Dictionary<string, object>();

    public void AddToCache(string key, object value)
    {
        _cache[key] = value;
    }

    public object GetFromCache(string key)
    {
        return _cache.TryGetValue(key, out var value) ? value : null;
    }
}

// Registro do serviço como Singleton
services.AddSingleton<CacheService>();

Enter fullscreen mode Exit fullscreen mode

Ciclo de vida Scoped

Scoped é um padrão de design no desenvolvimento de software que cria uma instância de um objeto por solicitação ou contexto específico. Em outras palavras, uma nova instância do objeto é criada para cada "escopo" ou contexto determinado, garantindo que diferentes partes do programa possam ter suas próprias instâncias independentes. Isso é frequentemente utilizado em ambientes web, onde uma nova instância é criada para cada solicitação HTTP.

Ok, imagine que você está jogando um game online e cada jogador tem sua própria área de jogo, certo? O "scoped" seria como criar uma mochila especial para cada jogador, e dentro dessa mochila, eles podem colocar suas próprias coisas.

Agora, pense que cada vez que um jogador entra no jogo, uma nova mochila é feita só para ele. Essa mochila é usada só enquanto ele está jogando. Outro jogador entra? Pronto, uma nova mochila é feita só para ele também.

Resumindo

Em programação, "scoped" funciona assim. Cada vez que algo precisa de uma mochila especial para fazer suas coisas, uma nova é criada só para aquela situação específica. Cada parte do jogo (ou do programa) tem sua própria mochila, sem confundir com a mochila de ninguém mais. É uma maneira organizada de manter as coisas separadas e arrumadinhas! 🎮

Image description

Benefícios:

  • Isolamento de Solicitação: Garante que a instância é compartilhada apenas durante uma solicitação HTTP.
  • Manutenção de Estado por Solicitação: Ideal para serviços que precisam manter um estado durante o processamento de uma única solicitação.
  • Contexto de Solicitação: Ideal para serviços que precisam compartilhar informações específicas de uma solicitação, como dados do usuário.

Malefícios:

  • Memória: Usa memória enquanto a solicitação está ativa, mas as instâncias são descartadas no final da solicitação.

Exemplo:
Suponha que você tenha um serviço de carrinho de compras em uma aplicação de comércio eletrônico:

public class ShoppingCartService
{
    private List<Item> _items = new List<Item>();

    public void AddItem(Item item)
    {
        _items.Add(item);
    }

    // Outros métodos relacionados ao carrinho de compras
}

// Registro do serviço como Scoped
services.AddScoped<ShoppingCartService>();

Enter fullscreen mode Exit fullscreen mode

Exemplo:
Suponha que você tenha um serviço de autenticação que precisa manter informações do usuário durante uma solicitação:

public class AuthenticationService
{
    private User _currentUser;

    public void SetCurrentUser(User user)
    {
        _currentUser = user;
    }

    public User GetCurrentuser()
    {
        return _currentUser;
    }
}

// Registro do serviço como Scoped
services.AddScoped<AuthenticationService>();

Enter fullscreen mode Exit fullscreen mode

Ciclo de vida Transient

Transient é um padrão de design em programação que cria uma nova instância de um objeto cada vez que é solicitado. Em termos mais simples, para cada vez que você precisa desse objeto, uma cópia fresquinha é feita na hora. Essa abordagem é útil quando se quer ter certeza de que cada solicitação recebe uma instância totalmente nova e independente.

Ok, imagine que você está em uma máquina de sorvetes super high-tech. O "transient" seria como pedir um sorvete de baunilha sempre que você quiser. Cada vez que você pede, o sorveteiro faz um sorvete novinho só para você naquele momento.

Traduzindo para a programação, "transient" funciona mais ou menos assim. Cada vez que alguma parte do programa pede por alguma coisa, como uma função ou um serviço, o sistema cria uma versão nova e fresquinha na hora, sem reaproveitar nada do que já existia. É como sempre ter o seu próprio sorvete de baunilha, garantindo que é sempre novinho em folha! 🍦

Benefícios:

  • Leveza e Simplicidade: Adequado para serviços que não precisam manter um estado significativo e são leves.

Malefícios:

  • Overhead de Criação: Pode ter um leve impacto no desempenho, pois uma nova instância é criada para cada solicitação.

Exemplo:
Considere um serviço de geração de números aleatórios:

public class RandomNumberService
{
    private Random _random = new Random();

    public int GetRandomNumber()
    {
        return _random.Next();
    }
}

// Registro do serviço como Transient
services.AddTransient<RandomNumberService>();

Enter fullscreen mode Exit fullscreen mode

Exemplo:
Considere um serviço de geração de identificadores únicos usando o ciclo de vida Transient:

public class UniqueIdService
{
    public string GenerateUniqueId()
    {
        return Guid.NewGuid().ToString();
    }
}

// Registro do serviço como Transient
services.AddTransient<UniqueIdService>();

Enter fullscreen mode Exit fullscreen mode

Mas pera, Scoped e transient então não são a mesma coisa?

Apesar de muito parecidos, não, não são a mesma coisa. Por exemplo vamos supor que eu tenho uma aplicação a onde eu faço uma requisição pro backend enviando o numero da minha conta bancaria:

  • o backend vai buscar o valor do meu saldo no banco
  • vai processar a operação que foi solicitada(deposito ou saque)
  • vai salvar no banco o novo valor do meu saldo

nesse fluxo o scoped abriria somente uma instancia para a operação de leitura, processamento e escrita depois descartaria a instancia. enquanto que o transient abriria uma nova instancia a cada operação totalizando 3 instancias e ao final de cada operação iria descartando as instancias.

Considerações Práticas

Agora que você já entende os conceitos e aplicações dessas 3 técnicas vamos a considerações praticas sobre cada:

Singleton - Considerações Práticas:

  1. Compartilhamento de Estado: Útil quando há a necessidade de compartilhar um estado global entre diferentes partes da aplicação.
  2. Configuração e Logging: Pode ser aplicado a serviços de configuração, logging e caching, onde manter uma única instância é benéfico.
  3. Cuidado com Objetos Grandes: Evite armazenar objetos grandes ou pesados em serviços Singleton, pois isso pode resultar em um consumo significativo de memória.

Scoped - Considerações Práticas:

  1. Estado por Solicitação: Ideal para serviços que precisam manter um estado específico durante o processamento de uma única solicitação HTTP.
  2. Evitar Escopo Manual: Evite criar escopos manualmente, pois a injeção de dependência e o ciclo de vida Scoped são gerenciados automaticamente pelo framework.

Transient - Considerações Práticas:

  1. Serviços Leves e Sem Estado: Melhor para serviços leves, sem estado significativo, onde uma nova instância é desejada para cada solicitação.
  2. Evitar Armazenamento de Estado: Evite armazenar um estado significativo em serviços Transient, pois isso pode levar a uma alta sobrecarga de criação de instâncias.

Melhores Práticas Gerais:

  1. Entendimento do Contexto: Escolha o ciclo de vida com base na necessidade e no contexto específico de cada serviço.
  2. Evitar Vazamentos de Memória: Ao usar Singleton, evite referências que podem impedir a coleta de lixo, levando a vazamentos de memória.
  3. Thread Safety: Ao usar Singleton, garanta que a implementação seja thread-safe, pois a mesma instância é compartilhada entre várias threads.
  4. Logica de Negócios em Serviços Transient: Evite colocar lógica de negócios significativa em serviços Transient, pois isso pode levar a um alto custo de criação de instâncias.
  5. Revisão Periódica: Reavalie periodicamente a escolha do ciclo de vida à medida que os requisitos da aplicação evoluem.

Casos de Uso Específicos:

  1. Singleton para Configuração: Use Singleton para serviços de configuração que precisam ser carregados uma vez e compartilhados em toda a aplicação.
  2. Scoped para Autenticação: Utilize Scoped para serviços de autenticação que precisam manter informações do usuário durante uma solicitação.
  3. Transient para Geração Aleatória: Aplique Transient para serviços que realizam operações leves e sem estado, como geração de números aleatórios.

Considerações para Escalabilidade:

  1. Singleton em Ambientes Distribuídos: Ao escalar horizontalmente (adicionando mais instâncias) em ambientes distribuídos, o uso de Singleton pode resultar em compartilhamento de estado indesejado. Considere alternativas como armazenamento distribuído.
  2. Impacto na Escala Vertical: O uso excessivo de Singleton pode impactar a escalabilidade vertical (adicionando mais recursos a uma única instância). Avalie o impacto na memória e nos recursos do servidor.

Conclusão:

Entender os ciclos de vida dos serviços e aplicar adequadamente o Dependency Injection é crucial para o desenvolvimento de software eficiente e escalável. Cada ciclo de vida tem seus benefícios e desafios, e a escolha certa depende das características específicas de cada serviço e da arquitetura da aplicação. A adoção de boas práticas e considerações práticas contribuirá para um design mais robusto e fácil de manter.

💖 💪 🙅 🚩
brunopizol
Bruno Pizol Camargo

Posted on February 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related