Desenvolvimento

23 ago, 2013

A programação defensiva merece um nome tão ruim?

Publicidade

Outro dia eu fui a uma palestra de uma hora sobre erlang, apenas como observador; eu não sei nada sobre erlang, exceto que soa interessante e que a sintaxe é… bem… incomum. A palestra foi dada a alguns programadores Java que recentemente aprenderam erlang, e era uma crítica justa sobre seu primeiro projeto erlang, que eles estavam apenas completando. O apresentador disse que esses programadores precisavam parar de pensar como programadores Java e começar a pensar como programadores erlang1 e, em particular, deveriam interromper a programação defensiva e deixar processos falharem rapidamente e corrigirem o problema.

Agora, aparentemente, essa é uma boa prática em erlang, porque uma das características do erlang, e por favor me corrijam se eu estiver errado, é que o trabalho é dividido em supervisores e processos. Supervisores supervisionam os processos, criando-os, destruindo-os e reiniciando-os, se necessário. A ideia de falha rápida não é nada nova, e é definida como a técnica a ser usada quando o seu código vem através de uma entrada ilegal. Quando isso acontece, o código só fracassa e interrompe o ponto que você corrige a entrada do fornecedor em vez de seu código. O sub-texto que o apresentador disse é que Java e programação defensiva são ruins e falham rápido se forem boas, o que é algo que realmente necessita de uma investigação mais aprofundada.

A primeira coisa a fazer é definir a programação defensiva, e a primeira definição que eu encontrei foi no que é hoje, possivelmente, um livro lendário: Writing Solid Code, de Steve Maguire, publicado pela Microsoft Press. Eu li esse livro há muitos anos, quando eu era um programador C, que na época era a linguagem defacto de escolha. No livro, Steve demonstra a utilização de uma macro _Assert:

[csharp]

/* Borrowed from Complete Code by Steve Maguire */
#ifdef DEBUG

void _Assert(char *,unsigned)     /* prototype */
#define ASSERT(f)        \
if(f)                          \
{ }                        \
else
_Assert(__FILE__,__LINE__)
#else
#define ASSERT(f)
#endif

// …and later on..

void _Assert(char *strFile,unsigned uLine) {

fflush(NULL);
fprintf(stderr, "\nAssertion failed: %s, line %u\n",strFile,uLine);
fflush(stderr);
abort();
}

/////// …and then in your code

void my_func(int a).  {

ASSERT(a != 0);

// do something…
}

[/csharp]

…como a definição dele de programação defensiva. A ideia aqui é que estamos definindo uma macro C que, quando DEBUG é ligado, my_func (…) irá testar sua entrada usando ASSERT (f), e que irá chamar a função _ASSERT (…) se a condição falha. Assim, quando no modo DEBUG, nessa amostra my_func (int a) tem a capacidade de abortar a execução se arg a é zero. Quando DEBUG é desligado, verificações não são realizadas, mas o código é menor e mais rápido; algo que foi, provavelmente, mais de uma consideração em 1993.

Olhando para essa definição, várias coisas vêm à mente. Em primeiro lugar, esse livro foi publicado em 1993, e ainda assim é válido? Não seria uma boa ideia matar Tomcat usando um System.exit (-1) se um de seus usuários digitasse a entrada errada! Em segundo lugar, Java sendo mais recente também é mais sofisticado, tem exceções e manipuladores de exceção, por isso, em vez de abortar o programa, nós lançaríamos uma exceção que, por exemplo, exibiria uma página de erro com destaque para as entradas ruins.

O ponto principal que vem à mente, no entanto, é que essa definição de programação defensiva soa muito parecida com falha rápida para mim, na verdade, é idêntico.

Esta não é a primeira vez que eu ouço queixas de programadores em programação defensiva, então por que ela tem uma reputação tão ruim? Por que o apresentador da palestra sobre elrang denegriu-a tanto? Meu palpite é que há uma boa utilização de programação defensiva e mau uso de programação defensiva. Deixe-me explicar com algum código…

Neste cenário, eu estou escrevendo uma calculadora de Índice de Massa Corporal (IMC) para um programa que informa aos usuários se eles estão ou não com excesso de peso. Um valor de IMC entre 18,5 e 25 é aparentemente bom, enquanto tudo acima de 25 é de excesso de peso a obesidade grave. O cálculo do IMC usa a seguinte fórmula simples:

IMC = peso (kg) /altura² (m)

A razão por que eu escolhi essa fórmula é que ela apresenta a possibilidade de um erro de divisão por zero, o que o código que eu escrevo deve defender.

[java]

public class BodyMassIndex {

/**
* Calculate the BMI using Weight(kg) / height(m)2
*
* @return Returns the BMI to four significant figures eg nn.nn
*/
public Double calculate(Double weight, Double height) {

Validate.notNull(weight, "Your weight cannot be null");
Validate.notNull(height, "Your height cannot be null");

Validate.validState(weight.doubleValue() > 0, "Your weight cannot be zero");
Validate.validState(height.doubleValue() > 0, "Your height cannot be zero");

Double tmp = weight / (height * height);

BigDecimal result = new BigDecimal(tmp);
MathContext mathContext = new MathContext(4);
result = result.round(mathContext);

return result.doubleValue();
}
}

[/java]

O código acima usa a ideia apresentada na definição de programação defensiva de Steve de 1993. Quando o programa chama calculate(Double weight,Double height), quatro validações são realizadas, testando o estado de cada argumento de entrada e lançando uma exceção apropriada em caso de falha. Como este é o século 21 eu não tenho como definir minhas próprias rotinas de validação, eu simplesmente usei aqueles fornecidas pelo commons-lang3 da biblioteca Apache e importei:

[java]

import org.apache.commons.lang3.Validate;

[/java]

…e acrescentei:

[xml]

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>

[/xml]

…ao meu pom.xml.

A biblioteca Apache commons lang contém a classe Validate, que fornece algumas validações básicas. Se você precisar de algoritmos de validação mais sofisticados, dê uma olhada na biblioteca validadora commons Apache.

Uma vez validado o método calculate(…), ele calcula o IMC e arredonda para quatro algarismos significativos (por exemplo, nn.nn). Em seguida, ele retorna o resultado para o caller. Usar Validate me permite escrever muitos testes JUnit para garantir que tudo corra bem em caso de problemas e para diferenciar cada tipo de falha:

[java]

public class BodyMassIndexTest {

private BodyMassIndex instance;

@Before
public void setUp() throws Exception {
instance = new BodyMassIndex();
}

@Test
public void test_valid_inputs() {

final Double expectedResult = 26.23;

Double result = instance.calculate(85.0, 1.8);
assertEquals(expectedResult, result);
}

@Test(expected = NullPointerException.class)
public void test_null_weight_input() {

instance.calculate(null, 1.8);
}

@Test(expected = NullPointerException.class)
public void test_null_height_input() {

instance.calculate(75.0, null);
}

@Test(expected = IllegalStateException.class)
public void test_zero_height_input() {

instance.calculate(75.0, 0.0);
}

@Test(expected = IllegalStateException.class)
public void test_zero_weight_input() {

instance.calculate(0.0, 1.8);
}
}

[/java]

Uma das “vantagens” do código C é que você pode desligar o ASSERT (f) por meio de um comutador no compilador. Se você precisar fazer isso em Java, dê uma olhada no uso da palavra-chave assert do Java.

O exemplo acima é o que eu espero que nós concordemos que se trata de uma amostra bem escrita – o bom código. Então, o que é necessário agora é a amostra mal escrita. A principal crítica de programação defensiva é que ela pode esconder os erros, e isso é muito verdadeiro, se você escrever um código ruim.

[java]

public class BodyMassIndex {

/**
* Calculate the BMI using Weight(kg) / height(m)2
*
* @return Returns the BMI to four significant figures eg nn.nn
*/
public Double calculate(Double weight, Double height) {

Double result = null;

if ((weight != null) && (height != null) && (weight > 0.0) && (height > 0.0)) {

Double tmp = weight / (height * height);

BigDecimal bd = new BigDecimal(tmp);
MathContext mathContext = new MathContext(4);
bd = bd.round(mathContext);
result = bd.doubleValue();
}

return result;
}
}

[/java]

O código acima verifica também contra argumentos null e zero, mas ele faz isso usando o seguinte argumento if:

Se ((peso! = null) && (altura! = null) && (peso> 0,0) && (altura> 0,0)) { CÓDIGO!

Olhando pelo lado positivo, o código não irá falhar se as entradas estiverem incorretas, mas não vai dizer ao caller o que está errado, ele vai simplesmente esconder o erro e retornar null. Embora não tenha deixado de funcionar, você tem que perguntar o que o caller vai fazer com um valor de retorno null? Ou ele vai ignorar o problema ou processar o erro lá usando algo parecido com isto:

[java]

if ((weight != null) && (height != null) && (weight > 0.0) && (height > 0.0)) {

[/java]

Olhando o lado bom, o código não vai travar se as entradas estiverem incorretas, mas ele não vai dizer ao caller o que deu errado – vai simplesmente esconder o erro e retornar null. Embora não tenha travado, você precisa perguntar o que o caller vai fazer com um valor de retorno null. Ou ele terá que ignorar o problema, ou processará o erro usando algo assim:

[java]

@Test
public void test_zero_weight_input_forces_additional_checks() {

Double result = instance.calculate(0.0, 1.8);
if (result == null) {
System.out.println("Incorrect input to BMI calculation");
// process the error
} else {
System.out.println("Your BMI is: " + result.doubleValue());
}
}

[/java]

Se essa técnica “ruim” de codificação é usada em toda a base de código, então haverá uma grande quantidade de código extra necessária para verificar cada valor de retorno.

É uma boa ideia NUNCA retornar valores null a partir de um método.

Para concluir, eu realmente acho que não há diferença entre a programação defensiva e a programação de falha rápida, elas são realmente a mesma coisa. Não há, como sempre, apenas boa e má codificação? Eu deixo você decidir.

————————————————– ——————————

Este código de exemplo está disponível no Github.

1 – é sempre uma mudança de paradigma no pensamento quando aprende uma nova linguagem. Haverá o ponto onde as gotas de um centavo e você “pegam”, seja ele qual for.

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.captaindebug.com/2013/04/does-defensive-programming-deserve-such.html#.UdR8v_lwrng