SOLID: Um assunto necessário | Parte 5 — Princípio da inversão de dependência
Chegamos finalmente ao último, mas não menos importante (e sinceramente, é o que mais me empolga), artigo sobre SOLID que é o princípio de inversão de dependência, onde diz que:
“Deve-se sempre depender de abstrações e não de suas implementações, ou seja, objetos concretos”.
A primeira vez que li a definição acima, pensei: É o quê?
Sim, quando paramos para ler essas definições, elas parecem que mais atrapalham que ajudam. Mas vamos por partes tentar entender o que cada termo dessa definição acima significa?
- Abstração: É uma forma de concentrar somente o contexto essencial do seu código, ignorando características menos importantes. Um exemplo: Uma interface e uma forma de abstração:
interface Seller {
fun methodA()
fun methodB()
}
Ou seja, acima temos uma abstração da classe `Seller` contendo somente a assinatura dos métodos. Ou seja, abstraímos a implementação do método da interface.
- Objeto concreto: E com isso, caímos no conceito de objeto concreto, que nada mais é a implementação da interface acima. É onde teremos propriamente dito a implementação dos métodos:
class SellerImpl() : Seller {
override fun methodA() {
TODO("Write your code here")
}
override fun methodB() {
TODO("Write your code here")
}
}
Ou seja, resumindo a definição que vimos anteriormente: nunca chame diretamente a classe SellerImpl
mas sim a Seller
.
Vamos ver outro exemplo prático:
class LoginViewModel() {
private val service = LoginService()
private fun checkLogin(login: String, password: String) {
service.checkLogin(login, password)
}
}
Nesse exemplo que eu coloquei aqui, coloquei um cenário em que uma viewModel de login precise acessar um serviço externo para checar se o usuário e senha estão corretos. O problema desse tipo de abordagem é que a minha viewModel está dependendo diretamente da implementação desse serviço.
O desenho do esquema acima ficaria mais ou menos assim:
Esse tipo de implementação causa um alto grau de acoplamento. Imagina se eu precisar utilizar esse serviço em várias viewModels e, em certo ponto, eu precisar incluir um novo parâmetro no meu construtor? Imaginem o impacto disso. Teria que sair refatorando todos os pontos do aplicativo que a utilizam.
Por isso, o princípio diz: Dependa sempre de abstrações, nunca de suas implementações. Ou seja, melhor fazer uma interface para fazer esse meio de campo.
Veja como fica aplicando o princípio: Eu criei uma interface para o Serviço:
interface LoginService {
fun checkLogin(login: String, password: String)
}
E assim, a minha viewModel está utilizando a interface e não sua implementação. Dessa forma, eu posso fazer a alteração que for na minha implementação, que não afetará quem a usa (claro, exceto se eu mexer diretamente na função `checkLogin`, por exemplo, incluindo um novo parâmetro:
class LoginViewModel(
private val service = LoginService
) {
// Está bom mas dá pra melhorar
private fun checkLogin(login: String, password: String) {
service.checkLogin(login, password)
}
}
Com essa abstração acima, conseguimos fazer com que as camadas fiquem independentes. Ou seja, a viewModel não sabe O QUE a LoginService
precisa para ser instanciada, pois a recebe por parâmetro no construtor.
O desenho fica dessa forma:
Além de que, utilizando interfaces, podemos ter mais de uma implementação para o mesmo contrato e sério, isso ajuda muito no dia a dia. Mas aqui em cima eu coloquei que “está bom mas dá pra ficar melhor” e eu vou explicar o motivo. Mas antes de tudo, vamos definir dois termos:
- Inversão de dependência: É um dos princípios de SOLID em que módulos de alto nível não devem depender dos módulos de baixo nível. Os dois devem ser baseados em abstrações.
- Injeção de dependência: É uma das formas mais populares de aplicar o princípio de inversão de dependência.
E é nessa parte que começa a ficar realmente interessante. Se você está estudando desenvolvimento Android a algum tempo, já deve ter se deparado com alguns termos como: Dagger, Koin ou até mesmo o novo Hilt. Estas são bibliotecas que nos ajudam a aplicar a injeção de dependência e consequentemente, respeitar o princípio de inversão de dependência.
No exemplo dado acima, sem a injeção de dependência, qualquer classe que chamar a nossa
LoginViewModel
precisará instanciar aLoginService
para passar por parâmetro. Já com a injeção, essa dependência é injetada para gente de forma "automática", nos facilitando esse trabalho.
E além disso, possui diversas outras vantagens como: reutilização do código, baixo acoplamento, facilidade de testar unitariamente e facilidade de refatoração. Se você nunca ouviu falar de nenhuma delas, vale muito a pena ler a série de artigos que escrevi sobre ferramentas fundamentais para desenvolvedores Android aqui.
Claramente o código fica maior seguindo SOLID, por qual motivo utilizar?
Lembram que eu comentei no primeiro artigo que muitas empresas não seguem os princípios de SOLID por considerarem overengineering?
Mas não podemos desconsiderar nessa balança todos os ganhos que temos com a correta utilização destes princípios como: Escalabilidade, manutenabilidade, reusabilidade e claro, a tão querida estabilidade.
Mas lembrem-se: SOLID são princípios, não regras.
Devemos extinguir esse pensamento de que cortar processos fará o desenvolvimento de um aplicativo ser mais rápido. Lembrem-se que o mais rápido nem sempre é o melhor. Seguir princípios e realizar testes fazem PARTE do desenvolvimento. Não podem (e nem deveriam) ser considerados como processos a parte.
E assim chegamos ao fim da série sobre SOLID. Curtiu? Deixa seu comentário.
Parte 1: Princípio da Responsabilidade Única
Parte 2: Princípio do Aberto/Fechado
Parte 3: Princípio da Substituição de Liskov
Parte 4: Princípio da Segregação de interface
Me siga no LinkedIn para ficar por dentro dos próximos artigos!
Code Like a Girl 👧