A necessidade de codificar seus projetos com qualidade é cada vez mais reconhecido com uma necessidade seja por profissionais e empresas de tecnologia, e dentre inúmeras abordagens que foram se consolidando como uma boa prática para tal efeito, uma delas é o princípios SOLID.

S.O.L.I.D é um acrônimo para os cinco primeiros princípios de design orientado a objetos indicados por Robert C. Martin, conhecido popularmente como tio Bob (Uncle Bob). Seriam elas:

  • Single-responsibility Principle (Princípio da Responsabilidade Única)
  • Open-closed Principle (Aberto/ Fechado)
  • Liskov substitution principle (Liskov)
  • Interface segregation principle (Segregação da Interface)
  • Dependency Inversion principle (Inversão de Dependência)

E claro que um código bem projetado pode demorar um pouco mais para escrever, do que um código mal estruturado para resolver um problema pontual, mas tenha certeza que o crime não compensa e cedo ou tarde o tempo que irá polpar, poderá lhe custar caro futuramente.

O código é algo que evolui e com o qual muitas vezes precisamos voltar para ele (mesmo que seja apenas para corrigir um bug) portanto, ao garantir que escrevemos um bom código, podemos tornar nossa vida muito mais fácil no futuro.

Com a utilização dos princípios de SOLID, quando combinados, facilitam o desenvolvimento de softwares que são fáceis de manter e estender. Eles também facilitam os desenvolvedores a evitar odores de código, refatam facilmente o código e também fazem parte do desenvolvimento de software ágil ou adaptável.

 

Princípio da Responsabilidade Única (SRP)

O Princípio de Responsabilidade Única (SRP), descrito por Robert C. Martin em “Agile Software Development, Principles, Patterns, and Practices(Desenvolvimento, Princípios, Padrões e Práticas de Software Ágil), afirma que “Uma classe deve ter apenas um motivo para mudar”. *

Em outras palavras, ele diz que uma classe ou um método deve ser responsável por fazer apenas uma coisa, e deve fazer uma coisa muito bem.

O código que está em conformidade com a responsabilidade única tem uma responsabilidade clara e bem definida – e isso pode ser aplicado em vários níveis do seu código – de pacotes e espaços de nome até métodos.

Uma dica para identificar se a implementação de tal princípio foi alcançado, basta que você olhe para uma classe, pergunte-se o que ela está fazendo – se ela não servir a um propósito único e bem definido, talvez você precise refatorá-la para duas classes diferentes, cada uma com um objetivo claro.

O mesmo se aplica quando você olha para um método – pergunte a si mesmo se é possível dividi-lo em partes distintas – como código que configura uma solicitação para chamar um serviço, valida a resposta e persiste em um banco de dados. Se um método estiver fazendo várias coisas, você pode decompô-lo em métodos separados, cada um com um nome que expresse claramente sua intenção. Não é apenas um código assim mais fácil de ler e entender, mas também se torna muito mais fácil testar – afinal, em vez de tentar testar um método fazendo várias coisas, você testa métodos específicos, cada um destinado a fazer algo específico.

Uma métrica que você pode usar para verificar se seu código atende à responsabilidade única é o tamanho dos seus métodos. Muitas pessoas são da opinião de que qualquer método deve caber em uma única tela – em outras palavras, você não precisa usar a barra de rolagem ao ler um único método.

 

Princípio Aberto / Fechado (OCP)

O Princípio Aberto / Fechado (OCP) afirma que “entidades de software (classes, módulos, funções etc.) devem estar abertas para extensão, mas fechadas para modificação”.

Em essência, queremos projetar nossos sistemas de forma que possamos adicionar recursos facilmente sem precisar modificar, recompilar e reimplementar componentes principais. Embora isso possa parecer complexo, na prática é bastante simples – podemos usar a herança para estender classes e adicionar comportamento a elas, ou podemos projetar nossas classes de maneira que bits de lógica possam ser conectados diretamente a elas – o que podemos realizar empregando certos padrões de design ou uma arquitetura de plugins.

É fácil expandir o código em conformidade com o Princípio Aberto / Fechado, sem a necessidade de modificar as partes principais do código. Se você encontrar um código que pareça conhecer bastante sobre diferentes bits de lógica que fazem coisas semelhantes, pergunte a si mesmo se precisará modificar esse código toda vez que alguma ou outra regra de empresa de pequeno porte for alterada. Se a resposta for sim, esse código pode estar violando esse princípio.

Uma abordagem para fazer com que seu código atenda ao Princípio Aberto / Fechado pode ser escrevê-lo de tal maneira que dependa de classes ou interfaces abstratas, em que cada implementação pode especificar sua própria lógica, permitindo expandir o código criando diferentes subclasses.

 

Princípio de Substituição de Liskov (LSP)

A descrição oficial do Princípio de Substituição de Liskov (LSP) parece bastante técnica e difícil de entender – “… Se para cada objeto o1 do tipo S houver um objeto o2 do tipo T tal que, para todos os programas P definidos em termos de T , o comportamento de P permanece inalterado quando o1 é substituído por o2, então S é um subtipo de T. “*

Ok, imagino que não tenha entendido nada, saiba que não é o primeiro. Sendo assim podemos avançar para a explicação simplificada que diz que quando o código depende da classe / interface X, qualquer subtipo de X deve ser totalmente válido no mesmo contexto (ou seja, as implementações podem ser substituídas).

O código que viola o Princípio de Substituição de Liskov não pode lidar de maneira confiável com uma situação em que temos código que se refere a uma superclasse, mas substitui-o por diferentes subclasses em tempo de execução.

Na programação orientada a objetos, é dito que uma subclasse é o mesmo tipo de coisa que seu pai. Se tivermos duas classes, Cão e Gato, que estendem uma classe Animal, um Cão e um Gato “são” um animal. No código que viola esse princípio, tentaremos efetivamente tratar duas coisas diferentes como a mesma coisa – e podemos tentar instruir um gato a latir, caso em que nosso código provavelmente causará um erro. Isso aponta para uma falha no design do nosso código – estamos tentando tratar duas coisas como iguais, mas, na verdade, elas são bem diferentes e não podem se comportar da mesma maneira.

Para corrigir problemas como este, recuamos e revisamos nossos projetos, a fim de extrair as coisas que precisamos extrair e refatorar nossas classes para representar com precisão as coisas que eles realmente devem representar.

 

Princípio de Segregação da Interface (ISP)

De acordo com o Princípio de Segregação de Interface (ISP), “nenhum cliente deve ser forçado a depender dos métodos que não usa”.

Em essência, isso significa que cada classe deve servir a um propósito bem definido e apenas expor o comportamento que está alinhado com esse objetivo. Os consumidores dessa classe não devem ser forçados a depender de métodos que eles não exigem. Isso está intimamente ligado ao Princípio da Responsabilidade Única.

Para estar em conformidade com o Princípio de Segregação de Interface, nosso código precisa ter interfaces bem definidas que atendam a um propósito específico. Em teoria, esse princípio parece semelhante ao princípio de responsabilidade única. Sua implicação é, no entanto, um pouco diferente.

O raciocínio por trás do Princípio de Segregação de Interface é que os clientes não devem ser forçados a depender dos métodos que não usam. Em outras palavras, nossas interfaces (e isso inclui os métodos públicos expostos em nossas classes) devem fornecer funcionalidades específicas para o que os usuários dessa interface precisarão. Se agruparmos várias coisas em um só lugar, as alterações em qualquer um desses métodos afetarão todos os usuários dessas interfaces.

Na prática, podemos ter uma classe que saiba como criar clientes e contas. Embora isso possa parecer bom, ele apresenta um problema em potencial. Vamos supor que tenhamos duas telas diferentes no front-end para capturar contas e clientes. Ambas as telas precisarão depender de uma classe que saiba lidar com contas e clientes. Em primeiro lugar, isso introduz o risco de que a tela do cliente (se um desenvolvedor for descuidado) possa chamar acidentalmente algum código que funcionará nas contas, simplesmente porque ele tem acesso a esse código. Em segundo lugar, se decidirmos que precisamos alterar a lógica de criação da conta, a tela do cliente também precisará ser recompilada, pois depende do código que está sendo modificado, mesmo que não haja motivo para isso.

Um bom sinal de violação desse princípio tende a ser ‘classes divinas’ – objetos maciços e complicados que podem conter lógica de negócios para um aplicativo inteiro, todos agrupados em um único local. Se você encontrar um código parecido com esse, considere refatorar essa classe em várias classes menores, cada uma com uma finalidade claramente definida.

 

Princípio de Inversão de Dependência (DIP)

O princípio de inversão de dependência (DIP) consiste em duas declarações distintas:

  • 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.

Em termos simples, esse princípio diz que devemos procurar reduzir o acoplamento entre componentes, introduzindo abstrações (pense em interfaces) entre eles. Em outras palavras, as classes não devem depender diretamente umas das outras. Isso está relacionado tanto ao Princípio Aberto / Fechado quanto ao Princípio de Substituição de Liskov.

O Princípio de Inversão de Dependência nos permite reduzir o nível de acoplamento entre nossas classes, forçando o código a depender de abstrações em vez de implementações concretas. Isso se assemelha ao Princípio Aberto / Fechado, na medida em que escrevemos o código de forma que permitamos que ele opere em diferentes implementações concretas sem exigir conhecimento aprofundado sobre essas implementações.

Isso também volta a uma frase que você já deve ter ouvido em outras partes do mundo da programação orientada a objetos – “codificação para uma interface”. Ao escrever nosso código de forma que dependa de interfaces (ou abstrações) em vez de implementações concretas, fica mais fácil trocar diferentes implementações.

Na verdade, o código sabe que depende de um objeto que pode fazer algo, mas não se importa com a maneira como isso é feito. Um lugar onde isso pode ser particularmente útil é quando queremos usar objetos simulados em nossos casos de teste; podemos injetar facilmente essas funcionalidades sem afetar o código de produção que está sendo testado – o código não sabe que sua dependência é realmente um objeto falso.