SOLI[D] – Dependency Inversion Principle (DIP)

Boa noite, leitores! Tudo bem?
A série de artigos sobre os princípios SOLID termina hoje! Para fechar com chave de ouro, conheceremos o Dependency Inversion Principle, bastante discutido na comunidade de programadores e considerado como o mais importante entre os cinco princípios. Mas há todo um motivo para isso. Confira!

 

Muito se fala sobre alto acoplamento em programação. Este termo se refere às fortes dependências entre classes, resultando em uma situação muito comum: uma simples alteração em uma classe impacta em várias classes adjacentes.
O Dependency Inversion Principle – ou DIP – orienta uma forma de inverter as dependências (como o próprio nome diz), elevando o nível de abstração em uma arquitetura. O objetivo é modificá-la para que que as dependências sejam representadas por abstrações, reduzindo o acoplamento.
Antes de continuar, vale uma observação importante: Inversão de Dependência e Injeção de Dependência são conceitos diferentes, ok? Este último ainda será abordado aqui no blog.

 

Pois bem, como uma demonstração dessas fortes dependências, considere o construtor da classe abaixo:

type
  TLeitorArquivoZip = class
  public
    constructor Create(ArquivoZip: TArquivoZip);
  end;

Observe que a classe recebe um objeto de um tipo concreto como parâmetro. Isso nos “força” a instanciar um objeto exclusivo desse tipo (TArquivoZip) ao utilizar o leitor. A princípio não há problemas, porém, caso surja a necessidade de interpretar um formato diferente de arquivo compactado, tanto a classe do leitor quanto a classe do arquivo receberão alterações que poderão, talvez, impactar em funcionalidades já existentes.
Mas há uma alternativa. Ao invés de alterá-las, podemos criar novas classes. Imagine, por exemplo, um novo leitor de arquivos com extensão RAR. Criaríamos então as classes TLeitorArquivoRar e TArquivoRar, certo?
Errado! Se fizermos isso, quebraríamos o conceito de abstração, pois certamente haveriam vários comportamentos em comum entre essas classes, e código duplicado não é uma boa ideia, concorda? 🙂

 

Para iniciar, julgo importante reproduzir as duas definições do DIP:

  • Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações;
  • Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

Quanto à primeira definição, poderíamos citar, por exemplo, uma classe básica na arquitetura que depende de uma classe muito específica. Quando isso ocorre, há justamente uma dependência que deveria ser invertida.
A segunda definição simplesmente condiz com o fato de que uma Interface não deve receber tipos concretos nos parâmetros de suas assinaturas. Se analisarmos bem, as duas definições estão relacionadas, já que mencionam o conceito de abstração.

 

Vamos entender melhor isso tudo?
A minha esposa, mais uma vez, elaborou um ótimo exemplo para a parte prática deste artigo.
Codificaremos um sistema de login que recebe as credenciais do usuário para conceder o acesso. Devo enfatizar, claro, que não haverá a codificação da validação do login, já que foge um pouco do foco do artigo, ok?

Considere a seguinte classe abaixo:

type
  TLoginSistema = class
  private
    FCredenciais: TCredenciaisSistema;
  public
    procedure SetCredenciais(aCredenciais: TCredenciaisSistema);
    procedure FazerLogin;
  end;
 
{ TLoginSistema }
 
procedure TLoginSistema.SetCredenciais(aCredenciais: TCredenciaisSistema);
begin
  FCredenciais := aCredenciais;
end;
 
procedure TLoginSistema.FazerLogin;
begin
  if FCredenciais.ValidarAcesso then
    // Exibe mensagem de boas-vindas e abre o sistema
end;

 

O método SetCredenciais é um mero setter para atribuir o valor à variável de classe FCredenciais.
Não há nada de errado com essa classe. Pelo menos por enquanto.

Após algum tempo, o cliente solicita o desenvolvimento de um módulo mobile do sistema. A autenticação do aplicativo deve se comportar da mesma forma, solicitando um login para acesso. Porém, não podemos mais usar a classe existente (TLoginSistema), já que ela é exclusiva para o módulo desktop.
Qual seria a solução?

 

Simples! Criar a classe “TLoginAplicativo”!
É uma opção realmente simples, mas não corresponde com as diretrizes da orientação a objetos. Devemos tirar proveito do conceito de abstração, lembram-se?

Codificaremos, então, uma Interface que declara os dois métodos principais para a autenticação:

type
  ILogin = interface
    procedure SetCredenciais(aCredenciais: TCredenciaisSistema);
    procedure FazerLogin;    
  end;

Boa! Basta criar duas classes que implementam essa Interface!
Opa… mas espere aí. Sinto algo de estranho…
Vamos recapitular uma das definições do DIP:

“Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.”

No código acima, a Interface ILogin, que é uma abstração, depende de TCredenciaisSistema, que é um tipo concreto (ou detalhe), portanto, o Dependency Inversion Principle foi violado! Além disso, ILogin é uma estrutura de alto nível, que servirá de base para qualquer tipo de login (desktop, web, mobile…), enquanto TCredenciaisSistema é uma estrutura de baixo nível, já que condiz apenas com o módulo desktop.

Essa violação pode se acentuar ainda mais caso surja um novo tipo de credenciais, como, por exemplo, usar os dados do Facebook para acessar o aplicativo móvel, visto que já é uma funcionalidade bem comum.

 

Para eliminar essa violação, devemos inserir mais abstrações no código. Codificaremos mais uma Interface, dessa vez, referente às credenciais:

type
  ICredenciais = interface
    procedure SetID(const aID: string);
    procedure SetSenha(const aSenha: string);    
    function ValidarAcesso: boolean;
  end;

 

O próximo passo é alterar a Interface de login para receber uma abstração como parâmetro:

type
  ILogin = interface
    procedure SetCredenciais(aCredenciais: ICredenciais);
    procedure FazerLogin;    
  end;

 

Por meio dessa simples alteração, invertemos a dependência: agora, os detalhes dependem de abstrações. Quer ver? Observe a diferença na classe que criamos no início do artigo:

type
  TLoginSistema = class(TInterfacedObject, ILogin)
  private
    FCredenciais: ICredenciais;
  public
    procedure SetCredenciais(aCredenciais: ICredenciais);
    procedure FazerLogin;      
  end;
 
{ TLoginSistema }
 
procedure TLoginSistema.SetCredenciais(aCredenciais: ICredenciais);
begin
  FCredenciais := aCredenciais;
end;
 
procedure TLoginSistema.FazerLogin;
begin
  if FCredenciais.ValidarAcesso then
    // Exibe mensagem de boas-vindas e abre o sistema desktop
end;

 

Notou que não há mais referência à classe concreta TCredenciaisSistema? 🙂
A classe de login do aplicativo móvel também seria semelhante, com exceção do algoritmo de boas vindas após validar o acesso.
As credenciais, por sua vez, também implementariam abstrações:

type
  TCredenciaisSistema = class(TInterfacedObject, ICredenciais)
    private
      FID: string;
      FSenha: string;
    public
      procedure SetID(const aID: string);
      procedure SetSenha(const aSenha: string);          
      function ValidarAcesso: boolean;
    end;  
 
  TCredenciaisFacebook = class(TInterfacedObject, ICredenciais)
    private
      FID: string;
      FSenha: string;
    public
      procedure SetID(const aID: string);
      procedure SetSenha(const aSenha: string);   
      function ValidarAcesso: boolean;
    end;

 

Já conseguimos identificar grandes vantagens, mas não é só isso!
A utilização dessas classes automaticamente se torna abstrata também. Veja o exemplo a seguir:

var
  Login: ILogin;
  Credenciais: ICredenciais;
begin
  Credenciais := FactoryLogin.GetClass;
  Credenciais.SetID ('andre.celestino');
  Credenciais.SetSenha('24681357');
 
  Login := FactoryCredenciais.GetClass;
  Login.SetCredenciais(Credenciais);
  Login.FazerLogin;
end;

 

Por este código, você consegue identificar se o login está sendo feito no sistema desktop ou no aplicativo móvel?
Não? Essa é a ideia!
Ao utilizar os tipos ILogin e ICredenciais, não importa, de antemão, qual é o sistema e o tipo de credenciais que serão utilizados. Essas variáveis podem receber qualquer tipo concreto, desde que eles implementem as Interfaces mencionadas. Para melhorar, há um Factory Method que encapsula o tipo de classe a ser retornado! Muito bom, não? 😀

Antes de encerrar essa série de artigos, vale observar que a proposta do DIP envolve implicitamente os quatro primeiros princípios:

  • Ao declarar abstrações e criar implementações específicas a partir delas, satisfazemos o SRP;
  • Trabalhar com variáveis do tipo de Interfaces e instanciar tipos concretos em tempo de execução nos remete ao propósito do OCP;
  • Abstrações favorecem a modelagem correta de classes e subclasses, cumprindo o LSP;
  • A inversão de dependência promove a segregação de Interfaces, representada pelo ISP, já que novas estruturas de alto nível são elaboradas.

São estes motivos que tornam o DIP tão importante! 😉

 

Obrigado por acompanhar mais essa série de artigos, pessoal!
Volto logo com algumas dicas em Delphi. Abraço!


 

2 comentários

  1. Bom dia!
    Mais uma excelente série de artigos!
    Obrigado por compartilhar conosco, amigo André!
    Meus agradecimentos também a sua esposa!
    Abração!

    1. Muito obrigado, Elton!
      Agradeço por ter acompanhado cada artigo dessa série!
      Um grande abraço!

      Obs: minha esposa é meu maior apoio para elaborar os artigos, rsrs!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.