Fragmental

5/29/2005

Contratos Nulos

NOTA: mais uma vez a droga do blogger ferrou a formatação. Assim que o primeiro relase do JRapido sair, eu converto os textos, mas prometo que da próxima vez eu tento fazer direto em HTML. Versão em PDF pelo renato aqui.

Um post por Todd Huss gerou algum burburinho recentemente. Todd reclama de ter que checar argumentos de métodos o tempo todo para ter certeza que não são nulos e ter uma linda NullPointerException.

Para quem não é de Java ou está começando na linguagem, uma NullPointerException, carinhosamente conhecida como NPE, acontece quando você chama um método em uma variável que aponta para null, por exemplo:

public static void fazerAlgo(String a){
a.split(" ");
}

public static void main(String args[]){
fazerAlgo(null);
}



Causaria uma NPE (chamando o método split em null).

Existe todo um misticismo com uma NPE, basicamente por um motivo: é um erro de programador. Seu sistema não devia lançar NPE. O grande problema é que geralmente as pessoas fazem assim:

public static void fazerAlgo(String a){
if(a==null) throw IssoNaoEhUmaNullPointerExceptionException("a deve ser definido");
a.split(" ");

}

Ou seja: fazem a mesma coisa que uma NPE faz, mas usando IllegalArgumentException ou outra exception qualquer.

Dos comentários que apareceram, uns indicaram o projeto nully, no java.net. O Nully é uma ferramenta para a IntelliJ IDEA que basicamente adiciona uma anotação @NotNull. que aciona warnings e erros na IDE quando você tenta implementar uma operação que poderia resultar am um valor null para o parâmetro marcado com a tal anotação. Você pode ver mais detalhes no site do projeto, mas eu não gostei mesmo disso.

Uma outra alternativa dada foi votar no BUG 5030232, da Sun, uma tentativa de adicionar o tratamento que a linguagem derivada de Java chamada Nice faz com nulls. Basicamente, Nice exige que você inicie com um "?" as variáveis que podem ser nulas, e não deixa (em tempo de compilação) acessar diretamente operações estas variáveis sem checar se a variável é null antes. Bela tentativa, mas ainda não é isso.

Adam Kruszewski comentou que no seu blog havia uma implementação com annotations e AspectJ. A implementação dele é bem simples, você anota um método e toda vez que este é chamado seus argumentos são checados, se algum for null ele solta uma exceção. Simples e prático, mas não foi desta vez.

A solução não sou eu quem vai dar, é um conceito muito antigo que surgiu junto com os primeiros pensamentos em objetos: Design By Contract.

Para explicar este conceito, vamos nos focar nos três pilares: invariantes, pré-condições e pós-condições. O conceito de Invariante, entretanto, pede uma breve explicação de Espaço-Estado.

Espaço-Estado
Você sabe que um objeto tem estados, não sabe? Pois bem. Cada componente do estado de um objeto define uma dimensão, pra simplificar, imagine um objeto da classe Ponto:

class Ponto{
String rotulo=null;
int x;

int y;
}

Esse é um ponto cartesiano, com três atributos que definem seu estado. O valor de x , e o valor de y definem o estado atual do ponto. x e y são as Dimensões do objeto.

Como o ponto muda de estado é seu comportamento. O objeto reage a estímulos, um objeto não muda de estado por si só. Em Java, implementamos o comportamento de objetos nos seus métodos. Vamos supor que nosso ponto só se mova de dez em dez espaços, na horizontal ou vertical, implementando isso:

class Ponto{
int x;
int y;

public void moverX(){
x=x+10;
}

public void moverY(){
y=y+10;
}


}

Sendo assim, os estados que um ponto pode assumir dependem do seu comportamento. No caso específico, o estado onde a dimensão x vale 2 é inválido, já que de zero x só poderia assumir o valor de 10.

O espaço-estado do objeto seria:

x=0,10,20,30,40,50,60,70,80...
y=0,10,20,30,40,50,60,70,80...


Ou seja: espaço-estado é o conjunto de valores de dimensões válidas para um objeto. Acho que isso dá uma clara visão do porque atributos públicos são perigosos.




Invariantes

Existem algumas condições que são necessárias para que um objeto esteja em estado válido. Um triângulo tem sempre três ângulos, um carro em funcionamento tem quatro rodas, um usuário tem lajem e senha definidos...



Essas são invariantes. "Invariantes" porque devem ser obedecidas em todos os momentos do ciclo de vida do objeto. Invariantes são restrições no espaço-estado do objeto.




Para alguns, é mais fácil pensar em invariantes pensam em como restrições de tabelas em um banco de dados. A tabela A tem uma chave estrangeira A1, que aponta para a chave primária de B e não deve ser nula.



Se, por exemplo, nosso ponto só puder se mover a uma distância máxima de 20 espaços de zero para x e 50 para y, a invariante seria o intervalo marcado em marrom abaixo:




x=0,10,20,30,40,50,60,70,80...

y=0,10,20,30,40,50,60,70,80...




É dever da implementação do objeto zelar que esteja sempre em estado válido. Sempre que fizer uma transição de estado, o objeto deve checar se está em um estado válido. Em Java, o que poderíamos fazer é:




public void moverX(){

x=x+10;

checarEstado();


}



public void moverY(){

y=y+10;

checarEstado();


}




protected void checarEstado()(){

if(!(x<20) || !(y<50)) throw new IllegalStateException();

}




Checar a invariante é apenas sanidade. Sua invariante nunca deve ser quebrada, e essa é a responsabilidade das pré e pós-condições.




Pré e Pós-Condições

Aqui está a resposta para o problema das NPE. Toda operação (ou seja: método) define contratos. O contrato de uma operação é:




Se quem chamar me garantir a pré-condição, eu garanto a pós-condição.




Ou seja: se o objeto que chama o método garantir que a pré-condição esteja cumprida, o método deve garantir que a pós-condição também esteja cumprida.




Para exemplificar, vamos criar um método que move nosso ponto um número de espaços qualquer, desde que seja no máximo 20 espaços por vez :



public void mover(int espacosX, int espacosY){

x=+espacosX;

y=+espacosy;


checarEstado();


}



Para evitar que a invariante seja quebrada e nós tenhamos certeza que o parâmetro é até vinte espaços, vamos estabelecer uma pré-condição através da checagem de parâmetros enviados e uma pós-condição:




public void mover(int espacosX, int espacosY){



//checando pre condição

if (!(espacosX > 15)) throw new IllegalArgumentException();

if( !((x+espacosX) < 20)) throw new IllegalArgumentException();

if( !((y+espacosy) < 50)) throw new IllegalArgumentException();





int antigoX = x;

int antigoY = y;



x=+espacosX;

y=+espacosy;



//checando pos condições

if(x!=(antigoX+espacoX)){


x=antigoX;



throw new IllegalStateException();


}

if(y!=(antigoY+espacoY)) {


y=antigoY;


throw new IllegalStateException();


}





checarEstado();

}




Acho que agora fica claro como resolver o problema das NPEs. O ponto é que você não deve checar contra valores nulos, mas sim contra valores inválidos. Você não deve evitar NullPointerExceptions, deve evitar que seus métodos processem argumentos inválidos.




A princípio, a pós-condição pode parecer inútil, mas lembre-se: você prometeu que ia entregar isso, não custa nada dar uma segunda conferida. Ok, em um caso simples como esse, é superficial.



Isso me lembra de uma ocasião onde tinha que gerar uma página HTML com no máximo 180 caracteres. O métodos que gerava o texto da página tinha no seu contrato a garantia que geraria no máximo 180 caracteres, e como era um processo muito complexo, sempre que terminava ele mesmo checava o texto gerado. Quando você não tem absoluta certeza que vai conseguir cumprir seu contrato (ou se seu método depender de outros métodos que podem não obedecer contrato nenhum), cheque antes de retornar.





Subclasses e Contratos

Como uma subclasse estabelece seu contrato?



Quanto á Invariante, a subclasse pode definir que sua definição de "válida" é diferente da classe mãe, por isso ela pode definir uma invariante diferente. Vamos exemplificar cirando uma classe-filha para nosso ponto:




class PontoLonge extends Ponto{




protected void checarEstado()(){

if( !(x < 1000) || !(y < 1000)) throw new IllegalStateException();

}

}




Nesse caso, a nova classe definiu que sua invariante permite valores maiores para
x e y.




Como nosso ponto pode se mover para lugares muito mais longínquos é trabalhoso ficar indo de pouco em pouco. Vamos redefinir os métodos de movimento para nos movermos mais rapidamente:




class PontoLonge extends Ponto{



protected void checarEstado()(){

if( !(x < 1000) || !(y < 1000)) throw new IllegalStateException();


}



public void mover(int espacosX, int espacosY){



//checando pre condição

if (!(espacosX > 100) || !(espacosY > 100) ) throw new IllegalArgumentException();


if( !((x+espacosX) < 1000)) throw new IllegalArgumentException();

if( !((y+espacosY) < 1000)) throw new IllegalArgumentException();




int antigoX = x;

int antigoY = y;



x=+espacosX;

y=+espacosy;




//checando pos condições

if(x!=(antigoX+espacoX)){


x=antigoX;


throw new IllegalStateException();


}

if(y!=(antigoY+espacoY)) {


y=antigoY;


throw new IllegalStateException();



}





checarEstado();

}

}



Parece legal? Primeiro, uma sugestão é refatorar os métodos, olhe só essa quantidade de coisas repetidas! Esse exercício fica para você já que foge ao escopo do post.




Voltando à sobrescrita, você consegue imaginar um problema? Se não, veja o código abaixo, retirado de uma classe que usa os objetos ponto:



public moverUmPoucoUmPonto(Ponto p){

p.mover(11,21);

}




Que tal? Essa classe funciona com Ponto, mas não com PontoLonge. Parabéns para nós, acabamos de mandar todo o polimorfismo para o espaço, já que não podemos utilizar nossa classe derivada como usamos a classe mãe. Solução?







A pré-condição de um método sobrescrito em uma classe deve ser igual ou menos restritiva que a pré-condição do método na classe mãe.






Isso significa que nós poderíamos aceitar cosias que a superclasse não aceita, mas nós temos que aceitar tudo que a superclasse aceitaria. No código, nós poderíamos, poderíamos, por exemplo, dizer que o mínimo de espaços que podemos mover passa de 15 para 5, porque assim o código feito para a superclasse ainda funcionaria. Corrigindo o exemplo (ainda sem refatoração):





public void mover(int espacosX, int espacosY){



//checando pre condição

if (!(espacosX > 15)) throw new IllegalArgumentException();


if( !((x+espacosX) < 1000))throw new IllegalArgumentException();

if( !((y+espacosY) < 1000))throw new IllegalArgumentException();




int antigoX = x;

int antigoY = y;



x=+espacosX;

y=+espacosy;



//checando pos condições

if(x!=(antigoX+espacoX)){


x=antigoX;



throw new IllegalStateException();


}

if(y!=(antigoY+espacoY)) {


y=antigoY;


throw new IllegalStateException();


}

checarEstado();

}

}




Não tem jeito de aumentar o valor mínimo, então voltamos ao original (e mais código repetido, anda logo com esse refactoring aí!).



Isso também te explica porque você pode, em java, aumentar a visibilidade de um método (tornar um método que era protected public), mas não diminuir (tornar um método public private).




Para pós-condições, é bem parecido:




Uma pós-condição de um método sobrescrito deve ser igual ou mais restritiva que a pós-condição do método original.






Isso quer dizer que se a pós-condição do seu método for que ele retorna um inteiro entre 1 e 100, os métodos que sobrescreverem este podem gerar um número entre 50 e 60, mas não um entre 1 e 500, por exemplo. Se uma classe estava esperando receber deste método entre 1 e 100 e recebe 50, não há problema, mas se ela receber 500, aí sim o polimorfismo deixa de funcionar como deveria (e pode ser que algo simplesmente exploda no seu sistema!).




Contratos Quebrados

Como qualquer contrato, o contrato de uma classe pode ser quebrado. Você já deve ter uma idéia de como reagir quando o cliente não cumpre sua parte, lançando uma exceção, mas lembre-se também que você pode não cumprir o contrato. è aí que entra a pós-condição, se ela não foi obedecida quem não cumpriu o contrato foi você.




Quase sempre é melhor você interromper o processamento com uma exceção do que retornar um valor que não cumpre a pós-condição.



Você poderia ter outro comportamento nesse caso, como retornar uma flag, ou mesmo não fazendo nada, mas exceções são a maneira padronizada de lidar com quebra de contratos. Com a pré-condição documentada, é obrigação do cliente provê-la, a checagem é só para evitar esforço desnecessário e identificar erros mais rapidamente.




Documentando

Um contrato só tem sentido se documentado. Muitas plataformas oferecem facilidades para documentar o contrato das classes, infelizmente Java não tem nada assim pronto.



O que você pode fazer é usar o bom e velho
JavaDoc.




Documente a invariante da classe na descrição desta. Você pode usar um pseudocódigo (evite copiar e colar o código, porque assim você expõe sua implementação).



O contrato dos métodos deve estar descrito na documentação destes, use as tags de
@param, @throws e @return para indicar o que você espera e o que provê.




Coloque na cabeça que checar contratos é documentação. qualquer um sabe como usar corretamente seu método se você especificar seu contrato na documentação.



Isso é Trabalhoso Demais!

Sim, eu sei. Infelizmente, Java não tem suporte nativo á design by contract, ao contrário de algumas outras linguagens (geralmente linguagens puramente OO). Sim, existe o assert herdado do C, e existem abordagens em AOP e Proxies (uma até sugerida no post mencionado), mas a linguagem por si só não tem esse recurso.




Uma linguagem com suporte à contratos vai te dar um modo fácil de definir e se referenciar a pré e pós-condições, invariantes e guardar valores antigos (o nosso
antigoX e antigoY). Vai fazer um or automático com uma pré-condição e a pré-condição do método que você sobrescrever, e um and com a pós-condição. Ainda deveria gerar um documento como um JavaDoc com espaço reservado para a pré e pós-condições (que, afinal, são públicas). Também deve ter um mecanismos eu permita ligar ou desligar as checagens, se quisermos mantê-las só para depuração.




Enquanto não vemos isso em java, nós temos que criar esses conceitos por nós mesmos. Isso geralmente implica num overhead enorme. de uma maneira geral:




  • Se uma classe possui estados inválidos e estes estados são atingíveis, providencie uma invariante e faça a checagem dela.

  • Se seus métodos realizam um processamento e retorne um valor, cheque este valor se no meio do caminho você precisou usar métodos não-confiáveis, como métodos de terceiros


  • Sempre estabeleça e documente um contrato, mesmo que você não faça checagem.

  • Se uma classe precisa de muitos objetos para não estar fora da invariante e você não quer passar estes objetos no construtor, use uma
  • Factory.
  • Tente utilizar o máximo possível de boas-práticas estabelecendo checagens, evite código duplicado ao máximo.


  • Tenha testes unitários que testem todas as possibilidades que você consiga pensar de quebrar o contrato dos métodos.



 
f