A relevância da expressividade no código

Olááá, leitor! Você se considera um profissional tecnicamente expressivo? Hoje trago uma discussão sobre a importância da clareza e objetividade no código-fonte, que se resumem no termo “expressividade”. Acompanhe as ideias, analogias e exemplos apresentandos neste artigo e verifique se você está sendo expressivo ao programar. Go, go, go!

Introdução

Há algum tempo, venho citando o termo “expressividade” em alguns artigos relacionados à programação. A intenção em mencionar esse termo é ressaltar o nível de entendimento que um código bem escrito pode revelar. Mas, em detalhes, o que seria essa tal de expressividade?

Suponha que você esteja escrevendo um e-mail para o seu chefe explicando um determinado problema no módulo que está desenvolvendo. Certamente, você tentará encontrar as melhores palavras e estruturar o texto de forma que o seu chefe não tenha dificuldades em entendê-lo, não é? Pois bem, assim como e-mails, recados, cartas, artigos, gostaríamos que outras pessoas nos compreendessem sem complicações, ou melhor, queremos nos expressar ao máximo.

Sendo assim… por que não podemos fazer o mesmo com o nosso código-fonte?

Expressividade no código

Eis que lhes introduzo o conceito de expressividade no código. Como bons desenvolvedores, devemos considerar cada unidade de código como uma página de um livro, não somente pela questão da regra decrescente, mas pelo propósito em propiciar uma boa leitura.

Se você trabalha em uma empresa de desenvolvimento, outros desenvolvedores eventualmente precisam ler o seu código para realizar manutenções ou interpretar a regra de negócio. Neste caso, é correto afirmar que eles são os seus “leitores”. Por outro lado, se você é um desenvolvedor autônomo, é bem provável que você mesmo irá retornar ao seu código com uma certa frequência, e, convenhamos: é gratificante quando abrimos o nosso próprio código e conseguimos entender, com facilidade, o que ele faz.

Exemplo 1: Princípio da Menor Surpresa

Bom, mas nada do que eu disse fará sentido se eu não apresentar alguns exemplos.
Considere a declaração do método abaixo na classe “Clientes” de um aplicativo:

Legal… consultar o que? Nome do cliente, contato, endereço, quantidade de compras?

Só a palavra “Consultar” não nos passa esclarecimento algum. Em primeira instância, teremos uma ideia subjetiva sobre a função deste método, ou seja, saberemos que ele faz algum tipo de consulta, mas não sabemos qual. Para entender o que o método faz, será necessário verificar a sua implementação, exigindo um pouco mais de esforço. O problema é que, se a implementação do método estiver tão ruim quanto a declaração, prepare-se para passar alguns minutos interpretando as linhas de código…

Esse equívoco de utilizar um verbo genérico como nome de método é um dos fatores que motivaram a elaboração do Princípio da Menor Surpresa (ou POLAPrinciple of Least Astonishment). Este princípio visa evitar que um desenvolvedor se surpreenda com o funcionamento de um método ao utilizá-lo para um determinado comportamento, quando, na verdade, ele possui outro. Para isso, o POLA sugere a busca pela previsibilidade. Ao se deparar com a declaração ou chamada do método, o desenvolvedor já deve ter uma ideia concreta da sua ação.

Se declararmos métodos com nomes mais objetivos, mesmo que ligeiramente mais extensos, teremos uma segurança maior em utilizá-los, além de evitar o desperdício de tempo interpretando suas reais finalidades:

Ao ler as declarações acima, torna-se mais fácil compreender o que cada método realiza, ou pelo menos deveria realizar. Observe como o código fica mais legível em uma condição IF:

Perfeito! É só bater o olho no código para identificar o fluxo da regra de negócio!

O mais interessante é que, por exemplo, o desenvolvedor pode utilizar a função VerificarSeClienteEstaCadastrado em qualquer ponto do software, evitando a duplicidade e concentrando a regra de negócio em apenas um local. Curiosamente, quando usamos um nome objetivo como esse, há uma tendência em memorizá-lo. Algumas vezes, já pensei: “Preciso verificar a regra de negócio X. Espere aí… acho que já existe uma função para isso!”.

Exemplo 2: Nome de métodos

Continuando, confira a declaração abaixo. O nível de expressividade está adequado, concorda?

Bom, eu não concordo. Pela declaração, eu não tenho certeza se o método converte o documento para PDF ou se converte um documento que é um PDF para outro formato. Mais uma vez, teríamos que gastar mais alguns minutinhos para investigar a implementação. Se, por acaso, o método estivesse declarado em uma das formas abaixo, não nos depararíamos com esse tipo de dúvida.

Atente-se que, além do próprio nome, a declaração dos parâmetros também é importante, principalmente quando se utiliza o recurso de Code Insight, no qual exibe a lista de parâmetros de um método sem a necessidade de acessar a declaração.

Exemplo 3: Assinatura dos parâmetros

Não somente os nomes dos métodos são importantes, mas também os parâmetros. Concordo que a clareza dos parâmetros pode parecer uma questão óbvia, porém, tenho quase certeza de que você já encontrou uma chamada de método parecida com essa:

Embora o nome do método esteja relativamente compreensível, é praticamente impossível desvendar o seu comportamento. Claro, pelo nome, sabemos que um relatório será gerado, mas essa quantidade de parâmetros obscuros nos deixa inseguros de como será a saída real deste relatório. Um simples “chaveamento” em um destes parâmetros (por exemplo, alterar o primeiro parâmetro de True para False) pode modificar consideravelmente o resultado. A única solução é abrir a classe que contém o método e conferir a finalidade de cada parâmetro, que, por ventura, pode não ser agradável:

Na busca pela expressividade, a minha sugestão é utilizar tipos enumerados e constantes para aprimorar a legibilidade dessa chamada. Provavelmente, ainda teríamos que verificar a declaração do método, porém, a própria chamada já seria meramente auto-explicativa. No exemplo abaixo, foi utilizado o prefixo enum para tipos enumerados e letras maiúsculas para constantes. Observe como a chamada se torna mais explícita:

Os tipos enumerados, por sua vez, poderiam ser declarados dessa forma:

Diga-se de passagem: o código fica bem mais atraente!

Exemplo 4: Quantidade de parâmetros

Entretanto, tenho mais uma crítica a respeito do método acima. Mesmo utilizando tipos enumerados e constantes, a quantidade de parâmetros ainda continua a mesma, ferindo o nosso compromisso com a expressividade. Várias vezes já tive que contar o número de parâmetros para saber qual deles se referia à minha necessidade. Algo como: “Vamos ver… 1… 2… 3… 4… 5… sexto parâmetro! É esse que preciso alterar.”. Chato, não? Imagine se houver 20 parâmetros e você precisa alterar o 14º?

Usando uma analogia ao código acima, é como se alguém nos pedisse, apenas verbalmente, para fazer compras em 7 lugares diferentes na mesma viagem. Podemos nos confundir sobre os lugares e o que devemos comprar em cada um deles. Seria mais fácil se nos entregassem, por exemplo, um único papel com as 7 tarefas descritas.

Boa ideia! É isso que também podemos aplicar ao nosso caso: ao invés de passar 7 parâmetros, poderíamos criar uma classe que tenha todos estes valores e utilizá-la como um parâmetro único, conforme abaixo:

Em seguida, basta alterar a definição do método GerarRelatorio para que ele receba um objeto do tipo TParametrosRelatorio como parâmetro. Na chamada do método, será preciso apenas preencher um objeto dessa classe com os mesmos valores que estávamos passando como parâmetros anteriormente:

Nossa, mas compensa mesmo trocar apenas aquela chamada por todas essas linhas de código?
Lembre-se de que o nosso objetivo aqui é aprimorar a expressividade do código-fonte, mesmo que isso implique no acréscimo de algumas linhas, desde que elas realmente sejam necessárias. Além disso, essa mesma classe de parâmetros poderá ser utilizada em outros locais do software, proporcionando uma padronização.

Por outro lado, vale ressaltar que o excesso de expressividade também pode ser ruim, tornando o código maçante. Eventualmente, para encontrar o equilíbrio adequado, é necessário “sacrificar” algumas diretrizes de expressividade. Neste caso, uma alternativa é fazer uso de comentários ou anotações no código-fonte, mas, novamente, só se forem realmente úteis.

Exemplo 5: Nome de variáveis

Se escrever nomes melhores para os nossos métodos é uma boa prática, por que não fazer o mesmo para as nossas variáveis? Assim como acontece com os métodos, nomes objetivos irão melhorar a leitura do nosso código, facilitando a interpretação. Considere as seguintes variáveis:

Ao ler essas declarações, eu pensaria: “Soma do quê? Quantidade do quê? Nome de quem?”. E sabe qual seria a única forma de descobrir suas finalidades? Lendo os valores que são atribuídos à elas. Aí já é tarde. Isso demonstra a evidência de que essas variáveis não são nada expressivas. Imagine, então, se elas forem declaradas com palavras compostas:

Bem melhor, concorda? Pelo nome, já temos uma ideia do tipo de informação que cada variável irá receber. A propósito, o uso de preposições nos nomes (“do”, “de”, “da”) são opcionais. Se a variável QuantidadeDeVendas fosse declarada somente como QuantidadeVendas, a interpretação seria essencialmente a mesma. Eu apenas optei por inclui-las para que o código se assemelhe à leitura natural de um texto.

Agora, mais uma dica que aprendi na empresa em que trabalho, mas é opcional: procure adquirir o hábito de utilizar notação húngara e adicionar um prefixo no nome da variável para indicar o seu tipo. Confira:

Dessa forma, em qualquer linha do código será possível identificar o tipo da variável, evitando a necessidade de mover a barra de rolagem para o início do método para verificá-lo. Olha só que interessante: só pelo fato de aprimorarmos o nome da variável, conseguimos deixar explícito tanto o tipo quanto o propósito.

No exemplo acima, utilizei a inicial de cada tipo (“r”, “i”, “s” e “b”) como prefixo, mas nada impede que você mesmo crie suas próprias convenções.

Exemplo 6: Condições booleanas

Cuidado com elas! Essas condições podem ser o motivo de uma falha de interpretação que pode resultar em um tempo longo (e chato) de depuração. Observe a condição abaixo:

Acho que nem preciso comentar, não é? 🙂

Seria algo como: “Se isso for verdade, e aquilo for diferente que N, e o status não for igual a P e opção 1 estiver marcada, então…” – observe que só a leitura da condição como um todo já traz um princípio de dificuldade. Bom, sem dizer que utilizei apenas 4 condições como exemplo. Imagine se fossem 6, 8?

A recomendação é transformar toda essa condição em uma função que retorne um valor booleano:

A função declarada acima, por sua vez, também pode ser aprimorada:

Sentiu-se incomodado com a quantidade de vezes que a palavra “and” foi utilizada? Eu também! Alternativamente, podemos empregar uma lógica diferente para o retorno dessa função, aplicando uma técnica conhecida como cláusula de guarda:

Exemplo 7: Tamanho dos métodos

A terceira e última recomendação é relacionada ao tamanho dos nossos métodos. Quanto menores eles forem, mais objetivos serão. Digo isso por dois motivos: primeiro, métodos grandes ferem o princípio de responsabilidade única (SRP), ou seja, tendem a realizar mais do que deveriam, assumindo mais de uma responsabilidade; segundo, a leitura fica comprometida, já que, dependendo dos loops e condições, teremos que mover a barra de rolagem várias vezes para interpretar o método ou acompanhar o fluxo de depuração.

Quando notar que o método está tomando uma dimensão extensa, considere imediatamente a possibilidade de refatorá-lo. Ou então, quando se deparar com um método grande, divida-o em vários pedaços, de modo que cada um deles fique pequeno.
Para finalizar, vale mencionar uma frase cômica que Uncle Bob, autor do livro Clean Code, escreveu sobre essa prática:

A primeira regra dos métodos é que eles devem ser pequenos. A segunda regra é que eles devem ser menores ainda.

 

Abraço, leitores!
Na próxima semana, bateremos um papo sobre avanço tecnológico.


André Celestino