Escrito por Claudio Ramon Trejes Dornelles,

5 minutos de leitura

SOLID: Entenda o que são os 5 princípios da Programação Orientada a Objetos

Embora os cinco princípios reunidos pelo acrônimo SOLID sejam relativamente simples em sua definição, suas oportunidades de aplicações podem ser muitas vezes difíceis de serem notadas, especialmente para quem está iniciando na carreira de desenvolvedor.

Compartilhe este post:
grupo de programadores em frente ao computador

SOLID é um acrônimo que une cinco princípios da programação orientada a objetos, criados por Robert Cecil Martin e mencionados no artigo The Principles of OOD. Segundo ele, estes princípios estão mais voltados para o gerenciamento de dependências em um software, que quando descuidados podem facilmente gerar códigos de difícil manutenção, frágeis e não reutilizáveis. Estes e outros princípios são abordados no livro Agile Software Development – Principles, Patterns, and Practices, todos relacionados ao gerenciamento de dependências, e que juntos, segundo o autor, formam a fundação para o desenvolvimento de códigos flexíveis, coesos e reutilizáveis.

Os cinco princípios que formam o acrônimo SOLID são respectivamente:
+++ Single Responsibility Principle (SRP) – Princípio da Responsabilidade Única
+++ Open Closed Principle (OCP) – Princípio Aberto-fechado
+++ Liskov Substitution Principle (LSP) – Princípio da Substituição de Liskov
+++ Interface Segregation Principle (ISP) – Princípio da Segregação de Interface
+++ Dependency Inversion Principle (DIP) – Princípio da Inversão de Dependências

 

Single Responsibility Principle (SRP)

O princípio da responsabilidade única diz que uma classe deve ter um, e apenas um, motivo para mudar. Em outras palavras, uma classe deve possuir apenas uma responsabilidade; ser responsável por apenas uma ação ou tarefa dentro de uma aplicação.
Digamos que iremos desenvolver uma aplicação para gerenciar consultas em um consultório médico e que iremos necessitar de uma conexão com um banco de dados para salvar informações de pacientes e suas consultas. Escolhemos então um banco que irá atender a demanda e criamos então uma classe chamada “Database” que fará o gerenciamento das informações com o banco de dados. Nela estarão informações como dados de acesso ao banco além de métodos responsáveis pelo gerenciamento de pacientes e consultas, como por exemplo funções básicas de CRUD (Create, Read, Update e Delete). Embora num primeiro momento, onde o sistema ainda é pequeno e de baixa complexidade, pareça uma abordagem pertinente, a classe “Database” possui mais de um motivo para mudar. Toda vez que informações sobre a conexão com o banco de dados forem alteradas (como dados de login por exemplo), essa classe precisará ser modificada, bem como se novos métodos de consulta se tornarem necessários para que se busquem as informações dos pacientes que mais realizaram consultas em um determinado mês, por exemplo. Esses dois exemplos de modificações não possuem relação direta um com o outro, mas demandariam modificações na mesma classe “Database”. Dessa forma, quaisquer alterações ou novas implementações que se tornem necessárias ao longo da evolução da aplicação farão com que mudanças ocorram em partes que já estavam desenvolvidas e funcionando. Caso ainda estejamos falando de uma aplicação que não conta com uma carga eficaz de testes automatizados, estamos expostos a um risco muito grande de modificar um comportamento já implementado sem perceber a mudança indevida. Em outras palavras, acabamos com um código não coeso, frágil, com alto teor de acoplamento entre diferentes partes do sistema, difícil de ser testado e difícil de ser reaproveitado.
Para ilustrar a aplicação desse princípio, podemos ter como exemplo uma classe chamada “DatabaseConnectionManager”, desenvolvida para gerenciar conexões com bancos de dados, que contempla apenas métodos e atributos necessários para que uma conexão seja estabelecida com sucesso. Sendo assim, vejamos alguns pontos positivos da abordagem:

1. Quaisquer modificações em nossa aplicação que não sejam especificamente relacionadas a conexões com bancos de dados não serão aplicadas a classe “DatabaseConnectionManager”, o que a torna coesa por possuir apenas uma única responsabilidade e um único motivo para mudar.
2. A classe se torna interdependente entre as demais partes do sistema, pois não necessita conhecer outros pontos que não são relacionados ao seu escopo de conexão, diminuindo o nível de acoplamento entre as classes.
3. Essa classe pode ser utilizada toda vez que seja necessário estabelecer uma nova conexão com bancos de dados em quaisquer pontos da nossa aplicação, facilitando o reaproveitamento de código.
4. Ao escrevermos testes automatizados para esta classe, devemos nos preocupar em testar comportamentos relacionados apenas ao seu escopo, se a classe se comporta da maneira correta ao estabelecer conexões ao banco de dados, e nada mais (a facilidade em escrever testes unitários automatizados é um grande indicativo de que uma classe possui apenas uma responsabilidade).

O princípio da responsabilidade única é um dos princípios mais simples, porém um dos mais difíceis de se implementar pois em alguns casos não é evidente a mistura de responsabilidades de uma classe, mas manter a atenção em busca de classes com responsabilidades desnecessárias fará com que o código sempre evolua para um ponto de maior coesão e flexibilidade.

 

Open Closed Principle (OCP)

O princípio aberto-fechado sustenta que um módulo, para respeitar este princípio, deve estar aberto para extensão, porém fechado para modificações. Ou seja, um módulo deve poder agir de maneiras diferentes conforme os requisitos da aplicação mudam, mas o seu código fonte deve ser inviolável. A priori parece ser algo impossível de se fazer, porém a resposta está no uso de abstrações.
Podemos utilizar como exemplo uma classe responsável por calcular a área de figuras geométricas, que num primeiro momento pode receber duas formas geométricas (círculo e quadrado), calculando a área dessas formas através do seguinte procedimento:

1. Se a forma for um quadrado, calcula a área a partir da multiplicação da base por sua altura.
2. Se a forma for um círculo, calcula a sua área a partir da multiplicação de uma constante Pi pelo quadrado do raio do círculo.

Agora digamos que a nossa aplicação evoluiu e que passou a ser necessário realizar o cálculo da área de triângulos. Nesse momento, a classe de cálculo deverá ser modificada para que uma nova verificação seja feita, validando se a forma geométrica é um triângulo para que então seja feito o novo cálculo. Com isso acabamos de violar o princípio aberto-fechado, pois estaremos modificando o código fonte da classe de cálculo. Para contornar essa situação devemos utilizar abstrações para “fechar” o nosso código contra mudanças, como por exemplo fazer com que a nossa classe de cálculo receba como argumento uma forma geométrica ao invés de círculos ou quadrados e que então invoque uma função ‘calcularÁrea()’ de cada uma das formas geométricas recebidas. Assim círculos, quadrados e triângulos podem estender/implementar essa forma genérica e sobrescrever o método ‘calcularÁrea()’ implementando o cálculo da sua própria maneira. Utilizando essa nova abordagem o código da classe de cálculo não será mais alterado toda vez que uma nova forma geométrica for introduzida na aplicação, apenas será feita uma nova extensão da forma geométrica genérica, respeitando o princípio aberto-fechado.
Embora ilustrado um exemplo de como implementar esse princípio, devemos ter em mente que uma aplicação não pode ser “fechada” por completo. Devemos sempre pensar nas possibilidades de mudança e evolução da aplicação para definir o que faz sentido ou não manter fechado para alterações. Como exemplo, pense na aplicação utilizada anteriormente e que uma nova regra de negócio foi definida: devemos sempre calcular primeiro a área de círculos, depois quadrados e por fim, de triângulos. A classe de cálculo não está fechada para uma mudança como esta, e ainda, nunca estará totalmente fechada para uma nova mudança.

 

Liskov Substitution Principle (LSP)

Este princípio foi primeiramente escrito por Barbara Liskov, definindo que funções que recebem objetos de uma classe devem ser capazes de receber objetos derivados dessa mesma classe sem a necessidade de saber distingui-los.
Se pensarmos na figura Retângulo podemos nos recordar dos tempos de escola onde aprendemos que todo Quadrado é um Retângulo, e a partir disso criamos uma classe Quadrado que herda os comportamentos de Retângulo. Nesse caso, Retângulo pode conter métodos que modificam a sua altura e largura enquanto Quadrado teria apenas um único método que modifica a sua única propriedade “lado”. Se em nossa aplicação possuirmos um método que em sua assinatura espera receber um Retângulo para que então dobre o seu tamanho, faríamos a chamada dos métodos de modificação da altura e largura multiplicando os valores por dois, tendo assim o seu tamanho duplicado. Todavia, se esse mesmo método receber como argumento um Quadrado, o comportamento já não será mais o mesmo pois Quadrado não possui os métodos de modificação de altura e largura, sendo necessário então validar o tipo específico do argumento recebido pela função, para que então seja possível realizar o comportamento adequado de dobrar o tamanho da forma. Portanto, Quadrado e Retângulo não são intercambiáveis e violam o princípio de substituição de Liskov. Para corrigir esse problema, pode-se criar uma nova superclasse abstrata, ou interface, chamada FormaQuadrada, que possua um método ‘definirLado()’ fazendo então com que o comportamento adicional seja definido em cada implementação através da forma mais adequada.
Esse conceito pode parecer um pouco confuso e de difícil entendimento, porém ele já foi abordado implicitamente quando exemplificamos o princípio aberto-fechado (OCP). A classe de cálculo da área pode receber qualquer subtipo de uma forma geométrica, sem que precise saber qual tipo está recebendo para que a sua tarefa seja executada com êxito. Em outras palavras, não seguir o princípio de substituição de Liskov acarreta também na violação do princípio aberto-fechado.

 

Interface Segregation Principle (ISP)

Este princípio sustenta que nenhum código deve ser forçado a depender de métodos que não irá utilizar, dividindo grandes interfaces em pequenas e mais específicas interfaces onde o cliente conhecerá apenas os métodos que são de seu interesse. O intuito é diminuir o acoplamento entre classes, facilitando a refatoração e evolução da aplicação.
Se retornarmos ao exemplo citado no OCP e inserirmos novas formas geométricas passíveis de terem sua área calculada, como por exemplo um cubo, faz sentido seguirmos a mesma lógica utilizada anteriormente criando uma nova classe de implementação da forma geométrica padrão e nomeando-a como ‘cubo’. Digamos agora que o sistema evolui novamente e necessitamos calcular o volume do cubo… Para seguirmos com a mesma lógica, deveríamos alterar a interface ‘forma geométrica’ para ter uma nova funcionalidade que calcula o seu volume e consequentemente todas as suas implementações necessariamente seriam modificadas também, precisando implementar a nova funcionalidade. Entretanto, círculo, quadrado e triângulo não possuem volume, pois são formas geométricas de apenas duas dimensões. Repare que essa evolução do sistema pode ter um impacto significativo (em questão de volume de trabalho) dependendo do número de implementações de ‘forma geométrica’, sem contar com a necessidade de estarem sendo modificadas classes que num primeiro momento não fazem parte do escopo da modificação. Isso faria com que o princípio da segregação de interface fosse violado, indicando um código extremamente acoplado e com difícil manutenção. Nesse caso, é mais interessante utilizarmos uma nova interface para representar formas geométricas de três dimensões, que por sua vez pode se beneficiar dos comportamentos já definidos na interface ‘forma geométrica’ através de herança/polimorfismo, assim podemos isolar comportamentos específicos sem impactar outras partes do sistema que não devem possuir relação direta, diminuindo o acoplamento entre classes e facilitando a manutenção do código.

 

Dependency Inversion Principle (DIP)

O princípio da inversão de dependência prega que devemos depender de abstrações ao invés de implementações.
Vejamos novamente o exemplo da classe “DatabaseConnectionManager” citada anteriormente no SRP. Caso o nosso sistema esteja utilizando um banco de dados PostgreSQL é tendencioso que essa classe se especialize em realizar conexões com esse tipo de banco de dados e ainda que tenha um nome um pouco mais intuitivo como “PostgresDatabaseConnectionManager”. Entretanto, digamos que ao longo do curso do desenvolvimento da aplicação, o time de arquitetura entendeu que teríamos um ganho significativo se usássemos outro banco de dados, MySQL por exemplo, decorrente dos motivos X, Y e Z… Neste momento teríamos de escrever uma nova classe “MySQLDatabaseConnectionManager” que fosse capaz de realizar conexão com o novo banco de dados escolhido e ainda, modificar as dependências em todos os locais da aplicação que utilizavam a antiga conexão para realizar alguma tarefa específica, para que então passem a utilizar a nova classe. (Se possuirmos muitas classes que utilizam essa dependência, já pensou no trabalho que daria?)
Todavia, a classe de exemplo “DatabaseConnectionManager” citada no SRP não foi escolhida de modo aleatório. Essa classe, facilmente representa o conceito de uma interface e “PostgresDatabaseConnectionManager” e “MySQLDatabaseConnectionManager” o conceito de implementações. Em outras palavras, todas as partes do sistema que necessitam se conectar a um banco de dados, necessitam de uma dependência que gerencie conexões mas não necessariamente de uma dependência que gerencie conexão a um banco de dados específico. Supomos que uma classe do nosso sistema (“ValueCalculator”) é responsável por calcular um valor que será então salvo em um banco de dados, a sua única preocupação deve ser com a realização de seus cálculos, apesar de depender de uma outra classe que fará a comunicação com o banco de dados para salvar estes valores. Neste momento, “ValueCalculator” não precisa necessariamente saber detalhes da comunicação com o banco de dados, muito menos qual tipo de banco de dados será utilizado, concorda? Como falamos anteriormente, o seu papel é realizar os cálculos. A partir dessa abordagem, podemos fazer então com que ela dependa da interface “DatabaseConnectionManager”, e não necessariamente de suas implementações, fazendo com que a utilização das implementações sejam intercambiáveis.

+++
Os cinco princípios reunidos pelo acrônimo SOLID prezam pelo gerenciamento de dependências no processo de desenvolvimento e manutenção de softwares, indicando pontos de atenção e formas de contornar possíveis problemas já conhecidos pela comunidade que trazem dificuldades para os programadores na hora de modificar ou adicionar algum comportamento novo à uma aplicação. A adesão a estes princípios tornam as aplicações mais robustas, coesas, reutilizáveis, de fácil manutenção e evolução.
Embora sejam relativamente simples em sua definição, suas oportunidades de aplicações podem ser muitas vezes difíceis de serem notadas, especialmente para quem está iniciando na carreira de desenvolvedor. Todavia, é interessante revisitar esses e outros princípios teóricos de tempos em tempos, para que as experiências vividas possam nos remeter a pontos em que poderíamos ter aplicado estes conceitos e outros em que ainda podemos aplicar. Com toda a certeza a sua interpretação destes conceitos não será a mesma se você reler esta publicação novamente em 6 meses.

Compartilhe este post: