Publicado 20 de novembro de 2023
A injeção de dependência é uma prática que envolve escrever classes de forma que elas não controlem suas dependências, mas sim que essas dependências sejam fornecidas a elas (injetadas). Vamos explorar um exemplo prático usando a classe Greeter e como a injeção de dependência torna os testes unitários mais simples.
Considere a classe Greeter
, cuja responsabilidade é exibir uma saudação. Ela possui duas dependências: IGreetingProvider
e IGreetingWriter
. Essas dependências são injetadas no construtor da classe Greeter
, permitindo que as versões simuladas (mock) delas sejam injetadas durante os testes unitários.
1public class Greeter
2{
3 private readonly IGreetingProvider _greetingProvider;
4 private readonly IGreetingWriter _greetingWriter;
5
6 public Greeter(IGreetingProvider greetingProvider, IGreetingWriter greetingWriter) {
7 _greetingProvider = greetingProvider;
8 _greetingWriter = greetingWriter;
9 }
10
11 public void Greet() {
12 var greeting = _greetingProvider.GetGreeting();
13 _greetingWriter.WriteGreeting(greeting);
14 }
15}
16
17public interface IGreetingProvider
18{
19 string GetGreeting();
20}
21
22public interface IGreetingWriter
23{
24 void WriteGreeting(string greeting);
25}
26
Neste exemplo, as implementações reais de IGreetingProvider
e IGreetingWriter
podem interagir com APIs ou bancos de dados. No entanto, durante os testes unitários, versões simuladas como TestGreetingProvider
e TestGreetingWriter
podem ser injetadas para isolar o comportamento da classe Greeter
.
Testando a Classe Greeter
1[TestClass]
2public class GreeterTests {
3 [TestMethod]
4 public void Greeter_WritesGreeting()
5 {
6 var greetingProvider = new TestGreetingProvider();
7 var greetingWriter = new TestGreetingWriter();
8 var greeter = new Greeter(greetingProvider, greetingWriter);
9
10 greeter.Greet();
11
12 Assert.AreEqual(greetingWriter[0], TestGreetingProvider.TestGreeting);
13 }
14}
15
Neste teste, não nos preocupamos com o comportamento específico de IGreetingProvider
e IGreetingWriter
. O objetivo é garantir que a classe Greeter
interaja corretamente com essas dependências. A injeção de dependência simplifica esse processo, permitindo a substituição fácil de implementações reais por versões simuladas durante os testes.
A injeção de dependência significa escrever classes de modo que elas não controlem suas dependências, mas sim que essas dependências sejam fornecidas a elas (injetadas).
O uso de um contêiner de injeção de dependência, também conhecido como "DI container" ou "IoC container," simplifica o processo de injeção de dependência. Ele é especialmente útil quando lidamos com várias classes que dependem umas das outras e têm diferentes requisitos de configuração.
Quando as classes dependem de várias interfaces e valores específicos, criar instâncias manualmente se torna complicado. O exemplo abaixo ilustra a complexidade:
1public CustomerData GetCustomerData(string customerNumber)
2{
3 var customerApiEndpoint = ConfigurationManager.AppSettings["customerApi:customerApiEndpoint"];
4 var logFilePath = ConfigurationManager.AppSettings["logwriter:logFilePath"];
5 var authConnectionString = ConfigurationManager.ConnectionStrings["authorization"].ConnectionString;
6
7 using (var logWriter = new LogWriter(logFilePath))
8 {
9 using (var customerApiClient = new CustomerApiClient(customerApiEndpoint))
10 {
11 var customerService = new CustomerService(
12 new SqlAuthorizationRepository(authConnectionString, logWriter),
13 new CustomerDataRepository(customerApiClient, logWriter),
14 logWriter
15 );
16
17 // ... Resto do código ...
18 }
19 }
20}
21
Essa abordagem manual torna-se impraticável à medida que as dependências aumentam. Além disso, lidar com classes que implementam IDisposable
pode se tornar um desafio.
Os contêineres de injeção de dependência simplificam esse processo, permitindo configurar quais classes ou valores devem ser usados para satisfazer cada dependência. No exemplo abaixo, usamos o Castle Windsor como contêiner de injeção de dependência:
1var container = new WindsorContainer();
2container.Register(
3 Component.For<CustomerService>(),
4 Component.For<ILogWriter, LogWriter>()
5 .DependsOn(Dependency.OnAppSettingsValue("logFilePath", "logWriter:logFilePath")),
6 Component.For<IAuthorizationRepository, SqlAuthorizationRepository>()
7 .DependsOn(Dependency.OnValue(connectionString, ConfigurationManager.ConnectionStrings["authorization"].ConnectionString)),
8 Component.For<ICustomerDataProvider, CustomerApiClient>()
9 .DependsOn(Dependency.OnAppSettingsValue("apiEndpoint", "customerApi:customerApiEndpoint"))
10);
11
Isso simplifica a criação de instâncias, permitindo que o contêiner resolva automaticamente as dependências e suas configurações.
var customerService = container.Resolve<CustomerService>();
var data = customerService.GetCustomerData(customerNumber);
container.Release(customerService);
Ao solicitar uma dependência ao contêiner, ele a resolve, criando todas as instâncias necessárias até que seja possível retornar uma instância de CustomerService
. Se uma classe exigir uma dependência não registrada, o contêiner lançará uma exceção indicando que não há nada registrado para atender a esse requisito.