Motivação

Todo software, seja ele escrito em Java ou qualquer outra linguagem de programação, é uma engrenagem construída para resolver um conjunto de problemas. Em linhas gerais, o fluxo se resume em:

  1. Operações são oferecidas ao cliente;
  2. Operações podem demandar parâmetros para nortear sua execução;
  3. Operações são executadas a partir dos parâmetros fornecidos;
  4. O resultado do processamento é retornado ao cliente.

Nesse interim, é muito provável que um mesmo parâmetro esteja relacionado a inúmeros caminhos lógicos possíveis, internamente. Quando isso acontece, não raro, o resultado é um código-fonte recheado de blocos condicionais excessivamente longos, prejudicando tanto a confiabilidade quanto a manutenibilidade do software.

Esse artigo mostrará como, a partir do uso de um padrão de projeto conhecido como Strategy, aliado à API de reflexão do Java, podemos transformar uma aplicação que, inicialmente, encontra-se na situação que acabamos de descrever, em outra totalmente refatorada, muito mais bem organizada e simples.

A aplicação

O programa que estudaremos é um utilitário de atualização de propriedades em um arquivo de texto, em que cada propriedade está necessariamente vinculada a um tipo de validador. Como premissa para uma propriedade desse arquivo ser atualizada, é necessário que o valor informado atenda as regras de validação estabelecidas para ela.

Soluções como essa são muito úteis, por exemplo, como utilitários para a configuração de sistemas maiores (automatizando a atualização de arquivos de propriedades, descritores de módulos, bancos de dados, dentre outras fontes).

A Tabela 1 apresenta os detalhes das três propriedades suportadas pelo projeto exemplo, bem como os critérios definidos para os valores por elas aceitos. Todas as condições nela estabelecidas são mapeadas, no código-fonte, como validadores, e cada validador é representado por uma classe específica, especialista, derivada de uma interface comum denominada Validator.

Propriedade (alias)

Propriedade real

Validação

host

host.address

Formato de endereço IPv4

enable-log

logging.enabled

Valores booleanos (false/true)

Servers

servers.count

Inteiros positivos maiores ou iguais a 1

Tabela 1. Propriedades suportadas pela aplicação

Primeira versão do código: O problema

Antes de darmos início aos nossos estudos, é importante ressaltar que avaliaremos apenas uma pequena porção da aplicação. Trata-se da lógica que envolve a validação, pois é nela que encontramos as características-alvo desse artigo.

Dito isso, observe o conteúdo da Listagem 1. Esta é a primeira versão da principal classe do projeto, denominada ConfigManager, responsável pela configuração de um arquivo de propriedades cuja localização é informada por meio de uma variável de ambiente chamada props.location. Algumas características dessa classe, e que precisam ser entendidas antes de prosseguirmos, são:

  • Propriedades não suportadas pela aplicação não serão adicionadas ao arquivo;
  • Propriedades não serão atualizadas caso o valor passado seja inválido;
  • O método principal dessa classe é o de assinatura applyChanges(Property).


  public class ConfigManager {
   
    private static ConfigManager manager;
    private static Properties properties;
   
    private NodeConfigurationManager () {}
   
    public static ConfigManager getManager() throws IOException {
      if (manager == null) {
        manager = new ConfigManager();
        loadProperties();
      }
      return manager;
    }
   
    public static void applyChanges (Property property) throws CommandException {
      try {
        LOGGER.info("arquivo de propriedades localizado em " + PROPS_LOCATION);
        validateInput(property);
        // ... salva-se o novo valor atribuído à propriedade
      } catch (ValidationException e) {
        throw new CommandException("o valor inserido para esta "
          + "propriedade é inválido", e);
      } catch (ConfigurationException e) {
        throw new CommandException("não foi possível salvar a propriedade " + 
          property.getName() + " no sistema", e);
      }
    }
   
    private static void validateInput(Property property) 
      throws ValidationException {
      if (PropertyAlias.INPUT_HOST.getAlias().
        equalsIgnoreCase(property.getName())) {
        new IpAddressValidator().validate(property.getValue());
      } else if (PropertyAlias.LOGGING_ENABLED.getAlias().
        equalsIgnoreCase(property.getName())) {
        new BooleanValidator().validate(property.getValue());
      } else if (PropertyAlias.SERVERS_COUNT.getAlias().
        equalsIgnoreCase(property.getName())){
        new MaxNodesValidator().validate(property.getValue());
      } else {
        LOGGER.debug("esta propriedade ainda não "
          + "é suportada pela ferramenta");
        throw new ValidationException("esta propriedade "
          + "ainda não é suportada pela ferramenta");
      }
    }
    
    ...
   
    }
  }
  
Listagem 1. Gerenciador de propriedades pré-refatoração

A primeira medida tomada no método applyChanges(Property) é a invocação de outro método, validateInput(Property). Observemos a composição desse por um instante: como único argumento, há um objeto do tipo Property, que encapsula tanto o nome da propriedade quanto o novo valor desejado para a mesma. O primeiro passo tomado em seu corpo é a validação do valor informado nesse objeto Property. Para isso, faz-se uso de um objeto validador, cuja identificação – e instanciação – é feita por meio de blocos condicionais (if-else).

Nesse caso, em que há apenas três propriedades suportadas, uma lógica como essa ainda não representa uma violação de código em si, mas já deve ser encarada como um sinal de alerta. E qual seria a razão para isso? Imagine que o número de propriedades suportadas tenha de ser aumentado para 10 ou 20 em uma próxima versão: isso já representaria uma complexidade ciclomática acima da máxima aceita pelos padrões internacionais. É esse problema que precisamos resolver.

Reflection API e Strategy: A solução

Ainda sobre o código da Listagem 1, é importante ressaltar que o padrão de projeto Strategy já se encontra, em partes, implementado. Um dos principais representantes da ‘família’ comportamental da popular (GoF), o padrão Strategy estabelece os seguintes pontos:

  1. Uma interface comum a todo um conjunto de algoritmos suportados;
  2. Um consumidor dessas estratégias, denominado ‘contexto’, é responsável por sua criação;
  3. O problema a ser resolvido por todas as estratégias é o mesmo;
  4. Implementa-se variadas estratégias concretas para o domínio do problema.

Neste caso, como o leitor poderá verificar no código da Listagem 2, todo validador deriva de uma classe abstrata chamada BaseValidator que, por sua vez, implementa uma interface denominada Validator. O problema comum que todas as estratégias têm de resolver é a validação de um parâmetro de entrada e, para as três propriedades suportadas pela aplicação (detalhadas na Tabela 1), foram criadas três implementações distintas e especialistas de validadores, sendo elas:

  1. BooleanValidator, para validação de valores booleanos;
  2. IpAddressValidator, para a verificação de endereços no formato IPv4;
  3. MaxNodesValidator, que certifica que o valor em questão é um inteiro positivo maior ou igual a 1.

O código dos validadores, que acabamos de descrever, é exatamente o mesmo nas versões original e refatorada do código da aplicação, isto é, não realizaremos mudanças nesses artefatos do projeto.


  public interface Validator {
         void validate (String input) throws ValidationException;
  }
   
  public abstract class BaseValidator implements Validator {
   
         @Override
         public void validate(String input) throws ValidationException {
               if (StringUtils.isEmpty(input)) {
                      throw new ValidationException(
                        "valor informado é vazio, o que é considerado dado"
                             + " inválido no escopo da solução");
               }
         }
   
  }
   
  public class BooleanValidator extends BaseValidator {
         
         @Override
         public void validate(String input) throws ValidationException {
               super.validate(input);
               if (Boolean.FALSE.toString().equalsIgnoreCase(input) || 
                             Boolean.TRUE.toString().equalsIgnoreCase(input)) {
                      throw new ValidationException("o dado informado "
                                    + "não corresponde a um valor booleano");
               }
         }
  }
   
  // ... demais implementações seguem a mesma estrutura
  
Listagem 2. Hierarquia de validadores do projeto

Analisemos, agora, o conteúdo da Listagem 3. Ela contém o mesmo código da classe ConfigManager, porém, refatorado. Observe como o método validateInput() encontra-se mais enxuto. Isso se deve à utilização de um novo enum introduzido no código, denominado ValidationMapper (vide Listagem 4). Sua função é identificar, para cada propriedade suportada pela aplicação, o tipo de validador a ser empregado. Por esse mapeamento, torna-se possível criar o validador através da API de reflexão do Java, conforme descrito a seguir:

  • Identifica-se, por meio do enum ValidationMapper, a classe do validador;
  • Cria-se uma instância do validador a partir da chamada Class.newInstance();
  • Invoca-se o método validate(String) para a validação do valor da propriedade.


  public class NodeConfigurationManager {
   
    private static NodeConfigurationManager manager;
    private static Properties properties;
   
    private NodeConfigurationManager () {}
   
    public static NodeConfigurationManager getManager() throws IOException {
      if (manager == null) {
        manager = new NodeConfigurationManager();
        loadProperties();
      }
      return manager;
    }
   
    public static void applyChanges (Property property) throws CommandException {
      try {
        validateInput(property);
        // ... salva-se o novo valor atribuído à propriedade
      } catch (ValidationException e) {
        throw new CommandException("o valor inserido para "
          + "esta propriedade é inválido", e);
      } catch (ConfigurationException e) {
        throw new CommandException("não foi possível salvar a propriedade " + 
          property.getName() + " no sistema", e);
      }
    }
   
    private static void validateInput(Property property) 
      throws CommandException, ValidationException {
      try {
        ValidationMapper.valueOf(property.getName()).
          getValidatorType().newInstance().validate(property.getValue());
      } catch (InstantiationException | IllegalAccessException e) {
        LOGGER.info("não foi possível identificar um "
          + "validador para a propriedade " + property.getName());
      }
    }
    ...
  }
  
Listagem 3. Código da classe ConfigManager refatorada

  public enum ValidationMapper {
         
         INPUT_HOST ("HOST", IpAddressValidator.class),
         INPUT_LOGGING_ENABLED ("ENABLE-LOG", BooleanValidator.class),
         INPUT_SERVERS_COUNT ("SERVERS", MaxNodesValidator.class);
         
         private Class<? extends Validator> validatorType;
         private String alias;
         
         ValidationMapper (String alias, Class<? extends Validator> validatorType) {
               this.alias = alias;
               this.validatorType = validatorType;
         }
   
         public Class<? extends Validator> getValidatorType() {
               return validatorType;
         }
   
         public String getAlias() {
               return alias;
         }
  }
  
Listagem 4. Código responsável pelo mapeamento entre validadores e propriedades

Além dessa abordagem substituir todos os blocos condicionais do código original, faz com que esse método não precise mais ser alterado, independentemente do número de propriedades adicionais que venham a ser suportadas em versões subsequentes. A cada nova propriedade, as únicas medidas necessárias serão:

  1. Mapear seu validador no enum ValidationMapper;
  2. Mapear o nome real da propriedade no enum PropertyAlias.

Eventualmente, novos validadores podem ser adicionados ao sistema, caso os pré-existentes não atendam às características de validação de propriedades que venham a ser suportadas pela aplicação futuramente. Mesmo nesse caso, o método de validação de entrada não precisará ser modificado, uma vez que o mapeamento de validadores e propriedades é feito externamente, fora da classe ConfigManager.