Ninject, StructureMap e Padrões de Injeção de Dependência

Comments
abstract factory di factory ioc ninject structuremap
04 August 2010

Este post foi motivado por esta thread no DNA e por essa question no StackOverflow.com a respeito de um problema que encontrei nesta última semana em um cenário relativamente complexo de injeção de dependência.

Contexto

A arquitetura em questão utiliza injeção de dependência com base em interfaces, ou seja, todas as dependências das minhas classes são sempre pra interfaces (contratos) e nunca para classes concretas (implementação) exatamente como manda o figurino.

Meu domínio é o padrão: carrinho de compras com Pedido e seus itens. Uma classe é responsável por efetuar o processamento do pedido (GeradorPedido). Ela executa todos os procedimentos necessários para que um pedido seja gerado de maneira correta, dentre eles aplicar um determinado fator de ajuste de preços dos itens acordo com a loja onde o pedido foi vendido. Este fator de ajuste é determinado pela classe Precificador e deve ser desconhecido para o restante dos outros objetos a fim de não violar o SRP.

O diagrama abaixo representa as classes envolvidas e suas dependências:

Como aplicar este fator ao preço do item não é responsabilidade do GeradorPedido, sempre que uma instância de GeradorPedido é criada, injeto via construtor uma nova instância de Precificador pra ser usado quando necessário. Neste caso, temos uma dependência direta de GeradorPedido para IPrecificador que é resolvida em tempo de execução.

Tudo estava sendo resolvido utilizando Ninject:

MyModule.cs

public class MyModule : NinjectModule
{
    public override void Load()
    {
        Bind<IPrecificador>().To<Precificador>();
        Bind<IGeradorPedido>().To<GeradorPedido>();
    }
}

UnitTest1.cs

[TestMethod]
public void consegue_resolver_GeradorPedido()
{
    MyModule module = new MyModule();
    StandardKernel kernel = new StandardKernel(module);
    var geradorPedido = kernel.Get();

    Assert.IsNotNull(geradorPedido);
}

Precificador.cs

class Precificador : IPrecificador
{
    private decimal _fatorAjuste;

    public Precificador()
    {
        _fatorAjuste = 10m;
    }

    public decimal CalcularPreco(ItemPedido item)
    {
        return item.Preco * _fatorAjuste;
    }
}

GeradorPedido.cs

class GeradorPedido : IGeradorPedido
{
    private IPrecificador _precificador;

    public GeradorPedido(IPrecificador precificador)
    {
        _precificador = precificador;
    }

    public string Processar(Pedido novoPedido)
    {
        decimal total = 0;

        foreach (var item in novoPedido.Itens)
        {
            total += _precificador.CalcularPreco(item);
        }

        //Faz mais algumas operações com pedido

        //Número do pedido
        return "ABC123";
    }
}

Até aqui, tudo OK, certo? Ai entra um novo requisito e com ele aparece o problema...

Novo Requisito

Alguém decidiu que o fator dos preços deveria variar de acordo com a loja em que o pedido estava sendo feito. Se isso é uma regra de preço, a qual classe pertence? Precificador!

Quais opções temos pra implementar isso com o mínimo de impacto possível no sistema (já que a interface IPrecificador já estava sendo usada) e de maneira que a coesão e desacoplamento seja mantido?

Opção 1 - Adicionar um paramêtro no método CalcularPreco

A primeira opção que me veio a cabeça foi adicionar o paramêtro com a loja diretamente no método CalcularPreco da interface IPrecificador.

IPrecificador.cs

class Precificador : IPrecificador
{
    private decimal _fatorAjuste;

    public decimal CalcularPreco(ItemPedido item, Loja vendaEfetuadaEm)
    {
        if (vendaEfetuadaEm == Lojas.LojaUm)
            _fatorAjuste = 10m;
        else if (vendaEfetuadaEm == Lojas.LojaDois)
            _fatorAjuste = 15.4m;
        else
            _fatorAjuste = 0.9m;

        return item.Preco * _fatorAjuste;
    }
}

Parecia uma boa saída, mas depois de pensar alguns minutos encontrei dois side-effects graves que me fizeram mudar de idéia:

  1. O novo paramêtro, Loja, passaria também a virar uma dependência direta pra todos que consumissem a interface IPrecificador já que estes [consumidores] seriam responsáveis por repassar a loja para poder realizar a chamada a CalcularPreco. Com isso estariamos violando o SRP adicionando um motivo a mais pra classe GeradorPedido e outras mudarem.

  2. Diversos testes seriam quebrados pela adição do novo paramêtro, indicando que talvez não fosse a melhor alternativa já que essa alteração deveria afetar apenas uma classe

Opção 2 - Adicionar este paramêtro ao construtor da classe Precificador e utilizar uma Factory

Já que esta alteração diz respeito apenas a responsabilidade da classe Precificador, que tal adicionar este paramêtro ao construtor da classe? Ótima idéia, não?

De fato foi a solução que fez mais sentido já que necessariamente para chegar ao fator de preço a ser aplicado a classe precisa saber com qual loja estamos lidando. Para implementar, bastaria fornecer a loja via construtor e armazenar o valor em um membro privado da classe pra uso posterior. Dessa forma, todos os consumidores dessa classe iriam receber uma instância de IPrecificador já configurada e pronta pra uso sem a necessidade de se preocupar em fornecer a loja.

Nosso novo Precificador.cs ficaria assim:

class Precificador : IPrecificador
{
    private decimal _fatorAjuste;

    public Precificador(Loja vendaEfetuadaEm)
    {
        if (vendaEfetuadaEm == Lojas.LojaUm)
            _fatorAjuste = 10m;
        else if (vendaEfetuadaEm == Lojas.LojaDois)
            _fatorAjuste = 15.4m;
        else
            _fatorAjuste = 0.9m;
    }

    public decimal CalcularPreco(ItemPedido item)
    {
        return item.Preco * _fatorAjuste;
    }
}

No entanto, na hora que fui implementar esta solução esbarrei no meu container de DI, até então o Ninject, que resolvia a dependência de IPrecificador pra mim automaticamente sempre que necessário. Contudo, pra esta implementação eu precisaria fornecer um valor (a Loja) que só podia ser obtido em runtime na hora de construir a instância de IPrecificador.

Então a solução seria utilizar uma Factory!

GeradorPedidoFactory.cs

class GeradorPedidoFactory
{
    public static IGeradorPedido Criar(Loja vendaEfetuadaEm)
    {
        IPrecificador precificador = new Precificador(vendaEfetuadaEm);
        return new GeradorPedido(precificador);
    }
}

Mas trinta segundos depois descartei essa idéia porque IPrecificador estava sendo usado por outras classes além de GeradorPedido então eu teria que criar uma factory pra cada uma das classes que fosse consumidora de IPrecificador. Também dessa forma eu estaria anulando meu Container de DI espalhando a criação de tipos concretos por diversas factories ao invés de centralizar em apenas um ponto.

Opção 3 – Manter o construtor e utilizar uma Abstract Factory pra criar IPrecificador

A terceira e ultima opção antes da solução final foi utilizar uma Abstract Factory, que seria injetada via DI em todo mundo que precisasse de IPrecificador eliminando a necessidade de uma factory pra cada construtor e mantendo a responsabilidade no lugar adequado. Algo assim:

PrecificadorFactory.cs

class PrecificadorFactory : IPrecificadorFactory
{
    public IPrecificador Criar(Loja vendaEfetuadaEm)
    {
        return new Precificador(vendaEfetuadaEm);
    }
}

MyModule.cs

public class MyModule : NinjectModule
{
    public override void Load()
    {
        Bind<IPrecificadorFactory>().To<PrecificadorFactory>();
        Bind<IGeradorPedido>().To<GeradorPedido>();
    }
}

GeradorPedido.cs

class GeradorPedido : IGeradorPedido
{
    private IPrecificadorFactory _precificadorFactory;

    public GeradorPedido(IPrecificadorFactory precificadorFactory)
    {
        _precificadorFactory = precificadorFactory;
    }

    public string Processar(Pedido novoPedido, Loja vendaEfetuadaEm)
    {
        IPrecificador precificador = _precificadorFactory.Criar(vendaEfetuadaEm);
        decimal total = 0;

        foreach (var item in novoPedido.Itens)
        {
            total += precificador.CalcularPreco(item);
        }

        //Faz mais algumas operações com pedido

        //Número do pedido
        return "ABC123";
    }
}

Contudo, na hora que implementei pra ver como ficaria, percebi que tinha o mesmo problema da primeira solução: todo mundo que precisasse consumir IPrecificador teria que receber o paramêtro informando a Loja onde a venda foi efetuada para repassar para a abstract factory de IPrecificador a fim de criar uma instância concreta da classe. A dependência tinha voltado, apesar de a responsabilidade estar um pouco melhor distribuida. :(

A Solução Final

Quando eu já estava quase desistindo de procurar outra alternativa (mais uma) o @pedroreys deu uma idéia excelente: fornecer previamente ao container de DI a instância de IPrecificador que deveria ser usada quando ela fosse necessária.

Na hora fui atrás de como fazer isso com o Ninject que, era meu container até então. Infelizmente ele não implementa essa funcionalidade especifíca. Pelo menos não da maneira que eu gostaria [1].

Foi ai que decidi mudar para o StructureMap, container utilizado no exemplo do @pedroreys. O StructureMap, apesar de ter sido um dos primeiros containers de DI/IoC lançados (e de ser um pouco verboso demais pro meu gosto), se mantém atualizado e com uma excelente interface fluente exatamente como o Ninject.

A troca foi simples já que eu utilizo um Wrapper (omitido por breviedade) pro Ninject e a aplicação não tem contato com o container em si. O resultado acabou sendo um Mix de todas as soluções:

PrecificadorFactory.cs

class PrecificadorFactory
{
    public static IPrecificador Criar(Loja vendaEfetuadaEm)
    {
        return new Precificador(vendaEfetuadaEm);
    }
}

UnitTest1.cs

[TestMethod]
public void consegue_resolver_GeradorPedido()
{
    Loja vendaEfetuadaEm = RecuperarLojaOndeVendaFoiEfetuada();

    IPrecificador precificador = PrecificadorFactory.Criar(vendaEfetuadaEm);

    Container container = new Container();

    IGeradorPedido geradorPedido = container.With<IPrecificador>(precificador).GetInstance<IGeradorPedido>();

    Assert.IsNotNull(geradorPedido);
}

Dessa forma consigo:

  1. Criar a instância de IPrecificador separadamente

  2. Dizer ao container que quero que esta instância especifica seja usada somente nesta resolução de IGeradorPedido

  3. Manter cada classe com sua responsabilidade

Any thoughts o this?

[1] – O Ninject, na última versão (2.0), implementa o método Rebind() que permite trocar o bind de uma interface em runtime. No entanto, esta troca é permanente e afeta todos os consumidores do container (Singleton, no meu caso) e no meu contexto, não faz muito sentido.


<< A caminho do Zen
Material da palestra no TDC2010 - ORM: Por que isso te interessa?>> 
comments powered by Disqus
tucaz

tucaz

.NET Software Developer
About
All Posts
RSS
@tucaz
GitHub