A Arte de Criar Funções Coerentes

Imagem para post A Arte de Criar Funções Coerentes

INTRODUÇÃO

Muitas vezes somos requisitados para “apagar fogo” – principalmente os colaboradores da equipe de Sustentação -, e nestes casos, haja vista que por um breve período de inoperância de um sistema, uma empresa pode sofrer consequências monetárias negativas, o principal é depositar esforços a fim de que o sistema volte a operar em produção o mais rápido possível. Logo, as boas práticas, os Design Patterns, não tem vez: O importante é fazer a empresa perder menos dinheiro possível, não é deixar o código bonito. Entretanto, em outros casos, quando a Sprint acabou de ser iniciada, quando não há a necessidade de se usar um “extintor”, criamos um código “mal-cheiroso” da mesma forma, o que pode acontecer por n motivos. Portanto, meu objetivo neste segundo post que escrevo é tentar passar para vocês, leitores, algumas dicas de como escrever métodos melhores, mais legíveis, mais coesos. Vamos lá?!

FUNÇÕES BEM DEFINIDAS

“As Funções devem fazer uma coisa. Devem fazê-la bem. Devem fazer apenas ela”.¹

O que significa fazer apenas uma coisa? Significa que o método contenha apenas uma única linha? Não exatamente!

Repare no algoritmo de fazer café descrito abaixo. Um algoritmo simples e tradicional, que obviamente não é regra:

  1. Ferver água;
  2. Depositar o pó de café no coador;
  3. Coar o café.

Podemos escrever este algoritmo em uma linguagem:

public Cafe fazerCafe(){
	ferverAgua();
	depositarCafeNoCoador();
	Cafe cafe = coar();
	return cafe;
}

Note que no método acima há 4 linhas e 3 chamadas para outros métodos. Este método faz apenas uma única coisa?

Os passos listados dentro da função estão em um nível de abstração logo abaixo do seu nome, ou seja, o corpo da função precede o resultado esperado (o café), sem expor ao leitor os detalhes específicos de cada etapa, detalhes que o nome da função não exige. Repare que o método principal não conhece os passos necessários para a fervura da água, por exemplo, pois para fazer café o método “precisa” apenas da água fervida. Logo, nós inserimos no método apenas aquilo que ele precisa para satisfazer o seu nome. Os detalhes, encapsulamos em outros métodos.

Podemos ainda utilizar o recurso da narrativa, lendo o código de cima para baixo – Regra Decrescente – o qual nos auxilia a escrever funções bem definidas, e, consequentemente, pequenas:
“Para fazer café, fervemos água, depositamos o pó de café no coador e coamos o café”.

A fim de compreender melhor, observe que o objetivo de uma função é Decompor Um Conceito, em outras palavras, decompor o nome da função em um nível de abstração logo abaixo. Imagine uma pirâmide (fig. 1), na qual há no topo o nome da função que deve ser desmembrado até o nível de abstração mais baixo possível (base da pirâmide)

Fig. 1 – Pirâmide de níveis de abstração

Escrevendo funções bem definidas, temos como resultado funções PEQUENAS!

PARÂMETROS DE FUNÇÕES

O ideal é não haver parâmetros em funções, porque em determinados casos o parâmetro dificulta a interpretação da definição do método, quando olhamos apenas a sua declaração, por exemplo:

public void sendEmail(String emailAddress){ … }

O que o parâmetro emailAddress tem a ver com a função? Ele representa o e-mail do destinatário, ou o do remetente? Para sabermos a resposta, precisaríamos conhecer a implementação da função, o que é um desperdício de tempo e esforço, pois deixaríamos de usar a função de autocomplete que quase todas – senão todas – as IDEs atuais possuem. O cenário torna-se mais custoso quando temos de escrever testes, mapeando todas as combinações possíveis dos parâmetros, seus possíveis valores, os retornos esperados, etc.

Há três razões para se ter um único parâmetro em uma função:

  • Pergunta;
  • Transformação;
  • Evento

Na Pergunta, retornamos um valor booleano, que responde a questão implementada no corpo do método sobre o parâmetro informado.

No Transformação, ocorre a alteração do parâmetro, e, neste caso, ele deve ser retornado. Como exemplo, podemos citar o método estático Integer.parseInt(String), que retorna um Integer a partir de uma String.

No Evento, não há retorno, e passamos no parâmetro um evento emitido, com o qual podemos alterar o estado do nosso sistema, conforme o método actionPerformed(ActionEvent e) da Interace ActionListener nos exemplifica.

TRATAMENTO DE ERROS

Provavelmente você já se deparou com uma estrutura similar à seguinte:

public void readFromFile(){
	File file = null;
	FileInputStream fileInputStream = null;
	try {
		file = new File("C:/users/file.txt");
		fileInputStream = new FileInputStream(file);
		int line;
		while ((line=fileInputStream.read()) != -1) {
			System.out.print((char)line);
		}
	} catch (Exception e) {
		e.printStackTrace();
	}finally {
		try {
			if (fileInputStream != null) {
				fileInputStream.close();
			}
		} catch (Exception e2) {
			e2.printStackTrace();
		}
	}
}

Podemos refatorar este método – nos atentando apenas à recomendação de um método fazer apenas uma única coisa – para:

public void readFromFile(){
	try {
                openStream();
		read();
	} catch (Exception e) {
		e.printStackTrace();
	}finally {
		closeStream();
	}
}

Porque um método que trata erro, já faz uma coisa: trata o erro!

DICAS RÁPIDAS

De acordo com o livro Código Limpo: Habilidades Práticas do Agile Software, de Robert Cecil Martin, é recomendado inserir no máximo 150 caracteres em uma linha, e as funções devem ter no máximo 20 linhas. Ainda de acordo com o livro, os blocos de estruturas de repetição e condição devem conter no máximo uma linha. Caso haja necessidade de escrever mais linhas, recomenda-se, então, criar um método e nomeá-lo – como descrito no artigo “A Arte de Nomear ‘Coisas’” – de modo que seu nome descreva claramente ao leitor o que seu corpo faz. Recomenda-se também que os métodos não tenham estruturas aninhadas acima de duas indentações, conforme o exemplo seguinte:

public void methodName(){
    // primeira identacao
    if(condicao){
        // segunda identacao
    }
}

Não se acanhe de criar nomes extensos para os métodos. É melhor um nome grande e claro, do que um pequeno e ambíguo.

Lembre-se: Sempre que o parâmetro for transformado, retorne esta transformação.
Não faça: public void encrypt(String password)
Faça: public String encrypt(String password)

Deve-se evitar que uma função faça simultaneamente Transformação e Pergunta, como exemplifico abaixo:
public boolean somar(int primeiraParcela, int segundaParcela){ … }
Quando este método retorna true? E false?

Evite obstinadamente parâmetros do tipo boolean, pois quando usados explicitam que a função faz duas coisas: uma se o parâmetro for verdadeiro, outra se for falso.

Uma boa dica para escolher bons nomes para funções com um parâmetro é observar o par verbo-substantivo. Sabemos que o uma função representa uma ação bem definida, e o parâmetro pode ser encarado como o objeto que vai receber a ação:
public String encrypt(String password)

Quando há a necessidade de se acrescentar mais de um parâmetro numa função, reflita se seria possível criar um objeto a partir deles, assim sua função teria apenas um, e não dois ou mais parâmetros. Quando não é possível criar objetos a partir de parâmetros verifique se eles são equivalentes, ou seja, se são do mesmo tipo, assim você poderá usar um vetor que os engloba, como é o caso do principal método de uma aplicação Java:
public static void main(String… args){ … }

CONCLUSÃO

Em suma, somos autores e, portanto, cabe a nós a clareza do nosso código.
#SejamosClaros

Compartilhe!