Lidando de Forma Eficiente com Validações Locais de Objetos - PDF
Download "Lidando de Forma Eficiente com Validações Locais de Objetos"
Margarida Santos Rodrigues
1 Lidando de Forma Eficiente com Validações Locais de Objetos Aprenda a construir um mini-framework para validar objetos locais sem afetar a complexidade do código. Autor Paulo César M. N. A. Coutinho é graduado em Tecnologia em Sistemas de Informação pelo Centro Federal de Educação Tecnológica de Pernambuco. Trabalha como Engenheiro de Sistemas no Centro de Estudos e Sistemas Avançados do Recife (C.E..S.A.R) com desenvolvimento móvel em C/C++. Também vem trabalhando com Java para web em projetos pessoais. Possui as certificações SCJP 5 e SCWCD. Gravata Validar objetos locais é uma prática essencial para garantir a qualidade final do software produzido. Por outro lado, esse tipo de validação geralmente traz consigo um aumento significativo na complexidade do código, o que pode nos levar a problemas de métricas. Este artigo tem como objetivo mostrar uma forma eficiente de implementar e executar esse tipo de validação sem comprometer a complexidade do código. Introdução No mundo do desenvolvimento de software, estamos sempre buscando meios de melhorar a qualidade do software que produzimos. E uma das características que indicam a qualidade de um software é a sua robustez, ou seja, sua capacidade de tratar situações inesperadas sem apresentar falhas. O mecanismo de exceções de Java nos ajuda bastante nesse aspecto, pois com ele podemos separar completamente os casos excepcionais do fluxo normal da aplicação. A validação de dados é uma pratica, indispensável, que visa garantir o estado dos dados durante seu processamento. Combinando o uso dessa prática com o mecanismo de exceções aumentamos as chances da construção um software mais robusto e, conseqüentemente, de mais qualidade. Quando falamos em validação, a primeira coisa que nos vem à cabeça é validação de formulários, validação de beans ou outras coisas do gênero. Porém, existe um tipo de validação que muitas vezes esquecemos, ignoramos ou, simplesmente, fazemos de qualquer maneira sem nos preocupar com conceitos básicos da engenharia de software como reusabilidade, manutenibilidade, clareza e complexidade. Essa é a validação local, ou seja, validação de variáveis locais e argumentos de métodos. Temos vários frameworks que definem estratégias de validação de beans com o intuito de centralizar e tornar essa tarefa o mais declarativa possível. Entre estes estão Struts, Spring e Hibernate Validator. Agora com a JSR 303, Bean Validator, teremos uma padronização para validação de Java Beans que visa centralizar o código de validação para que esse possa ser utilizado por todas as camadas da aplicação. Para validações locais, entretanto, não temos muitos recursos disponíveis e em conseqüência disso fazemos essas validações de qualquer forma, de um modo não padronizado que pode, muitas vezes, nos levar a desde erros de lógica, causados pela escrita repetitiva de trechos de validação, a problemas com métricas de complexidade de código devido aos vários ifs presentes nas validações. Nesse artigo veremos como criar um pequeno conjunto de classes com o objetivo de centralizar a lógica utilizada em validações locais de modo que possam ser reutilizadas por nossas aplicações de modo geral. Como resultado teremos um mini-framework que será empacotado num arquivo.jar, o qual pode ser distribuído e utilizado em diversas aplicações.
2 Validação de Bean X Validação local Validações locais são aquelas com o objetivo de validar o estado das variáveis locais de um método de acordo com o propósito desse método. Elas diferem das validações de beans principalmente no que diz respeito ao dinamismo e especificidade das condições de validação. Uma validação de bean é definida baseada nos requisitos da aplicação e é estática, ou seja, o conjunto de restrições que caracterizam um bean como válido não muda durante a execução da aplicação. Já as validações locais são dinâmicas e dependem diretamente da funcionalidade que está sendo executada pelo método. Um exemplo de validação de bean é verificar se os atributos obrigatórios estão preenchidos na hora de persistir um bean na base de dados. Já um exemplo de validação local é validar se um argumento de um método não é nulo ou se o objeto recebido encontra-se num estado válido para o método. Para entendermos melhor essa diferença, vamos imaginar uma classe Venda, mostrada na Figura 1, que possua os atributos vencimento, valor e aberta, este último para indicar se a venda encontra-se em aberto ou não. Podemos definir como um estado válido para esse bean o preenchimento correto dos atributos vencimento e valor. Não podemos, portanto, dizer que uma Venda só é válida se o valor do atributo aberta for true porque uma venda pode ser válida estando aberta ou não. Imaginemos agora um método liquidavenda() que receba um objeto Venda como argumento e lance uma exceção do tipo VendaJaLiquidadaException para vendas que já foram liquidadas anteriormente, ou seja, para aquelas que o valor do atributo aberta for false. Nesse tipo de situação a validação de bean não resolverá nosso problema e, sendo assim, teremos que criar uma validação local no método, validação essa que se aplica apenas para o propósito do método. Figura 1. Classe Venda. A primeira forma que imaginamos de implementar essa validação nesse método é a descrita na Listagem 1. Uma segunda alternativa seria executar a ação de liquidar a venda no if e lançar a exceção num else. Essa parece ser uma boa alternativa a princípio, mas começa a ficar complicada quando lançamos vários tipos de exceção. Essa implementação funciona, de fato, porém apresenta alguns problemas que discutiremos a seguir. Listagem 1. Método liquidavenda() inicial. public void liquidavenda(venda venda) throws VendaJaLiquidadaException { if (!venda.isaberta()) { throw new VendaJaLiquidadaException("Esta venda já foi liquidada!"); // Lógica para liquidar a venda... Primeiramente nossa implementação não está verificando se o objeto Venda recebido não é nulo, o que poderá levar a um NullPointerException sem nenhum tipo de informação que ajude a identificar o erro caso uma venda nula seja recebida. Para resolver esse problema o método liquidavenda() deveria testar se o argumento
3 recebido é diferente de nulo e, caso não o seja, deveria lançar uma exceção que informasse qual foi o problema ocorrido. Nesse caso uma IllegalArgumentException, com uma mensagem apropriada, seria mais interessante. Veja como ficaria o código corrigido na Listagem 2. Listagem 2. Método liquidavenda() com validação de argumento nulo. public void liquidavenda(venda venda) throws VendaJaLiquidadaException { if (venda == null) { throw new IllegalArgumentException("O argumento venda não pode ser null!"); if (!venda.isaberta()) { throw new VendaJaLiquidadaException("Esta venda já foi liquidada!"); // Lógica para liquidar a venda... Um outro problema no código da Listagem 1 diz respeito a poluição e complexidade do código, que aumentou ainda mais quando solucionamos o problema anterior (ver Listagem 2). Note que para essas simples validações foi preciso adicionar dois ifs ao nosso código aumentando a complexidade ciclomática (número de caminhos independentes possíveis) do método, o que pode ser motivo de um refactoring forçado no código, uma vez que existem clientes que estabelecem valores máximos para complexidade em métodos e utilizam essas métricas como critério de aceitação do sistema. Além do mais, um monte de ifs de validação deixa o código poluído e prejudica o entendimento do objetivo do método. Vejamos agora um outro tipo de cenário comum de validações locais. A Listagem 3 mostra o exemplo de um código de login que recebe dois argumentos (username e password) e retorna o usuário que corresponde aos argumentos recebidos ou lança uma exceção de acordo com o erro ocorrido. Primeiro validamos os argumentos, em seguida chamamos o método getuserbyusername() que recupera um usuário na base de dados, então validamos o status do usuário e caso nenhum problema seja encontrado o usuário é retornado. Note que nosso método lança uma exceção específica para cada tipo de erro encontrado. Nesse exemplo fica bem claro como código de validação local pode aumentar a complexidade de um método. A classe User é mostrada na Listagem 4. É um simples Java Bean composto dos atributos username, password e status. O atributo status é um enum que representa o status do usuário no sistema. Listagem 3. Método login() com várias validações locais. public User login(string username, String password) throws UserNotFoundException, InvalidPasswordException, BlockedUserException, NonActiveUserException { // Validação dos argumentos... if (username == null) { throw new IllegalArgumentException("O username não pode ser null."); if (password == null) { throw new IllegalArgumentException("O password não pode ser null."); User user = getuserbyusername(username); // Validação do estado do usuário if (user == null) {
4 throw new UserNotFoundException("O usuário não existe."); if (user.getpassword().equals(password)) { throw new InvalidPasswordException("A senha informada é inválida."); if (user.getstatus() == User.UserStatus.NON_ACTIVE) { throw new NonActiveUserException("O usuário não teve o cadastro ativado."); if (user.getstatus() == User.UserStatus.BLOCKED) { throw new BlockedUserException("O usuário teve o cadastro bloqueado."); return user; Listagem 4. Classe User. public class User { public enum UserStatus { ACTIVE, NON_ACTIVE, BLOCKED private String username; private String password; private UserStatus status; // Getters e setters Solucionando o problema da complexidade Podemos perceber algumas características comuns em todos os ifs de validação nos exemplos mostrados. Se olharmos bem, perceberemos que todos são compostos, basicamente, de uma condição booleana, uma exceção a ser lançada e uma mensagem de erro. Nosso primeiro passo então, será encapsular essas características numa classe que representará um caso de validação local. A Listagem 5 mostra o código da classe LocalValidationCase. Nossa classe possui três atributos que são exatamente os que acabamos de identificar. Note que o atributo exceptionclass deve ser do tipo RuntimeException ou alguma subclasse desse tipo. Isso se deve ao fato de não podermos lançar exceções checadas que não estejam declaradas no método e como estamos criando um mecanismo genérico não temos como saber quais tipos de exceções serão lançadas, daí a necessidade de usarmos apenas exceções não checadas. Listagem 5. Classe LocalValidationCase para representar um caso de validação local. // Declaração de pacote e imports omitidos. public class LocalValidationCase { private boolean successcondition; private Class<? extends RuntimeException> exceptionclass; private String exceptionmessage; public LocalValidationCase(boolean successcondition, Class<? extends RuntimeException> exceptionclass, String exceptionmessage) { this.successcondition = successcondition; this.exceptionclass = exceptionclass; this.exceptionmessage = exceptionmessage;
5 // Getters e setters. Agora que temos uma classe para representar um caso de validação local, podemos criar uma coleção de objetos desse tipo e validá-los através de um loop. A Listagem 6 mostra a classe LocalValidationUtil. Nela temos um método estático chamado validate() e que recebe como argumento uma lista de LocalValidationCases que representam os casos de validação local a serem validados. Nesse método simplesmente iteramos sobre a lista recuperando seus valores e para cada valor testamos sua condição de sucesso. Em caso de falha, criamos a mensagem de erro a ser usada na exceção que será lançada; para isso verificamos se alguma mensagem de erro foi informada, se foi, essa será usada, caso contrário usaremos uma mensagem padrão; e logo em seguida lançamos uma exceção que será criada a partir do método privado createresultexception(), para tal informamos a classe da exceção a ser lançada e a mensagem de erro. O método createresultexception() instancia a exceção com base na classe informada utilizando a API de reflexão de Java e retorna a instância criada. Observe que caso ocorra algum erro na criação da exceção informada ou mesmo se nenhuma classe de exceção for informada, criaremos uma IllegalArgumentException que será nossa exceção padrão. Listagem 6. Classe LocalValidationUtil que fornece um método para validar uma lista de LocalValidationCases. //Declaração de pacote e imports omitidos. public class LocalValidationUtil { public static void validate(final List<LocalValidationCase> validationcases) { if (validationcases!= null) { for (int i = 0; i < validationcases.size(); i++) { LocalValidationCase validationcase = validationcases.get(i); if (!validationcase.issuccesscondition()) { String message = validationcase.getexceptionmessage()!= null? validationcase.getexceptionmessage() : "Validation error [" + i + "]"; throw createresultexception(validationcase.getexceptionclass(), message); private static RuntimeException createresultexception(final Class<? extends RuntimeException> exceptionclass, final String exceptionmessage) { RuntimeException resultexception = null; if (exceptionclass!= null) { try { final Class parametertypes[] = { String.class ; final Constructor<? extends RuntimeException> ct = exceptionclass.getconstructor(parametertypes); final Object argumentlist[] = { exceptionmessage ;
6 resultexception = ct.newinstance(argumentlist); catch (InstantiationException e) { catch (IllegalAccessException e) { catch (SecurityException e) { catch (NoSuchMethodException e) { catch (InvocationTargetException e) { if (resultexception == null) { resultexception = new IllegalArgumentException(exceptionMessage); return resultexception; Até agora, já temos uma classe para representar os casos de validação e uma classe utilitária para validar uma coleção desses casos lançando exceções apropriadas de acordo com cada caso. Ainda temos, contudo, um problema relacionado à usabilidade. Não seria nada bom, sempre que fossemos validar variáveis locais, ter que instanciar uma lista de casos de validação, em seguida instanciar e adicionar cada caso à lista e depois invocar um método numa outra classe passando a lista criada. Para resolver esse problema criaremos uma classe que representará todo o contexto de validação local. A Listagem 7 mostra a classe LocalValidationContext. Essa será a classe que fará toda a interface entre nosso mini-framework e a aplicação cliente. Nossa classe possui um atributo, validationcases, que nada mais é do que uma lista de casos de validação local; e um segundo atributo, exceptionclass, que representa a classe padrão da exceção a ser lançada quando uma não for informada no caso de validação. Temos então dois construtores, o primeiro que recebe um valor inicial para exceptionclass e o segundo que usa uma IllegalArgumentException como padrão. O método clear() serve para limpar a lista de casos de validação e o método setexceptionclass() serve para configurar o valor do atributo exceptionclass. O método addvalidcondition() cria um objeto do tipo LocalValidationCase com os argumentos recebidos e o adiciona à lista de casos de validação do contexto. Esse método é sobrecarregado para prover uma flexibilidade maior ao usuário. Nesse método, a condição informada no argumento condition deve ser verdadeira e caso a mesma não seja, uma exceção será lançada. O método addinvalidcondition() é praticamente igual ao método addvalidcondition(), a única diferença é que nesse método a condição informada no argumento condition deve ser falsa para que a validação tenha sucesso. Omitimos as versões sobrecarregadas desse método e dos restantes uma vez que estas seguem o mesmo princípio do método addvalidcondition(). Os métodos addnotnullchecking(), addnotemptychecking() e addequalscheking() são simplesmente métodos utilitários para casos comuns de validação. Todos eles usam o addvalidcondition() internamente. Vale ressaltar que aqui estão apenas alguns exemplos de validações comuns, mas podemos criar vários outros, por exemplo addregexchecking(), addsamechecking(), etc. Com a ajuda desses métodos utilitários podemos encapsular a lógica para os testes comuns, deixando para o usuário apenas a tarefa de fornecer os objetos. Temos ainda um método addcustomvalidation() que recebe como argumento um objeto do tipo LocalValidator. Este tipo trata-se de uma interface que criaremos, o código encontra-se na Listagem 8. Essa interface declara apenas um método, validate(), e é útil quando temos alguma validação com uma lógica mais carregada, ou que lance alguma exceção que deva ser tratada localmente. Nesses casos podemos criar classes que implementem a interface LocalValidator, e implementar o método validate() nessas classes.
7 Por fim temos o método validate(), que simplesmente delega a validação para o método validate() do nosso LocalValidationUtil passando a lista de casos de validação. As versões estáticas do método validate() são úteis quando o método cliente possui apenas um caso de validação, evitando o overhead da criação de um objeto do tipo LocalValidationContext para adicionar apenas um caso de validação. Um detalhe ainda em relação a classe LocalValidationContext, é que todos os métodos não estáticos possuem um retorno do tipo da própria classe e retornam this. Eles foram implementados dessa forma para que possam ser chamados de forma encadeada. Mais adiante, veremos como isso ajuda na usabilidade da nossa classe. Listagem 7. Classe LocalValidationContext. // Declaração de pacote e imports omitidos. public class LocalValidationContext { private List<LocalValidationCase> validationcases; private Class<? extends RuntimeException> exceptionclass; public LocalValidationContext(Class<? extends RuntimeException> exceptionclass) { validationcases = new ArrayList<LocalValidationCase>(); this.exceptionclass = exceptionclass; public LocalValidationContext() { this(illegalargumentexception.class); public LocalValidationContext clear() { validationcases.clear(); public LocalValidationContext setexceptionclass(class<? extends RuntimeException> exceptionclass) { this.exceptionclass = exceptionclass; public LocalValidationContext addvalidcondition(boolean condition, Class<? extends validationcases.add(new LocalValidationCase(condition, exceptionclass, exceptionmessage)); public LocalValidationContext addvalidcondition(boolean condition, Class<? extends RuntimeException> exceptionclass) { addvalidcondition(condition, exceptionclass, null); public LocalValidationContext addvalidcondition(boolean condition, String exceptionmessage) { addvalidcondition(condition, exceptionclass, exceptionmessage); public LocalValidationContext addvalidcondition(boolean condition) { addvalidcondition(condition, exceptionclass, null); public LocalValidationContext addinvalidcondition(boolean condition, Class<? extends
8 validationcases.add(new LocalValidationCase(!condition, exceptionclass, exceptionmessage)); // versões sobrecarregadas do método addinvalidcondition... public LocalValidationContext addnotnullchecking(object object, Class<? extends addvalidcondition(object!= null, exceptionclass, exceptionmessage); // versões sobrecarregadas do método addnotnullchecking... public LocalValidationContext addnotemptychecking(string string, Class<? extends addvalidcondition(string!= null && string.trim().length() > 0, exceptionclass, exceptionmessage); // versões sobrecarregadas do método addnotemptychecking... public LocalValidationContext addequalschecking(object object1, Object object2, Class<? extends addvalidcondition(object1!= null && object1.equals(object2), exceptionclass, exceptionmessage); // versões sobrecarregadas do método addequalschecking... public LocalValidationContext addcustomvalidation(localvalidator validator, Class<? extends addvalidcondition(validator.validate(), exceptionclass, exceptionmessage); // versões sobrecarregadas do método addcustomvalidation... // Método para executar as validações registradas no contexto. public LocalValidationContext validate() { LocalValidationUtil.validate(validationCases); public static void validate(boolean validcontition, Class<? extends RuntimeException> exceptionclass, String exceptionmessage) { List<LocalValidationCase> validationcases = new ArrayList<LocalValidationCase>(); validationcases.add(new LocalValidationCase(validContition, exceptionclass, exceptionmessage)); LocalValidationUtil.validate(validationCases); public static void validate(boolean validcontition, Class<? extends RuntimeException> exceptionclass) { validate(validcontition, exceptionclass, null);
9 Listagem 8. Interface LocalValidator. public interface LocalValidator { boolean validate(); Com esse pequeno conjunto de classes e interfaces criados, temos então nosso mini-framework de validação local pronto para ser usado. Vejamos agora como ficariam os dois métodos que analisamos no início deste artigo com o uso do nosso mini-framework. As Listagens 9 e 10 mostram, respectivamente, os métodos liquidavenda() e login(). Veja como conseguimos manter as mesmas regras de validação, porém sem alterar, em nada, a complexidade dos métodos. Listagem 9. Método liquidavenda() utilizando o mini-framework de validação local. public void liquidavenda(venda venda) throws VendaJaLiquidadaException { // Cria o contexto de validação. new LocalValidationContext(IllegalArgumentException.class) // Casos de validação: // Verifica se a venda recebida não é nula..addnotnullchecking(venda, "O argumento venda não pode ser null!") // Verifica se a venda está aberta..addvalidcondition(venda.isaberta(), VendaJaLiquidadaException.class, "Esta venda já foi liquidada!") // Invoca a validação local..validate(); // Lógica para liquidar a venda... Listagem 10. Método login() utilizando o mini-framework de validação local. public User login(string username, String password) throws UserNotFoundException, InvalidPasswordException, BlockedUserException, NonActiveUserException { // Cria o contexto de validação. LocalValidationContext context = new LocalValidationContext(IllegalArgumentException.class) // Casos de validação: // Verifica se o login recebido não é nulo..addnotnullchecking(username, "O username não pode ser null.") // Verifica se a senha recebida não é nula..addnotnullchecking(password, "O password não pode ser null.") // Invoca a validação local..validate(); // Recupera o usuário com o login informado. User user = getuserbyusername(username); // Limpa o contexto de validação para ser reutilizado. context.clear() // Casos de validação: // Verifica se existe um usuário para o login informado..addnotnullchecking(user, UserNotFoundException.class, "O usuário não existe.") // Verifica se existe a senha do usuário é igual a informada..addequalschecking(user.getpassword(), password, InvalidPasswordException.class,
10 "A senha informada é inválida.") // Verifica se o usuário ainda não teve o cadastro ativado..addinvalidcondition(user.getstatus() == User.UserStatus.NON_ACTIVE, NonActiveUserException.class, "O usuário não teve o cadastro ativado.") // Verifica se o usuário está bloqueado..addinvalidcondition(user.getstatus() == User.UserStatus.BLOCKED, BlockedUserException.class, "O usuário não teve o cadastro ativado.") // Invoca a validação local..validate(); // Retorna o usuário. return user; Distribuindo a solução Agora vamos criar um arquivo.jar com nossas classes para que possam ser utilizadas facilmente em outros projetos. Vejamos uma maneira bem simples de empacotar nossas classes num.jar através do Eclipse. Para isso clique com o botão direito do mouse no projeto ou num pacote especifico que queira distribuir. Agora selecione a opção Export... e quando o diálogo abrir escolha a opção JAR file dentro da pasta Java. Selecione os projetos, pacotes ou arquivos que deseja exportar; clique no botão Browse... para escolher o caminho onde o arquivo.jar será criado e clique em Finish. Feito isso já temos nosso mini-framework pronto para ser utilizado em nossas aplicações. Para tal, basta incluir o.jar que acabamos de criar no classpath dos nossos projetos. Considerações Finais Nesse artigo, vimos como criar um conjunto de classes e interfaces para padronizar e centralizar a lógica de validação local e, por conseqüência, diminuir a complexidade dos métodos que utilizam esse tipo de validação. O resultado foi a construção de um mini-framework que foi empacotado num arquivo.jar e poderá ser utilizado pelo leitor em todas as suas aplicações daqui para a frente. Referências [Java Practices: Validate method arguments] [Best Practices for Exception Handling] [JSR 303: Bean Validation]