Princípios S.O.L.I.D: o que são e porque projetos devem utilizá-los

Versão do artigo em audio.

Criar um código de qualidade durante toda a fase de desenvolvimento é, sem dúvidas, a missão de qualquer desenvolvedor que realmente se importa com o produto final do software. O uso de boas práticas de programação tem por finalidade justamente reduzir a complexidade do código, o acoplamento entre classes, separar responsabilidades e definir muito bem as relações entre elas, como forma de melhorar a qualidade interna do código-fonte.

Nos dois primeiros textos dessa série, elencamos algumas dicas com boas práticas e as ferramentas que podemos utilizar para nos ajudar a avaliar e evoluir a qualidade do software. Porém, não nos aprofundamos nas teorias que regem essa ideia de um código SÓLIDO e de qualidade.

E quais são esses princípios básicos que nos ajudam a manter o código organizado, sem code-smells e ̶p̶a̶s̶s̶í̶v̶e̶l̶ ̶d̶e̶ ̶s̶e̶r̶ ̶c̶o̶m̶p̶a̶r̶t̶i̶l̶h̶a̶d̶o̶ ̶n̶o̶ ̶G̶i̶t̶h̶u̶b̶/̶B̶i̶t̶b̶u̶c̶k̶e̶t̶ ̶s̶e̶m̶ ̶m̶e̶d̶o̶ ̶d̶e̶ ̶s̶e̶r̶ ̶j̶u̶l̶g̶a̶d̶o̶ limpo? São os princípios S.O.L.I.D e vamos falar deles já.

O S.O.L.I.D é um acrônimo que representa cinco princípios da programação orientada a objetos e design de código teorizados pelo nosso querido Uncle Bob (Robert C. Martin) por volta do ano 2000. O autor Michael Feathers foi responsável pela criação do acrônimo:

[S]ingle Responsibility Principle (Princípio da Responsabilidade Única)
[O]pen/Closed Principle (Princípio do Aberto/Fechado)
[L]iskov Substitution Principle (Princípio da Substituição de Liskov)
[I]nterface Segregation Principle (Princípio da Segregação de Interfaces)
[D]ependency Inversion Principle (Princípio da Inversão de Dependências)

Falaremos em detalhes sobre esses cinco princípios a seguir.

A class should have one, and only one, reason to change.

Esse primeiro princípio diz que “uma classe deve ter apenas um motivo para mudar”, ou seja, deve ter uma única responsabilidade. Basicamente, esse princípio trata especificamente a coesão. A coesão é definida como a afinidade funcional dos elementos de um módulo. Se refere ao relacionamento que os membros desse módulo possuem, se possuem uma relação mais direta e importante. Dessa forma, quanto mais bem definido o que sua classe faz, mais coesa ela é.

Veja o exemplo da classe Funcionario. Ela que contém dois métodos: calculaSalario(), que calcula o salário do funcionário, com desconto do imposto; e o salva(), que é o método de controle de persistência, ou seja, abre uma conexão com o banco de dados e salva o funcionário no banco.

Exemplo da classe Funcionario que fere a SRP

Reparem que essa classe faz muitas coisas que não necessariamente são tarefas dela. Por exemplo, ao evoluir esse sistema, se quisermos persistir outra entidade desse modelo “Empresa”, como um Produto, um Pedido, vamos ter que ficar replicando o trecho de código do salva(). Ou até mesmo, se quisermos calcular o salário de outros funcionários de outros cargos, que tem descontos de imposto diferentes, seriamos obrigados a criar métodos diferentes de cálculo dentro da mesma classe. Todas essas soluções, com certeza, são inviáveis!!!

E como minimamente aplicar a SRP nesse cenário? Podemos quebrar essas responsabilidades da classe funcionário em mais classes: ConnectionDAO (para gerenciar as conexões com o banco), FuncionarioDAO (para implementar todos os métodos de persistência específicos do funcionário), o enum Cargo (para guardar as regras de cálculo por cargo), a RegraDeCalculo (que é uma interface que possui o método de cálculo) e para cada cenário de desconto no imposto, podemos definir uma classe para gravar essa informação.

Exemplo da Classe ConnectionDAO
Exemplo Classe FuncionarioDAO
Exemplo da Interface RegraDeCalculo
Exemplo do Enum Cargo, que conhece as regras de cálculo
Exemplo da classe que implementa a regra de desconto de 22.5%
Exemplo da classe que implementa a regra de desconto de 11%

Por fim, podemos mexer na classe Funcionario, para que todo funcionário tenha um Cargo e no método calculaSalario(), a regra de cálculo, que está encapsulada no cargo, seja acessada sem prejuízo.

Exemplo Classe Funcionario refatorada

Com essa refatoração, a classe Funcionario passou a ter uma única responsabilidade, que é conhecer as regras de negócio de um funcionário. Se uma classe tem apenas uma responsabilidade, tendem a ser mais reutilizáveis, mais simples, e propagam menos mudanças para o resto do sistema. Comumente, usamos os padrões de projeto FACADE e DAO para evitar uma arquitetura de forte acoplamento e baixa coesão.

You should be able to extend a classes behavior, without modifying it.

Diz que “as entidades de software (classes, módulos, funções etc.) devem ser abertas para ampliação, mas fechadas para modificação”. De forma mais detalhada, diz que podemos estender o comportamento de uma classe, quando for necessário, por meio de herança, interface e composição, mas não podemos permitir a abertura dessa classe para fazer pequenas modificações.

Para ilustrar o entendimento desse princípio, vamos observar a classe CalculadoraDePrecos, que pertence à um sistema de e-commerce fictício.

Exemplo Classe CalculadoraDePrecos

Essa classe possui um método calcula(), que verifica as regras de desconto e de frete a partir do meio de pagamento do produto. Se o produto for comprado à vista, tem um desconto específico na tabela TabelaDePrecoAVista. Se o produto for comprado à prazo, tem um desconto específico na tabela TabelaDePrecoAPrazo. Além disso, dentro das classes das tabelas, as regras de desconto também variam de acordo com o preço do produto. E no frete, o valor também varia por estado.

Exemplo da classe TabelaDePrecoAVista
Exemplo de classe TabelaDePrecoAPrazo
Exemplo da classe Frete

O problema dessa implementação está na complexidade. Quanto mais regras forem sendo criadas, mais cases vão existir e a manutenção dessa classe vai ficar inviável. Além disso, o acoplamento da classe CalculadoraDePrecos vai aumentar, porque vai cada vez mais depender de mais classes.

Então, como usar OCP para resolver esse problema? Simples! Vamos criar a interface TabelaDePreco para representar a abstração calculaDesconto(), independente do método de pagamento do produto. Além disso, vamos abstrair o calculaFrete() na interface ServicoDeFrete. Depois dessas alterações, as tabelas TabelaDePrecoAVista e TabelaDePrecoAPrazo passam a implementar a interface TabelaDePreco e a classe Frete passa a implementar a interface ServicoDeFrete.

Exemplo da interface TabelaDePreco
Exemplo da interface ServicoDeFrete
Exemplo da classe TabelaDePrecoAPrazo refatorada
Exemplo da classe TabelaDePrecoAVista refatorada
Exemplo da classe Frete refatorada

Com isso, conseguimos enxugar a classe CalculadoraDePrecos e fazer com que ela não precise conhecer o comportamento das diversas tabelas. Ou seja, vamos FECHAR as classes CalculadoraDePrecos, Frete e TabelaDePrecos para mudanças e caso outras regras surjam para serem utilizadas na CalculadoraDePrecos, vamos implementando novas classes para representá-las e recebendo-as pelo construtor.

Exemplo da classe CalculadoraDePrecos refatorada

Em resumo, esse princípio fala sobre manter boas abstrações. Além disso, ao utilizar o padrão de projeto STRATEGY, estamos obedecendo o OCP.

Derived classes must be substitutable for their base classes.

Diz que “Os subtipos devem ser substituíveis pelos seus tipos base”, e que as classes/tipos base podem ser substituídas por qualquer uma das suas subclasses, ponderando sobre os cuidados para usar a herança no seu projeto de software. Mesmo a herança sendo um mecanismo poderoso, ela deve ser utilizada de forma contextualizada e moderada, evitando os casos de classes serem estendidas apenas por possuírem algo em comum. Esse princípio foi descrito pela pesquisadora Barbara Liskov, em seu artigo de 1988, em que ela explica que, antes de optar pela herança, precisamos pensar nas pré-condições e pós-condições da sua classe.

Para ficar mais claro, vamos analisar o exemplo das classes ContaCorrenteComum e ContaSalario. A ContaCorrenteComum representa, dentro do nosso contexto simplificado, uma conta de banco qualquer. Tem os métodos deposita(), getSaldo(), saca() e rende().

Exemplo da classe ContaCorrenteComum

Uma ContaSalario é idêntica a classe ContaCorrenteComum, exceto pelo método rende(). Uma conta salário não tem rendimento, é só para recebimento.

Exemplo da classe ContaSalario

Dessa forma, podemos resolver esse problema estendendo a classe ContaCorrenteComum, como mostrado acima, e fazendo com o método rende() lance uma exceção, certo Rogerinho?

Rogerinho do Ingá sutilmente avisando os jovens da violação do LSP

Como expressado pelo Rogerinho, isso não é uma boa ideia. Se fossemos tentar acessar o método rende() de todas as contas do Banco em um loop, por exemplo, e uma delas é uma ContaSalario, pronto, nossa aplicação não funciona, porque para qualquer conta salário uma exceção é lançada nesse método.

Exemplo da classe Banco

Nesse cenário, deveríamos refatorar e optar pelo uso da composição. Vamos criar um GerenciadorDeConta e essa classe é que fará o controle das movimentações das contas. As classes ContaCorrenteComum e ContaSalario passam a ter um GerenciadorDeContas, retirando a relação de pai-filho desnecessária entre elas.

Exemplo da classe GerenciadorDeContas
Exemplo da classe ContaCorrenteComum refatorada
Exemplo da classe ContaSalario refatorada

Repare que na versão refatorada da classe ContaSalario, nem precisamos implementar o método rende(), só utilizamos o gerenciador em comportamentos que a classe possui. O Uncle Bob explica que o LSP é o princípio capacitador do Princípio do Aberto/Fechado, pois a possibilidade de substituição de subtipos permite que um módulo, expresso em termos de um tipo base, possa ser extensível sem modificações.

Make fine grained interfaces that are client specific.

Diz que “muitas interfaces específicas são melhores do que uma interface geral”. Esse princípio trata da coesão em interfaces, da construção de módulos enxutos, ou seja, com poucos comportamentos. Interfaces que possuem muitos comportamentos são difíceis de manter e evoluir, e devem ser evitadas.

Para melhor entendimento, vamos voltar ao exemplo do Funcionario e transformar essa classe em uma classe abstrata, com outros dois métodos abstratos: getSalario() e getComissao().

Exemplo da classe abstrata Funcionario

E na nossa empresa, temos dois cargos, que vão estender essa classe Funcionario: o Vendedor e o Desenvolvedor.

Exemplo da classe Vendedor
Exemplo da classe Desenvolvedor

Entretanto, repare que a classe Funcionario possui um comportamento que não faz sentido para o cargo Desenvolvedor: getComissao(). O salário do desenvolvedor é calculado com base nas horas trabalhadas e contratadas, não tendo relação com o total de vendas em um período.

Dessa forma, como resolver esse problema? Refatorando o código para quebrar esses comportamentos em duas interfaces: Comissionavel e Convencional. Assim, o Funcionario passa a implementar a interface Convencional, fazendo com que a classe Desenvolvedor nem precise existir, já que o Desenvolvedor é um Funcionario com regime Convencional. Da mesma forma, a classe Vendedor passa a implementar a interface Comissionavel, que agora terá como comportamento o método getComissao(), que é específico desse tipo de Funcionario.

Exemplo da interface Convencional
Exemplo da interface Comissionavel
Exemplo da classe Funcionario refatorada
Exemplo da classe Vendedor refatorada

O ISP nos alerta em relação às classes “gordas”, que causam acoplamentos bizarros e prejudiciais às regras de negócio. Só precisamos ter cuidado, e essa dica serve para os outros princípios também: não exagere! Verifique minuciosamente os casos em que uma segregação é necessária, evitando que centenas de interfaces diferentes sejam criadas de forma indevida.

Depend on abstractions, not on concretions.

Diz que devemos “depender de abstrações e não de classes concretas”. Uncle Bob quebra a definição desse princípio em dois sub-itens:

  • “Módulos de alto nível não devem depender de módulos de baixo nível.”
  • “As abstrações não devem depender de detalhes. Os detalhes devem depender das abstrações.”

E isso se dá porque abstrações mudam menos e facilitam a mudança de comportamento e as futuras evoluções do código.

Quando falamos do OCP, também vimos um exemplo do DIP, mas não falamos exclusivamente dele. Ao fazermos a refatoração da classe CalculadoraDePrecos, ao invés de termos dependência direta das classes concretas TabelaDePrecoAVista e TabelaDePrecoAPrazo para calcular o desconto tabelado do produto e o frete, invertemos e passamos a depender de duas interfaces TabelaDePreco e Frete. Assim, passamos a evoluir e manter apenas as classes concretas, ou seja, os detalhes.

A ideia de se aplicar os princípios em projetos de software é tirar proveito dos benefícios do uso correto da orientação a objetos, evitando problemas como: falta de padronização do código, duplicação (lembra do “Don’t repeat yourself”?), dificuldade de isolar funcionalidades que podem ser comuns para vários pontos do código e dificuldade de manutenção (código muito frágil, em que uma modificação pode quebrar uma funcionalidade já testada e funcional).

E se conseguirmos seguir passo a passo todas essas dicas, teremos um código fácil de se manter, testar, reaproveitar, estender e evoluir, sem dúvida alguma. Uma dica importante, além de ler as principais bibliografias sobre o tema, como o livro Agile Principles, Patterns, and Practices in C# e o Principles of Ood é começar treinando em projetos pessoais, pequenos e mais simples. Comece fazendo mudanças em pontos específicos, evitando a propagação de problemas e trechos “mal-cheirosos” (code-smells). Assim, você começa a treinar seu cérebro a pensar de forma mais madura quando estiver diante de situações de desenvolvimento mais complexas do seu dia-a-dia.

Os exemplos completos que foram utilizados no artigo podem ser encontrados no projeto artigo-solid-medium no Github.

Espero que tenham gostado do post. Abraços!

Referências

  1. Código Limpo: Habilidades Práticas do Agile Software (Robert C. Martin, 2011)
  2. Agile Principles, Patterns, and Practices in C# (Robert C. Martin; Micah Martin, 2011)
  3. Alura — Cursos Online de Tecnologia — Design Patterns Java I: Boas práticas de programação
  4. Alura — Cursos Online de Tecnologia — Design Patterns Java II: Boas práticas de programação
  5. Alura — Cursos Online de Tecnologia — SOLID com Java: Orientação a Objetos com Java

Senior Software Developer/Tech Lead, master in Computer Science/Software Engineering, Java, open source, and software quality enthusiast.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store