Contratos Nulos
From Fragmental Bliki
Conteúdo |
Introdução
Um post no blog de Todd Hussgerou 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 exceção qualquer.
Dos comentários que apareceram no blog, uns indicaram o projeto nully, no java.net. O Nully é uma ferramenta para a IDE IntelliJ que basicamente adiciona uma anotação @NotNull, acionando warnings e erros no editor quando você tenta implementar uma operação que poderia resultar em 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 disso mesmo.
Uma outra alternativa dada foi votar no BUG 5030232, da Sun, uma tentativa de adicionar o tratamento que a linguagem Nice faz com null. Nice é uma linguagem para a JVM (como Groovy ou BeanShell) e basicamente exige que você inicie com um "?" às variáveis que podem ser nulas, e não deixa (na compilação) utilizar estas sem checar se a variável é null antes. Boa tentativa, mas ainda não é isso...
Adam Kruszewski comentou que no seu blog havia uma implementação com metadados 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, mas não foi desta vez.
ALgumas linguagens mais dinâmicas (entre elas Ruby) têm um objeto especial que representa o valor null. Nestas linguagens, não há como você receber uma NPE ou algo aprecido, mas o objeto null (nil em Ruby) retorna semrpe uma exceção quando algum método é chamado. É uma implementação de algo parecido com um NullObject, mas só funciona em linguagens altamente dinâmicas (especialmente de tipagem dinâmica), o que Java não é.
A solução não sou eu quem vai dar, é um conceito muito antigo que surgiu junto com os primeiros pensamentos em objetos. Com o tempo foi sendo cunhada até que Bertrand Meyer a publicou e deu um nome: Design By Contract.
O pensamento nessa linha é diferente. Você define o que sua classe (ou seu método) espera de entrada e o que ela garante de saída. Qualquer coisa além destes valores esperados é rejeitado, e isso vai incluir null. O ponto é que você não deve lutar contra null, você deve lutar contra valores fora do que você considera válido. Êstes parâmetros válidos são expressos na forma de um contrato.
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 (um atributo, por exemplo) 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.
O modo 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 assim:
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 partindo do valor 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á mais 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 login 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 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 negrito 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
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 é de 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();
}
Você deve checar contra quebras de contrato, da sua parte ou de quem te chamou.
Como já foi dito antes, você não deve evitar NullPointerExceptions, deve evitar que seus métodos processem argumentos inválidos, estabelecendo e obedecendo a contratos. Note que é uma excelente prática checar se os valores não são válidos, ao invés de checar se os valores são inválidos. Confuso?
Partindo do ponto que seus valores válidos sejam inteiros de 1 a 10, você não deve fazer
if(x<0 || x >10) throw...
Mas sim:
if(!(x>0 && x < 10)) throw...
Pode parecer a mesma coisa, mas é este disciplina que garante a segurança e robustez do contrato. Você nunca sabe o que o usuário vai entrar num programa, então teste para encontrar o que você sabe que está certo, obviamente o que não está certo, seja o que for, está errado.
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 (teoricamente) uma segunda conferida. Ok, em um caso simples como esse, é superficial demais para valer sequer umas linhas de código.
Tenha atenção especial com contratos de classes de interface externa. Cuidado com dados que vêm ou ao do/para o usuário ou outros subsistemas.
Isso me lembra de uma ocasião onde tinha que gerar uma página HTML com no máximo 180 caracteres. O método 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 checava o texto que gerava. Isso garantiu que mesmo que o usuário recebesse uma mensagem de erro, ele não receberia uma mensgaem pela metade (dependendo do conteúdo, isso pode ser muito pior).
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?
Um princípio muito conhecido da teoria de Orientação a Objetos é o Princípio de Substituição de Liskov (LSP – Liskov Substitution Principle). Esse princípio diz, basicamente:
Subtipos podem substituir seus supertipos
Simples, óbvio... até boçal alguém diria. Pode até ser, mais isto implica diretamente nos contratos dos objetos, e nem sempre vemos isso.
Quanto à sua Invariante, uma subclasse pode ter uma definição do que é ser válida diferente da classe mãe, por isso ela pode definir uma invariante diferente. Vamos exemplificar criando 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, heim? 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 texto.
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 que acabamos de mandar todo o LSP para o espaço (provavelmente com o reusabilidade inteira do nosso sistema), 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 derivada deve ser igual ou menos restritiva que a pré-condição do método na classe base.
Isso significa que nós poderíamos aceitar coisas 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 (alterar a visibilidade de um método que era protected para public), mas não diminuir (mudar um método public para 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 LSP deixa de funcionar como deveria (e pode ser que algo simplesmente exploda no seu sistema – talvez literalmente!).
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 (na prática, diria que 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, produzindo formas compactas para visualizar os contratos de uma classe e checando possíveis quebras de contrato nos editores. 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 pseudocodigo (evite copiar e colar o código, porque assim você expõe sua implementação e geralmente não atualiza o JavaDoc toda vez que o código muda, descreva os algoritmos).
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). Existe o assert herdado do C, e existem abordagens em AOP, 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 mecanismo que 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 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.
--Phillip Calçado "Shoes" 22:12, 23 October 2005 (EST)
Page categories: Artigos | Design | Java

