Fique por dentro – Princípios SOLID para o TSE (TI)

Acesse também o material de estudo!


Este artigo apresenta uma explicação dos princípios SOLID, que é um dos tópicos cobrados no edital do concurso do TSE unificado.

Estruturamos este artigo da seguinte maneira:

  • Introdução
  • Single Responsibility Principle (Princípio da Responsabilidade Única)
  • Open/Closed Principle (Princípio Aberto/Fechado)
  • Liskov Substitution Principle (Princípio da Substituição de Liskov)
  • Interface Segregation Principle (Princípio da Segregação de Interfaces)
  • Dependency Inversion Principle (Princípio da Inversão de Dependência)
  • Resumo
  • Conclusão

Introdução

Os princípios SOLID são um conjunto de diretrizes que visam melhorar a qualidade do design de software na programação orientada a objetos (POO). Criados por Robert C. Martin (também conhecido como Uncle Bob), esses princípios ajudam desenvolvedores a criar sistemas mais flexíveis, compreensíveis, e fáceis de manter. 

Neste artigo, vamos explorar cada um dos cinco princípios SOLID, entendendo sua importância e como aplicá-los com exemplos práticos.

Single Responsibility Principle (Princípio da Responsabilidade Única)

Uma classe deve ter apenas uma razão para mudar, ou seja, ela deve ter apenas uma única responsabilidade.

O Princípio da Responsabilidade Única destaca que cada classe em um sistema deve ser responsável por uma única tarefa ou funcionalidade. Se uma classe faz mais de uma coisa, ela pode se tornar difícil de manter e de testar, pois mudanças em uma funcionalidade podem impactar outras.

Exemplo:

Suponha que temos uma classe Relatorio que é responsável por gerar um relatório e também enviá-lo por e-mail.

Essa classe viola o princípio da Responsabilidade Única porque ela tem duas responsabilidades: gerar um relatório e enviá-lo por e-mail. Para aderir a este princípio, devemos separar essas responsabilidades:

Agora, temos duas classes, cada uma com uma responsabilidade única, facilitando a manutenção e a reutilização do código.

Open/Closed Principle (Princípio Aberto/Fechado)

Software deve ser aberto para extensão, mas fechado para modificação.

O Princípio Aberto/Fechado sugere que uma classe deve ser fácil de estender para novos comportamentos sem precisar modificar o código existente. Isso pode ser feito através da herança ou da composição, permitindo adicionar novas funcionalidades sem alterar as já existentes, evitando a introdução de bugs.

Exemplo:

Considere uma classe que calcula o salário de diferentes tipos de funcionários:

Essa implementação viola o princípio Aberto/Fechado porque toda vez que um novo tipo de funcionário é adicionado, o código da classe precisa ser modificado. Vamos refatorar:

Agora, se precisarmos adicionar novos tipos de funcionários, podemos fazê-lo estendendo a classe Funcionario sem modificar o código existente.

Liskov Substitution Principle (Princípio da Substituição de Liskov)

Objetos de uma classe derivada devem poder substituir objetos da classe base sem alterar o comportamento desejado do programa.

O Princípio da Substituição de Liskov afirma que se S é uma subclasse de T, então os objetos do tipo T em um programa podem ser substituídos por objetos de tipo S sem que isso altere as propriedades corretas do programa. Em outras palavras, as subclasses devem manter o comportamento esperado pela superclasse.

Exemplo: Hierarquia de Veículos

Imagine que estamos desenvolvendo um sistema de veículos, onde temos uma classe base Veiculo e duas subclasses: Carro e Bicicleta.

Problema

Aqui, temos um problema claro: a classe Bicicleta não deveria herdar de Veiculo porque bicicletas não têm motor. O método ligarMotor não faz sentido para a classe Bicicleta e, ao tentar substituir Veiculo por Bicicleta, podemos causar um comportamento inesperado ou até exceções.

Isso viola o Princípio da Substituição de Liskov, que exige que uma subclasse possa ser substituída por sua superclasse sem quebrar o comportamento do sistema.

Solução

Para resolver esse problema, podemos refatorar a hierarquia de classes, separando a funcionalidade de “veículos com motor” e “veículos sem motor”. Vamos introduzir uma interface ou classe abstrata específica para veículos com motor.

Agora, temos uma hierarquia que respeita o Princípio da Substituição de Liskov:

  • A classe Carro herda de VeiculoComMotor, que tem o método ligarMotor. Isso faz sentido porque um carro tem motor.
  • A classe Bicicleta herda de Veiculo, mas não precisa implementar o método ligarMotor, pois ele não faz sentido para uma bicicleta.

Com essa refatoração, podemos substituir Veiculo por qualquer classe derivada (Carro ou Bicicleta) sem causar comportamentos inesperados ou erros no sistema. Assim, o código se torna mais robusto e alinhado com o princípio da Substituição de Liskov.

Interface Segregation Principle (Princípio da Segregação de Interfaces)

Os clientes não devem ser forçados a depender de interfaces que não utilizam.

O Princípio da Segregação de Interfaces sugere que é melhor ter várias interfaces específicas do que uma única interface genérica. Isso evita que as classes implementem métodos que não precisam, reduzindo a complexidade e aumentando a clareza do código.

Exemplo:

Vamos considerar uma interface Funcionario:

Essa interface é problemática porque força todos os funcionários a implementarem o método fazerCafe, mesmo que nem todos precisem fazer isso. Podemos dividir essa interface em várias interfaces menores:

Agora, podemos criar classes que implementam apenas as interfaces relevantes:

Isso torna o design mais modular e flexível.

Dependency Inversion Principle (Princípio da Inversão de Dependência)

Dependa de abstrações, não de implementações. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.

O Princípio da Inversão de Dependência sugere que devemos depender de abstrações (como interfaces ou classes abstratas) em vez de implementações concretas. Isso permite que o código seja mais flexível e fácil de modificar, pois podemos trocar uma implementação por outra sem alterar o código que depende dessa abstração.

Exemplo: 

Implementação Sem Inversão de Dependência

Vamos começar com um exemplo onde o Princípio da Inversão de Dependência não está sendo seguido:

Problema

Aqui, a classe LogService está fortemente acoplada à classe FileLogger. Isso significa que, se quisermos alterar a forma como os logs são armazenados (por exemplo, passar a armazenar logs em um banco de dados), teríamos que modificar a classe LogService. Esse tipo de acoplamento torna o código rígido e difícil de manter.

Solução com o Princípio da Inversão de Dependência

Para resolver esse problema, aplicamos o Princípio da Inversão de Dependência ao introduzir uma abstração (Logger) e fazer com que LogService dependa dessa abstração em vez de uma implementação concreta como FileLogger.

Passo 1: Criar uma Interface Logger

Essa interface define um contrato que qualquer classe de logger deve implementar.

Passo 2: Implementar FileLogger como uma Implementação de Logger

Agora, FileLogger implementa a interface Logger, o que significa que ele pode ser tratado como um Logger.

Passo 3: Modificar LogService para Depender de Logger

Aqui, LogService não depende mais de uma implementação específica (FileLogger). Em vez disso, ela depende da abstração Logger, que pode ser qualquer implementação de logger.

Vantagens da Abordagem com Princípio da Inversão de Dependência

  1. Flexibilidade: Podemos facilmente substituir FileLogger por outra implementação, como DatabaseLogger, sem modificar LogService.
  2. Testabilidade: No contexto de testes, podemos criar uma implementação mock de Logger para testar LogService sem precisar acessar o sistema de arquivos.

Exemplo com Outra Implementação

Vamos criar uma outra implementação de Logger para ver como é fácil trocar o comportamento:

Agora, podemos usar DatabaseLogger no lugar de FileLogger sem mudar nada em LogService:

Com essa abordagem, LogService é independente da forma como o log é armazenado. Ele pode usar qualquer implementação de Logger, seja para salvar em arquivo, banco de dados, enviar para a nuvem, etc., simplesmente alterando a implementação passada para ele.

Ao introduzir uma abstração como Logger, tornamos o código mais modular, flexível e aderente ao Princípio da Inversão de Dependência. O LogService agora depende de uma abstração e pode trabalhar com qualquer implementação dessa abstração, facilitando mudanças e mantendo o código mais limpo e sustentável.

Resumo

A imagem abaixo apresenta um resumo rápido dos princípios SOLID:

Conclusão

Os princípios SOLID são essenciais para o desenvolvimento de software de alta qualidade. Eles ajudam a criar sistemas mais flexíveis, modulares, e de fácil manutenção, o que é fundamental em ambientes de desenvolvimento ágeis e em constante mudança. Ao seguir esses princípios, os desenvolvedores podem evitar problemas comuns no design de software, como código frágil, difícil de testar, e difícil de entender. Portanto, incorporar esses princípios no dia a dia do desenvolvimento pode melhorar significativamente a qualidade do código e a eficiência das equipes de desenvolvimento.

Espero que o conteúdo apresentado tenha sido claro e seja útil para seus estudos e aprovação. Bons estudos!

Quer saber quais serão os próximos concursos?

Confira nossos artigos!

Concursos abertos

Concursos 2024

Créditos:

Estratégia Concursos

Acesse também o material de estudo!

Deixe uma mensagem

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *