Desenvolvedores.Net - TechBlog

Daily Archives: 24 de agosto de 2017

DIP – Dependency Inversion Principle (Princípio da inversão de dependência)

0
1 Estrela2 Estrelas3 Estrelas4 Estrelas5 Estrelas (1 votos, média: 5,00 de 5)
Loading...
24 de agosto de 2017

Veja o índice completo do tópico “S.O.L.I.D”

Olá pessoas …

whew Até que enfim, chegamos ao final da série “S.O.L.I.D“, vamos agora ver a letra do acróstico que define a sigla DIP Dependency Inversion Principle (Princípio da inversão de dependência).

Definição

note-taking
  • Classes de alto nível não devem depender de classes de baixo nível. Ambas devem depender de abstrações;
    • Isso quer dizer que na minha hierarquia de classes, as classes mais genéricas, não devem depender de classes  mais especializadas. (Veja: Generalização e Especialização).
  • Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
    • Com esta definição, eu quero dizer que as minhas classes abstratas devem ser sempre classes estáveis e os detalhes da implementação devem ficar nas classes mais instáveis.

O que NÃO vamos falar aqui

Neste artigo não iremos falar detalhadamente de:

  • Classes estáveis x Classes instáveis;
  • IoC (Inversion of Control);
  • DI (Dependency Injection);
  • Contêiner (Autofac, Ninject, Spring);

Mas prometo voltar aqui no futuro e corrigir os links para artigos sobre cada assunto que irei escrever.

Não confundir este princípio com Injeção de Dependência, injeção de Dependência é a ideia de injetar código através de algum contêiner ou framework que vai fazer isso para você.

Aqui, você vai inverter, não injetar, a maneira como você depende das coisas.

Problemas ao ferir o princípio

Quando ferimos o princípio da inversão de dependência, temos um código com baixa coesão e alto acoplamento. Dar manutenção em um código escrito desta maneira se torna complexo, uma vez que muitas classes dependem de outras classes concretas e as classes concretas não são estáveis.
Ferimos o princípio da responsabilidade única, uma vez que uma classe pode fazer coisas que não diz respeito à ela.
Em sistemas grandes, onde a customização em clientes se faz necessária, é praticamente impossível de conseguir uma boa customização.
Fica mais difícil escrever testes de unidade, pois não temos como garantir que todas as funcionalidades, ou parte sejam testadas.

certo-ou-errado

Certo ou errado?

Abaixo, iremos ver dois exemplos, começaremos pelo errado e onde erramos e em seguida o correto e o que corrigimos.

 

Ferindo o princípio

errado

Isto está errado.

Lembra do primeiro princípio? (Veja: Princípio da Responsabilidade Única). O código “errado” é reproduzido abaixo.

/*
 * Classe apenas para fins de exemplo e aprendizado.
 * Não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

 using System;
using System.Collections.Generic;

namespace Solid.Errado
{
    public class Cliente
    {
        #region Private Methods

        private void Adicionar()
        {
            Validar();
            Console.WriteLine($"O cliente {Nome} foi inserido.");
        }

        private void Atualizar()
        {
            Validar();
            Console.WriteLine($"O cliente {Nome} foi atualizado.");
        }

        #endregion Private Methods

        #region Public Properties

        public string CNPJ { get; set; }
        public int Codigo { get; set; }
        public DateTime DataCadastro { get; set; }
        public string Email { get; set; }
        public string Endereco { get; set; }
        public string Nome { get; set; }

        #endregion Public Properties

        #region Public Methods

        public void EnviarEmail()
        {
            Console.WriteLine($"O e-mail foi enviado para o endereço {Email}");
        }

        public void Excluir(long id)
        {
            Console.WriteLine($"O cliente {Nome} foi excluído.");
        }

        public void Salvar()
        {
            if(Codigo != 0)
                Atualizar();
            else
                Adicionar();

            EnviarEmail();
        }

        public IList<Cliente> Selecionar(long? id = null)
        {
            List<Cliente> result = new List<Cliente>();

            return result;
        }

        public bool Validar()
        {
            if(String.IsNullOrWhiteSpace(Nome))
                throw new Exception("O nome é obrigatório");

            if(!Email.Contains("@"))
                throw new Exception("O e-mail não é válido");

            if(CNPJ.Length != 14)
                throw new Exception("O CNPJ não é válido");

            return true;
        }

        #endregion Public Methods
    }
}

No código acima a classe de cliente é responsável por enviar e-mail, se validar, realizar CRUD,  tem acesso ao banco de dados, conhece a implementação do envio de e-mail, enfim. Faz tudo que não se pode fazer.

Realizar a manutenção neste código seria trabalhoso, demorado e totalmente passível de erros.
É demasiado complicado realizar um teste de unidade.

Corrigindo o princípio

certo

Isto está correto.

Aqui, iremos mostrar os fontes e detalhar as classes que foram comentadas no primeiro princípio. (Veja: Princípio da Responsabilidade Única).
Vamos definir aqui a classe de clientes com algumas alterações, para remover a depêndencia com validações, envios de e-mail e estratégias.

Podemos perceber que na classe de clientes, eu não tenho nenhuma chamada para validação e envio de e-mails, mas isto irá acontecer em nossa estratégia.

Devido ao fato de sempre trabalhar com sistemas ERP, onde se exige muitas customizações e mudanças de regras de negócio, é por isso que eu utilizo o Strategy Pattern.

/*
 * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

using Solid.Certo.Persistence.Abstract;
using Solid.Certo.Repository;
using System;

namespace Solid.Certo.Persistence
{
    public class Cliente: EntityBase
    {
        #region Public Properties

        public string CNPJ { get; set; }
        public DateTime DataCadastro { get; set; }
        public string Email { get; set; }
        public string Endereco { get; set; }
        public string Nome { get; set; }

        #endregion Public Properties

        #region Public Methods

        public override void Salvar()
        {
            //Não estou usando o Repository Pattern
            //E vou me permitir fazer isso na classe de cliente e não em um repositório
            DummyDBContext<Cliente>.Salvar(this);
        }

        #endregion Public Methods
    }
}

Quem é responsável por chamar as minhas estratégias é o “DummyDBContext“, ele se torna responsável por garantir que a estratégias será executada.

Vamos ver a implementação desta classe a seguir.

/*
 * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de
 validação ou regra ou se utiliza de algum framework.
 *
 * Iremos assumir que nossos objetos serão salvos por um contexto de base de dados.
 * Cada linguagem ou ORM irá implementar de forma diferente.
 *
 */

using Solid.Certo.Persistence.Contract;
using Solid.Certo.Strategy.Contract;
using System;
using System.Collections.Generic;

namespace Solid.Certo.Repository
{
    /*
     * Nesta classe a inversão acontece claramente, uma vez que a minha classe de acesso os
 dados não sabe quem vai validar e nem como vai validar os dados que serão enviados ao banco de dados. 
Ela simplesmente usa a interface de estratégia para achar a estratégia correto e realizar as validações necessárias.
     * 
     */
    public static class DummyDBContext<TEntity>
        where TEntity : class, IEntity
    {
        #region Private Methods

        private static void Adicionar(TEntity entity)
        {
            Console.WriteLine($"A entidade {typeof(TEntity).Name} foi salva na base de dados.");
        }

        private static void Atualizar(TEntity entity)
        {
            Console.WriteLine($"A entidade {typeof(TEntity).Name} foi atualizada na base de dados.");
        }

        private static void EhValida(TEntity entity)
        {
            foreach(string item in RecuperarEstrategia(entity).Validar())
            {
                throw new Exception(item);
            }
        }

        private static IPersistenceStrategy<TEntity> RecuperarEstrategia(TEntity entity)
        {
            IPersistenceStrategy<TEntity> result = 
                   DependencyService.StrategyDependencyManager.RecuperarEstrategiaPersistencia(entity);
            result.Entidade = entity;
            return result;
        }

        #endregion Private Methods

        #region Public Methods

        public static void Salvar(TEntity entity)
        {
            EhValida(entity);
            RecuperarEstrategia(entity).AntesDeSalvar(entity);

            if(entity.Codigo > 0)
                Atualizar(entity);
            else
                Adicionar(entity);

            RecuperarEstrategia(entity).DepoisDeSalvar(entity);
        }

        public static IEnumerable<TEntity> Selecionar(long? id = null)
        {
            List<TEntity> result = new List<TEntity>();

            foreach(TEntity item in result)
            {
                yield return item;
            }

            yield break;
        }

        #endregion Public Methods
    }
}

Podemos perceber que a classe “DummyDBContext” não precisa conhecer as estratégias, nem as validações, nem o serviço de e-mail.
Isso é feito pela classe de serviço “DependencyService.StrategyDependencyManager” no método “RecuperarEstrategiaPersistencia“.

Cada framework ou contêiner de injeção irá fazer de uma forma, aqui estamos fazendo desta forma, pois não é o nosso foco usar qualquer framework de injeção de depêndencia.

Ok. Mas como devo implementar a estratégia para que ela faça a validação da minha classe antes de salvar.

 

Primeiro temos que implementar nosso serviço de injeção de depêndencia. Veja abaixo a classe “StrategyDependencyManager”.
Esta classe vai registrar as nossas estratégias no método “RegistrarEstrategias”.

/*
 * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

using Solid.Certo.Persistence.Contract;
using Solid.Certo.Strategy;
using Solid.Certo.Strategy.Contract;
using System.Collections.Generic;

namespace Solid.Certo.DependencyService
{
    public static class StrategyDependencyManager
    {
        #region Private Fields

        private static Dictionary<string, IStrategy> estrategias = new Dictionary<string, IStrategy>();

        #endregion Private Fields

        #region Public Methods

        public static IPersistenceStrategy<TEntity> 
                      RecuperarEstrategiaPersistencia<TEntity>(TEntity entity)
            where TEntity : IEntity
        {
            return (IPersistenceStrategy<TEntity>)estrategias[entity.GetType().Name];
        }

        public static void RegistrarEstrategias()
        {
            //aqui eu mantive fixo, mas o correto é manter de forma dinâmica. (desacoplado)
            //Estude como o seu framework de injeção de dependência trabalha para que se mantenha esta parte do código dinâmica (desacoplada)
            //Não foi intuito deste exemplo de código mostrar nenhum contêiner ou framework de injeção de dependência.
            estrategias[typeof(Persistence.Cliente).Name] = new ClienteStrategy();
            estrategias[typeof(Persistence.Venda).Name] = new VendaStrategy();
        }

        #endregion Public Methods
    }
}

Abaixo vemos a implementação da classe “ClienteStrategy“, responsável por garantir que os dados de clientes sejam salvos de acordo com a regra de cada customização.
Aqui podemos usar a compilação “On the Fly“; em tempo de execução; Ou usar o “Plugin Pattern” para a definição de novas regras de negócio.

Utilizamos um serviço de e-mail para o envio de e-mail ao cliente.
Utilizamos o “Validator Pattern“, de forma simples, para verificação do CNPJ.

/*
 * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

using Solid.Certo.Persistence;
using Solid.Certo.Service;
using Solid.Certo.Strategy.Abstract;
using Solid.Certo.Validator;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Solid.Certo.Strategy
{

    /*
     * As classes de estratégia não conhecem a implementação do banco de dados, 
apenas verificam se está tudo correto para que seja salvo em banco de dados.&lt;/pre&gt;
&lt;pre&gt;     * 
     */

    //Sim, ao invés de "Cliente", podemos definir até aqui como sendo uma interface de cliente "ICliente".
    public class ClienteStrategy: PersistenceStrategyBase<Cliente>
    {
        #region Public Constructors

        public ClienteStrategy() : base()
        {
        }

        #endregion Public Constructors

        #region Public Methods

        public override void DepoisDeSalvar(Cliente entity)
        {
            base.DepoisDeSalvar(entity);
            //Podemos também injetar o serviço de envio de emails. Desta forma esta classe não precisaria nem conhecer o EmailService.
            EmailService.EnviarEmail(entity.Email);
        }

        public override IEnumerable<string> Validar()
        {
            if(String.IsNullOrWhiteSpace(Entidade.Nome))
                yield return "O nome é obrigatório";

            if(!Entidade.Email.Contains("@"))
                yield return "O e-mail não é válido";

            //Aqui, para diminuir ainda mais o acoplamento. 
            //Usando injeção de dependência podemos definir o nosso validador de CNPJ, 
            //uma vez que o mesmo implementa a interface IValidator<>
            //O que poderia ser customizado para cada cliente, por exemplo.
            string cnpjEhValido = new CNPJValidator().Validar(Entidade.CNPJ).FirstOrDefault();

            if(!String.IsNullOrEmpty(cnpjEhValido))
                yield return cnpjEhValido;

            yield break;
        }

        #endregion Public Methods
    }
}

Até o momento vimos as classes principais da nossa inversão, sentimos o gostinho da injeção de depêndencia, definimos classes estáveis e instáveis.
Foi muito utilizado o Princípio da Substituição, abusamos de interfaces e abstrações.

Apesar da escrita de código parecer mais demorada, a manutenção em um código bem escrito é muito mais simples.
Escrever testes de unidade, se torna mais simples, pois podemos atacar apenas o ponto que precisamos garantir que tudo ocorra de acordo com as regras.

Abaixo, as demais classes utilizadas no projeto. Não menos importantes que as demais acima.

Interface de validação de campos, CNPJ, CPF entre outros.

/*
 * Classe apenas para fins de exemplo e aprendizado 
 * não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

using System.Collections.Generic;

namespace Solid.Certo.Validator.Contract
{
    public interface IValidator<TValue>
    {
        #region Public Methods
        //Retorna todos os erros ao validar o objeto
        IEnumerable<string> Validar(TValue value);

        #endregion Public Methods
    }
}

Validação do CNPJ

/*
 * Classe apenas para fins de exemplo e aprendizado não considera 
 * nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

using Solid.Certo.Validator.Contract;
using System.Collections.Generic;

namespace Solid.Certo.Validator
{
    public class CNPJValidator: IValidator<string>
    {
        #region Public Methods

        public IEnumerable<string> Validar(string value)
        {
            if((value?.Length ?? 0) != 14)
                yield return "CNPJ não é válido.";

            yield break;
        }

        #endregion Public Methods
    }
}

Definição da inerface de estratégia de base

/*
 * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

using System.Collections.Generic;

namespace Solid.Certo.Strategy.Contract
{
    public interface IStrategy
    {
        #region Public Methods

        IEnumerable<string> Validar();

        #endregion Public Methods
    }
}

Definição de estrategia de persistência.

/*
 * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

using Solid.Certo.Persistence.Contract;

namespace Solid.Certo.Strategy.Contract
{
    public interface IPersistenceStrategy<TEntity>: IStrategy
        where TEntity : IEntity
    {
        #region Public Properties

        TEntity Entidade { get; set; }

        void DepoisDeSalvar(TEntity entity);

        void AntesDeSalvar(TEntity entity);

        #endregion Public Properties
    }
}

Abstração para a estratégia de base.

using Solid.Certo.Strategy.Contract;
using System.Collections.Generic;

namespace Solid.Certo.Strategy.Abstract
{
    public abstract class StrategyBase: IStrategy
    {
        #region Public Methods

        public abstract IEnumerable<string> Validar();

        #endregion Public Methods
    }
}

Abstração para definição da estratégia de peristência.

/*
 * Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
 */

using Solid.Certo.Persistence.Contract;
using Solid.Certo.Strategy.Contract;

namespace Solid.Certo.Strategy.Abstract
{
    public abstract class PersistenceStrategyBase<TEntity>: StrategyBase, IPersistenceStrategy<TEntity>
            where TEntity : class, IEntity, new()
    {
        #region Protected Constructors

        protected PersistenceStrategyBase()
        {
        }

        #endregion Protected Constructors

        #region Public Properties

        public TEntity Entidade { get; set; }

        #endregion Public Properties

        #region Public Methods

        public virtual void AntesDeSalvar(TEntity entity)
        {
            //na classe de base eu não faço nada
        }

        public virtual void DepoisDeSalvar(TEntity entity)
        {
            //na classe de base eu não faço nada
        }

        #endregion Public Methods
    }
}

Conclusão

Com isso, terminamos a série de artigos sobre S.O.L.I.D. Claro, que o assunto pode se estender mais. As classes acima podem sofrer modificações dependendo do framework utilizado ou do pattern escolhido para desenvolvimento.

Como tudo em excesso faz mal. O uso excessivo de IoC (Inversão de controles) e DI (Injeção de dependência) pode levar à um código extremamente complexo e de difícil entendimento. Existem programadores que sofrem de “Patternite”, quer dizer que não pode ver um pattern que já quer inserir no desenvolvimento da aplicação.
O uso excessivo pode causar até mesmo lentidão na execução da aplicação. Para se fazer uma simples tela, tem que se desenvolver cinco camadas, dois factories, um listener/ observable.

É! Eu me permito ferir, só um pouco, uma classe, quando tenho certeza de que esta classe não irá se corromper com o tempo. (Eu nunca disse isso).

 

O fonte utilizado aqui pode ser baixado pelo GITHub em https://github.com/desenvolvedores-net/ArtigoSOLID


É isso ai pessoal 🙂
Até o próximo
♦ Marcelo