código

27 mar, 2020

SOLID de verdade – Dependency Inversion Principle (DIP)

Publicidade

Sabe o que é mais legal no desenvolvimento de software contemporâneo? Você escreve um código qualquer em C#, por exemplo. Em qual computador você escreveu e executou esse código? Não importa. Você vai conseguir rodá-lo em qualquer outro PC, independente da configuração ser diferente ou não. E se tiver escrito em .Net Core, sequer o sistema operacional será importante. Até num Mac você conseguirá compilar e executar o código. Mas nem sempre foi assim.

Houve um tempo em que as pessoas desenvolvedoras precisavam se preocupar com o ambiente em que o software estava rodando. Além das regras de negócio, também era preciso escrever código para os dispositivos; escrever seu próprio sistemas de arquivos; interagir diretamente com o hardware, fazendo com que as luzinhas piscassem. Você não poderia pegar os rolos de fita e plugar em outro mainframe. A não ser que ele fosse IGUALZINHO, até com os mesmos endereços de interrupção, o programa não iria funcionar. Por quê?

O software escrito era acoplado ao ambiente em que rodava. Regras de negócio e detalhes técnicos estavam todos misturados na base de código. Isto quer dizer que, caso eu trocasse o modelo da lâmpada ou a capacidade de armazenamento do rolo, teria que escrever a aplicação toda (ou grandes partes dela) de novo. E aí está uma coisa que toda pessoa que desenvolve software odeia. Como resolver este problema de reescrita?

O Plug and Play

Os arquitetos de software perceberam que seria mais inteligente construir camadas de abstração. Quanto mais distante do hardware, menos modificações o sistema sofreria. Isto poderia deixar a base de código um pouquinho maior, mas ainda assim valeria a pena.

Assim, de um código onde regras de negócio e instruções de hardware se misturam, temos uma camada de abstração de hardware (Hardware Abstraction Layer – HAL) que isola e torna transparentes os detalhes de técnicos de ambiente.

Na imagem acima, as setas indicam o caminho das dependências. Na esquerda, Negócio e Firmware, além de misturados, dependem fortemente do hardware. Ou seja, qualquer mudança que o hardware sofrer, as duas camadas deverão ser alteradas.

Na imagem a direita, HAL depende de Negócios, Firmware e Software. É uma camada mais volátil, com certeza, contudo ela inverte a dependência da camada de negócios que se tornou totalmente ignorante do hardware. Troque o hardware e apenas HAL deverá ser atualizado.

Se o “Negócio” fosse um sistema operacional, nós provavelmente chamaríamos a camada HAL de Driver. Percebe que foi justamente a inversão de dependência que tornou sistemas operacionais Plug And Play? É o Dependecy Inversion Principle que te permite plugar e ejetar o seu pen-drive no Windows.

Mas o que isso tem a ver com software?

A forma como muito software é construído se assemelha muito ao primeiro exemplo que mostramos: Tudo misturado. Regras de Negócio se confundindo com detalhes da apresentação e regras de banco de dados. Ainda que o negócio seja o mesmo, alterar a interface ou o banco de dados é impossível.

Mas apenas separar em camadas não basta. Se partirmos para uma arquitetura de três camadas, mas sem inversão de dependência, ainda teremos a camada de Negócio dependendo de detalhes técnicos. Alterar o banco de dados irá causar alterações na camada de negócio também. E o melhor é que ela não sofresse alterações. Percebe como nós estamos muito próximos do problema anterior?

É preciso que uma camada seja criada para isolar as regras de Negócio do banco de dados. Assim a camada de negócios tem apenas as abstrações do que deve ser feito. Esta nova camada, por outro lado, possui as implementações necessárias para que o acesso ao banco de dados seja feito com sucesso. Vamos chama-la de Data Access Layer (DAL).

Apenas ter a DAL não quer dizer que tivemos automaticamente a inversão de dependência. Para diminuir o acoplamento entre as camadas, a comunicação entre elas é feita através de interfaces. Onde essas interfaces são definidas é que determina a origem da dependência. Por exemplo, se as interfaces de acesso a dados estivessem declaradas na camada DAL, Negócios é quem dependeria dela e não o contrário. Portanto, as interfaces de acesso a dados são declaradas na camada Negócios e a DAL tão somente as implementa. Agora sim, temos a dependência invertida.

Por motivos de espaço (afinal este artigo é sobre DIP e não a arquitetura completa de uma aplicação), deixei de acrescentar uma série de outras camadas. Mas mesmo com todos os cortes, com certeza você já viu alguma aplicação parecida.

O que diz o princípio?

Agora que entendemos qual o problema que a DIP se propõe a resolver e vimos alguns esquemas sobre como ela funciona, o enunciado do Dependency Inversion Principle fica muito mais claro:

Módulos de alto nível não devem depender de módulos de nível inferior.
Ambos devem depender de abstrações

Abstrações não podem depender dos detalhes.

Detalhes (implementações concretas) devem depender de abstrações.

Consegue ver esta regra nas figuras que desenhamos acima? E se eu colocar isso em código, fica mais fácil para você?

Eu escrevi algumas classlibraries em C# apenas para servir de exemplo para você. No código, estamos desenvolvendo uma aplicação de vendas, com um único caso de uso escrito: Salvar venda. Neste exemplo separei as várias camadas em DLL’s diferentes apenas para ficar bem clara a separação entre os módulos. Mas esse recorte poderia muito bem ser vertical e tudo estar dentro de um projeto apenas.

A camada Application depende da camada Dominio

Application funciona como uma orquestradora, chamando os vários serviços, instanciando as entities, disparando eventos (e claro que o código de exemplo faz bem pouco disso). Contudo, quando ela vai chamar os serviços de domínio, ela não conhece as implementações. Somente as interfaces. E essas são injetadas no construtor.

public VendaApplication(IVendaEntityFactory vendaFactory, ISalvarVendaService

  salvarVendaService) : base()

{

    _vendaFactory = vendaFactory;

    _salvarVendaService = salvarVendaService;

}

O trabalho que ela tem é:

public bool ProcessarVenda(VendaModel vendaModel)

{

    VendaEntity venda = _vendaFactory.Criar(vendaModel);

    var executouComSucesso = _salvarVendaService.Executar(venda);

    if (!executouComSucesso)

       CarregarErrosDe(_salvarVendaService);

    return executouComSucesso;

}

Como eu disse, o exemplo é simples. O que eu quero que você perceba é que em momento algum estou criando uma instância direta de SalvarVendaService. A todo instante estou trabalhando apenas com a interface. Você sabe o que é necessário para salvar uma venda? Quais erros podem ocorrer? Não. Pouco importa a implementação. Importa a comunicação entre os objetos.

Mas observando o diagrama, até que faz sentido. Afinal, Application depende de Domínio. Se você observar a estrutura de arquivos de Application, não verá a interface ISalvarVendaService declarada em lugar algum.

A camada Domínio concentra as dependências

Ao seguirmos o fluxo do sistema, vamos chegar até a classe de serviço. Assim como o passo anterior, ela não cria objetos. As dependências são passadas pelo construtor.

private readonly IVendaRepository _vendaRepository;

public SalvarVendaService(IVendaRepository vendaRepository) : base()

{

    _vendaRepository = vendaRepository;

}

Contudo, a dependência presente é do repositório, que é o responsável por salvar os dados. Onde esses dados serão salvos – se num banco de dados, na nuvem, num arquivo texto ou na memória (a implementação do repositório) pouco importa. Ao domínio interessa apenas o contrato estabelecido. Tanto importa que a interface IVendaRepository está declarada nele:

Desta forma, caso Dominio – que é o núcleo mais importante da aplicação – precise de novas informações ou em outros formatos de dados, basta que ele altere as definições da interface. Os módulos inferiores que mudem! E por falar neles

A módulo de Infra depende do domínio E do banco de dados

Este módulo é que efetivamente faz todo o controle com o banco de dados. Aliás, para abrir a sua mente: Este módulo é que efetivamente implementa todos os detalhes técnicos. Como já foi dito, seja no banco de dados, arquivo texto ou envio de e-mail, esta parte do código é quem define, implementa os detalhes.

using System;

using Venda.Dominio.Repository;

using Venda.Dominio.Entities;

namespace Venda.Infra.Repository

{

    public class VendaRepository : IVendaRepository

    {

        public bool Salvar(VendaEntity venda)

        {

            return true;

        }

    }

}

Desta vez coloquei a unit inteira. Assim você consegue ver as dependências na cabeçalho. Veja que todas apontam para Venda.Dominio. Inclusive a definição de VendaEntity está localizada em Venda.Dominio.

Espero que tenha ficado bem claro pra você o que é o Dependency Inversion Principle (DIP), ou Inversão de dependência para os íntimos. Você nunca mais vai poder dizer que “é só fazer tudo depender de interface”. Tá, em partes isso pode até ser legal. Mas não é DIP que você está fazendo. É outra coisa.

Também espero que você tenha percebido o valor do desacoplamento. Aplicando este princípio, sua aplicação fica Plug And Play com muito mais facilidade. Não vai ser preciso redesenhar tudo. Apenas uma parte – o que é bem melhor – e sem ter medo de destruir a regra de negócio.