Bom uso do try-blocks?

15

Eu sempre me pego lutando com isso ... tentando encontrar o equilíbrio certo entre tentar / capturar e o código não se tornar essa bagunça obscena de guias, colchetes e exceções sendo lançadas de volta na pilha de chamadas como uma batata quente. Por exemplo, eu tenho um aplicativo que estou desenvolvendo agora que usa SQLite. Eu tenho uma interface de banco de dados que abstrai as chamadas SQLite e um modelo que aceita coisas para entrar / sair do banco de dados ... Portanto, se / quando ocorrer uma exceção SQLite, ela deverá ser lançada no modelo (que a chamou) ), que deve distribuí-lo a quem chamou o AddRecord / DeleteRecord / o que for ...  

Sou fã de exceções, em vez de retornar códigos de erro, porque eles podem ser ignorados, esquecidos etc., enquanto uma exceção precisa ser tratada (concedida, eu poderia pegar e seguir imediatamente ...) certo de que deve haver uma maneira melhor do que o que estou acontecendo agora.

Edit:  Eu deveria ter formulado isso um pouco diferente. Eu entendo re-jogar como tipos diferentes e tal, eu disse isso muito mal e isso é culpa minha. Minha pergunta é ... como é melhor manter o código limpo ao fazer isso? Começa a parecer extremamente confuso para mim depois de um tempo.

trycatch
fonte
Qual linguagem de programação?
Apalala 31/03
2
C # no momento, mas estou tentando pensar em geral.
trycatch
O C # não força a declaração de exceções lançadas, o que facilita o tratamento de exceções onde for razoável e evita que os programadores sejam tentados a capturá-las sem realmente lidar com elas. Anders Hejlsberg, designer de C #, defende as
Apalala 31/03/11

Respostas:

14

Pense nisso em termos de digitação forte, mesmo se você não estiver usando uma linguagem fortemente tipada - se o seu método não puder retornar o tipo que você esperava, deve estar lançando uma exceção.

Além disso, em vez de lançar o SQLException até o modelo (ou pior, interface do usuário), cada camada deve capturar exceções conhecidas e agrupar / alterar / substituir por exceções adequadas a essa camada:

Layer      Handles Exception
----------------------------
UI         DataNotFoundException
Model      DatabaseRetrievalException
DAO        SQLException

Isso deve ajudar a limitar o número de exceções que você está procurando em cada camada e a manter um sistema de exceção organizado.

Nicole
fonte
Fiz algumas edições, escrevi mal o Q original. Minha pergunta é mais sobre como manter o código limpo enquanto lida com todas as tentativas e capturas e outros enfeites. Começa a parecer muito confuso quando os blocos de captura estão por toda parte ...
trycatch 31/03
1
@ Ed não incomoda você que sua interface do usuário ou modelo esteja capturando "SQLException"? Para mim, isso não é muito relevante. Para cada um deles, eu acho.
31511 Nicole
1
@ Ed, isso pode ser bom em idiomas que não têm exceções verificadas, mas em idiomas com exceções verificadas é realmente feio ter throws SQLExceptionum método que não implica que o SQL esteja envolvido. E o que acontece se você decidir que algumas operações devem ir para um armazenamento de arquivos? Agora você tem que declarar throws SQLException, IOException, etc. Isso ficará fora de controle.
Mike Daniels
1
@Ed para o usuário final SQLException não significa muito, especialmente se o seu sistema puder suportar vários tipos de persistência além dos bancos de dados relacionais. Como programador de GUI, prefiro lidar com DataNotFoundException do que um conjunto inteiro de exceções de baixo nível. Muitas vezes, uma exceção a uma biblioteca de baixo nível é apenas parte da vida normal acima, nesse caso, eles são muito mais fáceis de lidar quando seu nível de abstração corresponde ao do aplicativo.
Newtopian
1
@ Newtopian - eu nunca disse apresentar a exceção bruta ao usuário final. Para os usuários finais, basta uma mensagem simples 'O programa parou de funcionar' enquanto registra a Exceção em algum lugar útil para o suporte receber. No modo de depuração, é útil exibir a exceção. Você não deve se preocupar com todas as exceções possíveis que uma API possa lançar, a menos que você tenha um manipulador específico para essas exceções; deixe o seu coletor de exceções sem tratamento lidar com tudo isso.
Ed11
9

As exceções permitem escrever código mais limpo, porque a maior parte dele cuida do caso normal, e os casos excepcionais podem ser tratados posteriormente, mesmo em um contexto diferente.

A regra para lidar com exceções (captura) é que ela deve ser feita por contexto que possa realmente fazer algo sobre isso. Mas isso tem uma exceção:

As exceções devem ser capturadas nos limites do módulo (especialmente os limites da camada) e mesmo que apenas para incluí-las e lançar uma exceção de nível superior que tenha significado para o chamador. Cada módulo e camada deve ocultar seus detalhes de implementação, mesmo com relação a exceções (um módulo heap pode gerar HeapFull, mas nunca ArrayIndexOutOfBounds).

No seu exemplo, é improvável que as camadas superiores possam fazer algo sobre uma exceção SQLite (se o fizerem, tudo será tão acoplado ao SQLite que você não poderá mudar a camada de dados para outra coisa). Existem várias razões previsíveis para que coisas como Adicionar / Excluir / Atualizar falhem, e algumas delas (alterações incompatíveis nas transações simultâneas) são impossíveis de recuperar, mesmo na camada de dados / persistência (violação das regras de integridade, fi). A camada de persistência deve converter as exceções em algo significativo nos termos da camada de modelo, para que as camadas superiores possam decidir se devem tentar novamente ou falhar normalmente.

Apalala
fonte
Eu concordo e editei minha pergunta para refletir onde eu a havia pronunciado mal. Eu pretendia que isso fosse mais uma questão de como manter a tentativa e a recuperação de bagunçar as coisas.
trycatch
Você pode aplicar os princípios que Edward e eu mencionamos em uma camada ou módulo. Em vez de chamar o SQLite diretamente de todo o lugar, tenha alguns métodos / funções que conversam com o SQLite e se recuperam das exceções ou as traduzem para outras mais genéricas. Se uma transação envolver várias consultas e atualizações, você não precisará lidar com todas as exceções possíveis em cada uma: uma única tentativa externa de captura pode converter as exceções, e uma interna pode cuidar de atualizações parciais com uma reversão. Você também pode mover as atualizações para suas próprias funções para simplificar o tratamento de exceções.
Apalala 31/03
1
esse material seria mais fácil de entender com exemplos de código ..
Clique em Upvote
Provavelmente, essa era uma pergunta mais adequada ao fluxo de pilha desde o início.
Apalala
5

Como regra geral, você deve capturar apenas exceções específicas (por exemplo, IOException) e somente se tiver algo específico a fazer depois de capturar a exceção.

Caso contrário, geralmente é melhor deixar as Exceções borbulharem até a superfície para que possam ser expostas e tratadas. Algumas pessoas chamam isso de falha rápida.

Você deve ter algum tipo de manipulador na raiz do seu aplicativo para capturar Exceções não tratadas que surgiram abaixo. Isso permite que você apresente, relate ou gerencie a exceção de maneira adequada.

O agrupamento de exceções é útil quando você precisa lançar uma exceção em um sistema distribuído e o cliente não tem a definição da falha do servidor.

Ed James
fonte
Capturar e silenciar exceções é uma coisa terrível a se fazer. Se o programa estiver com defeito, ele deverá travar com uma exceção e um rastreamento. Ele deve atrapalhar o registro silencioso de coisas, mas causando uma bagunça nos dados.
S.Lott
1
@ S.Lott Concordo que não se deve silenciar exceções, porque as consideram irritantes, mas apenas travar um aplicativo é um pouco extremo. Existem muitos casos em que é possível capturar a exceção e redefinir o sistema em um estado conhecido; nesses casos, o manipulador global de tudo é bastante útil. Se, no entanto, o processo de redefinição falhar de uma maneira que não pode ser tratada com segurança, então sim, deixe que a batida seja muito melhor do que enfiar a cabeça na areia.
Newtopian
2
mais uma vez, tudo depende do que você está construindo, mas geralmente não concordo em deixar que eles borbulhem, pois isso gera vazamentos de abstração. Quando eu uso uma API, prefiro vê-la expor exceções que estão em sintonia com o modelo da API. Eu também odeio surpresas, por isso odeio quando uma API apenas permite que exceções relacionadas à sua implementação sejam vistas sem aviso prévio. Se eu não tenho idéia do que está vindo para mim, como posso reagir! Eu me vejo com muita frequência usando redes de captura muito mais amplas para impedir surpresas de travarem meu aplicativo sem aviso prévio.
Newtopian
@ Newtopian: Exceções de "quebra automática" e "reescrita" reduzem o vazamento de abstrações. Eles ainda borbulham apropriadamente. "Se eu não tenho ideia do que está vindo para mim, como posso reagir! Eu me vejo frequentemente usando redes de captura muito mais amplas" É o que sugerimos que você pare de fazer. Você não precisa pegar tudo. 80% do tempo, a coisa certa é pegar nada. 20% das vezes há uma resposta significativa.
S.Lott
1
@ Newtopian, acho que precisamos distinguir entre exceções que normalmente serão lançadas por um objeto e, portanto, devem ser quebradas, e exceções que surgem devido a erros no código do objeto e não devem ser quebradas.
Winston Ewert
4

Imagine que você está escrevendo uma classe de pilha. Você não coloca nenhum código de manipulação de exceção na classe, pois isso pode produzir as seguintes exceções.

  1. ArrayIndexError - gerado quando o usuário tenta sair da pilha vazia
  2. NullPtrException - gerado devido a um erro na implementação, causando uma tentativa de referenciar uma referência nula

Uma abordagem simplista para agrupar exceções pode decidir agrupar essas duas exceções em uma classe de exceção StackError. No entanto, isso realmente perde o objetivo de agrupar exceções. Se um objeto lança uma exceção de baixo nível, isso significa que o objeto está quebrado. No entanto, há um caso em que isso é aceitável: quando o objeto é de fato quebrado.

O ponto de agrupar exceções é que o objeto deve fornecer exceções apropriadas para erros normais. A pilha deve aumentar StackEmpty e não ArrayIndexError ao saltar de uma pilha vazia. A intenção não é evitar lançar outras exceções se o objeto ou código estiver quebrado.

O que realmente queremos evitar é capturar exceções de baixo nível que foram passadas por objetos de alto nível. Uma classe de pilha que lança um ArrayIndexError ao saltar de uma pilha vazia é um problema menor. Se você realmente pegar esse ArrayIndexError, temos um problema sério. A propagação de erros de baixo nível é um pecado muito menos sério do que pegá-los.

Para trazer isso de volta ao seu exemplo de SQLException: por que você está recebendo SQLExceptions? Um motivo é porque você está passando consultas inválidas. No entanto, se sua camada de acesso a dados estiver gerando consultas incorretas, ela será quebrada. Ele não deve tentar reorganizar sua quebra em uma exceção DataAccessFailure.

No entanto, uma SQLException também pode surgir devido a uma perda de conexão com o banco de dados. Minha estratégia nesse ponto é capturar a exceção na última linha de defesa, relatar ao usuário que a conectividade do banco de dados foi perdida e desligada. Como o aplicativo tem perda de acesso ao banco de dados, não há realmente muito mais a ser feito.

Não sei como é o seu código. Mas parece que você pode traduzir cegamente todas as exceções em exceções de nível superior. Você deve fazer isso apenas em um número relativamente pequeno de casos. A maioria das exceções de nível inferior indica bugs no seu código. Pegá-los e embalá-los novamente é contraproducente.

Winston Ewert
fonte