Boas Práticas Nathan Geeksman

Princípios SOLID: Construindo Software Escalonável

Princípios SOLID: Construindo Software Escalonável

Princípios SOLID: Construindo Software Escalonável

Introdução

O desenvolvimento de software está cada vez mais sujeito às demandas crescentes por escalabilidade, flexibilidade e manutenibilidade. O contexto atual da indústria de tecnologia, com a necessidade constante de implementar novas funcionalidades e melhorias em produtos existentes, torna essenciais as práticas de desenvolvimento eficientes.

Nesse cenário, a aplicação dos princípios SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle e Dependency inversion principle) se destaca como uma estratégia fundamental para garantir que os sistemas de software sejam escaláveis, facilmente manuteníveis e abertos a futuras mudanças.

Através da adoção desses princípios, os desenvolvedores podem criar soluções mais robustas, menos propensas a problemas de complexidade crescente. Além disso, esses conceitos ajudam a promover o código limpo, facilitando a colaboração entre times e reduzindo o tempo necessário para implementar novas funcionalidades.

Ao final deste artigo, você estará capaz de:

  • Compreender os cinco princípios SOLID em detalhes;
  • Reconhecer como esses princípios são aplicados no desenvolvimento de software escalável;
  • Identificar áreas onde a adoção dos princípios SOLID pode melhorar a estrutura do seu código atual.

O que é e por que importa

O SOLID é um acrônimo formado pelos primeiros letras das siglas dos cinco princípios de design de software propostos pelo programador Robert C. Martin (conhecido como "Uncle Bob") para ajudar a criar sistemas mais robustos, escaláveis e fáceis de manter. Os princípios SOLID são uma forma de guiar o desenvolvimento de código que seja flexível e fácil de alterar ao longo do tempo.

Cada um dos princípios se concentra em melhorar a estrutura de software, contribuindo para:

  • Reduzir a complexidade;
  • Melhorar a manutenibilidade;
  • Facilitar a reutilização de código;
  • Ampliar a flexibilidade da solução.

Esses conceitos não são apenas uma coleção de abstrações teóricas, mas sim ferramentas práticas para resolver problemas comuns no desenvolvimento de software. Ao aplicá-los corretamente, os desenvolvedores podem criar sistemas que atendem às necessidades crescentes da empresa, desde a implementação inicial até as etapas de manutenção e expansão.

Como funciona na prática

Seja S: Princípio da Abertura para Substituição

O princípio da abertura para substituição estabelece que as classes devem ser projetadas de forma a permitir que subclasses possam substituir a implementação das superclasses. Isso significa que, ao invés de uma classe ser rígida e não permitem alterações, ela deve ser flexível e permitir que novas implementações sejam adicionadas.

  • Benefício: facilita a manutenção e expansão do código.
  • Exemplo prático:
  • Suponha um sistema de gerenciamento de estoque que tem uma classe Produto com método calcularPreco.

+ Inicialmente, o método é simples e calcula o preço com base na categoria (ou seja, alimentos, eletrônicos, etc.).

+ Com o passar do tempo, é necessário adicionar mais categorias de produtos.

+ Em vez de modificar a classe Produto, podemos criar subclasses como Alimento ou Eletronico que herdam da classe Produto.

+ Cada subclass pode ter sua própria implementação específica do método calcularPreco.

Seja O: Princípio da Abertura para Objeto

O princípio da abertura para objeto estabelece que um objeto deve ser capaz de se comportar como qualquer outro objeto no sistema. Isso significa que os objetos devem ser intercambiáveis e não há dependências específicas entre eles.

  • Benefício: facilita a mudança de implementação do código.
  • Exemplo prático:
  • Suponha um sistema de gerenciamento de login que tem uma classe Autenticador com método autenticar.

+ Inicialmente, o método é simples e autentica os usuários apenas com senha.

+ Com o passar do tempo, é necessário adicionar suporte para login via biometria (fingerprint ou face).

+ Em vez de criar uma classe separada para AutenticadorBiometrico, podemos criar um objeto que implemente a interface Autenticador e use o método autenticar.

+ A classe responsável por gerenciar os logs pode estar desconhecida do tipo de autenticação utilizado.

Seja L: Princípio da Responsabilidade Única

O princípio da responsabilidade única estabelece que uma classe deve ter apenas uma responsabilidade e não vá além disso. Isso significa que as classes devem ser especializadas em realizar uma tarefa específica e não estar sobrecarregadas com muitas funções.

  • Benefício: facilita a manutenção do código.
  • Exemplo prático:
  • Suponha um sistema de gerenciamento de estoque que tem uma classe Produto com métodos para calcular o preço, verificar se está em estoque e imprimir o código de barras.

+ A classe Produto não deve ter mais de 1 ou 2 responsabilidades (como calcular o preço e verificar se está em estoque), pois isso pode tornar seu código difícil de manter.

Seja S: Princípio da Segregação de Interface

O princípio da segregação de interface estabelece que uma classe não deve ter mais de um motivo para mudar. Isso significa que as classes devem ter interfaces especializadas, ou seja, se uma classe tem várias responsabilidades e é necessário alterá-la em apenas uma dessas responsabilidades, não é necessário modificar toda a classe.

  • Benefício: facilita a evolução do código.
  • Exemplo prático:
  • Suponha um sistema de gerenciamento de estoque que tem uma classe Produto com métodos para calcular o preço e imprimir o código de barras.

+ Se é necessário adicionar suporte para impressão em formatos diferentes, pode-se criar subclasses especializadas para esses casos.

Seja D: Princípio da Inversão de Dependência

O princípio da inversão de dependência estabelece que as classes devem depender das abstrações, não dos detalhes. Isso significa que as classes devem ser projetadas para serem independentes das implementações específicas e apenas dependente do contrato.

  • Benefício: facilita a mudança de implementação do código.
  • Exemplo prático:
  • Suponha um sistema de gerenciamento de estoque que tem uma classe Produto que depende da classe CalculadoraPreco.

+ A classe Produto não deve depender da implementação específica da classe CalculadoraPreco, mas apenas do contrato definido por ela.

Exemplo real

Aplicação ao sistema de gerenciamento de estoque

Suponha que um sistema de gerenciamento de estoque esteja sendo desenvolvido e que ele possa lidar com diferentes tipos de produtos, incluindo artigos de papelaria e materiais de construção. Nesse caso, o sistema precisará lidar com diferentes métodos de cálculo de preços para esses tipos de produtos.

// Exemplo de como aplicar os princípios SOLID ao sistema de gerenciamento de estoque
interface ICalculadoraPreco {
    // Contrato para a calculadora de preço
    float calcularPreco(float precoUnitario, int quantidade);
}

class CalculadoraPrecoPapelaria implements ICalculadoraPreco {
    @Override
    public float calcularPreco(float precoUnitario, int quantidade) {
        // Implementação específica para papelaria, com desconto de 10%
        return precoUnitario * quantidade * 0.9f;
    }
}

class CalculadoraPrecoConstrução implements ICalculadoraPreco {
    @Override
    public float calcularPreco(float precoUnitario, int quantidade) {
        // Implementação específica para construção, sem desconto
        return precoUnitario * quantidade;
    }
}

public class Produto {
    private String nome;
    private float precoUnitario;
    private ICalculadoraPreco calculadoraPreco;

    public Produto(String nome, float precoUnitario) {
        this.nome = nome;
        this.precoUnitario = precoUnitario;
    }

    // O produto depende do contrato da interface ICálculadoraPreco,
    // não da implementação específica
    public void setCalculadoraPreco(ICalculadoraPreco calculadoraPreco) {
        this.calculadoraPreco = calculadoraPreco;
    }

    public float calcularTotal(int quantidade) {
        return precoUnitario * quantidade * calculadoraPreco.calcularPreco(precoUnitario, quantidade);
    }
}

Esse exemplo demonstra como o princípio da inversão de dependência pode ser aplicado para que as classes sejam independentes das implementações específicas e apenas dependentes do contrato. Isso facilita a mudança de implementação do código sem afetar outros módulos. Além disso, o exemplo também mostra como o sistema de gerenciamento de estoque pode lidar com diferentes tipos de produtos, utilizando classes especializadas para cada tipo de produto, o que demonstra o princípio da segregação de interface.

Boas práticas

Utilize interfaces para definir contratos e deixe a implementação flexível

  • Evite a dependência rígida de classes específicas em seu código, pois isso limita a capacidade de mudança ou atualização do sistema.
  • Defina interfaces claras que descrevam as responsabilidades das classes e permitam que esses contratos sejam implementados de diferentes maneiras.
  • Utilize o princípio da inversão de dependência para garantir que suas classes sejam independentes das implementações específicas.

Foco na reutilização e no baixo acoplamento

  • Desenvolva componentes reutilizáveis que possam ser integrados a diferentes partes do sistema.
  • Busque manter as classes com acoplamento baixo, o que significa evitar fortes dependências entre elas.

Armadilhas comuns

Polimorfismo não é sinônimo de SOLID

  • O polimorfismo pode ser alcançado sem aplicar os princípios SOLID.
  • Utilize o polimorfismo apenas quando necessário, para evitar a sobrecarga excessiva da código.

Sobrecarga excessiva: um risco ao uso do polimorfismo

  • A adição de muitas classes que implementam interfaces pode levar a uma sobrecarga excessiva.
  • Certifique-se de não exceder o número razoável de classes e de manter a hierarquia clara.

Conclusão

Os princípios SOLID são uma ferramenta valiosa para desenvolvedores que buscam construir sistemas escaláveis e manuteníveis. Ao adotar esses princípios, você pode criar código mais flexível, reutilizável e testável.

Alguns dos pontos principais incluem a utilização de interfaces claras e flexíveis, que permitem implementações diferentes para atender às necessidades específicas do sistema. Além disso, é importante manter as classes com baixo acoplamento, evitando fortes dependências entre elas.

Para aprofundar seu conhecimento nessa área, recomenda-se estudar mais sobre os princípios SOLID e como aplicá-los em diferentes contextos. Além disso, é importante estar atento às armadilhas comuns relacionadas à polimorfia e ao baixo acoplamento.

Em termos de próximos passos, você pode considerar:

  • Estudar os princípios SOLID em mais detalhes, incluindo a Lei do Depósito (D), o Princípio da Responsabilidade Única (SRP) e a Lei da Substituição de Liskov (LSP).
  • Aprender sobre design de classes e padrões de projeto que sejam compatíveis com os princípios SOLID.
  • Focar em melhorias contínuas do seu código, aplicando os princípios SOLID para torná-lo mais escalável e manutenível.

Referências

  • Fowler, M. Princípios da Orientação a Objetos. Disponível em: https://martinfowler.com/books/eaa.html. Acesso: 2024.
  • "SOLID: The First Five Principles of Object-Oriented Design" disponível em: https://www.thoughtworks.com/insights/blog/solid-first-five-principles-object-oriented-design
  • "The SOLID Principles in C# - .NET Core Tutorials" Disponível em: https://dotnet.microsoft.com/en-us/tutorials/designers-apis/principles-of-oop.
  • "Princípios SOLID para projetos de software escaláveis" disponível em: https://12factor.net/pt_br/build/repositores-de-codigo
  • "Princípios SOLID na prática: um guia completo" disponível em: https://owasp.org/www-pdf-archive/OWASP-Principles-of-OOP.pdf