Introdução
Este é o quinto (e último) artigo de uma série na qual descrevemos cada um dos princípios do SOLID – conjunto de princípios que orientam o desenvolvimento de código compreensível e fácil de manter. No artigo anterior, discorremos sobre o Princípio de Segregação de Interface. Aqui falaremos sobre o Princípio de Inversão de Dependência (Dependency Inversion Principle).
Dependency Inversion Principle (DIP)
O Princípio de Inversão de Dependência possui duas definições: (1) módulos de alto nível não devem depender de módulos de baixo nível e ambos devem depender de abstrações; e (2) abstrações não devem depender de detalhes, mas detalhes devem depender de abstrações.
Para entendermos melhor, vamos dividir nossa aplicação em três camadas simples: uma camada para o negócio e suas regras, que é independente de tecnologia; uma camada de infraestrutura, que corresponde a redes, dispositivos, bancos de dados, comunicações externas e outras coisas; e uma camada intermediária, que corresponde ao framework e à linguagem de programação que ficarão responsáveis pela comunicação entre a camada de negócio e a de infraestrutura.
Se organizarmos os módulos em camadas com grau de importância de cima para baixo – onde a mais alta é a mais importante –, podemos dizer então que a camada de negócio ficaria no topo e corresponde ao módulo de alto nível.
A camada de negócio não deveria depender de como as outras camadas funcionam (detalhes de implementação de código), pois, analisando o contexto do ponto de vista da aplicação, não importa se estou solucionando o problema utilizando Ruby e Rails, Python e Django ou Elixir e Phoenix. A ideia é que a camada de negócio comunique-se com a camada intermediária e esta seja responsável pela comunicação com a camada de infraestrutura, ou outra camada qualquer, independente de como ela faz isso.
Em essência, esse é o conceito que o Princípio de Inversão de Dependência tenta postular: módulos devem depender de conceitos (abstrações), independentemente de como eles funcionam.
Quando falamos de módulos de baixo e de alto nível, estamos nos referindo a quão próximo eles estão da infraestrutura ou do domínio/negócio, respectivamente. A camada de domínio, não deveria se importar com a maneira que um dado é persistido no banco de dados. E o inverso também é verdade, o banco possui os dados, não importa como ou quem os utiliza.
Digamos que a nossa aplicação possui uma regra de negócio para realizar um cadastro. A camada de domínio indica as regras e o modelo de negócio; outra camada faz o processo intermediário entre essas regras e o modelo que utilizaremos para persistir no banco de dados; outra se responsabiliza por converter esse modelo em algo mais próximo do banco; e outra realiza a comunicação com o banco e persiste os dados.
Do ponto de vista de domínio, a próxima camada se encarrega de converter os dados e passar para a frente para ser persistido. As próximas camadas passam a receber uma informação, processá-la e passar adiante para uma camada que saiba lidar de forma mais específica.
No final, nossa abstração deve ser capaz de processar os dados de acordo com as regras de negócio e armazená-los. Cada camada se responsabiliza por algum aspecto desse processo em maior ou menor detalhe.
Muitos frameworks se encarregam de criar essas camadas de abstração. Por exemplo, usando Ruby on Rails, quando vamos persistir algum model, nós instanciamos um objeto dessa classe e chamamos o método save
, que por sua vez se encarrega pelo processamento dos dados do objeto e repassa para outras classes, até que ele seja persistido no banco. Nesse processo, cada classe vai repassando as informações para outras, atravessando as camadas de abstração até chegar no banco. Para nós, não importa como isso é feito em detalhes, o que importa é que chamamos um método e esperamos que o modelo seja persistido.
Exemplo
class NewsReport
def post_report(news)
Newsletter.new(news).publish
end
end
class Newsletter
def initialize(news)
@news = news
end
def publish
publish_to_newsletter(news, 'Assine a newsletter da Campus Code')
end
end
Nós temos uma classe NewsReport
que é responsável por publicar uma determinada notícia em uma plataforma. Nessa implementação ela só pública por meio de Newsletter
.
Estamos quebrando o Princípio de Inversão de Dependência porque temos uma dependência de algo concreto: a implementação da classe Newsletter
. Além disso, o que acontecerá se quisermos publicar essa notícia em uma rede social ou outra plataforma? Podemos adicionar um método específico para cada plataforma:
class NewsReport
def post_newsletter(news)
Newsletter.new(news).publish
end
def post_social_network(news)
# ...
end
# ...
end
Ou passar um valor que corresponde a uma plataforma mapeada em hash:
class NewsReport
def post_report(platform, news)
platforms[platform].new(news).publish
end
def platforms
{
newsletter: Newsletter,
twitter: Twitter,
facebook: Facebook,
podcast: Podcast
}
end
# ...
end
class Newsletter; end
class Twitter; end
class Facebook; end
class Podcast; end
Utilizando essa solução seria necessário adicionar um novo método ou classe toda vez que decidirmos usar uma plataforma nova, quebrando os princípios de responsabilidade única e de aberto/fechado. O que poderíamos fazer para não alterar essa classe desnecessariamente?
Uma alternativa seria passar a classe da plataforma como parâmetro e instanciá-la dentro de NewsReport
.
class NewsReport
def post_report(platform = Newsletter, news)
platform.new(news).publish
end
end
class Newsletter
def initialize(news)
@news = news
end
def publish
publish_to_newsletter(news, 'Assine a newsletter da Campus Code')
end
end
Dessa forma não precisamos nos preocupar com as implementações das plataformas ou com o nome de cada uma das classes. Nós esperamos apenas que uma classe seja passada e que ela seja capaz de responder ao método publish
.
Com isso conseguimos inverter a dependência de uma classe concreta para uma abstração: a classe NewsReport
, que dependia da classe Newsletter
, agora espera receber uma plataforma, independente de qual seja.
Considerações finais
O Princípio de Inversão de Dependência permite a criação de um código mais flexível e duradouro.
Depender de abstrações assegura o princípio de responsabilidade única: permite que classes dependam menos umas das outras, tornando-as bem definidas e reduzindo a necessidade de modificá-las devido a alterações em outras classes. As classes passam a ser alteradas somente quando é realmente necessário.
Também facilita o reuso, pois depende menos de detalhes de implementação e reduz a necessidade de código duplicado para os casos excepcionais onde é muito difícil reaproveitar o código.
Ao aplicarmos o DIP, buscamos simplificar os componentes que formam nosso sistema, fazendo com que eles saibam o mínimo possível sobre outros componentes. Quanto menos detalhes cada componente souber a respeito dos outros, melhor. Temos mais chances de reaproveitar trechos de código, além de ficarem bem definidos em suas funções.
Quando falamos de detalhes, estamos falando de saber muito sobre o comportamento de outra classe ou utilizar muitos métodos dela. Às vezes, além de saber demais de uma classe específica, acabamos sabendo demais até de outras classes que se relacionam com ela.
Quem nunca encadeou várias chamadas para acessar um objeto bem distante? Algo como user.account.credit_card.charge
.
A ideia é reduzir essa dependência para que, quando uma classe for alterada, não seja necessário mexer em muitas outras classes por elas terem algum tipo de dependência com a primeira.
Vale também lembrar que, em alguns casos, ficará mais fácil de entender e testar, porém, em outros casos, o nível de abstração ficará alto e levará mais tempo para entender o código e/ou debugar algum problema.
À medida que o projeto cresce, é sempre bom avaliar quais são os pontos que dificultam o desenvolvimento e tentar, a partir daí, ajustar o código aos poucos.
Se você estiver interessado em ler mais sobre os princípios de S.O.L.I.D., recomendamos os demais artigos dessa série:
- Single Responsibility Principle (Responsabilidade Única)
- Open/Closed Principle (Aberto e Fechado)
- Liskov Substitution Principle (Substituição de Liskov)
- Interface Segregation Principle (Segregação de Interface)
Referências
Vídeos
- GORUCO 2009 - SOLID Object-Oriented Design by Sandi Metz
- S.O.L.I.D. Principles of Object-Oriented Design - A Tutorial on Object-Oriented Design