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?