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!
Introdução
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:
1 2 3 4 5 |
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.
Cenário de exemplo
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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:
1 2 3 4 5 |
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.
Aplicando o DIP
Para eliminar essa violação, devemos inserir mais abstrações no código. Codificaremos mais uma Interface, dessa vez, referente às credenciais:
1 2 3 4 5 6 |
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:
1 2 3 4 5 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
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? Esse é o objetivo!
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, hein? 😀
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 Dependency Inversion Principle tão importante! 😉
Obrigado por acompanhar mais essa série de artigos, pessoal!
Volto logo com algumas dicas em Delphi. Abraço!
Bom dia!
Mais uma excelente série de artigos!
Obrigado por compartilhar conosco, amigo André!
Meus agradecimentos também a sua esposa!
Abração!
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!
Boa trade André,
Você tem um exemplo de uso de BPLs no Delphi para chamada de telas?
Olá, Judeir.
Infelizmente não tenho um projeto de exemplo do uso de BPLs para formulários, mas pretendo publicar em breve!
Abraço!
Artigo muito bom! Onde posso obter o fonte desse exemplo?
Olá, José Moacir! Tudo bem?
Obrigado!
Infelizmente não criei um projeto para esse artigo, até porque o código é bem pequeno e foi elaborado apenas para explicar o conceito de forma didática.
Abraço, meu caro!
André,
a sua serie de artigos sobre os princÃpios SOLID esta excelente.
A sua abordagem pratica e simples é muito TOP.
Estou usando como base para meu aprimoramento para além do Delphi.
Olá, Sergio!
Muito obrigado pelo feedback! Fiquei muito feliz com o seu comentário 🙂
Pretendo retomar o trabalho no blog ainda esse ano com mais artigos.
Abraço!