Código limpo é, essencialmente, um código bem organizado, legível e fácil de manter. Isso envolve a seleção cuidadosa de nomes claros e a organização eficiente do código para facilitar tanto a compreensão quanto a manutenção.
Neste artigo, exploraremos aspectos importantes relacionados ao tratamento de erros, objetos e estrutura de dados, limites, testes de unidade e situações de emergência dentro desse contexto.
Não sabe o que significa Código Limpo? Leia já a primeira parte deste artigo antes de avançar para as boas práticas de código limpo avançado.
Código Limpo Avançado: Tratamento de Erros
O tratamento de erro domina completamente muitos códigos-fonte devido a estarem espalhados por todos os lados. Esse recurso é importante, mas se obscurecer a lógica, está errado.
Use Exceções em Vez de Retornar Códigos
Num passado longínquo, havia muitas linguagens que não suportavam exceções. Nelas, as técnicas para tratar e informar erros eram limitadas. Ou você criava uma flag de erro ou retornava um código de erro que o chamador pudesse verificar. O problema era que essas técnicas entupiam o chamador, que devia verificar erros imediatamente após a chamada. Infelizmente, facilmente se esqueciam de fazer isso. Por esse motivo, segundo as regras do código limpo, é melhor lançar uma exceção quando um erro for encontrado. O código de chamada fica mais limpo e sua lógica não fica ofuscada pelo tratamento de erro.
Forneça Exceções Com Contexto
Cada exceção lançada deve fornecer contexto suficiente para determinar a fonte e a localização de um erro. Crie mensagens de erro informativas e as passe juntamente com as exceções. Mencione a operação que falhou e o tipo da falha.
Defina as Classes de Exceções Segundo as Necessidades do Chamador
Há muitas formas de classificar erros. Pode ser pela origem (eles vieram desse componente ou daquele?) ou pelo tipo (são falhas de dispositivo, de redes ou erros de programação?). No entanto, quando definimos as classes de exceção num aplicativo, nossa maior preocupação deveria ser como elas são capturadas.
Geralmente, uma única classe de exceção está boa para uma parte específica do código. As informações enviadas com a exceção podem distinguir os erros. Use classes diferentes apenas se houver casos em que você queira capturar uma exceção e permitir que a outra passe normalmente.
Não Retorne Null
Uma das coisas que fazemos que levam a erros é retornar null. Segundo Robert C. Martin:
“Já perdi a conta dos aplicativos que já vi que em quase toda linha verificava por null…“.
Quando retornamos null, basicamente estamos criando mais trabalho para nós mesmos e jogando problemas em cima de nossos chamadores. Só basta esquecer uma verificação null para que o aplicativo fique fora de controle. Se você ficar tentado a retornar null de um método, em vez disso, considere lançar uma exceção ou retorne um objeto de caso especial.
Não Passe Null
Retornar null dos métodos é ruim, mas passar null para eles é pior. A menos que esteja trabalhando com uma API que espere receber null, você deve evitar passá-lo em seu código sempre que possível.
Código Limpo Avançado: Objetos e Estrutura de Dados
Abstração de Dados
Ocultar a implementação não é só uma questão de colocar uma camada de funções entre as variáveis. É uma questão de abstração! Uma classe não passa suas variáveis simplesmente por meio de métodos de escrita e leitura (getters e setters). Em vez disso, ela expõe interfaces abstratas que permitem aos usuários manipular a essência dos dados, sem precisar conhecer a implementação.
A Dicotomia Entre Objetos e Estruturas de Dados
O código procedural (usado em estruturas de dados) facilita a adição de novas funções sem precisar alterar as estruturas de dados existentes. O código orientado a objeto (OO), por outro lado, facilita a adição de novas classes sem precisar alterar as funções existentes. Já o código OO dificulta a adição de novas funções enquanto o código procedural dificulta a adição de novas estruturas de dados, pois todas as funções teriam de ser alteradas. Portanto, o que é difícil para OO é fácil para o procedural, e o que é difícil para procedural é fácil para OO.
A Lei de Demeter
Há uma heurística chamada Lei de Demeter: um módulo não deve enxergar o interior dos objetos que ele manipula. Os objetos escondem seus dados e expõem as operações. Isso significa que um objeto não deve expor sua estrutura interna por meio dos métodos acessores, pois isso seria expor, e não ocultar, sua estrutura interna.
Mais precisamente, a lei de Demeter diz que um método f de uma classe C só deve chamar
* C
* Um objeto criado por f
* Um objeto passado como parâmetro para f
* um Objeto dentro de uma variável de instância C
Train Wrecks (Acidentes ferroviários)
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
O tipo de código acima costuma ser chamado de train wreck, pois parece com um monte de carrinhos de trem acoplados. Cadeias de chamadas como essa geralmente são consideradas descuidadas e devem ser evitadas. Uma sugestão seria assim:
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
Código Limpo Avançado: Limites
Raramente controlamos todos os softwares em nossos sistemas. De vez em quando compramos pacotes de outros fabricantes ou usamos códigos livres, ou dependemos de equipes em nossa própria empresa para construir componentes ou subsistemas para nós. De algum modo, devemos integrar de forma limpa, esse código externo ao nosso.
O Uso de Código de Terceiros
Há uma tensão natural entre o fornecedor de uma interface e seu usuário. Os fornecedores de pacotes e frameworks de outros fabricantes visam a uma maior aplicabilidade de modo que possam trabalhar com diversos ambientes e atender a um público maior. Já os usuários desejam uma interface voltada para suas próprias necessidades. Essa tensão pode causar problemas nos limites de nossos sistemas.
Explorando e Aprendendo Sobre Limites
Códigos de terceiros nos ajudam a obter mais funcionalidade em menos tempo. Por onde começar quando desejamos utilizar pacotes de terceiros? Não é tarefa nossa testá-los, mas pode ser melhor para nós criar testes para os códigos externos que formos usar.
Entender códigos de terceiros é difícil. Integrá-los ao seu também é. Fazer ambos ao mesmo tempo dobra a dificuldade. E se adotássemos uma outra abordagem? Em vez de experimentar e tentar o novo código, poderíamos criar testes para explorar nosso conhecimento sobre ele. Jim Newkirk chama isso de testes de aprendizagem.
Nesses testes, chamamos a API do código externo como faríamos ao usá-la em nosso aplicativo. Basicamente estaríamos controlando os experimentos que verificam nosso conhecimento daquela API. O teste se focaliza no que desejamos saber sobre a API.
Código Limpo Avançado: Testes de Unidade
As Três Leis do TDD
Primeira Lei: Não se deve escrever o código de produção até criar um teste de unidade de falhas.
Segunda Lei: Não se deve escrever mais de um teste de unidade do que o necessário para falhar, e não compilar é falhar.
Terceira Lei: Não se deve escrever mais códigos de produção do que o necessário para aplicar o teste de falha atual.
Como Manter os Testes Limpos
Quanto pior o teste, mais difícil será mudá-lo. Quanto mais confuso for o código de teste, maiores são as chances de você levar mais tempo espremendo novos testes para dentro da coleção do que na criação do código de produção. Conforme você modifica o código de produção, os testes antigos começam a falhar e a bagunça no código de teste dificulta fazê-los funcionar novamente. Sendo assim, os testes começam a ser vistos como um problema em constante crescimento.
Robert C. Martin comentou uma experiência que teve em um equipe onde a cada distribuição, o custo de manutenção da coleção de testes aumentou. Com o tempo, isso se tornou a maior reclamação entre os desenvolvedores. Quando os gerentes perguntaram o motivo da estimativa de finalização estar ficando tão grande, os desenvolvedores culparam os testes. No final, foram forçados a descartar toda a coleção de testes. Mas, sem uma coleção de testes, eles perderam a capacidade de garantir que, após alterações no código-fonte, ele funcionasse como o esperado e que mudanças em uma parte do sistema não afetariam as outras partes. Então, a taxa de defeitos começou a crescer. Conforme aumentava o número de bugs indesejáveis, começaram a temer fazer alterações e pararam de limpar o código de produção porque temiam que isso poderia fazer mais mal do que bem. O código de produção começou a degradar. No final das contas, ficaram sem testes, com um código de produção confuso e cheio de bugs, com consumidores frustrados e o sentimento de que o esforço para criação de testes não valeu de nada. A moral da história é simples: Os códigos de testes são tão importantes quanto o código de produção.
Eles não são um componente secundário. Requerem raciocínio, planejamento e cuidado. É preciso mantê-los tão limpos quanto o código de produção.
São os testes de unidade que mantêm seus códigos flexíveis, reutilizáveis e passíveis de manutenção. A razão é simples. Se você tiver testes, não terá medo de alterar o código. Sem os testes, cada modificação pode gerar um bug. Não importa o grau de flexibilidade de sua arquitetura ou de divisão de seu modelo, pois sem os testes você ficará relutante em fazer mudanças por temer gerar bugs não detectados. Portanto, ter uma coleção de testes de unidade automatizados que cubram o código de produção é o segredo para manter seu projeto e arquitetura os mais limpos possíveis.
Para Além de Código Limpo: Testes Limpos
O que torna um teste limpo? Três coisas: legibilidade, legibilidade e legibilidade. O que torna os testes legíveis? O mesmo que torna todos os códigos legíveis: clareza, simplicidade e consistência de expressão.
Uma Afirmação por Teste
Há uma linha de pensamento que diz que cada função de teste deve ter uma e apenas uma instrução de afirmação. Essa regra pode parecer perversa, mas a vantagem é que os testes chegam a uma única conclusão que é fácil e rápida de entender.
Um Único Conceito por Teste
Segundo Robert C. Martin, talvez a melhor regra seja que desejamos um único conceito em cada função de teste. Não queremos funções longas que saiam testando várias coisas uma após a outra.
F.I.R.S.T.
Testes limpos seguem outras cinco regras que formam o acrônimo, em inglês, F.I.R.S.T.
São Fast (rápidos), Independent (independentes), Repeatable (reprodutíveis), Self-validating (auto-validáveis) e Timely (pontuais).
- Rapidez: os testes devem ser rápidos. Quando os testes rodam devagar você não desejará executá-los com frequência. E, consequentemente, não encontrará problemas cedo o suficiente para consertá-los facilmente.
- Independência: os testes não devem depender uns dos outros. Um teste não deve configurar as condições para os próximos.
- Repetitividade: deve-se poder repetir os testes em qualquer ambiente.
- Autovalidação: os testes devem ter uma saída booleana. Obtenham ou não êxito, você não deve ler um arquivo de registro para saber o resultado. Se os testes não possuírem autovalidação, então uma falha pode se tornar subjetiva, e executar os testes pode exigir uma longa validação manual.
- Pontualidade: os testes precisam ser escritos em tempo hábil. Devem-se criar os testes de unidade imediatamente antes do código de produção no qual serão aplicados.
Código Limpo Avançado: Emergência
E se houvesse quatro regras simples que você pudesse usar para lhe ajudar na criação de bons projetos? Muitos acham que as quatro regras do Projeto Simples de Kent Beck, são de ajuda considerável na criação de um software bem projetado.
De acordo com Kent, um projeto é “simples” se seguir as seguintes regras:
- Efetuar todos os testes;
- Sem duplicação de código;
- Expressar o propósito do programador;
- Minimizar o número de classes e métodos.
Segundo Robert C. Martin, seguir as regras de projeto simples pode incentivar e possibilitar desenvolvedores a aderirem a bons princípios e padrões que, de outra forma, levariam anos para aprender.
Explorando Código Limpo Avançado: Aspectos e Boas Práticas – Considerações Finais
Ao considerarmos o tratamento de erros como uma questão separada e assegurarmos que nosso código funcione de forma precisa, conseguimos alcançar um equilíbrio entre a limpeza e a robustez do software que estamos desenvolvendo.
A flexibilidade e a adaptabilidade são elementos essenciais no desenvolvimento de software, seja para lidar com novos tipos de dados ou para implementar novas funcionalidades. A escolha entre o uso de objetos e estruturas de dados/procedimentos depende do contexto específico de cada situação. Além disso, a capacidade de lidar com mudanças é fundamental para manter um projeto de software saudável e sustentável ao longo do tempo. Isso inclui garantir que o código seja modular e contar com testes robustos para validar seu comportamento.
Portanto, investir na manutenção e na qualidade dos testes é tão crucial quanto no próprio código de produção, pois isso contribui significativamente para a longevidade e a eficácia do projeto como um todo.