Se você estiver validando alguns dados, geralmente não deve usar exceções a falhas de validação de sinal. Aqui eu descrevo como eu descreveria refatorar esse código para usar o padrão de notificação.

Eu estava recentemente olhando para algum código para fazer alguns básicos validação de algumas mensagens JSON de entrada. Parecia mais ou menos assim.

public void check() {
   if (date == null) throw new IllegalArgumentException("date is missing");
   LocalDate parsedDate;
   try {
     parsedDate = LocalDate.parse(date);
   }
   catch (DateTimeParseException e) {
     throw new IllegalArgumentException("Invalid format for date", e);
   }
   if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
   if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
   if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
 }

Essa é uma maneira comum de abordar a validação. Você executa uma série de verificações em alguns dados (aqui apenas alguns campos dentro da classe em pergunta). Se qualquer uma dessas verificações falhar, você lança uma exceção com uma mensagem de erro.

Tenho alguns problemas com esta abordagem. Em primeiro lugar, eu não sou feliz com o uso de exceções para algo assim. Exceções sinalizar algo fora dos limites esperados de comportamento do código em questão. Mas se você estiver executando algumas verificações em entradas externas, isso ocorre porque você espera que algumas mensagens falhem - e se um falha é o comportamento esperado, então você não deve ser usando exceções.

se uma falha é um comportamento esperado, então você não deve estar usando exceções

O segundo problema com um código como este é que ele falha com o primeiro erro que detecta, mas geralmente é melhor relatar todos erros com os dados recebidos, não apenas o primeiro. Dessa forma, um o cliente pode optar por exibir todos os erros para o usuário corrigir em um interação única, em vez de dar a ela a impressão de que ela é jogando um jogo de whack-a-mole com o computador.

Minha maneira preferida de lidar com problemas de validação de relatórios, como esta é a Notificação padrão. Uma notificação é um objeto que coleta erros, cada falha de validação adiciona um erro à notificação. Um método de validação retorna uma notificação, que você pode então interrogar para obter mais informações. Um uso simples parece tem código como este para as verificações.

private void validateNumberOfSeats(Notification note) {
  if (numberOfSeats < 1) note.addError("number of seats must be positive");
  // more checks like this
}

Podemos então ter uma chamada simples, como reagir se houver algum Erros. Outros métodos na notificação podem detalhar mais detalhes sobre os erros. [1]aNotification.hasErrors()

image

Quando usar essa refatoração

Eu preciso enfatizar aqui, que eu não estou defendendo se livrar de exceções em toda a sua base de código. Exceções são um muito técnica útil para lidar com um comportamento excepcional e obter afasta-se do fluxo principal da lógica. Esta refatoração é uma boa um para usar somente quando o resultado sinalizado pela exceção não é realmente excepcional e, portanto, deve ser tratado através do lógica principal do programa. O exemplo que estou vendo aqui, validação, é um caso comum disso.

Uma regra prática útil ao considerar exceções vem dos Programadores Pragmáticos:

Acreditamos que as exceções raramente devem ser usadas como parte do o fluxo normal de um programa: as exceções devem ser reservadas para eventos inesperados. Suponha que uma exceção não detectada encerre seu programa e pergunte a si mesmo: “Este código ainda será executar se eu remover todos os manipuladores de exceções?” Se a resposta for “não”, então talvez exceções estejam sendo usadas em não-excepcionais Circunstâncias.

– Dave Thomas e Andy Hunt

Uma consequência importante disso é que se deve usar exceções para uma tarefa específica depende do contexto. Então, como os prags continuam a dizer, lendo a partir de um arquivo que não é pode ou não haver uma exceção, dependendo do Circunstâncias. Se você está tentando ler um arquivo bem conhecido localização, como em um sistema Unix, em seguida, é provável que você possa assumir que o arquivo deve estar lá, então lançar uma exceção é razoável. Por outro lado, se você estão tentando ler um arquivo de um caminho que o usuário digitou na linha de comando, então você deve esperar que seja provável que o arquivo não está lá, e deve usar outro mecanismo - um que comunica a natureza não excepcional do erro./etc/hosts

Há um caso em que pode ser sensato usar exceções para falhas de validação. Seriam situações em que você tem dados que você espera que já tenham sido validados anteriormente em processando, mas você deseja executar as verificações de validação novamente para proteger contra um erro de programação que permite que alguns dados inválidos escorreguem através.

Este artigo é sobre a substituição de exceções para notificação no contexto da validação da entrada bruta. Você também pode encontrar isso técnica útil em outras situações em que uma notificação é um melhor escolha do que lançar uma exceção, mas estou me concentrando em o caso de validação aqui, pois é comum

Ponto de partida

Eu não mencionei o domínio de exemplo até agora, já que eu estava apenas interessado na forma ampla do código. Mas à medida que exploramos o Além disso, precisarei me envolver com o domínio. Neste caso seja algum código que recebe mensagens JSON reservando assentos em um teatro. O código está em uma classe de solicitação de reserva preenchida do JSON usando a biblioteca gson.

gson.fromJson(jsonString, BookingRequest.class)

Gson faz uma aula, procura por todos os campos que correspondem a uma chave em o documento JSON e, em seguida, preenche os campos correspondentes.

O pedido de reserva contém apenas dois elementos que nós estão validando aqui, a data do desempenho e quantos assentos estão sendo solicitados

classe BookingRequest...

  private Integer numberOfSeats; 
  private String date;
  

As verificações de validação são as que mostrei acima

classe BookingRequest...

  public void check() {
     if (date == null) throw new IllegalArgumentException("date is missing");
     LocalDate parsedDate;
     try {
       parsedDate = LocalDate.parse(date);
     }
     catch (DateTimeParseException e) {
       throw new IllegalArgumentException("Invalid format for date", e);
     }
     if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
     if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
     if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
   }
   

Criando uma notificação

Para usar uma notificação, você precisa criar o objeto de notificação. Uma notificação pode ser muito simples, às vezes, apenas uma lista de strings fará o truque.

Uma Notificação reúne erros

List<String> notification = new ArrayList<>();
if (numberOfSeats < 5) notification.add("number of seats too small");
// do some more checks

// then later…
if ( ! notification.isEmpty()) // handle the error condition

Embora uma simples linguagem de lista faça uma implementação leve de o padrão, eu geralmente gosto de fazer um pouco mais do que isso, criando em vez disso, uma classe simples.

public class Notification {
  private List<String> errors = new ArrayList<>();

  public void addError(String message) { errors.add(message); }
  public boolean hasErrors() {
    return ! errors.isEmpty();
  }
  
  

Ao usar uma classe real, posso tornar minha intenção mais clara - o leitor não tem que executar o mapa mental entre o idioma e seu pleno significado.

Dividindo o método de verificação

Meu primeiro passo é dividir o método de verificação em duas partes, um parte interna que acabará por lidar apenas com notificações e não lançar quaisquer exceções, e uma parte externa que irá preservar o comportamento atual do método de verificação, que é lançar um exceção é que há quaisquer falhas de validação.

Meu primeiro passo para fazer isso é usar o Método de Extração de uma maneira incomum, pois estou extraindo o todo o corpo do método de verificação em um método de validação.

classe BookingRequest...

  public void check() {
    validation();
  }

  public void validation() {
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
    if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
  }
  

Em seguida, ajusto o método de validação para criar e retornar um notificação.

classe BookingRequest...

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
    if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
    return note;
  }

Agora posso testar a notificação e lançar uma exceção se ela contém erros.

classe BookingRequest...

  public void check() {
    if (validation().hasErrors()) 
      throw new IllegalArgumentException(validation().errorMessage());
  }

Tornei o método de validação público, porque estou esperando que a maioria dos chamadores no futuro preferirá usar este método, em vez do método de verificação.

Dividir o método original me permite separar o verificação de validação da decisão sobre como responder à falha.

Neste ponto, eu não mudei o comportamento do código em tudo, a notificação não conterá quaisquer erros e quaisquer as verificações de validação que falharem continuarão a lançar uma exceção e ignorar a nova maquinaria que coloquei. Mas agora eu defini coisas prontas para começar a substituir lançamentos de exceção por manipulando a notificação.

Antes de passar a isso, no entanto, preciso dizer algo sobre mensagens de erro. Quando estamos fazendo uma refatoração, a regra é evitar mudanças no comportamento observável. Em situações como esta, tal regra leva imediatamente à questão de que o comportamento é observável. Obviamente o arremesso do correto exceção é algo que o programa externo observará - mas para até que ponto eles se importam com a mensagem de erro? O a notificação acabará por coletar vários erros e poderá resumi-los juntos em uma única mensagem com algo gostar

Notificação de classe...

  public String errorMessage() {
    return errors.stream()
      .collect(Collectors.joining(", "));
  }

Mas isso seria um problema se os níveis mais elevados do programa estava confiando em apenas receber a mensagem do primeiro erro detectado, caso em que você precisaria de algo como

Notificação de classe...

  public String errorMessage() { return errors.get(0); }

Você tem que olhar não apenas para a função de chamada, mas também quaisquer manipuladores de exceção para descobrir qual é a resposta certa nessa situação.

Embora não haja nenhuma maneira que eu deveria ter introduzido quaisquer problemas neste ponto, eu certamente compilaria e testaria antes de fazer as próximas mudanças. Só porque não há chance de qualquer sensato pessoa poderia ter estragado essas mudanças não significa que eu não posso bagunçar.

Validando o número

A coisa óbvia a fazer agora é substituir o primeiro validação

classe BookingRequest...

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) note.addError("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
    if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
    return note;
  }

Um movimento óbvio, mas ruim, pois isso quebrará o código. Se passarmos uma data nula para a função, ela adicionará um erro para a notificação, mas depois alegremente tentar analisá-lo e lançar uma exceção de ponteiro nulo - que não é a exceção que nós estavam procurando.

Portanto, a coisa não óbvia, mas mais eficaz a fazer nisso caso é andar para trás.

classe BookingRequest...

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
    if (numberOfSeats < 1) note.addError("number of seats must be positive");
    return note;
  }

A verificação anterior é uma verificação nula, por isso precisamos usar um condicional para evitar a criação de uma exceção de ponteiro nulo.

classe BookingRequest...

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) note.addError("number of seats cannot be null");
    else if (numberOfSeats < 1) note.addError("number of seats must be positive");
    return note;
  }

Vejo que a próxima verificação envolve um campo diferente. Junto com ter que introduzir uma condicional com o anterior refatoração, agora estou pensando que este método de validação está ficando muito complexo e poderia ter a ver com ser decomposto. Então eu extraio o partes de validação de números.

classe BookingRequest...

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    validateNumberOfSeats(note);
    return note;
  }

  private void validateNumberOfSeats(Notification note) {
    if (numberOfSeats == null) note.addError("number of seats cannot be null");
    else if (numberOfSeats < 1) note.addError("number of seats must be positive");
  }

Olhando para a validação extraída para o número, eu não gosto muito da sua estrutura. Eu não gosto de usar if-then-else blocos na validação, uma vez que pode facilmente levar a excessivamente aninhados código. Eu prefiro código linear que aborta uma vez que não pode ir em qualquer além disso, o que podemos fazer com uma cláusula de guarda. Então eu aplico Substituir Condicional Aninhado por Cláusulas de Guarda.

classe BookingRequest...

  private void validateNumberOfSeats(Notification note) {
    if (numberOfSeats == null) {
      note.addError("number of seats cannot be null");
      return;
    }
    if (numberOfSeats < 1) note.addError("number of seats must be positive");
  }

quando refatoramos nós devemos sempre tentar dar os menores passos que pudermos que preservar o comportamento

Minha decisão de retroceder para manter o código verde é um exemplo de um elemento crucial da refatoração. Refatoração é uma técnica específica para reestruturar o código através de uma série de transformações que preservam o comportamento. Então, quando refatoramos, nós deve sempre tentar dar os menores passos que pudermos que preservar o comportamento. Ao fazer isso, reduzimos as chances de um erro que nos prenderá no depurador

Validando a data

Com a validação de data, acho que vou começar com o Método de Extração:

classe BookingRequest...

  public Notification validation() {
    Notification note = new Notification();
    validateDate(note);
    validateNumberOfSeats(note);
    return note;
  }

  private void validateDate(Notification note) {
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
  }

Quando usei o método de extração automatizada no meu IDE, o código resultante não incluía a notificação argumento. Então eu tive que adicionar isso manualmente.

Agora é hora de começar a rolar para trás através da data validação

classe BookingRequest...

  private void validateDate(Notification note) {
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");
  }

Com o segundo passo, há um complicação no tratamento do erro, uma vez que a exceção lançada inclui uma exceção de causa. Para lidar com isso, preciso alterar o notificação para aceitar uma exceção de causa. Já que estou no meio de alterar a jogada para adicionar um erro ao notificação, meu código está vermelho, então eu faço backup do que estou fazendo para deixe o método validateDate no estado acima, enquanto eu preparar a notificação para incluir uma exceção de causa.

Começo a modificar a notificação adicionando um novo addError método que assume a causa e ajustando o método original para chamar o novo. [2]

Notificação de classe...

  public void addError(String message) {
    addError(message, null);
  }

  public void addError(String message, Exception e) {
    errors.add(message);
  }

Isso significa que aceitamos a exceção da causa, mas a ignoramos. Para colocá-lo em algum lugar que eu preciso alterar o registro de erro de um simples para um objeto apenas ligeiramente menos simples.

Notificação de classe...

  private static class Error {
    String message;
    Exception cause;

    private Error(String message, Exception cause) {
      this.message = message;
      this.cause = cause;
    }
  }

Eu geralmente não gosto de campos não-privados em java, mas desde que isso é uma classe interna privada, estou bem com isso. Se eu fosse expor esta classe de erro fora da notificação, eu encapsularia esses campos.

Agora que tenho a classe, preciso modificar a notificação para use-o em vez da cadeia de caracteres.

Notificação de classe...

  private List<Error> errors = new ArrayList<>();

  public void addError(String message, Exception e) {
    errors.add(new Error(message, e));
  }
  public String errorMessage() {
    return errors.stream()
            .map(e -> e.message)
            .collect(Collectors.joining(", "));
  }

Com a nova notificação em vigor, agora posso fazer a alteração ao pedido de reserva

classe BookingRequest...

  private void validateDate(Notification note) {
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      note.addError("Invalid format for date", e);
      return;
    }
    if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");

Como já estou em um método extraído, é fácil abortar o restante da validação com um retorno.

E a última mudança é simples

classe BookingRequest...

  private void validateDate(Notification note) {
    if (date == null) {
      note.addError("date is missing");
      return;
    }
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      note.addError("Invalid format for date", e);
      return;
    }
    if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");
  }

Movendo-se para cima na pilha

Uma vez que temos o novo método, a próxima tarefa é olhar para o chamadores do método de verificação original e considere ajustá-los para fazer uso do novo método de validação. Isso implicará um olhar mais amplo sobre como a validação se encaixa no fluxo do portanto, está fora do escopo dessa refatoração. Mas o objectivo a médio prazo deve ser o de eliminar a utilização de exceções em qualquer circunstância em que possamos esperar falhas de validação.

Em muitos casos, isso deve levar a ser capaz de se livrar de o método de verificação inteiramente. Nesse caso, quaisquer testes sobre isso método deve ser retrabalhado para usar o método validar. Podemos também deseja ajustar os testes para sondar para a coleta adequada de vários erros usando a notificação.

Rodapé

  1. Outra abordagem de validação comum é apenas retornar um booleano indicando se a entrada é válida ou não. Enquanto isso faz com que ele fácil para o chamador invocar um comportamento diferente, ele não dar qualquer maneira de fornecer diagnósticos além do inútil “um erro ocorreu”.

  2. Isso às vezes é chamado de construtor encadeado. É possível também pense em se como um exemplo de aplicação parcial - não que programadores funcionais aceitariam o uso de tal terminologia nas favelas de um programa Java.

Leitura adicional

Muito tem sido escrito sobre quando usar exceções. Como você pode adivinhar, minha primeira sugestão para mais leitura sobre isso é o The Pragmatic Programmer. Há também uma discussão sólida no Code Complete. Ambos os livros devem ser familiares a qualquer um programador profissional.

Eu também gostei da discussão de como lidar com condições de erro em Avdi Rubi excepcional de Grimm. Embora diretamente um livro Ruby, a maioria de seus conselhos se aplica a qualquer ambiente de programação.

Frameworks

Várias estruturas fornecem algum tipo de validação que usa o padrão de notificação. No Java mundo, existe o Java Bean Validation work e Spring’s validation. Estes frameworks fornecem alguma forma de interface para iniciar a validação e usar uma notificação para coletar os erros (a para validação de bean e a no caso de Spring).SetErrors

Você deve dar uma olhada em seu idioma e plataforma e ver o que eles têm para um mecanismo de validação que usa um notificação. Os detalhes de como isso funciona alterarão o detalhes da refatoração, mas a forma geral deve ser bem parecido.


Artigo Original