8 dicas para melhorar seus códigos
Depois que a gente já tem uma certa experiência com programação, nos perguntamos:
Será que sou um bom programador?
Como ser um bom programador?
Como posso melhorar meus códigos?
Será que escrevo bons códigos?
A resposta para estas questões é simples: ESCREVA CÓDIGOS LEGÍVEIS! FAÇA UM CÓDIGO LIMPO!
Antes de aprender a como escrever códigos mais legíveis, é necessário entender o motivo de ter interesse neste assunto.
No meio da computação diversos códigos são produzidos diariamente, mas nem todos possuem uma qualidade razoável ou boa. Além disso, muitas vezes será necessário fazer manutenção de algum código e se a leitura deste código for dificultosa, como você irá fazer a manutenção deste? Você pode acreditar que para um código ser ruim ou bom varia de programador para programador, mas com esta publicação espero mudar seu pensamento.
Nota
Tudo aqui dito teve como base experiências passadas e guiado pelo livro Código Limpo (nome original Clean Code), de Robert C. Martin.
O interessante para determinar se um código é limpo ou não, é analisá-lo de maneira sistemática, verificando quantos caracteres devem haver por linha, quantas linhas cada módulo deve ter, duplicação de código, etc. E se você ler o Clean Code, verá que o autor faz isso, trazendo diversos exemplos.
Pode-se verificar se o código é / está limpo a partir dos seguintes pontos:
- Nomes descritivos;
- Módulos pequenos;
- Comentários;
- Código repetido;
- Quantidade de parâmetros;
- Padronização;
- Testes unitários;
- Separação em arquivos;
1. Nomes descritivos#
A escolha de nomes descritivos é muito importante para facilitar a leitura. Bem… Quem nunca viu uma função que recebe como parâmetros a
, b
e c
. Daí você se pergunta:
Mas que diabos são esses
a
,b
ec
?
Este é exatamente o problema, ao ler o nome destas variáveis, você não sabe o que elas representam, somente lendo o restante do código é que elas exprimem algum significado. O mesmo se aplica a nome de funções, arquivos, etc. Como deve ter notado, a escolha de nome dificulta a leitura, pois é necessário ficar “cavucando” o funcionamento e a regra do negócio para se ter uma compreensão mínima do que aquilo descrito representa. Sendo assim, também não abrevie para reduzir caracteres. Veja o seguinte exemplo:
float calc (float b, float c) {
float a = b * c;
return a;
}
Você deve ter percebido que somente lendo o nome da função não é possível determinar o que ela faz, assim sendo necessário olhar sua implementação, só que ao vê-la, não há como dizer o que ela faz (seu significado) além de multiplicar dois números, consequentemente sendo necessário conhecer “a regra de negócio”. Obs: Esta função tem como objetivo calcular a área de um retângulo, será que se eu não explicasse você saberia que era isso?
2. Módulos pequenos#
Ao criar módulos grandes, a leitura é dificultosa (por fazerem mais de uma coisa), assim, é provável que existam bugs nestes trechos, pois vários detalhes de implementação estão numa mesma parte. Agora em uma função pequena, a ideia é que ela faça apenas uma coisa, ou seja, criando funções especializadas no que fazem e de fácil leitura, o que por sua vez diminui a probabilidade de bugs. Desse modo, o escopo é mais claro, o que faz com que o tempo gasto para encontrar as falhas sejam bem menores.
Exemplo de problemas com funções que fazem mais de uma coisa. Um amigo meu estava fazendo um programa em que precisava descobrir o número de linhas de um arquivo, sortear uma linha e fazer a leitura desta linha. O problema estava no fato de que ao descobrir o número de linhas do arquivo, o ponteiro era movido para o final do arquivo, assim ao tentar fazer a leitura do conteúdo do arquivo, não devolvia nada. Este problema só ocorreu pelo fato do meu colega não ter criado um módulo para apenas pegar o número de linhas do arquivo, outro para sortear uma linha e outro para ler cada uma das linhas.
// [...]
FILE *pArquivo;
int nLinhas = 0;
char caracter;
char palavra[100];
if ( (pArquivo=fopen("meuArquivo.txt", "r")) != NULL) {
while(fscanf(pArquivo, "%c", &caracter) != EOF) {
if (caracter == '\n') {
nLinhas+=1;
}
}
// Aqui está o problema, pois o ponteiro pArquivo não está mais na posição inicial
for(i=0; i<rand()%nLinhas+1; i++){
fscanf(pArquivo, "%s\n", &palavra);
}
fclose(pArquivo);
// [...]
A solução mais simples encontrada foi mandar o pArquivo
para a posição inicial novamente, como mostra o código abaixo:
// [...]
FILE *pArquivo;
int nLinhas = 0;
char caracter;
char palavra[100];
if((pArquivo=fopen("meuArquivo.txt", "r")) != NULL) {
while(fscanf(pArquivo, "%c", &caracter) != EOF) {
if(caracter == '\n'){
nLinhas+=1;
}
}
rewind(pArquivo);
for (i=0; i<rand()%nLinhas+1; i++) {
fscanf(pArquivo, "%s\n", &palavra);
}
fclose(pArquivo);
// [...]
Existem soluções melhores, mas seguindo a mesma lógica, o código com módulos pequenos ficaria:
// [...]
void lerLinhaSorteada(char *nomeDoArquivo) {
int quantidadeDeLinhas = contarNumeroDeLinhas(nomeDoArquivo);
if (quantidadeDeLinhas == -1) {
return; // não conseguiu contar o número de linhas
}
int linha = sortear(quantidadeDeLinhas);
FILE *arquivo = fopen(nomeDoArquivo, "r");
if (arquivo == NULL) {
return; // não abriu o arquivo
}
moverPonteiroArquivo(&arquivo, linha);
char palavra[100];
fscanf(arquivo,"%s\n",palavra);
fclose(arquivo);
printf("%s\n",palavra);
}
int contarNumeroDeLinhas(char *nomeDoArquivo) {
int quantidadeDeLinhas = 0;
char caracter;
FILE *arquivo = fopen(nomeDoArquivo, "r");
if (arquivo == NULL) {
quantidadeDeLinhas = -1;
} else {
while(fscanf(arquivo, "%c", &caracter) != EOF) {
if (caracter == '\n') {
quantidadeDeLinhas++;
}
}
fclose(arquivo);
}
return quantidadeDeLinhas;
}
int sortear(int limite) {
return rand()%limite+1;
}
void moverPonteiroArquivo(FILE **arquivo, int linha) {
char palavra[100];
for(int i=0; i<linha-1; i++) {
fscanf(*arquivo, "%s\n",&palavra);
}
}
// [...]
3. Comentários#
“Se precisa comentar, obviamente não está legível.”
Muitos programadores utilizam-se de comentários para explicar o que o código faz, e isso é uma péssima prática, pois você acaba tendo dois trabalhos, o de escrever os códigos e o de escrever os comentários dos mesmos, como mostra o exemplo abaixo (atente-se da linha 1 à linha 6):
/*
Função para calcular a área de um retângulo
float b um lado do retângulo
float c outro lado do retângulo
Retorno área do retângulo
*/
float calc (float b, float c) {
float a = b * c; // a recebe b * c
return a; // retorna a
}
Sendo que era possível escrever um código descritivo (deixando de lado a necessidade de comentários). Além disso, os comentários podem ser altamente destrutivos já que existe a possibilidade de lermos um comentário achando que a função faz X, quando na verdade faz Y, o que pode gerar diversas falhas e com a modificação da função ao longo do tempo, sem saber o que ela faz por completo, pode tornar-se um monstro que ninguém entende o funcionamento, como o seguinte:
#include <string.h>
[...]
/*
Função para contar o tamanho da string
char *string String a ter seu tamanho contado
Retorno tamanho da string
*/
int contarTamanho(char *string) {
int tamanho=0;
int i=0;
while(string[i]!='\0' && i < strlen(string)){
if(string[i]==' '){
string[i]='\0';
}
tamanho++;
i++;
}
return tamanho;
}
Note que lendo os comentários da linha 3 à 9, espera-se que a função somente conte o tamanho da string, mas além de “contar” ela retina o espaço e coloca um '\0'
, que indica o final da string, em outras palavras, a função que somente deveria contar está modificando a string. Este código é pequeno, mas se estivesse no meio de um código maior provavelmente você não enxergaria tal problema.
Também existem os comentários que são extremamente desnecessários, onde o próprio código já é autoexplicativo, como o exemplo a seguir:
/*
Função para calcular a área de um retângulo
float b um lado do retângulo
float c outro lado do retângulo
Retorno área do retângulo
*/
float calc (float b, float c) {
float a = b * c; // a recebe b * c
return a; // retorna a
}
Note que as linhas 8 e 9 já dizem o que elas fazem, não é necessário um comentário para explicá-las.
Além disso, existem casos em que trechos de códigos que foram comentados, e, ao se ter isso num projeto grande em equipe, não se sabe se o motivo daquele trecho comentado e se pode removê-lo, veja abaixo:
module inicial ( giro, entrada, saida, metais, ledVerde, ledVermelho, display, clock );
input giro, entrada, saida, metais, clock;
output [1:0] ledVerde, ledVermelho;
output [6:0] display;
reg [3:0] tmp;
[...]
tmp = { giro, entrada, saida, metais };
/*tmp[3] = giro;
tmp[2] = entrada;
tmp[1] = saida;
tmp[0] = metais;*/
[...]
endmodule
Nota: A linha 8 é equivalente as linhas 9 à 12. Assim você pode utilizar comentários para explicar o motivo de utilizar determinada solução e não outra. Exemplo:
module inicial ( giro, entrada, saida, metais, ledVerde, ledVermelho, display, clock );
input giro, entrada, saida, metais, clock;
output [1:0] ledVerde, ledVermelho;
output [6:0] display;
reg [3:0] tmp;
[...]
tmp = { giro, entrada, saida, metais }; // Equivalente as linhas 9 à 12, só é uma maneira mais simples de escrever
/*tmp[3] = giro;
tmp[2] = entrada;
tmp[1] = saida;
tmp[0] = metais;*/
[...]
endmodule
4. Código repetido#
Você pode pensar que duplicar pequenos trechos de códigos pode ser bom para melhorar a eficiência de um programa, mas deve-se ter em mente que códigos duplicados só trazem problemas de manutenção, pois caso seja necessário mudar este trecho, será necessário verificar todos os locais em que ele foi utilizado para fazer tal modificação e isso se resolveria facilmente utilizando funções. Exemplo:
#include <stdbool.h>
#include <string.h>
// [...]
bool substituirVogaisPorEspaco(char *string) {
char vogais[]="aeiouAEIOU";
bool substituiu = false;
for (int i=0; i<strlen(string); i++) {
for( int j=0; j<strlen(vogais); j++){
if(string[i] == vogais[j]){
string[i] = ' ';
substituiu = true;
}
}
}
return substituiu;
}
bool substituirNumerosPorLetras(char *string) {
char numeros[]="0123456789";
bool substituiu = false;
for (int i=0; i<strlen(string); i++) {
for (int j=0; j<strlen(numeros); j++) {
if(string[i] == numeros[j]){
string[i] = 'A';
substituiu = true;
}
}
}
return substituiu;
}
// [...]
Uma possível solução seria:
#include <stdbool.h>
#include <string.h>
// [...]
bool substituirVogaisPorEspaco(char *string) {
char vogais[]="aeiouAEIOU";
bool substituiu, tmp;
for (int i=0; i<strlen(vogais); i++) {
tmp = substituirCaractere(string, vogais[i], ' ');
if(tmp){
substituiu = tmp;
}
}
return substituiu;
}
bool substituirNumerosPorLetras(char *string) {
char numeros[]="0123456789";
bool substituiu, tmp;
for (int i=0; i<strlen(numeros); i++) {
tmp = substituirCaractere(string, numeros[i], 'A');
if(tmp){
substituiu = tmp;
}
}
return substituiu;
}
bool substituirCaractere(char *string, char antigoCaractere, char novoCaractere) {
bool substituiu = false;
for (int i=0; i<strlen(string); i++) {
if(string[i] == antigoCaractere){
string[i] = novoCaractere;
substituiu = true;
}
}
}
return substituiu;
}
// [...]
Note que as funções substituirVogaisPorEspaco
e substituirNumerosPorLetras
ficaram ligeiramente menores e mais simples de serem lidas.
5. Quantidade de parâmetros#
Uma função com vários parâmetros pode dificultar muito a leitura, por isso é recomendável a criação de estruturas que comportem tais variáveis, pois assim a função necessitará de muito menos parâmetros. Exemplo: pense numa função que irá desenhar um triângulo na tela. Este triângulo tem posição dos vértices na tela, cor de preenchimento e cor da linha. Assim você poderia criar
typedef struct {
int x, y;
} Ponto;
void desenhar_triangulo(Ponto ponto1, Ponto ponto2, Ponto ponto3, char *corLinha, char *corPreenchimento) {
// [...]
}
mas é possível diminuir o número de parâmetros e facilitar a leitura da função
typedef struct {
int x, y;
} Ponto;
typedef struct {
char corPreenchimento[100];
char corLinha[100];
Ponto vertices[3];
} Triangulo;
void desenhar(Triangulo triangulo) {
// [...]
}
Veja quantos detalhes foram suprimidos e mesmo assim somente lendo o nome da função e o parâmetro recebido, é possível afirmar que é feita a ação de desenhar um triângulo.
6. Padronização#
Padrão é importante para que você encontre facilmente o que deseja, seja uma função, uma classe, um arquivo, uma variável, ou seja, facilita encontrar qualquer coisa, assim, qualquer pessoa que for usar seu código saberá o que esperar/ como procurar. Exemplo:
#include <stdbool.h>
#include <string.h>
// [...]
bool trocarVogaisPorEspaco(char *string) {
char vogais[]="aeiouAEIOU";
bool substituiu, tmp;
for (int i=0; i<strlen(vogais); i++) {
tmp = substituirCaractere(string, vogais[i], ' ');
if (tmp) {
substituiu = tmp;
}
}
return substituiu;
}
bool substituirNumerosPorLetras(char *string){
char numeros[]="0123456789";
bool substituiu, tmp;
for (int i=0; i<strlen(numeros); i++) {
tmp = substituirCaractere(string, numeros[i], 'A');
if (tmp) {
substituiu = tmp;
}
}
return substituiu;
}
bool substituirCaractere(char *string, char antigoCaractere, char novoCaractere) {
bool substituiu = false;
for (int i=0; i<strlen(string); i++) {
if (string[i] == antigoCaractere) {
string[i] = novoCaractere;
substituiu = true;
}
}
}
return substituiu;
}
// [...]
Veja que embora seja uma ligeira mudança no nome, ao tentar usar a função que troca números por uma letra, você provavelmente procuraria por trocarNumerosPorLetras
, pois na função que troca vogais por espaço está trocarVogaisPorEspaco
, além disso, como o nome está diferente, você pode pensar que existe alguma mudança na implementação das duas funções, já que os nomes são diferentes.
7. Testes automatizados#
Pensando que seu código está dividido em funções pequenas, e sendo estas funções especialistas no que fazem. É possível fazer a utilização de teste para procurar falhas e verificar a integridade do sistemas conforme as atualizações ocorrem. Além disso, com teste automatizados você não precisa ficar testando manualmente seu projeto, o que demanda muito menos tempo em comparação a testes manuais. Exemplo: Pensando que a função (citada no ponto 4 - Código Repetido) substituirVogaiPorEspaco
faça parte de um projeto maior e que o projeto passou por atualizações. Abaixo temos um teste desta função.
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
// [...]
bool testSubstituirVogaisPorEspaco(void) {
char *string ="abcdef";
char *resultadoEsperado = " bcd f";
substituirVogaisPorEspaco(string); // string = "AbcdEf"
if (!strcmp( resultadoEsperado, string)) {
printf("Erro no teste da funcao substituirVogaisPorEspaco\n");
return false;
}
return true;
}
// [...]
Veja que a função está colocando as vogais em maiúsculas e não trocando por espaço. Talvez manualmente você não encontraria tal erro facilmente, mas com o teste automatizado foi bem simples e rápido encontrar a falha. Obs: Este teste foi apenas um simples exemplo, para um projeto real seria interessante ter-se mais testes.
8. Separação em arquivos#
Agora se seu código segue todos estes padrões, é interessante que as coisas (funções, classes, etc) estejam separados em arquivos, de modo que as mesmas estejam agrupadas de acordo com sua proximidade. Por exemplo, pense num sistema que possua algum tipo de relação entre cliente e produto, neste caso, se você tivesse que alterar algum dado do cliente, você iria procurar na pasta / arquivo referente ao cliente e não em produto. Arquivo do cliente:
#include<stdbool.h>
// [...]
typedef struct {
int id;
char nome[100];
char CPF[12];
} Cliente;
bool cadastrar(Cliente cliente) {
// [...]
}
bool editar(Cliente cliente) {
// [...]
}
Arquivo do produto:
#include<stdbool.h>
typedef struct {
int id;
char nome[100];
char descricao[300];
float preco;
} Produto;
bool cadastrar(Produto produto) {
// [...]
}
bool editar(Produto produto) {
// [...]
}
Assim, se você quisesse adicionar mais um atributo ao cliente, você iria ao arquivo do cliente. E se quisesse implementar uma função para excluir um produto, iria fazer isso no arquivo do produto. Também observe que como estão separados em arquivos, não é necessário colocar os nomes como cadastrarCliente
e cadastrarProduto
.
Conclusão#
Note que para escrever um código limpo não é simples ou rápido, mas também não é algo de outro mundo, somente requer bastante treino. Mostrei diversos exemplos de como os códigos podem ser melhorados e mesmo que você não tenha total domínio da linguagem usada nos exemplos ainda sim é possível notar a melhoria de legibilidade trazidas pelas mudanças.
Tudo isso foi apenas uma visão geral, recomendo a leitura do livro Clean Code que aí você realmente verá mais aprofundadamente como produzir bons trechos de código e apresentação de dados sobre o assunto.