Exceções - "o que aconteceu" vs "o que fazer"

19

Usamos exceções para permitir que o consumidor do código lide com comportamentos inesperados de uma maneira útil. Geralmente, as exceções são criadas em torno do cenário "o que aconteceu" - como FileNotFound(não foi possível encontrar o arquivo que você especificou) ou ZeroDivisionError(não foi possível executar a 1/0operação).

E se houver a possibilidade de especificar o comportamento esperado do consumidor?

Por exemplo, imagine que temos um fetchrecurso, que executa uma solicitação HTTP e retorna dados recuperados. E, em vez de erros como ServiceTemporaryUnavailableou RateLimitExceededgostaríamos de RetryableErrorsugerir ao consumidor que ele deveria apenas tentar novamente a solicitação e não se importar com falhas específicas. Então, estamos basicamente sugerindo uma ação para o chamador - o "o que fazer".

Não fazemos isso com frequência porque não conhecemos todos os casos de uso dos consumidores. Mas imagine que seja algum componente específico que conhecemos o melhor curso de ações para um chamador - então devemos usar a abordagem "o que fazer"?

Roman Bodnarchuk
fonte
3
O HTTP já não faz isso? 503 é uma falha temporária na resposta; portanto, o solicitante deve tentar novamente; 404 é uma ausência fundamental; portanto, não faz sentido tentar novamente; 301 significa "movido permanentemente"; portanto, você deve tentar novamente, mas com um endereço diferente etc.
Kilian Postado
7
Em muitos casos, se realmente sabemos "o que fazer", podemos fazer com que o computador faça isso automaticamente e o usuário nem precisa saber que algo deu errado. Presumo que sempre que meu navegador recebe um 301, ele simplesmente vai para o novo endereço sem me perguntar.
Ixrec
@Ixrec - teve a mesma idéia também. no entanto, o consumidor pode não querer esperar por outra solicitação e ignorar o item ou falhar completamente.
Roman Bodnarchuk
1
@RomanBodnarchuk: Eu discordo. É como dizer que uma pessoa não precisa saber chinês para falar chinês. HTTP é um protocolo e espera-se que o cliente e o servidor o conheçam e o sigam. É assim que um protocolo funciona. Se apenas um lado conhece e cumpre, então você não pode se comunicar.
Chris Pratt
1
Parece honestamente que você está tentando substituir suas exceções por um bloco de captura. É por isso que mudamos para exceções - não mais if( ! function() ) handle_something();, mas poder lidar com o erro em algum lugar em que você realmente conhece o contexto de chamada - por exemplo, dizer ao cliente para ligar para um administrador do sistema se o servidor falhar ou recarregar automaticamente se a conexão cair, mas alertá-lo em caso o chamador seja outro microsserviço. Deixe os blocos de captura lidar com a captura.
Sebb

Respostas:

47

Mas imagine que seja algum componente específico que conhecemos o melhor curso de ações para um chamador.

Isso quase sempre falha em pelo menos um dos chamadores, para o qual esse comportamento é incrivelmente irritante. Não presuma que você conhece melhor. Diga a seus usuários o que está acontecendo, não o que você supõe que eles devam fazer a respeito. Em muitos casos, já está claro o que deve ser um curso de ação sensato (e, se não for, faça uma sugestão no manual do usuário).

Por exemplo, mesmo as exceções fornecidas em sua pergunta demonstram sua suposição quebrada: a ServiceTemporaryUnavailableequivale a "tente novamente mais tarde" e RateLimitExceededequivale a "uau, relaxe, talvez ajuste os parâmetros do temporizador e tente novamente em alguns minutos". Mas o usuário pode querer acionar algum tipo de alarme ServiceTemporaryUnavailable(o que indica um problema no servidor), e não para RateLimitExceeded(o que não).

Dê a eles a escolha .

Corridas de leveza com Monica
fonte
4
Concordo. O servidor deve transmitir apenas as informações corretamente. A documentação, por outro lado, deve descrever claramente o curso de ação adequado nesses casos.
22415 Neil
1
Uma pequena falha nisso é que, para alguns hackers, certas exceções podem dizer muito sobre o que o seu código está fazendo e elas podem usá-lo para descobrir maneiras de explorá-lo.
Pharap
3
@Pharap Se seus hackers tiverem acesso à exceção em vez de uma mensagem de erro, você já estará perdido.
corsiKa
2
Gosto dessa resposta, mas falta o que considero um requisito de exceções ... se você soubesse se recuperar, não seria uma exceção! Uma exceção deve ser apenas quando, quando você não puder fazer algo: entrada inválida, estado inválido, segurança inválida - você não poderá corrigi-los programaticamente.
corsiKa
1
Acordado. Se você insistir em indicar se uma nova tentativa é possível, sempre poderá fazer as exceções concretas relevantes herdadas RetryableError.
sapi
18

Atenção! Programador C ++ chegando aqui com idéias possivelmente diferentes de como o tratamento de exceções deve ser feito, tentando responder a uma pergunta que certamente é sobre outra linguagem!

Dada esta ideia:

Por exemplo, imagine que temos o recurso de busca, que executa a solicitação HTTP e retorna os dados recuperados. E, em vez de erros como ServiceTemporaryUnavailable ou RateLimitExceeded, geraríamos apenas um RetryableError, sugerindo ao consumidor que ele deveria apenas tentar novamente a solicitação e não se importar com falhas específicas.

... uma coisa que eu sugiro é que você possa estar misturando preocupações de relatar um erro com cursos de ação para responder a ele de uma maneira que possa degradar a generalidade do seu código ou exigir muitos "pontos de conversão" para exceções .

Por exemplo, se eu modelar uma transação envolvendo o carregamento de um arquivo, ela poderá falhar por vários motivos. Talvez o carregamento do arquivo envolva o carregamento de um plug-in que não existe na máquina do usuário. Talvez o arquivo esteja simplesmente corrompido e encontramos um erro ao analisá-lo.

Não importa o que aconteça, digamos que o curso de ação seja relatar o que aconteceu com o usuário e avisá-lo sobre o que ele deseja fazer ("tente novamente, carregue outro arquivo, cancele").

Lançador vs. Apanhador

Esse curso de ação se aplica independentemente do tipo de erro que encontramos neste caso. Ele não está incorporado na idéia geral de um erro de análise, não está incorporado na idéia geral de não carregar um plug-in. Ele está incorporado na idéia de encontrar esses erros durante o contexto preciso de carregar um arquivo (a combinação de carregar e falhar). Então, tipicamente, eu vejo isso de maneira grosseira como a catcher'sresponsabilidade de determinar o curso da ação em resposta a uma exceção lançada (por exemplo: solicitando opções ao usuário), não a thrower's.

Dito de outra forma, os sites em que as throwexceções geralmente não têm esse tipo de informação contextual, especialmente se as funções lançadas forem geralmente aplicáveis. Mesmo em um contexto totalmente degeneralizado quando eles têm essas informações, você acaba se esquivando em termos de comportamento de recuperação, incorporando-o ao throwsite. Os sites que catchgeralmente têm a maior quantidade de informações disponíveis para determinar um curso de ação e oferecem a você um local central para modificar se esse curso de ação mudar alguma vez para a transação em questão.

Quando você começa a tentar lançar exceções, não relatando mais o que está errado, mas tentando determinar o que fazer, isso pode degradar a generalidade e a flexibilidade do seu código. Um erro de análise nem sempre deve levar a esse tipo de prompt, varia de acordo com o contexto em que uma exceção é lançada (a transação sob a qual foi lançada).

O Lançador Cego

Em geral, muito do design do tratamento de exceções geralmente gira em torno da ideia de um lançador cego. Não sabe como a exceção será capturada ou onde. O mesmo se aplica a formas ainda mais antigas de recuperação de erros usando propagação manual de erros. Os sites que encontram erros não incluem um curso de ação do usuário, eles incorporam apenas as informações mínimas para relatar que tipo de erro foi encontrado.

Responsabilidades invertidas e generalização do apanhador

Ao pensar sobre isso com mais cuidado, eu estava tentando imaginar o tipo de base de código em que isso pode se tornar uma tentação. Minha imaginação (possivelmente errada) é que sua equipe ainda está desempenhando o papel de "consumidor" aqui e implementando a maior parte do código de chamada. Talvez você tenha muitas transações díspares (muitos tryblocos) que podem ser executadas nos mesmos conjuntos de erros e todas, da perspectiva do design, levam a um curso uniforme de ação de recuperação.

Levando em conta os conselhos sábios da Lightness Races in Orbit'sresposta correta (que eu acho que realmente vem de uma mentalidade avançada orientada a bibliotecas), você ainda pode ser tentado a lançar exceções "o que fazer", apenas mais perto do site de recuperação de transações.

Pode ser possível encontrar um site intermediário comum de manipulação de transações a partir daqui, que realmente centralize as preocupações "o que fazer", mas ainda dentro do contexto da captura.

insira a descrição da imagem aqui

Isso se aplicaria apenas se você pudesse projetar algum tipo de função geral usada por todas essas transações externas (por exemplo: uma função que insere outra função para chamar ou uma classe base de transação abstrata com comportamento substituível que modela esse site intermediário de transações que realiza a captura sofisticada )

No entanto, esse poderia ser responsável por centralizar o curso de ação do usuário em resposta a uma variedade de possíveis erros, e ainda dentro do contexto de pegar e não jogar. Exemplo simples (pseudocódigo Python-ish, e eu não sou nem um desenvolvedor experiente em Python, por isso pode haver uma maneira mais idiomática de fazer isso):

def general_catcher(task):
    try:
       task()
    except SomeError1:
       # do some uniformly-designed recovery stuff here
    except SomeError2:
       # do some other uniformly-designed recovery stuff here
    ...

[Espero que com um nome melhor que general_catcher]. Neste exemplo, você pode transmitir uma função que contém qual tarefa executar, mas ainda se beneficiar do comportamento de captura generalizada / unificada para todos os tipos de exceções em que você está interessado, e continuar a estender ou modificar a parte "o que fazer" você gosta deste local central e ainda dentro de um catchcontexto em que isso normalmente é incentivado. O melhor de tudo é que podemos impedir que os locais de lançamento se preocupem com "o que fazer" (preservando a noção de "atirador cego").

Se você não encontrar nenhuma dessas sugestões aqui e houver uma forte tentação de lançar exceções "o que fazer" de qualquer maneira, lembre-se principalmente de que isso é muito anti-idiomático, pelo menos, além de potencialmente desencorajar uma mentalidade generalizada.

ChrisF
fonte
2
+1. Eu nunca ouvi falar da idéia "Blind Thrower" como tal antes, mas ela se encaixa na maneira como penso no tratamento de exceções: indique o erro ocorrido, não pense em como ele deve ser tratado. Quando você é responsável pela pilha cheia, é difícil (mas importante!) Separar as responsabilidades corretamente, e o destinatário é responsável por notificar sobre problemas e o responsável pela chamada para lidar com problemas. O chamado só sabe o que foi solicitado a fazer, não o porquê. O tratamento de erros deve ser feito no contexto do 'porquê', portanto: no chamador.
Sjoerd Job Postmus
1
(Também: Eu não acho que sua resposta é específico C ++, mas aplicável a manipulação de exceção em geral)
Sjoerd Job Postmus
1
@SjoerdJobPostmus Yay thanks! O "Blind Thrower" é apenas uma analogia idiota que eu criei aqui - eu não sou muito inteligente ou rápido em digerir conceitos técnicos complexos, então muitas vezes eu quero encontrar poucas imagens e analogias para tentar explicar e melhorar meu próprio entendimento das coisas. Talvez um dia eu possa tentar escrever um pequeno livro de programação cheio de desenhos animados. :-D
1
Heh, essa é uma bela imagem. Um pequeno personagem de desenho animado usando uma venda nos olhos e jogando exceções em forma de beisebol, sem ter certeza de quem as pegará (ou mesmo se serão pegas), mas cumprindo seu dever de atirador cego.
Blacklight Shining
1
@DrunkCoder: Por favor, não vandalize suas postagens. Já temos coisas suficientes na Internet. Se você tiver um bom motivo para excluir, sinalize sua postagem para obter atenção do moderador e faça seu caso.
Robert Harvey
2

Eu acho que na maioria das vezes seria melhor passar argumentos para a função dizendo como lidar com essas situações.

Por exemplo, considere uma função:

Response fetchUrl(URL url, RetryPolicy retryPolicy);

Posso passar RetryPolicy.noRetries () ou RetryPolicy.retries (3) ou qualquer outra coisa. No caso de falha repetível, ele consultará a política para decidir se deve ou não tentar novamente.

Winston Ewert
fonte
Isso realmente não tem muito a ver com o lançamento de exceções no site de chamadas. Você está falando de algo um pouco diferente, o que é bom, mas não é realmente parte da pergunta ..
Leveza raças com Monica
@LightnessRacesinOrbit, pelo contrário. Estou apresentando isso como uma alternativa à idéia de lançar exceções de volta ao site de chamada. No exemplo do OP, fetchUrl lançaria uma RetryableException, e estou dizendo que você deve informar ao fetchUrl quando ele deve tentar novamente.
Winston Ewert
2
@WinstonEwert: Embora eu concorde com LightnessRacesinOrbit, também entendo o seu ponto, mas penso nele como uma maneira diferente de representar o mesmo controle. Mas considere que você provavelmente desejaria passar new RetryPolicy().onRateLimitExceeded(STOP).onServiceTemporaryUnavailable(RETRY, 3)ou algo assim, porque RateLimitExceededtalvez seja necessário lidar de maneira diferente ServiceTemporaryUnavailable. Depois de escrever isso, meu pensamento é: melhor lançar uma exceção, porque ela oferece um controle mais flexível.
Sjoerd Job Postmus
@SjoerdJobPostmus, acho que depende. Pode ser que a lógica de limitação e repetição de taxas faça desde a sua biblioteca; nesse caso, acho que minha abordagem faz sentido. Se fizer mais sentido deixar isso para o interlocutor, jogue de qualquer maneira.
Winston Ewert