Por que um método não deve lançar vários tipos de exceções verificadas?


47

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?


7
O que você acha? A explicação que você citou do SonarQube parece sensata; você tem algum motivo para duvidar disso?
Robert Harvey

3
Escrevi muito código que lança mais de uma exceção e fiz uso de muitas bibliotecas que também lançam mais de uma exceção. Além disso, em livros / artigos sobre tratamento de exceções, o tópico de limitar o número de exceções lançadas geralmente não é abordado. Mas muitos dos exemplos mostram jogando / pegando múltiplos, dando uma aprovação implícita à prática. Portanto, achei a regra surpreendente e queria fazer mais pesquisas sobre as melhores práticas / filosofia de tratamento de exceções do que apenas os exemplos básicos de instruções.
Sdoca

Respostas:


32

Vamos considerar a situação em que você tem o código fornecido de:

public void delete() throws IOException, SQLException {      // Non-Compliant
  /* ... */
}

O perigo aqui é que o código que você escreve para chamar delete()terá a seguinte aparência:

try {
  foo.delete()
} catch (Exception e) {
  /* ... */
}

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:

ThrowsCount

Restrições lança instruções para uma contagem especificada (1 por padrão).

Fundamentação da petição: Exceções fazem parte da interface de um método. Declarar um método para lançar muitas exceções com raízes diferentes torna oneroso o tratamento de exceções e leva a práticas de programação ruins, como escrever código como catch (Exception Ex). Essa verificação força os desenvolvedores a colocar exceções em uma hierarquia, de modo que, no caso mais simples, apenas um tipo de exceção precise ser verificado por um chamador, mas qualquer subclasse pode ser capturada especificamente, se necessário.

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:

public void delete() throws IOException, SQLException {
  /* ... */
}

e

try {
  foo.delete()
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

ou

try {
    foo.delete()
} catch (Exception ex) {
    logger.log(ex);
    throw ex;
}

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 Exceptionque significa que a assinatura do método seria deleteSomething() throws Exceptionapenas uma bagunça na pilha, pois as pessoas que usam seu código agora são forçadas a fazê -lo catch(Exception e).

Com o Java 7, isso não é tão importante porque você pode:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

Além disso, a verificação de tipo, se um não pegar os tipos de exceções sendo lançadas:

public void rethrowException(String exceptionName)
throws IOException, SQLException {
    try {
        foo.delete();
    } catch (Exception e) {
        throw e;
    }
}

O verificador de tipos reconhecerá que epode ser apenas do tipo IOExceptionou SQLException. 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.


Aceitar isso como resposta, pois responde à minha pergunta sobre ser um padrão bem aceito. No entanto, ainda estou lendo as várias discussões pró / contra começando com o link fornecido por Panzercrisis em sua resposta para determinar qual será meu padrão.
Sdoca

"O verificador de tipos reconhecerá que e pode ser apenas do tipo IOException ou SQLException.": O que isso significa? O que acontece quando uma exceção de outro tipo é lançada foo.delete()? Ainda é capturado e relançado?
Giorgio

@Giorgio Será um erro de tempo de compilação se deletelanç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 ao Exceptiontipo comum, o que deixa triste a análise estática e outros codificadores.

Eu vejo. Parece um pouco complicado para mim. Eu achava mais intuitivo proibir catch (Exception e)e forçá-lo a ser um catch (IOException e)ou catch (SQLException e)outro.
Giorgio

@Giorgio É um passo incremental do Java 6 para facilitar a gravação de um bom código. Infelizmente, a opção de escrever código incorreto estará conosco por um longo tempo. Lembre-se de que o Java 7 pode fazer isso 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.

22

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:

public String getData(int id) throws 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 um SQLException:

public String getData(int id) throws SQLException

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:

public String getData(int id) throws InvalidRecordException

Agora, se mudarmos a implementação interna, podemos simplesmente agrupar essa exceção em InvalidRecordExceptione transmiti-la (ou não, e simplesmente lançar uma nova InvalidRecordException). 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:

public Record parseFile(String filename) throws IOException, ParseException

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:

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

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 parsemais 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.


Concordo totalmente com a quebra de exceções de nível inferior, como no seu exemplo. Fazemos isso regularmente e lançamos variações de MyAppExceptions. Um dos exemplos em que estou lançando várias exceções é ao tentar atualizar um registro em um banco de dados. Este método lança um RecordNotFoundException. No entanto, o registro só pode ser atualizado se estiver em um determinado estado; portanto, o método também lança uma InvalidRecordStateException. Eu acho que isso é válido e fornece ao chamador informações valiosas.
Sdoca

@sdoca Se seu updatemé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.
Cbojar

2
Mas se eu tenho um método que lê dados de um fluxo e o analisa à medida que avança, não posso dividir essas duas funções sem ler o fluxo inteiro em um buffer, o que pode ser complicado. Além disso, o código que decide como lidar adequadamente com as exceções pode ser separado do código que faz a leitura e a análise. Quando estou escrevendo o código de leitura e análise, não sei como o código que chama o meu código pode lidar com qualquer tipo de exceção, portanto, preciso deixar os dois passarem.
user3294068

+1: Gosto muito dessa resposta, principalmente porque lida com o problema do ponto de vista da modelagem. Freqüentemente, não é necessário usar outro idioma (como catch (IOException | SQLException ex)) porque o problema real está no modelo / design do programa.
Giorgio

3

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 throwsclá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 throwsclá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.


2
Por que não apenas declarar arremessos Throwablee terminar com ele, em vez de inventar toda a sua própria hierarquia?
Deduplicator

@Duplicador Esse é o motivo pelo qual o autor também não gostou da idéia; ele imaginou que você poderia usar todos os itens desmarcados, se quiser fazer isso. Mas se quem estiver usando a API (possivelmente seu colega de trabalho) tiver uma lista de todas as exceções subclassificadas que derivam de sua classe base, eu posso ver um pouco de benefício em pelo menos deixá-los saber que todas as exceções esperadas são dentro de um certo conjunto de subclasses; então, se eles sentirem que um deles é mais "manejável" que os outros, eles não estariam tão propensos a renunciar a um manipulador específico para ele.
Panzercrisis

A razão pela qual as exceções verificadas em geral são um karma ruim é simples: elas são virais, infectando todos os usuários. Especificar uma especificação intencional o mais ampla possível é apenas uma maneira de dizer que todas as exceções estão desmarcadas e, assim, evitar a bagunça. Sim, documentar o que você pode querer lidar é uma boa idéia, para a documentação : saber apenas quais exceções podem vir de uma função tem um valor estritamente limitado (além de nenhuma / talvez uma, mas o Java não permite isso de qualquer maneira) .
Deduplicator

1
@Duplicator Não estou endossando a idéia e nem necessariamente a favor. Estou apenas falando sobre a direção de onde os conselhos do OP foram dados.
Panzercrisis

1
Obrigado pelo link. Foi um ótimo ponto de partida para a leitura deste tópico.
Sdoca

-1

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

public void doSomething(Credentials cred, Work work) 
    throws CredentialsRequiredException, TryAgainLaterException{...}

isso viola a regra de exceção do pne, mas faz sentido.

Infelizmente, o que geralmente acontece são métodos como

void doSomething() 
    throws IOException, JAXBException,SQLException,MyException {...}

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".


1
"várias coisas razoáveis ​​para fazer" dificilmente estão em conformidade com o srp - esse ponto foi exposto em resposta anterior
gnat

Faz. Você pode DETECTAR um problema em uma função (manipulação de um aspecto) e outro problema em outra (também manipulação de um aspecto) chamado de uma função que trata de uma coisa, ou seja, chamar essas duas funções. E o chamador pode, em uma camada de tentativa, capturar (em uma função) MANIPULAR um problema e passar para cima outro e lidar com esse problema sozinho.
user470365
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.