Eu tenho dito que exceções só devem ser usadas em casos excepcionais. Como sei se o meu caso é excepcional?

94

Meu caso específico aqui é que o usuário pode passar uma string para o aplicativo, o aplicativo o analisa e atribui a objetos estruturados. Às vezes, o usuário pode digitar algo inválido. Por exemplo, sua entrada pode descrever uma pessoa, mas eles podem dizer que sua idade é "maçã". O comportamento correto nesse caso é reverter a transação e informar ao usuário que ocorreu um erro e eles terão que tentar novamente. Pode haver um requisito para informar sobre cada erro que podemos encontrar na entrada, não apenas o primeiro.

Neste caso, argumentei que deveríamos lançar uma exceção. Ele discordou, dizendo: "Exceções devem ser excepcionais: espera-se que o usuário possa inserir dados inválidos, então este não é um caso excepcional" Eu realmente não sabia como argumentar esse ponto, porque, por definição da palavra, ele parece estar certo.

Mas, é meu entendimento que é por isso que as exceções foram inventadas em primeiro lugar. Costumava ser que você tinha para inspecionar o resultado para ver se ocorreu um erro. Se você deixar de verificar, coisas ruins podem acontecer sem que você perceba.

Sem exceções, cada nível da pilha precisa verificar o resultado dos métodos que eles chamam e, se um programador se esquecer de verificar em um desses níveis, o código pode prosseguir acidentalmente e salvar dados inválidos (por exemplo). Parece mais propenso a erros dessa maneira.

De qualquer forma, sinta-se à vontade para corrigir qualquer coisa que eu tenha dito aqui. A minha principal questão é se alguém diz que as excepções devem ser excepcionais, como sei se o meu caso é excepcional?

    
por Daniel Kaplan 24.01.2013 / 09:48
fonte

13 respostas

81

Exceções foram inventadas para ajudar a facilitar o tratamento de erros com menos confusão de código. Você deve usá-los nos casos em que eles facilitam o manuseio de erros com menos confusão de código. Esse negócio de "exceções apenas para circunstâncias excepcionais" deriva de uma época em que o tratamento de exceções era considerado um impacto inaceitável no desempenho. Isso não é mais o caso na grande maioria do código, mas as pessoas ainda usam a regra sem lembrar a razão por trás disso.

Especialmente em Java, que talvez seja a linguagem que mais gosta de exceção já concebida, você não deve se sentir mal ao usar exceções quando simplifica seu código. Na verdade, a própria classe Integer do Java não tem meios de verificar se uma string é um inteiro válido sem potencialmente gerar NumberFormatException .

Além disso, embora não seja possível confiar em apenas na validação da interface do usuário, lembre-se de que sua interface do usuário foi projetada adequadamente, como usar um controle giratório para inserir valores numéricos curtos e, em seguida, um valor não numérico Torná-lo no back-end verdadeiramente seria uma condição excepcional.

    
por 24.01.2013 / 18:18
fonte
71

Quando uma exceção deve ser lançada? Quando se trata de código, acho que seguir a explicação é muito útil:

Uma exceção é quando um membro não consegue completar a tarefa que deveria executar como indicado pelo seu nome . (Jeffry Richter, CLR via C #)

Por que isso é útil? Isso sugere que depende do contexto em que algo deve ser tratado como uma exceção ou não. No nível das chamadas de método, o contexto é dado por (a) o nome, (b) assinatura do método e (b) o código do cliente, que usa ou espera-se usar o método.

Para responder à sua pergunta, você deve dar uma olhada no código, onde a entrada do usuário é processada. Pode parecer algo assim:

public void Save(PersonData personData) { … }

O nome do método sugere que alguma validação seja feita? Não. Neste caso, um PersonData inválido deve lançar uma exceção.

Suponha que a classe tenha outro método semelhante a este:

public ValidationResult Validate(PersonData personData) { … }

O nome do método sugere que alguma validação seja feita? Sim. Nesse caso, um PersonData inválido não deve lançar uma exceção.

Para juntar as coisas, ambos os métodos sugerem que o código do cliente deve ser assim:

ValidationResult validationResult = personRegister.Validate(personData);
if (validationResult.IsValid())
{
    personRegister.Save(personData)
}
else
{
    // Throw an exception? To answer this look at the context!
    // That is: (a) Method name, (b) signature and
    // (c) where this method is (expected) to be used.
}

Quando não está claro se um método deve lançar uma exceção, então talvez seja devido a um nome de método ou assinatura mal escolhido. Talvez o design da aula não esteja claro. Às vezes, você precisa modificar o design do código para obter uma resposta clara à pergunta se uma exceção deve ser lançada ou não.

    
por 24.01.2013 / 14:05
fonte
30

Sempre penso em coisas como acessar o servidor de banco de dados ou uma API da web ao pensar em exceções. Você espera que a API do servidor / web funcione, mas, em um caso excepcional, talvez não (o servidor está inativo). Uma solicitação da Web pode ser rápida geralmente, mas em circunstâncias excepcionais (carga alta) pode expirar. Isso é algo fora do seu controle.

Os dados de entrada dos usuários estão sob seu controle, pois você pode verificar o que eles enviam e fazer com o que você gosta. No seu caso, eu validaria a entrada do usuário antes mesmo de tentar salvá-lo. E tenho a tendência de concordar que os usuários que fornecem dados inválidos devem ser esperados, e seu aplicativo deve contabilizá-lo validando a entrada e fornecendo uma mensagem de erro amigável.

Dito isso, eu uso exceções na maioria dos setters dos meus modelos de domínio, onde não há absolutamente nenhuma chance de dados inválidos entrarem. No entanto, esta é uma última linha de defesa, e tenho a tendência de criar meus formulários de entrada com regras de validação avançadas, para que praticamente não haja chance de acionar essa exceção de modelo de domínio. Então, quando um levantador está esperando uma coisa e outra, essa é uma situação excepcional, que não deveria ter acontecido em circunstâncias comuns.

EDIT (algo mais a considerar):

Ao enviar dados fornecidos pelo usuário para o banco de dados, você sabe de antemão o que deve e não deve entrar em suas tabelas. Isso significa que os dados podem ser validados em relação a algum formato esperado. Isso é algo que você pode controlar. O que você não pode controlar é o seu servidor falhando no meio da sua consulta. Então você sabe que a consulta está ok e os dados são filtrados / validados, você tenta a consulta e ainda falha, esta é uma situação excepcional.

Da mesma forma com as solicitações da web, você não pode saber se a solicitação expirará ou não conseguirá se conectar antes de tentar enviá-la. Portanto, isso também garante uma abordagem try / catch, já que você não pode perguntar ao servidor se ele funcionará alguns milissegundos depois quando você enviar a solicitação.

    
por 24.01.2013 / 10:03
fonte
29

Exceptions should be exceptional: It's expected that the user may input invalid data, so this isn't an exceptional case

Nesse argumento:

  • Espera-se que um arquivo não exista, de modo que não seja uma exceção caso.
  • Espera-se que a conexão com o servidor possa ser perdida, então esse não é um caso excepcional
  • Espera-se que o arquivo de configuração possa ser confundido, de modo que não seja um caso excepcional
  • Espera-se que o seu pedido possa, às vezes, cair, por isso não é um caso excepcional

Qualquer exceção que você pegar, você deve esperar porque, bem, você decidiu pegá-lo. E assim, por essa lógica, você nunca deve lançar nenhuma exceção que realmente pretenda pegar.

Portanto, acho que "exceções devem ser excepcionais" é uma péssima regra prática.

O que você deve fazer depende do idioma. Diferentes idiomas têm diferentes convenções sobre quando exceções devem ser lançadas. O Python, por exemplo, lança exceções para tudo e, quando em Python, eu faço o mesmo. C ++, por outro lado, lança relativamente poucas exceções, e lá eu sigo o exemplo. Você pode tratar C ++ ou Java como o Python e lançar exceções para tudo, mas o seu trabalho está em desacordo com a forma como a linguagem espera ser usada.

Eu prefiro a abordagem do Python, mas acho uma má ideia incluir outras linguagens nele.

    
por 24.01.2013 / 15:57
fonte
15

Referência

De O programador pragmático:

We believe that exceptions should rarely be used as part of a program's normal flow; exceptions should be reserved for unexpected events. Assume that an uncaught exception will terminate your program and ask yourself, "Will this code still run if I remove all the exception handlers?" If the answer is "no," then maybe exceptions are being used in nonexceptional circumstances.

Eles vão examinar o exemplo de abrir um arquivo para leitura, e o arquivo não existe - isso deve levantar uma exceção?

If the file should have been there, then an exception is warranted. [...] On the other hand, if you have no idea whether the file should exist or not, then it doesn't seem exceptional if you can't find it, and an error return is appropriate.

Mais tarde, eles discutem por que escolheram essa abordagem:

[A]n exception represents an immediate, nonlocal transfer of control - it's a kind of cascading goto. Programs that use exceptions as part of their normal processing suffer from all the readability and maintainability problems of classic spaghetti code. These programs break encapsulation: routines and their callers are more tightly coupled via exception handling.

Em relação à sua situação

Sua pergunta resume-se a "Erros de validação devem gerar exceções?" A resposta é que isso depende de onde a validação está acontecendo.

Se o método em questão estiver dentro de uma seção do código em que é assumido que os dados de entrada já foram validados, dados de entrada inválidos devem gerar uma exceção; se o código for projetado de forma que esse método receba a entrada exata inserida por um usuário, dados inválidos são esperados, e uma exceção não deve ser levantada.

    
por 24.01.2013 / 20:00
fonte
10

Há muita pontificação filosófica aqui, mas em geral, condições excepcionais são simplesmente aquelas condições que você não pode ou não quer lidar com (além de limpeza, relatórios de erros e afins) sem intervenção do usuário. Em outras palavras, elas são condições irrecuperáveis.

Se você entregar um programa a um caminho de arquivo, com a intenção de processar esse arquivo de alguma forma, e o arquivo especificado por esse caminho não existir, essa é uma condição excepcional. Você não pode fazer nada sobre isso em seu código, além de relatá-lo ao usuário e permitir que ele especifique um caminho de arquivo diferente.

    
por 24.01.2013 / 16:24
fonte
7

Existem duas preocupações que você deve considerar:

  1. você discute uma única preocupação - vamos chamá-la de Assigner , já que essa preocupação é atribuir entradas a objetos estruturados - e você expressa a restrição de que suas entradas sejam válidas

  2. Uma interface de usuário bem implementada tem uma preocupação adicional: validação da entrada do usuário & feedback construtivo sobre erros (vamos chamar essa parte de Validator )

Do ponto de vista do componente Assigner , lançar uma exceção é totalmente razoável, já que você expressou uma restrição que foi violada.

Do ponto de vista da experiência do usuário , o usuário não deve estar falando diretamente com este Assigner em primeiro lugar. Eles devem estar falando com ele via o Validator .

Agora, no Validator , a entrada de usuário inválida não é excepcional, é realmente o caso de você estar mais interessado. Então, aqui uma exceção não seria apropriada, e isso é também onde você gostaria de identificar erros todos em vez de desistir do primeiro.

Você notará que não mencionei como essas preocupações são implementadas. Parece que você está falando sobre o Assigner e seu colega está falando sobre um Validator+Assigner combinado. Uma vez que você percebe que existem duas preocupações separadas (ou separáveis), pelo menos você pode discutir isso de maneira sensata.

Para abordar o comentário de Renan, estou apenas assumindo que, depois de identificar suas duas preocupações separadas, é óbvio quais casos devem ser considerados excepcionais em cada contexto.

Na verdade, se não for óbvio se algo deve ser considerado excepcional, eu diria que você provavelmente não terminou de identificar as preocupações independentes em sua solução.

Eu acho que isso faz a resposta direta para

... how do I know if my case is exceptional?

continue simplificando até que seja óbvio . Quando você tem uma pilha de conceitos simples que você entende bem, você pode raciocinar claramente sobre compor-los de volta ao código, classes, bibliotecas ou qualquer outra coisa.

    
por 24.01.2013 / 12:24
fonte
4

Outros responderam bem, mas aqui está minha resposta curta. Exceção é a situação em que algo no ambiente está errado, o que você não pode controlar e seu código não pode avançar. Neste caso, você também terá que informar ao usuário o que deu errado, porque você não pode ir além e qual é a resolução.

    
por 24.01.2013 / 15:14
fonte
2

As exceções devem representar condições que provavelmente o código de chamada imediato não estará preparado para manipular, mesmo que o método de chamada possa. Considere, por exemplo, o código que está lendo alguns dados de um arquivo, pode legitimamente assumir que qualquer arquivo válido terminará com um registro válido e não é necessário extrair nenhuma informação de um registro parcial.

Se a rotina de dados de leitura não usasse exceções, mas simplesmente informasse se a leitura foi bem-sucedida ou não, o código de chamada teria que se parecer com:

temp = dataSource.readInteger();
if (temp == null) return null;
field1 = (int)temp;
temp = dataSource.readInteger();
if (temp == null) return null;
field2 = (int)temp;
temp = dataSource.readString();
if (temp == null) return null;
field3 = temp;

gastando três linhas de código para cada peça útil de trabalho. Por outro lado, se readInteger lançar uma exceção ao encontrar o final de um arquivo, e se o chamador puder simplesmente passar a exceção, o código se tornará:

field1 = dataSource.readInteger();
field2 = dataSource.readInteger();
field3 = dataSource.readString();

Muito mais simples e com aparência mais clara, com ênfase muito maior no caso em que as coisas funcionam normalmente. Observe que, nos casos em que o chamador imediato estaria esperando manipular uma condição, um método que retorna um código de erro geralmente será mais útil do que aquele que lança uma exceção. Por exemplo, para totalizar todos os inteiros em um arquivo:

do
{
  temp = dataSource.tryReadInteger();
  if (temp == null) break;
  total += (int)temp;
} while(true);

versus

try
{
  do
  {
    total += (int)dataSource.readInteger();
  }
  while(true);
}
catch endOfDataSourceException ex
{ // Don't do anything, since this is an expected condition (eventually)
}

O código que pede os números inteiros está esperando que uma dessas chamadas irá falhar. Ter o código usando um loop sem fim que será executado até que isso aconteça é muito menos elegante do que usar um método que indique falhas por meio de seu valor de retorno.

Como as turmas geralmente não sabem quais condições seus clientes esperarão ou não, é sempre útil oferecer duas versões de métodos que podem falhar de maneiras que alguns chamadores esperariam e outras pessoas que não ligam. Isso permitirá que esses métodos sejam usados de maneira limpa com os dois tipos de chamadores. Note também que mesmo os métodos "try" devem lançar exceções se surgirem situações que o chamador provavelmente não esteja esperando. Por exemplo, tryReadInteger não deve lançar uma exceção se encontrar uma condição de fim de arquivo limpa (se o responsável pela chamada não estivesse esperando isso, o chamador teria usado readInteger ). Por outro lado, provavelmente deveria lançar uma exceção se os dados não pudessem ser lidos, por exemplo, o cartão de memória que o continha estava desconectado. Embora esses eventos devam ser sempre reconhecidos como uma possibilidade, é improvável que o código de chamada imediata esteja preparado para fazer qualquer coisa útil em resposta; certamente não deve ser relatado da mesma maneira que seria uma condição de fim de arquivo.

    
por 24.01.2013 / 19:30
fonte
2

Eu nunca fui um grande fã do conselho de que você só deveria lançar exceções em casos excepcionais, em parte porque não diz nada (é como dizer que você deve comer apenas alimentos que são comestíveis), mas também porque é muito subjetivo, e muitas vezes não está claro o que constitui um caso excepcional e o que não é.

No entanto, há boas razões para este conselho: lançar e capturar exceções é lento, e se você estiver executando o código no depurador no Visual Studio com ele configurado para notificá-lo sempre que uma exceção é lançada, você pode acabar sendo enviado por dezenas, se não centenas de mensagens, muito antes de você chegar ao problema.

Como regra geral, se:

  • seu código está livre de erros e
  • os serviços dos quais depende estão todos disponíveis e
  • seu usuário está usando seu programa da maneira que deveria ser usado (mesmo que algumas das entradas sejam inválidas)

então seu código nunca deve lançar uma exceção, mesmo uma que seja capturada depois. Para capturar dados inválidos, você pode usar validadores no nível da IU ou código, como Int32.TryParse() na camada de apresentação.

Para qualquer outra coisa, você deve seguir o princípio de que uma exceção significa que seu método não pode fazer o que seu nome diz. Em geral, não é uma boa idéia usar códigos de retorno para indica falha (a menos que o nome do seu método indique claramente que ele faz isso, por exemplo, TryParse() ) por dois motivos. Primeiro, a resposta padrão para um código de erro é ignorar a condição de erro e continuar independentemente; segundo, você pode facilmente acabar com alguns métodos usando códigos de retorno e outros métodos usando exceções, e esquecendo qual é qual. Eu já vi codebases onde duas implementações intercambiáveis diferentes da mesma interface usam diferentes abordagens aqui.

    
por 25.01.2013 / 14:29
fonte
2

A coisa mais importante na escrita de software é torná-lo legível. Todas as outras considerações são secundárias, inclusive tornando-as eficientes e corretas. Se for legível, o resto pode ser mantido em manutenção e, se não for legível, é melhor jogá-lo fora. Portanto, você deve lançar exceções quando melhorar a legibilidade.

Quando você está escrevendo algum algoritmo, pense na pessoa que o lerá no futuro. Quando você chegar a um lugar onde poderia haver um problema em potencial, pergunte a si mesmo se o leitor quer ver como você lida com esse problema agora , ou o leitor prefere continuar com o algoritmo?

Eu gosto de pensar em uma receita para bolo de chocolate. Quando lhe diz para adicionar os ovos, tem uma escolha: pode supor que você tem ovos e continuar com a receita, ou pode começar uma explicação de como você pode obter ovos se não tiver ovos. Poderia preencher um livro inteiro com técnicas para caçar galinhas selvagens, tudo para ajudá-lo a fazer um bolo. Isso é bom, mas a maioria das pessoas não vai querer ler essa receita. A maioria das pessoas prefere apenas assumir que os ovos estão disponíveis e continuar com a receita. Essa é uma avaliação que os autores precisam fazer ao escrever receitas.

Não pode haver nenhuma regra garantida sobre o que faz uma boa exceção e quais problemas devem ser tratados imediatamente, porque requer que você leia a mente do seu leitor. O melhor que você já fez foi usar as regras, e "as exceções são apenas para circunstâncias excepcionais" é muito bom. Normalmente, quando um leitor lê seu método, ele procura o que o método fará em 99% do tempo, e prefere não ter que ficar atulhado de casos esquisitos como lidar com usuários que entram em entradas ilegais e outras coisas que quase nunca acontecem. Eles querem ver o fluxo normal do seu software diretamente, uma instrução após a outra como se os problemas nunca acontecessem. Compreender o seu programa vai ser difícil o suficiente sem ter que lidar constantemente com as tangentes para lidar com cada pequeno problema que possa surgir.

    
por 19.11.2013 / 08:35
fonte
2

There may be a requirement to report on every error we can find in the input, not just the first.

É por isso que você não pode jogar uma exceção aqui. Uma exceção interrompe imediatamente o processo de validação. Então, haveria muito trabalho para concluir isso.

Um mau exemplo:

Método de validação para a classe Dog usando exceções:

void validate(Set<DogValidationException> previousExceptions) {
    if (!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        DogValidationException disallowedName = new DogValidationException(Problem.DISALLOWED_DOG_NAME);
        if (!previousExceptions.contains(disallowedName)){
            throw disallowedName;
        }
    }
    if (this.legs < 4) {
        DogValidationException invalidDog = new DogValidationException(Problem.LITERALLY_INVALID_DOG);
        if (!previousExceptions.contains(invalidDog)){
            throw invalidDog;
        }
    }
    // etc.
}

Como chamar:

Set<DogValidationException> exceptions = new HashSet<DogValidationException>();
boolean retry;
do {
    retry = false;
    try {
        dog.validate(exceptions);
    } catch (DogValidationException e) {
        exceptions.add(e);
        retry = true;
    }
} while (retry);

if(exceptions.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

O problema aqui é que o processo de validação, para obter todos os erros, exigiria ignorar as exceções já encontradas. O acima pode funcionar, mas este é um claro uso indevido de exceções . O tipo de validação que você pediu deve ocorrer antes de o banco de dados ser tocado. Portanto, não há necessidade de reverter nada. E os resultados da validação provavelmente serão erros de validação (esperamos que zero, no entanto).

A melhor abordagem é:

Chamada de método:

Set<Problem> validationResults = dog.validate();
if(validationResults.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

Método de validação:

Set<Problem> validate() {
    Set<Problem> result = new HashSet<Problem>();
    if(!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        result.add(Problem.DISALLOWED_DOG_NAME);
    }
    if(this.legs < 4) {
        result.add(Problem.LITERALLY_INVALID_DOG);
    }
    // etc.
    return result;
}

Por quê? Existem muitas razões, e a maioria das razões tem sido apontada nas outras respostas. Para simplificar: É muito mais simples ler e entender por outros. Em segundo lugar, você quer mostrar os rastreamentos de pilha do usuário para explicar que ele configurou seu dog errado?

Se , durante o commit no segundo exemplo, ainda ocorrerá um erro , mesmo que seu validador tenha validado o dog com zero problemas, em seguida, jogando uma exceção é a coisa certa . Como: Nenhuma conexão de banco de dados, a entrada do banco de dados foi modificada por outra pessoa enquanto isso, ou algo assim.

    
por 15.02.2016 / 10:31
fonte
-3

Essa resposta é melhor encontrada nos guias de sistemas operacionais e nos guias de programação da CPU. De fato, essa resposta é muito simples e existe uma diferença física entre as declarações if e as exceções. Eu não acho nenhuma filosofia por trás da resposta "real". A legibilidade pode vir depois da razão física, mas essa parte não é discutível, e é isso:

Exceções são quando uma função passa um erro para uma função de chamada. Basicamente, se o seu programa não pode lidar com algo, mas o sistema operacional pode, ele irá "jogá-lo" para o sistema operacional (a função de chamada, também, e os pais). Se as instruções não passarem erros para chamar funções.

As exceções sinalizarão fisicamente o Registro de Exceção / Registro de Depuração no hardware. A máquina inteira saberá que tem que parar o que está fazendo para consertar seu processo.

E é isso. Sem filosofia, sem argumentos, sem estilos. Agora "Excepcional" tem uma definição real de "processo de chamada deve lidar com erro". Agora que estamos cientes das diferenças físicas entre If e Try / Catch, a filosofia pode vir a seguir. Mas esta é a diferença real, real, na medida em que minha pesquisa encontrou da arquitetura para os conceitos do sistema operacional

    
por 20.12.2017 / 22:17
fonte