Como criar exceções

11

Estou lutando com uma pergunta muito simples:

Agora estou trabalhando em um aplicativo de servidor e preciso inventar uma hierarquia para as exceções (algumas exceções já existem, mas é necessária uma estrutura geral). Como eu começo a fazer isso?

Estou pensando em seguir esta estratégia:

1) O que está errado?

  • Algo é perguntado, o que não é permitido.
  • Algo é solicitado, é permitido, mas não funciona, devido a parâmetros incorretos.
  • Algo é perguntado, é permitido, mas não funciona, devido a erros internos.

2) Quem está iniciando a solicitação?

  • O aplicativo cliente
  • Outro aplicativo de servidor

3) Entrega de mensagens: como estamos lidando com um aplicativo de servidor, trata-se de receber e enviar mensagens. E daí se o envio de uma mensagem der errado?

Dessa forma, podemos obter os seguintes tipos de exceção:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (caso o servidor não saiba de onde vem a solicitação)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

Essa é uma abordagem muito geral para definir a hierarquia de exceções, mas receio que esteja faltando alguns casos óbvios. Você tem idéias sobre as áreas que não estou abordando, conhece alguma desvantagem desse método ou existe uma abordagem mais geral para esse tipo de pergunta (no último caso, onde posso encontrá-lo)?

desde já, obrigado

Dominique
fonte
5
Você não mencionou o que deseja alcançar com sua hierarquia de classes de exceção (e isso não é de todo óbvio). Registro significativo? Permitir que os clientes reajam razoavelmente às diferentes exceções? Ou o que?
Ralf Kleberhoff
2
Provavelmente é útil analisar algumas histórias e casos de uso primeiro e ver o que sai. Por exemplo: o cliente solicita X , não é permitido. O cliente solicita X , mas a solicitação é inválida. Trabalhe com eles pensando sobre quem deve lidar com a exceção, o que eles podem fazer com ela (prompt, nova tentativa, o que for) e quais informações eles precisam para fazer isso bem. Então , depois de saber quais são suas exceções concretas e quais informações os manipuladores precisam para processá-las, você pode transformá-las em uma boa hierarquia.
Inútil
1
De relevância, eu diria: softwareengineering.stackexchange.com/questions/278949/…
Martin Ba
1
Eu realmente nunca entendi o desejo de querer usar tantos tipos diferentes de exceção. Normalmente, para a maioria dos catchblocos que uso, não tenho muito mais uso para a exceção do que a mensagem de erro que ela contém. Eu realmente não tenho nada diferente que eu possa fazer por uma exceção envolvida na falha na leitura de um arquivo, como uma falha na alocação de memória durante o processo de leitura, por isso eu apenas capto std::exceptione relate a mensagem de erro que ele contém, talvez decorando com "Failed to open file: %s", ex.what()um buffer de pilha antes de imprimi-lo.
3
Além disso, não posso prever todos os tipos de exceção que serão lançados em primeiro lugar. Talvez eu possa antecipá-los agora, mas os colegas podem introduzir novos no futuro, por exemplo, então é meio sem esperança para mim conhecer, por agora e para sempre, todos os diferentes tipos de exceções que poderiam ser lançadas no processo de um Operação. Então, eu pego super geralmente com um único bloco de captura. Eu já vi exemplos de pessoas usando muitos catchblocos diferentes em um único site de recuperação, mas geralmente é apenas para ignorar a mensagem dentro da exceção e imprimir uma mensagem mais localizada ...

Respostas:

5

Observações gerais

(um pouco tendencioso)

Eu normalmente não iria para uma hierarquia de exceção detalhada.

O mais importante: uma exceção informa ao chamador que seu método falhou ao concluir seu trabalho. E seu interlocutor deve receber um aviso sobre isso, para que ele simplesmente não continue. Isso funciona com qualquer exceção, independentemente da classe de exceção que você escolher.

O segundo aspecto é o log. Você deseja encontrar entradas de log significativas sempre que algo der errado. Isso também não precisa de classes de exceção diferentes, apenas mensagens de texto bem projetadas (suponho que você não precise de um automático para ler seus logs de erros ...).

Terceiro aspecto é a reação do seu interlocutor. O que seu interlocutor pode fazer quando recebe uma exceção? Aqui, pode fazer sentido ter classes de exceção diferentes, para que o chamador possa decidir se deseja repetir a mesma chamada, usar uma solução diferente (por exemplo, usar uma fonte de fallback) ou desistir.

E talvez você queira usar suas exceções como base para informar o usuário final sobre o problema. Isso significa criar uma mensagem amigável ao usuário além do texto do administrador para o arquivo de log, mas não precisa de diferentes classes de exceção (embora talvez isso possa facilitar a geração de texto ...).

Um aspecto importante para o log (e para as mensagens de erro do usuário) é a capacidade de alterar a exceção com informações de contexto, capturando-a em alguma camada, adicionando algumas informações de contexto, por exemplo, parâmetros de método e lançando-a novamente.

Sua hierarquia

Quem está iniciando a solicitação? Acho que você não precisará das informações que estavam iniciando a solicitação. Não consigo nem imaginar como você sabe disso profundamente em uma pilha de chamadas.

Tratamento de mensagens : esse não é um aspecto diferente, mas apenas casos adicionais para "O que está acontecendo de errado?".

Em um comentário, você fala sobre um sinalizador " sem registro " ao criar uma exceção. Eu não acho que no local em que você cria e lança uma exceção, você pode tomar uma decisão confiável se deve ou não registrar essa exceção.

A única situação que posso imaginar é que uma camada superior usa sua API de uma maneira que às vezes produz exceções, e essa camada sabe que não precisa incomodar nenhum administrador com a exceção, portanto, silenciosamente, engole a exceção. Mas isso é um cheiro de código: uma exceção esperada é uma contradição em si, uma dica para alterar a API. E é a camada superior que deve decidir, não o código gerador de exceção.

Ralf Kleberhoff
fonte
Na minha experiência, um código de erro em combinação com um texto amigável ao usuário funciona muito bem. Os administradores podem usar o código de erro para encontrar informações adicionais.
Sjoerd
1
Eu gosto da idéia por trás dessa resposta em geral. Pela minha experiência, as exceções realmente nunca deveriam se tornar um animal muito complexo. O principal objetivo de uma exceção é permitir que o código de chamada resolva um problema específico e obtenha as informações relevantes de depuração / nova tentativa sem interferir na resposta da função.
precisa saber é o seguinte
2

O principal aspecto a ser lembrado ao projetar um padrão de resposta a erros é garantir que seja útil para os chamadores. Isso se aplica se você estiver usando exceções ou códigos de erro definidos, mas nos restringiremos a ilustrar com exceções.

  • Se sua linguagem ou estrutura já fornecer classes de exceção gerais, use-as onde forem apropriadas e onde devem ser razoavelmente esperadas . Não defina suas próprias classes ArgumentNullExceptionou ArgumentOutOfRangeexceções. As pessoas que ligam não esperam capturá-las.

  • Defina uma MyClientServerAppExceptionclasse base para abranger os erros exclusivos no contexto do seu aplicativo. Nunca jogue uma instância da classe base. Uma resposta de erro ambígua é a pior coisa de todos os tempos . Se houver um "erro interno", explique qual é esse erro.

  • Na maior parte, a hierarquia abaixo da classe base deve ser ampla, não profunda. Você só precisa aprofundá-lo em situações em que seja útil para o chamador. Por exemplo, se houver 5 razões pelas quais uma mensagem pode falhar do cliente para o servidor, você poderá definir uma ServerMessageFaultexceção e definir uma classe de exceção para cada um desses 5 erros abaixo. Dessa forma, o chamador pode simplesmente capturar a superclasse se precisar ou quiser. Tente limitar isso a casos específicos e razoáveis.

  • Não tente definir todas as suas classes de exceção antes que elas sejam realmente usadas. Você acabará refazendo a maior parte. Quando você encontrar um caso de erro ao escrever um código, decida qual a melhor forma de descrever esse erro. Idealmente, ele deve ser expresso no contexto do que o chamador está tentando fazer.

  • Relacionado ao ponto anterior, lembre-se de que apenas porque você usa exceções para responder a erros, isso não significa que você deve usar somente exceções para estados de erro. Lembre-se de que lançar exceções é geralmente caro, e o custo de desempenho pode variar de um idioma para outro. Em alguns idiomas, o custo é mais alto, dependendo da profundidade da pilha de chamadas; portanto, se um erro for profundo em uma pilha de chamadas, verifique se você não pode usar tipos primitivos simples (códigos de erro inteiro ou sinalizadores booleanos) para pressionar o erro faz backup da pilha, para que possa ser jogado mais perto da chamada do chamador.

  • Se você estiver incluindo o log como parte da resposta a erro, será fácil para os chamadores anexar informações de contexto a um objeto de exceção. Comece de onde as informações são usadas no código de log. Decida quanta informação é necessária para que o log seja útil (sem ser uma parede gigante de texto). Em seguida, trabalhe para trás para garantir que as classes de exceção possam ser facilmente fornecidas com essas informações.

Por fim, a menos que seu aplicativo possa lidar absolutamente com um erro de falta de memória, não tente lidar com eles ou outras exceções de tempo de execução catastróficas. Apenas deixe o sistema operacional lidar com isso, porque, na realidade, é tudo o que você pode fazer.

Mark Benningfield
fonte
2
Exceto pelo penúltimo marcador (sobre o custo das exceções), a resposta é boa. Essa bala sobre o custo das exceções é enganosa, porque, em todas as maneiras comuns de implementar exceções no nível baixo, o custo de lançar uma exceção é completamente independente da profundidade da pilha de chamadas. Métodos alternativos de relatório de erros podem ser melhores se você souber que o erro será tratado pelo chamador imediato, mas não para tirar algumas funções da pilha de chamadas antes de lançar uma exceção.
Bart van Ingen Schenau
@BartvanIngenSchenau: Tentei não vincular isso a um idioma específico. Em algumas linguagens ( Java, por exemplo ), a profundidade da pilha de chamadas afeta o custo da instanciação. Vou editar para refletir que não é tão cortado e seco.
precisa saber é o seguinte
0

Bem, eu recomendo que você crie, em primeiro lugar, uma Exceptionclasse base para todas as exceções verificadas que possam ser lançadas pelo seu aplicativo. Se seu aplicativo foi chamado DuckType, faça uma DuckTypeExceptionclasse base.

Isso permite capturar qualquer exceção de sua DuckTypeExceptionclasse base para manipulação. A partir daqui, suas exceções devem se ramificar com nomes descritivos que possam destacar melhor o tipo de problema. Por exemplo, "DatabaseConnectionException".

Deixe-me esclarecer, todas essas exceções devem ser verificadas para situações que possam acontecer que você provavelmente desejaria manipular normalmente em seu programa. Em outras palavras, você não pode se conectar ao banco de dados; portanto, DatabaseConnectionExceptioné lançado um que você pode pegar para aguardar e tentar novamente após um período de tempo.

Você não veria uma exceção verificada para um problema muito inesperado, como uma consulta SQL inválida ou uma exceção de ponteiro nulo, e eu encorajo você a permitir que essas exceções transcendam a maioria das cláusulas catch (ou capturadas e reconfiguradas conforme necessário) até chegar à página principal. próprio controlador, que pode ser capturado RuntimeExceptionpuramente para fins de registro.

Minha preferência pessoal é não repetir uma desmarcada RuntimeExceptioncomo outra exceção, pois, por natureza de uma exceção desmarcada, você não esperaria e, portanto, recontar novamente em outra exceção está ocultando informações. No entanto, se essa for a sua preferência, você ainda pode pegar um RuntimeExceptione lançar um DuckTypeInternalExceptionque, ao contrário, DuckTypeExceptionderiva de RuntimeExceptione é, portanto, desmarcado.

Se preferir, você pode subcategorizar suas exceções para fins organizacionais, como DatabaseExceptionpara qualquer coisa relacionada ao banco de dados, mas eu ainda encorajo essas subceções a derivarem da sua exceção base DuckTypeExceptione serem abstratas e, portanto, derivadas com nomes descritivos explícitos.

Como regra geral, suas tentativas de captura devem ser cada vez mais genéricas à medida que você sobe na pilha de chamadas para lidar com exceções e, novamente, em seu controlador principal, você não lidaria com um, DatabaseConnectionExceptionmas com o simples DuckTypeExceptionde que todas as exceções verificadas derivam.

Neil
fonte
2
Observe que a pergunta está marcada com "C ++".
Martin Ba
0

Tente simplificar isso.

A primeira coisa que o ajudará a pensar em outra estratégia é: capturou muitas exceções é muito semelhante ao uso de exceções verificadas do Java (desculpe, eu não sou desenvolvedor de C ++). Isso não é bom por vários motivos, por isso sempre tento não usá-los, e sua estratégia de exceção de hierarquia lembra muito de mim.

Portanto, recomendo outra estratégia flexível: use exceções não verificadas e erros de código.

Por exemplo, veja este código Java:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

E sua exceção única :

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

Encontrei esta estratégia neste link e você pode encontrar a implementação Java aqui , onde você pode ver mais detalhes, porque o código acima é simplificado.

Como você precisa separar as diferentes exceções entre os aplicativos "cliente" e "outro servidor", você pode ter várias classes de código de erro implementando a interface ErrorCode.

Dherik
fonte
2
Observe que a pergunta está marcada com "C ++".
Sjoerd
Eu editei para adaptar a ideia.
Dherik
0

Exceções são gotos irrestritos e devem ser usadas com cuidado. A melhor estratégia para eles é restringi-los. A função de chamada deve lidar com todas as exceções geradas pelas funções que chama ou o programa morre. Somente a função de chamada possui o contexto correto para manipular a exceção. Tendo as funções mais adiante na árvore de chamadas, os manipula são gotos irrestritos.

Exceções não são erros. Eles ocorrem quando as circunstâncias impedem que o programa conclua uma ramificação do código e indicam outra ramificação para seguir.

Exceções devem estar no contexto da função chamada. Por exemplo: uma função que resolve equações quadráticas . Ele deve manipular duas exceções: division_by_zero e square_root_of_negative_number. Mas essas exceções não fazem sentido para funções que chamam esse solucionador de equações quadráticas. Elas acontecem por causa do método usado para resolver equações e simplesmente repeti-las novamente expõe o interior e quebra o encapsulamento. Em vez disso, division_by_zero deve ser mostrado novamente como not_quadratic e square_root_of_negative_number e no_real_roots.

Não há necessidade de uma hierarquia de exceções. Uma enum das exceções (que as identifica) lançadas por uma função é suficiente porque a função de chamada deve tratá-las. Permitir que eles processem a árvore de chamadas é um passo fora do contexto (irrestrito).

shawnhcorey
fonte