sábado, 5 de julho de 2014

Compreender a “Dependency Injection” (II)

Introdução

Esta é a segunda parte de uma série de artigos dedicados ao tema da Dependency Injection (Injecção de Dependência).

Parte I – Apresentação e Conceitos Base
Parte II – Introdução prática da DI (este artigo)
Parte III – Boa programação da DI
Parte IV – Contentores IoC – Exemplos de Aplicação

Depois de termos feito uma apresentação teórica dos conceitos base, neste artigo vamos iniciar a exploração da DI. Começamos com um exemplo simples que levanta uma série de questões relacionadas com as dependências do código e depois vamos analisando, passo-a-passo, a evolução das soluções para este problema, até introduzirmos o conceito de DI. 

Conteúdo

O problema do acoplamento

Vamos analisar um exemplo muito simples para percebermos esta questão do acoplamento do código e por que razão o alto acoplamento representa um problema.

No nosso exemplo, necessitamos criar uma classe que represente uma pessoa, classe essa que deve implementar uma funcionalidade que permita à pessoa cumprimentar um amigo, através do envio de um email.

public class Pessoa
{
    public void CumprimentaAmigo()
    {
        // cdigo com mecanismo para enviar email
    }
}

Esta primeira versão da nossa classe não respeita o Princípio da Responsabilidade Única. A nossa classe, para além da responsabilidade de implementar as funcionalidades relacionadas com a entidade Pessoa, também tem a responsabilidade de implementar um mecanismo de envio de email.

Para resolvermos esta questão, vamos separar estas responsabilidades em duas classes distintas:

public class ServicoEmail
{
    public void EnviaEmail(string assunto, string msg)
    {
        // cdigo com mecanismo para enviar email
    }
}
public class Pessoa
{
    private ServicoEmail email = new ServicoEmail();
    public void CumprimentaAmigo()
    {
        email.EnviaEmail("Ola", "Ola amigo, como vai isso?");
    }
}

E pronto! Resolvemos o problema do Princípio da Responsabilidade Única. Criámos uma nova classe ServicoEmail que trata do envio das mensagens de Email e a nossa classe Pessoa cria uma instância dessa classe para implementar  correctamente o método CumprimentaAmigo.

Todos concordamos que este é um exemplo muito simples e directo, mas que enferma de algumas limitações, conforme passamos a descrever:

  • A classe Pessoa depende da classe ServicoEmail. Existe uma forte conexão (alto acoplamento) entre estas duas classes.
  • Vamos imaginar que temos uma nova versão melhorada da classe de envio de email, ServicoEmailRapido. A única forma de utilizarmos esta nova classe será alterarmos a classe Pessoa.
  • Digamos que resolvemos inserir um parâmetro no construtor da classe ServicoEmail. Mais uma vez, somos obrigados a alterar a classe Pessoa.
  • Por uma decisão de design, a classe ServicoEmail passa a ser singleton. Lá temos que alterar a classe Pessoa
  • De forma a melhorar o sistema de notificações, decide-se criar novos sistemas de envio de mensagens, como SMS ou Twiter. A classe Pessoa tem que ser modificada para poder usar estas novas implementações.
  • Outro programador necessita utilizar a classe Pessoa, mas quer usar outro sistema de mensagens. Isto não pode ser feito com a versão actual da classe Pessoa, porque está agarrada à classe ServicoEmail. O que acontece normalmente é este programador duplicar a classe Pessoa e fazer as alterações de que necessita. O projecto acaba com duas versões da classe Pessoa.
  • Temos estado a analisar cenários em que ocorrem alterações de código. Todas as alterações devem ser testadas. Como podemos testar a classe Pessoa sem incluir a classe ServicoEmail? Como podemos criar testes unitários automatizados (com NUnit, p.ex.), neste caso?

Essas limitações podem ser melhoradas se alterarmos a nossa forma de pensar e recriarmos o código de uma forma mais modular. Isto é importante, mas é independente da DI, como veremos na próxima secção.

Programar baseado em Abstrações

O que pretendemos é eliminar a dependência entre a classe Pessoa e a classe ServicoEmail, que está na origem das limitações atrás descritas. Esta dependência está expressa na linha de código:

    private ServicoEmail email = new ServicoEmail();

O que necessitamos é remover a referência explícita à classe ServicoEmail, que causa a dependência. Fazendo uma pequena análise a esta situação, o que a classe Pessoa necessita, no método CumprimentaAmigo, é de um serviço genérico, que lhe permita enviar mensagens. Estamos aqui a introduzir um conceito de abstração, que pode ser implementado através da criação e utilização dum Interface.

A alteração da forma de pensar mencionada na secção anterior, passa então por aplicar o Princípio da Abstração, através da utilização de interfaces. No entanto, muitos programadores não usam interfaces porque os veem como código adicional, não necessário. Como em tudo, temos de ver as coisas dentro do contexto e haverá casos em que não necessitamos de interfaces (o caso do “Hello World”, p.ex.). No entanto, a programação com interfaces permite produzir um código muito mais modular e extensível, como ilustrado nos exemplos a seguir. Esta abordagem também melhora a testabilidade. O exemplo discutido na seção anterior foi bastante simples, mas incluiu várias armadilhas que podem ser facilmente evitadas se usarmos interfaces em vez de classes concretas para definir os serviços.

A correcta utilização dos interfaces envolve as três etapas seguintes:

1. Definir o Interface

Começamos por definir o interface IServicoMensagens que inclui a definição da assinatura do método EnviaMensagem.

public interface IServicoMensagens
{
    void EnviaMenssagem(string assunto, string msg);
}

2. Implementar o Interface
Na nossa lista de limitações, mencionámos várias formas de enviar mensagens. Vamos criar uma classe para cada uma delas, que implementa o novo interface.

public class ServicoEmail : IServicoMensagens
{
    public void EnviaMenssagem(string assunto, string msg)
    {
        // cdigo com mecanismo para enviar email
    }
}
public class ServicoEmailRapido : IServicoMensagens
{
    public void EnviaMenssagem(string assunto, string msg)
    {
        // cdigo com mecanismo para enviar email
    }
}

public class ServicoSms : IServicoMensagens
{
    public void EnviaMenssagem(string assunto, string msg)
    {
        // cdigo com mecanismo para enviar sms
    }
}

public class ServicoTwiter : IServicoMensagens
{
    public void EnviaMenssagem(string assunto, string msg)
    {
        // cdigo com mecanismo para enviar tweet
    }
}

3. Usar o Interface
Finalmente, em vez de usarmos classes, utilizamos interfaces. Na classe Pessoa, substituímos o campo email pelo o interface IServicoMensagens conforme indicado abaixo.

public class Pessoa
{
    private IServicoMensagens servicoMsg;

    public void CumprimentaAmigo()
    {
        servicoMsg.EnviaMenssagem("Ola", "Ola amigo, como vai isso?");
    }
}

Com a introdução do interface, criámos um nível de abstração que nos permitiu remover a dependência da classe Pessoa sobre as classes de serviço de envio de mensagens.

Todavia, o código acima ainda tem um problema. Declarámos uma variável que representa o serviço, mas esse serviço nunca é instanciado:

    private IServicoMensagens servicoMsg;

Como não podemos instanciar serviços directamente, temos que instanciar uma classe. Mas se instanciarmos uma classe, voltamos a criar uma dependência, como antes:

    private IServicoMensagens servicoMsg = new ServicoEmail();

Vamos então recapitular as questões que nos surgiram:

  • Ao referenciarmos directamente as classes de serviço, criámos uma dependência.
  • Removemos a dependência, utilizando um interface para a declaração da variável do serviço.
  • Mas não podemos instanciar interfaces, temos sempre que instanciar uma classe. Todavia, se instanciarmos uma classe, voltamos a ter a dependência.

Vamos fazer uma pequena modificação no código da classe Pessoa, que nos permita ultrapassar estas duas questões: remover a dependência e ter um objecto instanciado que implemente o interface do serviço.

public class Pessoa
{
    private IServicoMensagens servicoMsg;

    public Pessoa(IServicoMensagens servico)
    {
        this.servicoMsg = servico;
    }

    public void CumprimentaAmigo()
    {
        servicoMsg.EnviaMenssagem("Ola", "Ola amigo, como vai isso?");
    }
}

Note-se que a classe Pessoa não está a inicializar o serviço, mas espera por ele como um parâmetro do seu construtor. Este é um elemento-chave no design, que melhora a modularidade, extensibilidade e testabilidade. A classe Pessoa não é dependente de qualquer implementação, mas apenas de um serviço definido por um interface. Isso significa que podemos usar a classe Pessoa, sem nos preocuparmos com a implementação subjacente do serviço de mensagens. Além disso, diferentes instâncias da classe Pessoa podem ser criadas utilizando diferentes serviços de mensagens.

Vamos então criar uma pequena classe que instancie objectos da classe Pessoa e que injecte as dependências nos respectivos construtores.

public class Sistema
{
    public void main()
    {
        IServicoMensagens servico = new ServicoEmail();
        Pessoa socio = new Pessoa(servico);
        socio.CumprimentaAmigo();
    }
}

Acabámos de introduzir o conceito de Dependency Injection. Uma classe não depende directamente de outras classes, mas apenas de abstrações, representadas por interfaces. Os objectos com instâncias concretas que implementam os interfaces, são “injectados” em runtime. No exemplo anterior, estamos a injectar no construtor, mas esta não é a única forma de realizar a DI.

Na abordagem inicial, a classe definia exactamente e controlava as suas dependências. As instruções para instanciar as dependências estavam na própria classe. Com esta abordagem, não é a classe que decide quem são os objectos que implementam as suas dependências, mas terá que ser outra entidade a tomar essa decisão antes de instanciar a própria classe. Temos assim o conceito de Inversion of Control (IoC).

A ajuda da DI

Podemos facilmente verificar que a DI nos permite resolver as limitações indicadas nas secções anteriores. Com a injecção da dependência no construtor da classe Pessoa, esta deixa de ser afectada por qualquer alteração nas classes dos serviços (desde que não se altere o interface, claro). A questão de podermos ter diferentes instâncias da classe Pessoa a usarem diferentes serviços, também é fácil de resolver.

    public void Central()
    {
        ServicoEmail mail = new ServicoEmail();
        ServicoSms sms = new ServicoSms();
        ServicoTwiter tweet = new ServicoTwiter();

        List<Pessoa> socios = new List<Pessoa>();
        socios.Add(new Pessoa(mail));
        socios.Add(new Pessoa(sms));
        socios.Add(new Pessoa(tweet));
        socios.Add(new Pessoa(mail));

        foreach (Pessoa s in socios)
            s.CumprimentaAmigo();
    }

Começamos por criar três instâncias de serviços de tipos distintos, mas cada uma delas implementa o interface IServicoMensagens. Depois criamos uma lista de objectos da classe Pessoa e injectamos serviços diferentes em elementos diferentes. Finalmente percorremos todos os elementos da lista e executamos o método CumprimentaAmigo. Neste exemplo, o primeiro e o quarto sócios enviam uma mensagem de email, o segundo uma mensagem SMS e o terceiro uma mensagem do Twiter.

Queremos ainda fazer uma breve referência à questão dos testes unitários (serão o tema de outro artigo). Vamos imaginar que necessitamos testar se o método CumprimentaAmigo da classe Pessoa está correctamente implementado. Nem sempre é possível garantir toda a infraestrutura de envio de mensagens de Email, SMS ou Twiter. Normalmente isto estará presente no ambiente de produção, mas não nos computadores dos programadores. Então vejamos como a DI pode ajudar os nossos testes.

    public class ServicoFake: IServicoMensagens
    {
        public string assunto;
        public string msg;

        public void EnviaMenssagem(string assunto, string msg)
        {
            this.assunto = assunto;
            this.msg = msg;
        }
    }

    [TestClass]
    public class TestesPessoa
    {
        [TestMethod]
        public void PessoaPodeCumprimentar()
        {
            // set
            ServicoFake servico = new ServicoFake();
            Pessoa pessoa = new Pessoa(servico);

            // act
            pessoa.CumprimentaAmigo();

            // assert
            Assert.AreEqual(servico.assunto, "Ola");
            Assert.AreEqual(servico.msg, "Ola amigo, como vai isso?");

        }
    }

Como a classe Pessoa só depende do interface IServicoMensagens, para a testarmos, basta criar um serviço mock ou fake, que implemente o interface, que vamos usar com valores pré-determinados, para validarmos o funcionamento do método CumprimentaAmigo.

Algumas conclusões

Acabámos de ver, através de pequenos exemplos, alguns dos benefícios da DI. Permite-nos uma clara separação de conceitos, a garantia de respeitarmos os princípios SOLID e a construção de código modular, extensível e facilmente testável.

Pode-se argumentar que a introdução da DI tornou a classe Pessoa mais complexa de instanciar uma vez que o construtor requer parâmetros. Este é um aspecto a ter conta, a DI acrescenta complexidade à solução. Já na introdução do primeiro artigo tínhamos mencionado que a DI em sistemas simples pode ser mais prejudicial do que benéfica. Há que analisar sempre o contexto, dimensão e complexidade do sistema.

De qualquer forma, hoje em dia existem várias frameworks de IoC que tornam a implementação e configuração da DI numa tarefa simples e directa. Vamos ver alguns exemplos de utilização destas frameworks noutro artigo.

 

Notas:

- Neste artigo optou-se por explorar o tema da DI através de pequenos exemplos de código, muito simples, cujo objectivo é apenas fazer uma introdução e aguçar o apetite do(a) leitor(a).

- Os exemplos de código apresentados foram desenvolvidos em linguagem C#. Poderiam estar em C++, Java, VB.NET ou qualquer outra linguagem OO. a opção pelo C# deveu-se simplesmente a uma maior facilidade pessoal e abrangência em termos de documentação disponível para consulta.

- Ainda nos exemplos, optou-se por usar uma nomenclatura de classes, métodos e variáveis em língua Portuguesa, para melhor clarificação e compreensão do tema. Convenções e regras de nomenclatura recomendadas serão tema de outro artigo.

Sem comentários:

Enviar um comentário