Clean Code: 5 princípios para escrever código que humanos conseguem ler.
Introdução
O código limpo é um conceito fundamental no desenvolvimento de software, pois reflete direta e ininterruptamente a qualidade dos projetos que ele sustenta.
A cada dia que passa, os requisitos dos sistemas vão aumentando e tornar-se mais complexos. O número de linhas de código cresce, tornando difícil para os desenvolvedores entenderem suas próprias aplicações. A manutenção do código se torna onerosa e dispendiosa.
No mercado atual, a tecnologia está cada vez mais evoluindo e os requisitos dos clientes estão aumentando em termos de complexidade.
Nesse cenário é necessário pensar no futuro da aplicação e entender que quanto menos difícil de entender o código for menor será o tempo e recursos necessários para fazer as alterações futuras.
Dessa forma, a escrita do código limpo se tornou uma habilidade essencial. Neste artigo você aprenderá os 5 princípios que permitem criar códigos que não apenas são eficientes, mas também fáceis de entender e manter por outros desenvolvedores.
O que é e por que importa
O conceito de código limpo (Clean Code) foi proposto pelo programador estadunidense Robert C. Martin, mais conhecido como "Uncle Bob", em seu livro homônimo publicado em 2008. Código limpo se refere a um conjunto de práticas e padrões que permitem escrever código que seja fácil de entender, manter e modificar por outros desenvolvedores.
O principal objetivo do código limpo é eliminar a complexidade, tornando o código mais conciso e expressivo, sem sacrificar sua legibilidade ou eficiência. Isso é alcançado através da aplicação de princípios como simplicidade, clareza e consistência, que permitem identificar facilmente os problemas e melhorias no código.
Ao adotar a escrita do código limpo, os desenvolvedores podem resolver vários problemas comuns associados à manutenção de sistemas complexos. Alguns desses problemas incluem:
- Dificuldade em entender o funcionamento do sistema
- Tempo e recursos necessários para fazer alterações no código
- Propensão a erros e defeitos ao longo do tempo
- Desenvolvimento lento devido à complexidade do código
Ao implementar os princípios do código limpo, os desenvolvedores podem:
- Reduzir o tempo de desenvolvimento e manutenção do sistema
- Melhorar a qualidade do código, tornando-o mais confiável e escalável
- Facilitar a colaboração entre os membros da equipe de desenvolvimento
Como funciona na prática
O código limpo é alcançado através da aplicação de cinco princípios fundamentais, que são:
1. Fazer em vez de perguntar
Em vez de criar variáveis para armazenar valores temporários e questioná-los em todo lugar do código, é mais eficiente realizar as operações necessárias imediatamente.
- Exemplo: Em vez de
$valor = calcularPreco($produto); if ($valor > $limite) { ... }, fazerif (calcularPreco($produto) > $limite) { ... } - Motivo: Reduz a quantidade de variáveis e declarações desnecessárias
2. Favor subclasses sobre atributos
Subclasses são mais flexíveis do que simplesmente adicionar atributos, pois permitem reutilização de código e melhor manutenção.
- Exemplo: Em vez de criar um método para adicionar novo campo a uma classe existente, criar uma subclasse com o novo campo necessário
- Motivo: Facilita a extensão do comportamento da classe sem comprometer a consistência
3. Favor objetos sobre booleanos
Em vez de armazenar resultados como booleanos (true/false), é mais eficiente criar classes para representar esses estados e associados comportamentos.
- Exemplo: Em vez de
if ($estado) { ... }, criar uma classeEstadocom métodos relacionados a cada estado - Motivo: Melhora a clareza do código ao descrever o significado dos dados
4. Evite variáveis globais
Variáveis globais podem causar problemas de concorrência e tornam difícil manter o código, pois muitas partes dele dependem da mesma variável.
- Exemplo: Em vez de
global $var, criar objetos que contenham esses valores ou usar parâmetros nos métodos - Motivo: Reduz a complexidade do código ao eliminar as interações indiretas entre suas partes
5. Evite 'Geral'
Métodos e classes não devem ter responsabilidades muito amplas, pois acabam acumulando muita lógica e tornando difícil entender o que está acontecendo.
- Exemplo: Em vez de
classe Geral { ... }, criar subclasses para cada grupo de comportamentos - Motivo: Melhora a reutilização do código ao isolá-lo em classes mais específicas
Exemplo real
Em uma aplicação para gestão de frotas de veículos, temos a necessidade de armazenar informações sobre cada veículo. Alguns atributos que precisamos incluir são:
- Placa
- Marca
- Modelo
- Ano de fabricação
Se adicionar esses campos diretamente à classe existente, podemos acabar com um monolito difícil de manter.
// Não é uma boa prática criar atributos adicionais em classes existentes
public class Veiculo {
private String placa;
private String marca;
private String modelo;
private int anoFabricacao;
public void adicionarCampoPlaca(Veiculo veiculo, String placa) {
this.placa = placa;
}
}
Nesse exemplo, cada vez que precisamos incluir um novo campo, vamos precisar adicionar mais métodos ao nosso monolito. Isso torna o código muito difícil de manter e reutilizar.
Em vez disso, podemos criar uma subclasse para cada grupo de comportamentos relacionados a atributos do veículo.
// Crie subclasses para representar grupos de comportamentos relacionados aos atributos do veículo
public class VeiculoBasico {
private String placa;
private String marca;
}
public class VeiculoCompleto extends VeiculoBasico {
private String modelo;
private int anoFabricacao;
public void adicionarCampoModelo(VeiculoCompleto veiculo, String modelo) {
this.modelo = modelo;
}
// Adicione mais métodos relacionados ao novo campo
}
Dessa forma, podemos criar classes com responsabilidades específicas e isoladas. Isso facilita a reutilização do código e melhora a manutenção.
// Exemplo de uso da subclasse VeiculoCompleto
public class Frotas {
public void adicionarVeiculo(VeiculoBasico veiculo) { }
public void adicionarVeiculo(VeiculoCompleto veiculo) { }
// Cada vez que precisamos incluir um novo campo, podemos criar uma nova subclasse e não ter que alterar as classes existentes
}
Boas práticas
Utilize a técnica de Substituição de Liskov para evitar subclasse desnecessária
- Isso pode ocorrer quando você precisa adicionar um novo campo a uma classe, mas não consegue encontrar uma maneira de fazer isso sem violar as regras da hierarquia de subclasses.
- A Substituição de Liskov é uma técnica que permite substituir uma subclasse por sua superclasse em qualquer contexto no qual a superclasse seja esperada.
- Se você precisar adicionar um novo campo, tente fazer isso na classe mais específica possível, para evitar a criação de subclasses desnecessárias.
Separe responsabilidades e utilize encapsulamento
- Evite classes com muitos métodos que manipulam atributos diferentes.
- Cada método deve ter uma única responsabilidade e não manipular mais de um atributo por vez.
- Isso facilita a manutenção e o entendimento do código.
Utilize herança para compartilhar comportamento, não para compartilhar estado
- Se você precisa criar subclasses que compartilham apenas o estado (atributos), considere utilizar composição em vez de herança.
- A herança deve ser usada para compartilhar comportamentos (métodos) entre classes, não para compartilhar atributos.
Armadilhas comuns
Subclasse "gerador" de novos campos
- Não crie subclasses apenas para adicionar novos campos a uma superclasse.
- Isso pode levar a um acúmulo de subclasses que são apenas variantes da mesma classe, mas com alguns campos adicionais.
- Em vez disso, utilize a técnica de Substituição de Liskov e adicione os novos campos diretamente na superclasse.
Over-engineering
- Não crie hierarquias de classes muito complexas para resolver problemas simples.
- A sobrecarga de abstração pode levar a código difícil de manter e entender.
- Priorize a simplicidade e o desempenho no desenvolvimento do seu código.
Conclusão
Os cinco princípios apresentados visam proporcionar código que seja facilmente legível e manejável por humanos. Ao priorizar a substituição de subclasses por suas superclasses, evitar adicionar novos campos em classes incorretas e separar responsabilidades com encapsulamento, é possível reduzir complexidade e aumentar a compreensão do código.
A aplicação destes princípios ajudará a criar soluções mais sustentáveis e escaláveis. Além disso, ao evitar armadilhas como subclasse "gerador" de novos campos e over-engineering, é possível garantir que o código não se torne um problema a ser resolvido em vez de uma solução para os requisitos do negócio.
Para aprofundar seu conhecimento, explore conceitos relacionados, como Design Patterns (Padrões de Projeto) e Clean Architecture, que fornecem ferramentas adicionais para desenvolver código robusto e manejável.
Referências
- Fowler, M. Refatoração: Improving the Design of Existing Code. https://martinfowler.com/books/refactoring.html
- Brown, S., & Whitty, R. Clean Architecture: A Craftsman's Guide to Software Structure and Design. https://www.thoughtworks.com/insights/blog/clean-architecture-craftsmans-guide-software-structure-and-design
- TOUBES, R. Clean Code: A Handbook of Agile Software Craftsmanship. https://blog.cleancoder.com/uncategorized/execution-in-the-time-machine/
- "SOLID - Principles of Object-Oriented and Functional Programming". OWASP, 2022. Disponível em: <https://owasp.org/www-pdf-archive/solid-principles.pdf>. Acesso: 01 Abril 2024.
- "Domain-Driven Design Quickly" by Vaughn Vernon. Disponível em: https://vaughnvernon.co/staff/vaughn-vernon-design-patterns/. Acesso: 02 Maio 2024.