Fatores de qualidade na programação
Esse artigo mostra aos desenvolvedores alguns fatores importantes para um bom desenvolvimento de código-fonte.
Durante o desenvolvimento de um software, os desenvolvedores costumam se deparar com uma série de regras de qualidade. Muitas vezes essas regras são ignoradas, e é assim que os grandes problemas acontecem. Hoje em dia, com praticamente todos os serviços sendo totalmente dependentes de algum tipo de software, é muito importante que eles tenham qualidade. Porém, como é possível realizar-se uma medição de qualidade?
O conceito de qualidade é muito abstrato: o que é bom para alguns, certamente não o será para outros, ou seja, é um conceito que não aceita unanimidade. O que, de certa forma, é bom, pois como diz o ditado, "toda unanimidade é burra". No caso do software, a qualidade geralmente é definida por uma série de regras a serem seguidas, boas práticas que, segundo especialistas, geram o bom software. No caso específico da programação, existem alguns pontos que devem ser seguidos, como a necessidade de documentar o código e habilitar o reuso de código.
Por mais trivial que possa parecer, é extremamente importante analisar atentamente a linguagem de programação que será utilizada. Hoje em dia, com as chamadas linguagens de quarta geração, existe muito trabalho pronto, as chamadas APIs. Embora facilite o trabalho do desenvolvedor, a tendência é que essas APIs compliquem o trabalho da máquina, prejudicando o desempenho. Quando há grande disponibilidade de recursos de máquina, como memória e processamento, isso não é sentido. Porém, isso precisa ser verificado em projetos maiores, que precisarão de cada recurso que a máquina tem para oferecer.
Como um exemplo, vale ressaltar o C# .NET. Como mostra a Figura 1, o .NET tem um meio especial de compilar os aplicativos desenvolvidos para ele. O trabalho do compilador é transformar o programa em uma linguagem intermediária, bytecodes .NET, que são interpretados pela CLR (Common Language Runtime), que nada mais é do que uma máquina virtual. O Java funciona de maneira similar. O que é preciso ter em mente é o quanto isso custa em processamento. O tempo perdido para realizar essa tradução, embora não seja sensível para o ser humano, é absurdo para a máquina.
Figura 1. .NET framework
É claro que, muitas vezes, o desenvolvedor se vê empurrado em direção ao que o mercado precisa. Ninguém irá voltar a desenvolver programas em C só porque o desempenho é superior; seria completamente improdutivo. Porém, é preciso balancear corretamente a importância do desempenho no produto final. O usuário quer interfaces ricas, mas também quer desempenho, e esse é o ponto de equilíbrio que precisa ser encontrado.
No que diz respeito à linguagem, outro ponto importante é o paradigma de programação. A orientação a objetos é o que há de mais interessante do ponto de vista de manutenção e reuso de código, embora seja péssima do ponto de vista do desempenho. Grandes aplicativos corporativos precisam ser quebrados em classes para que possam ser construídos, uma vez que uma pessoa só não conseguiria tal façanha. Novamente, é preciso atenção ao fim, para então definirem-se os meios. Em outras palavras, é preciso verificar o que é mais importante para cada projeto, e então começar a fazer definições.
Criando uma boa documentação
O principal fator que diz respeito à qualidade de um software é o quão boa é sua documentação. De nada adianta o código utilizar técnicas avançadas e extremamente benéficas se ninguém irá entendê-lo daqui a dois meses. Todo desenvolvedor com o mínimo de experiência já deve ter se deparado com algum código mal documentado, e passado maus bocados para entender o que o código realizava e por que. Portanto, quando estiver criando um novo software, é importante estar atento a esse ponto. Poucos desenvolvedores gostam de explicar detalhadamente o que estão fazendo, mas a importância dessa atitude irá se revelar no futuro.
Porém, a questão de como criar essa documentação permanece. Ou seja, qual é o método que deve ser utilizado para mostrar como o software se comporta. O principal deles, e mais utilizado, é o diagrama. A principal linguagem formal utilizada para a modelagem de software é a UML – Unified Modeling Language. Trata-se de uma linguagem que se expressa através de diagramas, e esses diagramas são responsáveis por oferecer múltiplas visões do sistema, modelando-o de acordo com algum enfoque. A UML possui diagramas estáticos e dinâmicos, sendo os mais famosos (e importantes) o diagrama de classes (estático) e o diagrama de casos de uso (dinâmico), demonstrados na Figura 2.
Figura 2. Diagramas UML
Porém, a documentação não significa apenas os diagramas da UML. Uma boa documentação contém uma boa descrição de métodos, além de seguir um padrão claro e simples de ser entendido. É importante notar que um bom software acompanhado de uma má documentação tende a ser tachado como um mau software. Isso é muito complicado, pois se somente o desenvolvedor da aplicação é capaz de entender o quão boa ela é, houve uma falha grande na documentação da mesma.
Outro ponto a se ficar atento é que, para o desenvolvedor, sempre há dois tipos de documentação, e isso acaba complicando um pouco as coisas. A primeira é a descrição de um projeto ainda inexistente, e a segunda é o registro interno das estruturas do programa, ou seja, a documentação dentro do código. Enquanto a primeira é essencial para que o software possa sair como o planejado, sendo que, para isso, é imprescindível que toda a equipe tenha acesso ao mesmo documento, a segunda é capaz de evitar um esforço de engenharia reversa caso uma manutenção no código seja necessária.
Documentando o código-fonte
Quando analisa um código escrito por outro programador, é preciso primeiro compreendê-lo como um todo, para depois então partir para o entendimento das partes que compõe o todo. A documentação do código-fonte é um grande passo para que esse entendimento seja atingido. Uma linha de comentário, como a vista na Listagem 1, pode economizar a leitura de dezenas de linhas de código, além de ajudar a criar a compreensão total do sistema pelo desenvolvedor que está lendo.
Listagem 1. Exemplo de comentário no código-fonte
/* Método para obter o número de vezes que cada palavra aparece no texto */
É importante notar que o número de funções utilizadas ao final da implementação de um software é sempre maior que o que foi projetado. Isso é inevitável, e aumenta a importância da documentação durante a codificação, uma vez que cada elemento do código precisa ser entendido pelo leitor. Além disso, essa documentação que vai sendo criada juntamente com o código também contribui com a programação, ajudando o desenvolvedor a se organizar e dividir suas tarefas em partes.
Quando se analisam códigos-fonte, vale notar que existem alguns comentários que diferem dos outros, às vezes na cor, às vezes simplesmente na implementação. No caso do C#, existem comentários de duas barras ("//") e comentários permeados por tags XML. Os comentários com XML geralmente são utilizados para explicar o funcionamento de funções, enquanto os de duas são comentários internos às mesmas. Essa diferenciação é utilizada devido à existência de algumas ferramentas que são capazes de gerar a documentação do software através dos comentários que foram escritos. Nesse caso, a ferramenta utiliza os comentários com XML para criar um documento (em formato PDF ou RTF, geralmente) que contém a documentação total do aplicativo.
É importante ressaltar que não irá adiantar absolutamente nada criar uma linda documentação de um código péssimo. Não adianta nada que o código esteja perfeitamente documentado se o código, embora faça o que está escrita, o faça da pior maneira possível. A garantia de uma leitura mais fácil do programa pode gerar uma série de recompensas para o desenvolvedor, entre as quais se destacam o maior controle sobre a complexidade do código e a grande facilidade de depuração de código. Além disso, um código bem escrito garante um grande avanço no trabalho em equipe. Isto é, se todos os programadores da equipe codificam bem, a probabilidade de eles se entenderem é muito maior.
Mas a questão que fica é: como escrever código legível? Existe uma frase, de Abelson e Sussman [1985], que coloca esse assunto sobre uma ótica um tanto diferente: "Programas devem ser escritos para as pessoas lerem e somente acidentalmente para as máquinas executarem". É claro que os desenvolvedores não podem ser radicais a esse ponto, e, portanto, é preciso encontrar o ponto de equilíbrio entre código legível e código facilmente executável pela máquina. Existem algumas técnicas que facilitam a leitura de código e que devem ser seguidas pelos desenvolvedores, sem prejudicar o desempenho do software:
Uso de identificadores facilmente identificáveis: é impossível para quem está lendo o código entender, inicialmente, para que serve a variável "x", por exemplo. Portanto, o ideal é utilizar um nome que fale mais sobre a função da mesma: no caso de uma operação matemática, algo como "num1" poderia ser interessante;
Espaçamento, recuos e alinhamento de código;
Definição de regras gerais, para projetos em equipe.
Nesse ponto, vale ressaltar que não há uma regra internacionalmente aceita para o que é código legível. Normalmente, as empresas de desenvolvimento definem uma série de normas que elas gostariam que seus programadores seguissem. Isso facilita o trabalho em equipe, bem como evita que o código fique excessivamente dependente do desenvolvedor que o construiu.
O reaproveitamento de código é uma das principais causas para empresas com muito tempo de mercado criar software em tão menos tempo em comparação com as iniciantes. Esse conceito parte do princípio que, se um problema foi resolvido em algum momento do passado, não é necessário que ele seja resolvido novamente. Existem diversos meios de reaproveitamento de código, uns mais e outros menos recomendados. Porém, cada um deles é muito útil em diversas situações no desenvolvimento de software.
Antes de tudo, é importante um pouco de pensamento crítico. O que aconteceria se, em uma equipe de desenvolvimento, todos os desenvolvedores precisassem de uma função de validação de CPFs, por exemplo, e cada um deles criasse uma? O primeiro problema, mais claro, é o tempo que isso adicionaria ao projeto. Mas há um problema, um pouco menos trivial, que diz respeito ao fato de que, se há 20 funções diferentes para fazer a mesma coisa, a chance de um erro em uma delas é 20 vezes maior. Isso pode fazer com que o erro se propague por todo o software, prejudicando imensamente o software e muitas vezes a própria reputação da empresa e daquela equipe de projeto. A principal maneira de evitar esse problema é utilizar um repositório de funções, muitas vezes utilizada em grandes projetos em equipe. A ideia é que, antes de criar uma função, o desenvolvedor vá ao repositório para verificar se ninguém o fez antes dele.
Conforme foi comentado, existem diversas maneiras de reaproveitamento de código. A mais intuitiva delas é o reuso de trechos de código. Geralmente, essa técnica é utilizada por desenvolvedores iniciantes, que, quando criaram o código, não pensaram na possibilidade de reutilizá-lo futuramente. Com isso, acabam simplesmente utilizando o famoso "ctrl+c", "ctrl+v" para reutilizar o código. Essa técnica também é utilizada por programadores mais experientes, mas em casos mais específicos e simples, onde seria muito pouco vantajosa a utilização de uma técnica mais elaborada. O ponto forte dessa técnica é que, ao utilizar código já validado, é possível poupar tempo de testes.
Testes é um assunto que poderia render, sozinho, vários artigos. Porém, é um ponto essencial no desenvolvimento de qualquer projeto de software. Existem vários tipos de teste, que devem ser observados de acordo com as vantagens e limitações que eles oferecem. A classificação mais genérica dos mesmos é a que divide os testes em dois tipos: testes de caixa-preta e testes de caixa-branca. O primeiro é também chamado de teste funcional, e é utilizado para analisar simplesmente o comportamento do objeto, ignorando sua construção interna. Já o segundo, ao contrário, analisa a estrutura do objeto, e é conhecido como teste estrutural. Em software, além desses dois tipos de testes, outros quatro tipos aparecem: testes de estresse, responsáveis por verificar como o software reage a situações extremas, ou seja, testar os limites do software; testes de integração, responsáveis por verificar como os elementos do software reagem à integração entre eles, ou em outras palavras, se os elementos continuarão funcionando de acordo quando forem conectados; teste orientado a objetos, capaz de explorar o maior número de estados que o objeto é capaz de assumir e as transições entre eles; e o teste de aceitação, observando-se a qualidade externa do produto, sendo realizado, geralmente, entre os usuários do software.
Existe outra técnica, que sem dúvida nenhuma, com as linguagens de 4ª geração, é a mais utilizada. Muitas vezes, os programadores nem se dão conta, mas estão utilizando bibliotecas de funções, as chamadas APIs. Essas bibliotecas, geralmente, provêm de uma fonte externa, muitas vezes da internet. Outras vezes, são nativas na própria linguagem. E, algumas vezes, são criadas pela própria empresa ou desenvolvedor para uso pessoal. Um dos pontos altos de utilizar bibliotecas de funções é a organização que isso proporciona ao software. Se uma função da biblioteca foi utilizada, basta procurar na documentação da mesma para verificar o funcionamento da função. Ou seja, essa técnica poupa documentação própria do software, além de acrescentar organização. Porém, caso não seja uma biblioteca própria, dificilmente o programador irá saber como foi realizada a implementação dentro da função. Isso pode gerar problemas de desempenho, caso essa implementação tenha sido realizada de forma pífia.
Existem vários exemplos de bibliotecas de funções, e geralmente elas são utilizadas para áreas que não são tão triviais, e não faz parte do portfólio de todo desenvolvedor. Então, elas são utilizadas para facilitar a vida do programador, evitando que se perca um tempo grande em busca de conhecimento em áreas como criptografia, manipulação gráfica, comunicação em rede, entre outras.
Segurança na programação
Para garantir que o código que está sendo criado é seguro, é interessante seguir algumas técnicas de programação defensiva. É como se fosse uma atitude de desconfiança com relação ao código, para prevenir o que possa vir a dar errado. Basicamente, essa técnica consiste em assumir um maior controle sobre a execução do código, incluindo um maior número de verificações no mesmo.
O primeiro ponto a ser observado: em nenhuma hipótese o aplicativo deve aceitar entradas de dados incorretas. Para isso, deve haver um controle muito rígido, evitando comportamentos inesperados, e, consequentemente, erros e reclamações dos clientes. Por isso, além de detectar um erro, é preciso saber o que fazer caso eles aconteçam. As linguagens de programação mais avançadas, como C# e Java possuem blocos específicos de tratamento de erros (blocos try/catch), que devem ser utilizados em situações críticas de entrada de dados, por exemplo. Outro ponto que também cai no mesmo caso de utilizam de blocos de tratamento de erros é o acontecimento de exceções. Aqui, tudo é englobado: uma divisão por zero, falta de memória (algo incomum hoje em dia, mas possível), entre outros. Nas linguagens mais antigas, isso era tratado com blocos condicionais, como if/else e afins. Porém, tais técnicas acabavam complicando o código, deixando-o muito confuso e pouco propenso a leituras posteriores. As Listagem 2 e 3, abaixo, mostra as diferenças entre as duas abordagens. Repare que, na Listagem 2, o código fica muito mais organizado. Como o próprio nome sugere, ele tenta (em inglês, try) executar o código e, caso não consiga (caso o denominador seja igual a zero, por exemplo), entra no bloco catch, onde o erro é mostrado na tela. Já na Listagem 3, isso não fica tão explícito, embora aconteça exatamente o mesmo. Além disso, no segundo exemplo ele não é capaz de tratar nenhum outro tipo de exceção, somente o erro de divisão por zero.
Listagem 2. Tratamento de erros - Bloco Try/Catch
try{ a = numerador / denominador; }
catch (Exception e){ Console.WriteLine(e.Message); }
Listagem 3. Tratamento de erros - Bloco If/Else
if ( denominador != 0 ) a = numerador / denominador;
else Console.WriteLine("Erro: divisão por zero.");
Por fim, o ponto que mais deve ser dado atenção é o estilo de codificação. Não adianta nada utilizar exaustivamente tratamento de exceções se o estilo de codificação não faz nada para aumentar a segurança do código. Um texto bem documentado, organizado em funções e classes é muito mais legível e seguro que um texto desorganizado. Portanto, para aumentar a segurança do código, é importante ter atenção ao que está sendo escrito, bem como a forma como isso é feito. O programador deve estar absolutamente convicto de que tomou a decisão mais correta. Muitas vezes, em casos de segurança, é mais seguro utilizar uma função pronta da API da linguagem do que criar uma função nova que, embora mais rápida, talvez não funcione tão eficientemente.
Com esse artigo foi possível criarmos uma compreensão maior a respeito do que significa código bem escrito. Além disso, é possível observarmos a importância que o código bem escrito tem para o software. A diferença entre o bom e o mau software pode estar em uma linha de código, e é preciso muita atenção por parte do desenvolvedor para evitar que isso aconteça.
Em outro ponto, é preciso planejamento na hora de documentar um código, bem como no momento de gerar código reutilizável. É preciso que haja muita atenção do desenvolvedor e da equipe de projeto para que o documento contenha todos os aspectos importantes (e até os menos importantes) do software. Em caso de manutenção, é essencial que haja uma boa documentação, tanto no código como fora dele. E, caso essas técnicas falhem em ser seguidas, o software terá uma vida bem curta.