Eae, cara! Já ouviu falar de S.O.L.I.D?
E como você está? Programando muito? Eu sei, eu sei… não precisa chorar, vai dar tudo certo. Eu compreendo sua dor de abrir um código e estar mais bagunçado do que seu quarto. 😢
É bem provável que boa parte desta bagunça seja pela falta dos princípios ensinados pelo nosso querido Uncle Bob: os princípios de S.O.L.I.D.
O quê?! Você nunca ouviu falar de S.O.L.I.D? 😮
Sem problemas, eu te explico. 😊
S.O.L.I.D ou SOLID é um acrônimo mnemônico que nos ajuda a lembrar dos cinco princípios do OOD (Object-Oriented Design). Estes princípios são:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Eu sei, parece muita coisa, mas não precisa ficar assustado e nem querer desistir da sua carreira. Usarei alguns exemplos enquanto apresento cada princípio pra você.
Single Responsibility Principle (SRP)
Este é nosso primeiro princípio. O Single Responsibility Principle nos diz que:
Uma classe deve ter um, e somente um, motivo para mudar.
Ou seja, pequeno gafanhoto, uma entidade de software (classes, métodos, módulos e etc.) deve ter apenas uma única funcionalidade dentro dela. Se houver mais de uma funcionalidade o princípio foi quebrado. Vamos para um exemplo prático.
Temos o seguinte método:
O método acima define qual será a cor do Sabre de Luz de acordo com a classe da pessoa. Se for um Jedi recebe a cor azul, se for um Sith recebe a cor vermelha e todos os outros recebem a cor verde (eu sei que não é assim, não me apedreje 🙄).
O problema é que nosso método está com mais de uma responsabilidade, porque ele compara a classe da pessoa e retorna a cor, assim ferindo o SRP. Precisamos refatorar este método para que ele fique dentro do padrão do princípio.
Criaremos outros dois métodos que terão uma única responsabilidade:
Note que os métodos acima têm como responsabilidade apenas checar se a pessoa é de determinada classe. Estes métodos poderiam ser ainda mais genéricos, fazendo com que pudéssemos quebrá-los em mais partes. Mas, por motivos de simplicidade e tempo, manterei desta maneira.
Agora, faremos com que a classe da pessoa seja passada para os métodos que criamos anteriormente e ele irá dizer se é verdadeiro ou falso. Em seguida, nosso método setLightSaberColor fará sua parte, que será apenas retornar a cor do Sabre de Luz sem ter que fazer comparações dentro dele mesmo, como estava sendo feito na primeira versão do nosso método.
Espero que tenha conseguido compreender o SRP. Vamos para nosso segundo princípio.
Open-Closed Principle
Nosso segundo princípio, o Open-Closed Principle, nos diz que:
Entidades de software (classes, módulos, funções, etc.) devem ser abertos para extensão, porém, fechados para modificações.
Ué, como assim? 🤨
Basicamente, pequeno gafanhoto, o OCP pede que escrevamos nossas classes, módulos e funções de maneira que, quando uma nova funcionalidade for adicionada, não precisaríamos modificar nada do código existente e sim, que nosso código antigo possa utilizar o novo sem problemas de compatibilidade.
Pois é, parece confuso, mas talvez um exemplo prático deixe o conceito um pouco mais claro. Vamos usar um bem simples, introduzido pelo Uncle Bob.
Imagine que precisamos calcular a área de algumas formas geométricas, um retângulo e um círculo. Então, criemos suas classes:
Agora, criaremos nossa classe que controlará os cálculos:
Aqui podemos ver dois problemas na construção do método. Consegue encontrá-los?
Não encontrou? Sem problemas, eu te conto. Primeiro, estamos quebrando o SRP porque nosso método está com mais de uma responsabilidade. E segundo, se precisarmos colocar uma nova forma geométrica, teríamos que alterar nosso método calculateArea com mais uma condicional. É nesse momento que entra o OCP.
Para resolver este problema, criaremos uma interface que será a base para todas as formas geométricas que tivermos:
Faremos com que cada forma geométrica implemente nossa interface Shape. E sobrescreveremos o método area criado anteriormente, retornando o resultado do cálculo da área de acordo com a forma (cada classe cuidando do seu próprio cálculo).
Agora, atualizaremos nossa classe controladora passando o tipo da lista para Shape e chamando o método area para retornar o valor final do cálculo.
Compare a diferença de tamanho dessa nova versão do método com antiga. Surreal!!! 😮 Poderíamos acrescentar qualquer outra forma geométrica e não precisaríamos fazer nenhuma alteração no método já existente (desde que esta forma implemente Shape).
Acho que sobre OCP é tudo. Vamos para o próximo princípio!
Liskov Substitution Principle
Este princípio foi criado por nossa querida cientista Barbara Liskov (inclusive, recomendo fortemente a leitura de seus livros, são excelentes). O LSP nos diz:
Classes derivadas devem ser substitutíveis por suas classes base.
Basicamente, isso significa que uma subclasse deve ter total compatibilidade com sua superclasse, ou seja, ela não deve quebrar os métodos já definidos pela classe pai. Eu sei, você gosta de exemplos 😁 vou te dar um bem simples.
Imagine que temos uma classe que cria um retângulo e precisamos criar um quadrado a partir desta classe. Como todos nós sabemos, um quadrado é um tipo de retângulo.
Então, temos a seguinte classe:
E criaremos um classe para o quadrado estendendo a nossa classe retângulo.
Neste momento, temos acesso a todos os métodos da classe Rectangle. Porém, se utilizarmos os mesmos métodos sem alterar nada, estaremos abrindo espaço para que seja criado um retângulo usando um quadrado, então precisamos fazer algumas alterações.
Primeiramente, precisamos sobrescrever os métodos setHeight e setLength, de modo que qualquer um que seja chamado faça com que altura e comprimento tenham o mesmo valor.
Não se esqueça de alterar os atributos da classe Rectangle para protected, assim a classe Square conseguirá ter acesso a eles.
Nossa classe Square deverá estar assim:
Agora, posso ter uma variável de instância do tipo retângulo e, a partir dela, posso substituí-la por um quadrado a qualquer momento. E isso não quebrará minha aplicação.
Resultados:
Esse foi um exemplo bem básico, mas, acredito que é suficiente para sentir um pouquinho do poder do LSP.
Agora, vamos para o próximo princípio.
Interface Segregation Principle
Este é um dos meus preferidos (junto com o DIP que veremos em breve). O ISP nos diz:
Crie interfaces refinadas que são específicas do cliente.
Traduzindo: não faça classes implementarem métodos que elas não usarão!
Exemplo:
No Android (meu bebê 🥺) precisamos de Click Listeners, interfaces que ficam “ouvindo” os cliques que são recebidos em determinados componentes.
Poderíamos fazer algo mais ou menos assim:
Quando essa interface fosse implementada, a classe que a recebeu sobrescreveria seus métodos e só alegria! SQN!
Acredito que você concordará comigo que nem todos os componentes são selecionáveis ou possuem alguma ação com um clique longo.
Dependendo do caso, a classe que implementasse esta interface não utilizaria alguns destes métodos e os mesmos ficariam em branco (má prática, nunca faça isso!).
É nesse momento que entra nosso grande amigo ISP 😊
Devemos separar cada método em sua própria interface:
É claro que você pode e deve variar as combinações dos métodos, como deixar o clique longo junto com o clique rápido. Não há nenhum problema em ter mais de um método dentro de uma interface, porém, seus implementadores precisam utilizar todos. NADA DE MÉTODOS EM BRANCO! 😇
Agora, finalmente, chegamos em nosso último, porém, não menos importante…
Dependency Inversion Principle
Se você chegou até aqui, acredito que realmente que aprender a usar o SOLID, o que é muito bom! O DIP é muito semelhante ao OCP.
A principal regra do DIP é:
Dependa de abstrações, não de concreções.
Ou se você preferir algo mais detalhado:
– 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.
Exatamente isso! Não deixe que uma classe seja dependente de outra. Se uma classe realmente precisa depender de outra classe, faça com que ambas dependam de uma mesma interface.
Vamos para o exemplo:
Imaginemos que precisamos criar um sistema de iluminação de LED em um cômodo de uma casa.
Criaremos um trecho de código que representará um interruptor:
Aqui temos um problema que fere gravemente o princípio DIP: a classe LightSwitch depende da classe concreta LedLamp. Se houver alguma alteração na classe LedLamp, poderemos ser forçados a fazer grandes alterações em nossa classe LightSwitch.
Mas, como evitar isso?
Está é a mágica do DIP. Para isso, criaremos um interface que irá intermediar a dependência entre as classes:
Agora, precisamos fazer com que nossa classe LedLamp implemente a nova interface e sobrescreva os métodos. Também precisaremos colocar uma variável de instância da nossa interface na classe LightSwitch. Assim, criaremos uma “ligação” entre as classes sem que uma dependa diretamente da outra, porém, ambas dependendo de uma abstração:
LedLamp implementa LightService e sobrescreve seus métodos. Note que temos um método que retorna o estado da lâmpada:
LightSwitch recebe uma instância de referência da nossa interface. Agora temos acesso aos métodos e estados da nossa lâmpada. Ou seja, ao clicarmos no interruptor, ele chamará a interface responsável pelo evento que chamará a classe que está responsável por tratar este evento de acordo com a necessidade.
Conclusão
SOLID deixará seu código mais limpo, flexível e conciso. Use e abuse do seus princípios e veja os benefícios que ele trará.
Eu acho que é isso… este é o básico da coisa. Espero que não tenha ficado confuso(a). É o primeiro artigo que escrevo, então se houver algum erro corrija-me. Obrigado, abs!!!
Tem alguma dica para acrescentar ? 😊