Se você se preocupa em desacoplar suas classes e camadas, com certeza já precisou descobrir uma maneira de passar um objeto construído, que vou chamar de objeto de serviço (OS), a um outro objeto que vai utilizá-lo, que vou chamar de objeto de aplicação (OA). Da mesma forma, se preocupou em gerar interfaces ou classes abstratas para o OS, gerando uma interface de serviço (IS) para permitir a inversão de controle, e facilitar tarefas como testes, além de facilitar o polimorfismo.
Essas preocupações trazem necessariamente a questão: como então passar um OS construído a um OA que depende de uma IS?
Falando em exemplos concretos. Você tem um controller de produtos (do ASP.Net MVC) que depende um repositório de produtos e de um repositório de categorias para obter e alterar os seus dados. Depende ainda de uma classe que faz o tratamento de erros. Como tratar as dependências do controlador com essas classes? Vejam o diagrama abaixo:
Poderia fazer assim, com alto acoplamento, ainda que eu tivesse as interfaces (código do controlador, nosso OA):
public ActionResult Index() { try { IProductRepository productRepository = new Models.ProductRepository(); ViewData["products"] = productRepository.GetProducts(); ICategoryRepository categoryRepository = new Models.CategoryRepository(); ViewData["categories"] = categoryRepository.GetCategories(); return View(); } catch (Exception ex) { IExceptionManager exceptionManager = new Models.ExceptionManager(); exceptionManager.HandleError(ex); return View("Error"); } }
Qual o problema deste código? Ainda que eu utilize ISs, é o OA que está criando os OSs, ele os conhece fortemente. O controlador conhece o repositório concreto de produtos, o repositório concreto de categorias, e o tratador de erros concreto, além de suas interfaces. Com isso estou com acoplamento alto, além de coesão baixa, já que o método Index quebra o princípio da responsabilidade única (SRP) porque tem mais de um motivo para mudar (sabe obter produtos e categorias e sabe criar repositórios – nem vou entrar no mérito de saber tratar erro, apesar de ele existir).
Há duas opções que quero discutir: crio um service locator (SL), ou trabalho com injeção de dependência (Dependency Injection – DI).
Com SL fica assim:
public ActionResult Index() { try { var registry = new Registry(); IProductRepository productRepository = registry.Resolve<IProductRepository>(); ViewData["products"] = productRepository.GetProducts(); ICategoryRepository categoryRepository = registry.Resolve<ICategoryRepository>(); ViewData["categories"] = categoryRepository.GetCategories(); return View(); } catch (Exception ex) { IExceptionManager exceptionManager = registry.Resolve<IExceptionManager>(); exceptionManager.HandleError(ex); return View("Error"); } }
O que mudou? Bom, o objeto Registry (que é aplicação do padrão Registry com alguns anabolizantes) é responsável por resolver as dependências. É ele quem faz o trabalho de obter uma instância dos meus objetos e me devolvê-la. Como ele faz isso é você quem decide, mas provavelmente um contêiner de DI, como o Unity, ou o Windsor, mas pode até ser hardcoded, no punho.
Nesse modelo, meu controlador está acoplado apenas ao meu registry, e às interfaces de serviço. Posso substituir tudo por mocks e testar com facilidade. É só substituir o registry com alguma herança, algo bem simples de fazer, e acabou. Este código teria que evoluir um pouco, porque da maneira com que está não é testável porque o controlador cria o registry. Um acesso via uma propriedade estática resolveria o problema.
A outra opção, trabalhando com DI:
public class ProductsController : Controller { private IProductRepository _productRepository; private readonly ICategoryRepository _categoryRepository; private readonly IExceptionManager _exceptionManager; //construtor public ProductsController( IProductRepository productRepository, ICategoryRepository categoryRepository, IExceptionManager exceptionManager) { _productRepository = productRepository; _categoryRepository = categoryRepository; _exceptionManager = exceptionManager; } //Dependency injection public ActionResult Index() { try { ViewData["products"] = _productRepository.GetProducts(); ViewData["categories"] = _categoryRepository.GetCategories(); return View(); } catch (Exception ex) { _exceptionManager.HandleError(ex); return View("Error"); } } }
Nesse caso, as dependências (os três OS) são passados via construtor para a classe de controlador. Ela não sabe como criá-los. Isso fica melhor alinhado com o SRP e torna o OA efetivamente um cliente das ISs, sem ter o menor conhecimento dos OA concretos.
Esse é o modelo que eu prefiro. Trabalho com DI na maioria dos casos, usando SL só onde não é possível. É bem mais fácil de testar, porque não preciso mockar o registry, e com um bom contêiner de injeção de dependência, como o Unity, fica ridículo de trabalhar as dependências, que, depois de pouco código, se resolvem sozinhas. (Nota mental: postar um exemplo de autoresolução de dependência com Unity. Nota para vocês, leitores: se eu não postar me cobrem!)
Esse cenário é plenamente suportado no ASP.Net MVC, que trabalha com uma Factory para controladores (depois eu mostro isso aqui também, mas tem um exemplo no código da palestra de MVC em N camadas se quiserem ver). Já no ASP.Net Webforms isso não existe, quando um WF é criado pela infraestrutura do ASP.Net ele já foi criado e nenhuma dependência foi injetada. Isso significa que você está preso ao SL, mas isso não chega a ser um problema. Aplicações Windows Forms são triviais também no uso de DI, porque controlamos o lifeline dos objetos do começo ao fim.
Por fim, quero deixar aqui uma recomendação minha: nunca exponha o conteiner de injeção de dependência, como o UnityContainer, diretamente, como se ele fosse o SL. Abstraia-o, porque você pode precisar trocar a tecnologia, além de ter dificuldades nos testes. Uma classe Registry com um método “Resolve” é geralmente suficiente e resolve o problema.
Sugiro também a leitura das considerações do Martin Fowler, que fala no site dele que prefere SL com Registries, mas em seu livro PoEAA, ele diz que só usa Registries em última opção.
E você, o que usa para desacoplar suas classes? Como resolve o problema de testabilidade e substituição de dependências? Prefere DI ou SL?
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.