O que é um 'bom número' de exceções a serem implementadas na minha biblioteca?

20

Eu sempre me perguntei quantas classes de exceção diferentes eu deveria implementar e lançar para várias partes do meu software. Meu desenvolvimento específico geralmente é relacionado a C ++ / C # / Java, mas acredito que essa seja uma pergunta para todas as linguagens.

Quero entender o que é um bom número de exceções diferentes e o que a comunidade de desenvolvedores espera de uma boa biblioteca.

As compensações que vejo incluem:

  • Mais classes de exceção podem permitir níveis muito detalhados de tratamento de erros para usuários da API (propensos a configuração do usuário ou erros de dados ou arquivos não encontrados)
  • Mais classes de exceção permitem que informações específicas sobre erros sejam incorporadas à exceção, em vez de apenas uma mensagem de string ou código de erro
  • Mais classes de exceção podem significar mais manutenção de código
  • Mais classes de exceção podem significar que a API é menos acessível aos usuários

Os cenários em que desejo entender o uso de exceções incluem:

  • Durante o estágio 'configuração', que pode incluir o carregamento de arquivos ou a definição de parâmetros
  • Durante uma fase do tipo 'operação', em que a biblioteca pode estar executando tarefas e realizando algum trabalho, talvez em outro encadeamento

Outros padrões de relatório de erros sem usar exceções ou menos exceções (como comparação) podem incluir:

  • Menos exceções, mas incorporando um código de erro que pode ser usado como uma pesquisa
  • Retornando códigos de erro e sinalizadores diretamente de funções (às vezes não é possível a partir de threads)
  • Implementou um sistema de evento ou retorno de chamada em caso de erro (evita o desenrolamento da pilha)

Como desenvolvedores, o que você prefere ver?

Se houver MUITAS exceções, você se incomoda em lidar com elas separadamente?

Você prefere os tipos de tratamento de erros, dependendo do estágio da operação?

Fuzz
fonte
5
"a API é menos acessível aos usuários" - pode ser tratada com várias classes de exceção que são herdadas de uma classe base comum da API. Assim que você está começando, pode ver de qual API a exceção veio sem ter que se preocupar com todos os detalhes. Quando você conclui seu primeiro programa usando a API, se precisar de mais informações sobre o erro, começa a analisar o que realmente significam as diferentes exceções. É claro que você simultaneamente precisa jogar bem com a hierarquia de exceções padrão do idioma que está usando.
21711 Steve Jobs (
ponto pertinente Steve
Fuzz

Respostas:

18

Eu mantenho isso simples.

Uma biblioteca possui um tipo de exceção base estendido de std ::: runtime_error (que é do C ++ aplicável conforme apropriado a outros idiomas). Essa exceção recebe uma sequência de mensagens para que possamos registrar; todo ponto de lançamento tem uma mensagem única (geralmente com um ID exclusivo).

É sobre isso.

Nota 1 : Nas situações em que alguém que está capturando a exceção pode corrigi-las e reiniciar a ação. Adicionarei exceções derivadas para coisas que podem ser potencialmente corrigidas exclusivamente em um local remoto. Mas isso é muito, muito raro (lembre-se que é improvável que o apanhador esteja próximo do ponto de lançamento, assim, corrigir o problema será difícil (mas tudo depende da situação)).

Nota 2 : Às vezes, a biblioteca é tão simples que não vale a pena dar sua própria exceção e o std :: runtime_error fará. É importante apenas ter uma exceção se a capacidade de diferenciá-lo de std :: runtime_error puder fornecer ao usuário informações suficientes para fazer algo com ele.

Nota 3 : Dentro de uma classe, geralmente prefiro códigos de erro (mas eles nunca escapam pela API pública da minha classe).

Observando suas vantagens e desvantagens:

As compensações que vejo incluem:

Mais classes de exceção podem permitir níveis muito detalhados de tratamento de erros para usuários da API (propensos a configuração do usuário ou erros de dados ou arquivos não encontrados)

Mais exceções realmente oferecem controle de grão mais fino? A questão é: o código de captura pode realmente corrigir o erro com base na exceção? Estou certo de que existem situações como essa e, nesses casos, você deve ter outra exceção. Mas todas as exceções listadas acima, a única correção útil, são gerar um grande aviso e interromper o aplicativo.

Mais classes de exceção permitem que informações específicas sobre erros sejam incorporadas à exceção, em vez de apenas uma mensagem de string ou código de erro

Esse é um ótimo motivo para usar exceções. Mas as informações devem ser úteis para a pessoa que as está armazenando em cache. Eles podem usar as informações para executar alguma ação corretiva? Se o objeto for interno à sua biblioteca e não puder ser usado para influenciar nenhuma API, as informações serão inúteis. Você precisa ser muito específico, pois as informações lançadas têm um valor útil para a pessoa que pode obtê-las. A pessoa que a captura geralmente está fora da API pública, portanto adapte suas informações para que possam ser usadas com itens da API pública.

Se tudo o que eles podem fazer é registrar a exceção, é melhor lançar apenas uma mensagem de erro em vez de muitos dados. Como o coletor geralmente cria uma mensagem de erro com os dados. Se você criar a mensagem de erro, ela será consistente em todos os coletores; se você permitir que o coletor crie a mensagem de erro, poderá obter o mesmo erro relatado de maneira diferente, dependendo de quem está ligando e capturando.

Menos exceções, mas incorporando um código de erro que pode ser usado como uma pesquisa

Você deve determinar o clima, o código de erro pode ser usado de maneira significativa. Se puder, você deve ter sua própria exceção. Caso contrário, seus usuários agora precisam implementar instruções de troca dentro de catch (o que anula todo o ponto de ter catch manipular automaticamente as coisas).

Se não puder, por que não usar uma mensagem de erro na exceção (não é necessário dividir o código e a mensagem torna difícil procurar).

Retornando códigos de erro e sinalizadores diretamente de funções (às vezes não é possível a partir de threads)

O retorno de códigos de erro é ótimo internamente. Ele permite que você corrija erros de vez em quando e tenha certeza de corrigir todos os códigos de erro e prestar contas deles. Mas vazá-los pela API pública é uma má ideia. O problema é que os programadores geralmente esquecem de verificar os estados de erro (pelo menos, com uma exceção, um erro não verificado forçará o aplicativo a sair de um erro não tratado geralmente corromperá todos os seus dados).

Implementou um sistema de evento ou retorno de chamada em caso de erro (evita o desenrolamento da pilha)

Esse método é frequentemente usado em conjunto com outro mecanismo de tratamento de erros (não como alternativa). Pense no seu programa do Windows. Um usuário inicia uma ação selecionando um item de menu. Isso gera uma ação na fila de eventos. A fila de eventos eventualmente atribui um encadeamento para manipular a ação. O encadeamento deve lidar com a ação e, eventualmente, retornar ao conjunto de encadeamentos e aguardar outra tarefa. Aqui, uma exceção deve ser capturada na base pelo thread encarregado do trabalho. O resultado da captura da exceção geralmente resulta na geração de um evento para o loop principal, o que acabará resultando na exibição de uma mensagem de erro para o usuário.

Mas, a menos que você possa continuar diante da exceção, a pilha se descontrairá (pelo menos para o encadeamento).

Martin York
fonte
+1; Tho "Caso contrário, seus usuários agora precisam implementar instruções de chave dentro do catch (o que anula todo o ponto de o catch lidar com as coisas automaticamente)." - Desenrolar a pilha de chamadas (se você ainda pode usá-la) e o tratamento forçado de erros ainda são ótimos benefícios. Mas isso certamente prejudicará a capacidade de desenrolar a pilha de chamadas quando você precisar capturá-la no meio e reproduzir novamente.
Merlyn Morgan-Graham
9

Normalmente começo com:

  1. Uma classe de exceção para erros de argumento . Por exemplo, "argumento não permitido para ser nulo", "argumento deve ser positivo" e assim por diante. Java e C # têm classes predefinidas para elas; em C ++, geralmente crio apenas uma classe, derivada de std :: exception.
  2. Uma classe de exceção para erros de pré-condição . Esses são para testes mais complexos, como "o índice deve ser menor que o tamanho".
  3. Uma classe de exceção para erros de asserção . Esses são para verificar o estado em busca de métodos constantes de meio caminho. Por exemplo, ao iterar sobre uma lista para contar elementos negativos, zero ou positivo, no final, esses três devem aumentar o tamanho.
  4. Uma classe de exceção básica para a própria biblioteca. No começo, basta jogar esta aula. Somente quando surgir a necessidade, comece a adicionar subclasses.
  5. Prefiro não quebrar exceções, mas sei que as opiniões diferem nesse ponto (e está muito correlacionado ao idioma usado). Se você quebrar, precisará de classes de exceção de quebra adicionais .

Como as classes dos três primeiros casos são auxiliares de depuração, elas não devem ser tratadas pelo código. Em vez disso, eles devem ser capturados apenas por um manipulador de nível superior que exiba as informações para que o usuário possa copiá-las e colá-las no desenvolvedor (ou melhor ainda: pressione o botão "enviar relatório"). Portanto, inclua informações úteis para o desenvolvedor: arquivo, função, número da linha e alguma mensagem que identifique claramente qual verificação falhou.

Como os três primeiros casos são os mesmos para cada projeto, em C ++ eu normalmente apenas os copio do projeto anterior. Como muitos estão fazendo exatamente o mesmo, os designers de C # e Java adicionaram classes padrão para esses casos à biblioteca padrão. [ATUALIZAÇÃO:] Para programadores preguiçosos: uma classe pode ser suficiente e, com alguma sorte, sua biblioteca padrão já possui uma classe de exceção adequada. Prefiro adicionar informações como nome do arquivo e número da roupa, que as classes padrão no C ++ não fornecem. [Fim da atualização]

Dependendo da biblioteca, o quarto caso pode ter apenas uma classe ou pode se tornar um punhado de classes. Eu prefiro a abordagem ágil para começar simples, adicionando subclasses quando necessário.

Para uma discussão detalhada sobre o meu quarto caso, veja a resposta de Loki Astari . Eu concordo totalmente com sua resposta detalhada.

Sjoerd
fonte
+1; Há uma diferença definida entre como você deve escrever exceções de gravação em uma estrutura versus como você deve escrever exceções em um aplicativo. A distinção é um pouco confusa, mas você a menciona (adiando Loki para o caso da biblioteca).
Merlyn Morgan-Graham