Usamos o SonarQube para analisar nosso código Java e ele possui esta regra (configurada como crítica):
Métodos públicos devem lançar no máximo uma exceção verificada
O uso de exceções verificadas força os chamadores de métodos a lidar com erros, propagando-os ou manipulando-os. Isso torna essas exceções parte integrante da API do método.
Para manter a complexidade dos chamadores razoável, os métodos não devem lançar mais de um tipo de exceção verificada ".
Outra parte do Sonar tem isso :
Métodos públicos devem lançar no máximo uma exceção verificada
O uso de exceções verificadas força os chamadores de métodos a lidar com erros, propagando-os ou manipulando-os. Isso torna essas exceções parte integrante da API do método.
Para manter a complexidade dos chamadores razoável, os métodos não devem lançar mais de um tipo de exceção verificada.
O código a seguir:
public void delete() throws IOException, SQLException { // Non-Compliant /* ... */ }
deve ser refatorado para:
public void delete() throws SomeApplicationLevelException { // Compliant /* ... */ }
Os métodos de substituição não são verificados por esta regra e têm permissão para lançar várias exceções verificadas.
Nunca encontrei essa regra / recomendação em minhas leituras sobre tratamento de exceções e tentei encontrar alguns padrões, discussões etc. sobre o assunto. A única coisa que encontrei é a do CodeRach: quantas exceções um método deve lançar no máximo?
Esse é um padrão bem aceito?
fonte
Respostas:
Vamos considerar a situação em que você tem o código fornecido de:
O perigo aqui é que o código que você escreve para chamar
delete()
terá a seguinte aparência:Isso também é ruim. E será detectado com outra regra que sinaliza a captura da classe Exception básica.
A chave é não escrever código que faça você querer escrever código incorreto em outro lugar.
A regra que você está encontrando é bastante comum. O Checkstyle tem em suas regras de design:
Isso descreve precisamente o problema, qual é o problema e por que você não deve fazê-lo. É um padrão bem aceito que muitas ferramentas de análise estática identificarão e sinalizarão.
E embora você possa fazer isso de acordo com o design da linguagem, e às vezes seja a coisa certa a fazer, é algo que você deve ver e imediatamente dizer "hum, por que estou fazendo isso?" Pode ser aceitável para código interno, onde todos são disciplinados o suficiente para nunca
catch (Exception e) {}
, mas mais frequentemente do que nunca , vi pessoas cortando cantos, especialmente em situações internas.Não faça as pessoas que usam sua classe desejarem escrever códigos incorretos.
Devo salientar que a importância disso é diminuída com o Java SE 7 e posterior, porque uma única instrução catch pode capturar várias exceções ( capturando vários tipos de exceção e relendo exceções com verificação de tipo aprimorada do Oracle).
Com o Java 6 e anterior, você teria um código parecido com:
e
ou
Nenhuma dessas opções com o Java 6 é ideal. A primeira abordagem viola o DRY . Vários blocos fazendo a mesma coisa, repetidamente - uma vez para cada exceção. Deseja registrar a exceção e repeti-la novamente? Está bem. As mesmas linhas de código para cada exceção.
A segunda opção é pior por vários motivos. Primeiro, significa que você está capturando todas as exceções. Ponteiro nulo é pego lá (e não deveria). Além disso, você está refazendo novamente o
Exception
que significa que a assinatura do método seriadeleteSomething() throws Exception
apenas uma bagunça na pilha, pois as pessoas que usam seu código agora são forçadas a fazê -locatch(Exception e)
.Com o Java 7, isso não é tão importante porque você pode:
Além disso, a verificação de tipo, se um não pegar os tipos de exceções sendo lançadas:
O verificador de tipos reconhecerá que
e
pode ser apenas do tipoIOException
ouSQLException
. Ainda não estou muito entusiasmado com o uso desse estilo, mas ele não está causando um código tão ruim quanto no Java 6 (onde forçaria você a ter a assinatura do método como a superclasse que as exceções estendem).Apesar de todas essas mudanças, muitas ferramentas de análise estática (Sonar, PMD, Checkstyle) ainda estão aplicando os guias de estilo do Java 6. Não é uma coisa ruim. Costumo concordar com um aviso de que eles ainda devem ser aplicados, mas você pode alterar a prioridade neles para maior ou menor de acordo com a prioridade da equipe.
Se exceções deve ser verificada ou não ... isso é uma questão de g r um e uma t debate que se pode facilmente encontrar inúmeros posts ocupando cada lado do argumento. No entanto, se você estiver trabalhando com exceções verificadas, provavelmente evite lançar vários tipos, pelo menos no Java 6.
fonte
foo.delete()
? Ainda é capturado e relançado?delete
lançar uma exceção verificada que não seja IOException ou SQLException nesse exemplo. O ponto principal que eu estava tentando destacar é que um método que chama rethrowException ainda terá o tipo de exceção no Java 7. No Java 6, tudo isso é mesclado aoException
tipo comum, o que deixa triste a análise estática e outros codificadores.catch (Exception e)
e forçá-lo a ser umcatch (IOException e)
oucatch (SQLException e)
outro.catch(IOException|SQLException ex)
. Mas, se você apenas repetir a exceção, permitir que o verificador de tipos propague o tipo real da exceção, simplificando o código, não é uma coisa ruim.O motivo pelo qual você idealmente desejaria lançar apenas um tipo de exceção é porque, de outra forma, isso provavelmente viola os princípios de Responsabilidade Única e Inversão de Dependência . Vamos usar um exemplo para demonstrar.
Digamos que temos um método que busca dados de persistência e que persistência é um conjunto de arquivos. Como estamos lidando com arquivos, podemos ter um
FileNotFoundException
:Agora, temos uma alteração nos requisitos e nossos dados são de um banco de dados. Em vez de a
FileNotFoundException
(como não estamos lidando com arquivos), agora lançamos umSQLException
:Agora teríamos que passar por todo o código que usa nosso método e alterar a exceção que precisamos verificar, caso contrário o código não será compilado. Se nosso método for chamado amplamente, isso pode ser muito para mudar / fazer com que outros mudem. Leva muito tempo e as pessoas não serão felizes.
A inversão de dependência diz que realmente não devemos lançar nenhuma dessas exceções porque elas expõem detalhes de implementação interna que estamos trabalhando para encapsular. O código de chamada precisa saber que tipo de persistência estamos usando, quando realmente deveria se preocupar se o registro pode ser recuperado. Em vez disso, devemos lançar uma exceção que transmita o erro no mesmo nível de abstração que estamos expondo por meio de nossa API:
Agora, se mudarmos a implementação interna, podemos simplesmente agrupar essa exceção em
InvalidRecordException
e transmiti-la (ou não, e simplesmente lançar uma novaInvalidRecordException
). O código externo não sabe ou se importa com o tipo de persistência que está sendo usado. Está tudo encapsulado.Quanto à responsabilidade única, precisamos pensar em código que gera várias exceções não relacionadas. Digamos que temos o seguinte método:
O que podemos dizer sobre esse método? Podemos dizer apenas a partir da assinatura que ele abre um arquivo e o analisa. Quando vemos uma conjunção, como "e" ou "ou" na descrição de um método, sabemos que ele está fazendo mais de uma coisa; tem mais de uma responsabilidade . Métodos com mais de uma responsabilidade são difíceis de gerenciar, pois podem mudar se alguma das responsabilidades mudar. Em vez disso, devemos dividir os métodos para que eles tenham uma única responsabilidade:
Extraímos a responsabilidade de ler o arquivo da responsabilidade de analisar os dados. Um efeito colateral disso é que agora podemos passar dados de String para os dados de análise de qualquer fonte: memória, arquivo, rede, etc. Também podemos testar com
parse
mais facilidade agora, porque não precisamos de um arquivo em disco executar testes contra.Às vezes, existem realmente duas (ou mais) exceções que podemos lançar de um método, mas se mantivermos o SRP e o DIP, os momentos em que encontramos essa situação se tornam mais raros.
fonte
update
método é o mais atômico possível, e as exceções estão no nível apropriado de abstração, sim, parece que você precisa lançar dois tipos diferentes de exceções, pois existem dois casos excepcionais. Essa deve ser a medida de quantas exceções podem ser lançadas, e não essas (às vezes arbitrárias) regras de linter.catch (IOException | SQLException ex)
) porque o problema real está no modelo / design do programa.Lembro-me de brincar um pouco com isso quando brincava com Java há algum tempo, mas não estava realmente consciente da distinção entre marcado e desmarcado até ler sua pergunta. Encontrei este artigo no Google rapidamente, e ele entra em parte da aparente controvérsia:
http://tutorials.jenkov.com/java-exception-handling/checked-or-unchecked-exceptions.html
Dito isso, um dos problemas que esse cara estava mencionando com exceções verificadas é que (e eu o deparei pessoalmente desde o início com Java) se você continuar adicionando várias exceções verificadas às
throws
cláusulas nas declarações de método, não apenas você precisa colocar muito mais código clichê para suportá-lo à medida que você avança para métodos de nível superior, mas também cria uma confusão maior e quebra a compatibilidade quando você tenta introduzir mais tipos de exceção nos métodos de nível inferior. Se você adicionar um tipo de exceção verificado a um método de nível inferior, precisará executar novamente seu código e ajustar várias outras declarações de método.Um ponto de mitigação mencionado no artigo - e o autor não gostou disso pessoalmente - foi criar uma exceção de classe base, limitar suas
throws
cláusulas para usá-la apenas e depois elevar subclasses internamente. Dessa forma, você pode criar novos tipos de exceção verificados sem precisar executar novamente todo o seu código.O autor deste artigo pode não ter gostado muito disso, mas faz todo o sentido na minha experiência pessoal (especialmente se você puder procurar o que todas as subclasses são de qualquer maneira), e aposto que é por isso que o conselho que você está recebendo é: limite tudo a um tipo de exceção verificado cada. Além disso, os conselhos que você mencionou permitem vários tipos de exceções verificadas em métodos não públicos, o que faz todo o sentido se esse for o motivo (ou mesmo o contrário). Se for apenas um método privado ou algo semelhante, você não terá metade da sua base de código quando mudar uma coisinha.
Você perguntou em grande parte se esse é um padrão aceito, mas entre sua pesquisa mencionada, este artigo razoavelmente pensado e apenas falando da experiência pessoal em programação, certamente não parece se destacar de maneira alguma.
fonte
Throwable
e terminar com ele, em vez de inventar toda a sua própria hierarquia?Lançar várias exceções verificadas faz sentido quando há várias coisas razoáveis a serem feitas.
Por exemplo, digamos que você tenha um método
isso viola a regra de exceção do pne, mas faz sentido.
Infelizmente, o que geralmente acontece são métodos como
Aqui há poucas chances de o chamador fazer algo específico com base no tipo de exceção. Portanto, se quisermos convencê-lo a perceber que esses métodos PODEM e, às vezes, darão errado, lançar apenas SomethingMightGoWrongException é muito melhor.
Portanto, regra no máximo uma exceção verificada.
Mas se o seu projeto usar o design onde houver várias exceções verificadas significativas, essa regra não se aplicará.
Sidenote: Algo pode realmente dar errado em quase todos os lugares, para que você possa pensar em usar? estende RuntimeException, mas há uma diferença entre "todos cometemos erros" e "isso fala do sistema externo e, às vezes, fica inativo, lida com isso".
fonte