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

44

Usamos o SonarQube para analisar nosso código Java e essa regra (definida como crítica):

Public methods should throw at most one checked exception

Using checked exceptions forces method callers to deal with errors, either by propagating them or by handling them. This makes those exceptions fully part of the API of the method.

To keep the complexity for callers reasonable, methods should not throw more than one kind of checked exception."

Outra parte do Sonar tem isto :

Public methods should throw at most one checked exception

Using checked exceptions forces method callers to deal with errors, either by propagating them or by handling them. This makes those exceptions fully part of the API of the method.

To keep the complexity for callers reasonable, methods should not throw more than one kind of checked exception.

The following code:

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

should be refactored into:

public void delete() throws SomeApplicationLevelException {  // Compliant
    /* ... */
}

Overriding methods are not checked by this rule and are allowed to throw several checked exceptions.

Nunca encontrei essa regra / recomendação em minhas leituras sobre o tratamento de exceções e tentei encontrar alguns padrões, discussões, etc. sobre o tópico. A única coisa que eu encontrei é isso do CodeRach: Quantas exceções um método deve lançar mais?

Este é um padrão bem aceito?

    
por sdoca 26.11.2014 / 21:56
fonte

4 respostas

31

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() será:

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

Isso também é ruim. E ele será pego com outra regra que sinaliza a captura da classe Exception base.

A chave é não escrever o código que faz você querer escrever um código ruim em outro lugar.

A regra que você está encontrando é bastante comum. O Checkstyle tem nas suas regras de design:

ThrowsCount

Restricts throws statements to a specified count (1 by default).

Rationale: Exceptions form part of a method's interface. Declaring a method to throw too many differently rooted exceptions makes exception handling onerous and leads to poor programming practices such as writing code like catch(Exception ex). This check forces developers to put exceptions into a hierarchy such that in the simplest case, only one type of exception need be checked for by a caller but any subclasses can be caught specifically if necessary.

Isso descreve precisamente o problema e qual é o problema e por que você não deve fazer isso. É um padrão bem aceito que muitas ferramentas de análise estática identificarão e sinalizarão.

E enquanto você pode fazê-lo de acordo com o design da linguagem, e pode ser momentos em que é a coisa certa a fazer, é algo que você deve ver e imediatamente "hum, por que estou fazendo isso?" Pode ser aceitável para o código interno, onde todos são disciplinados o suficiente para nunca catch (Exception e) {} , mas na maioria das vezes eu vi pessoas cortando cantos, especialmente em situações internas.

Não faça pessoas usando sua turma quiserem escrever códigos ruins.

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 (Captando vários tipos de exceção e relançando as exceções com verificação de tipos aprimorada do Oracle).

Com o Java 6 e anterior, você teria um código semelhante a:

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 DRY . Vários blocos fazendo a mesma coisa, de novo e de novo - uma vez para cada exceção. Você deseja registrar a exceção e relançá-la? Está bem. As mesmas linhas de código para cada exceção.

A segunda opção é pior por vários motivos. Primeiro, isso significa que você está pegando todas as exceções. Ponteiro nulo é pego lá (e não deveria). Além disso, você está relançando um Exception , o que significa que a assinatura do método seria deleteSomething() throws Exception , o que só faz uma bagunça na pilha, já que as pessoas que usam seu código agora são forçadas para catch(Exception e) . / p>

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

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

Além disso, o tipo que verifica se um faz capturar os tipos de exceção que estão sendo lançados:

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

O verificador de tipos reconhecerá que e pode somente ser dos tipos IOException ou SQLException . Eu ainda não estou muito entusiasmado com o uso desse estilo, mas ele não está causando código tão ruim quanto era no Java 6 (onde ele forçaria você a ter a assinatura do método como a superclasse que as exceções estendem). / p>

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. Tenho a tendência de concordar com um aviso para que ainda seja aplicado, mas você pode alterar a prioridade para eles em maior ou menor de acordo com a forma como sua equipe os prioriza.

Se as exceções devem ser marcadas ou desmarcadas ... essa é uma questão de g r e um t debate que é fácil encontrar inúmeros posts ocupando cada lado do argumento. No entanto, se você estiver trabalhando com exceções verificadas, provavelmente deverá evitar lançar vários tipos, pelo menos no Java 6.

    
por 27.11.2014 / 01:59
fonte
21

A razão pela qual você deseja, idealmente, lançar apenas um tipo de exceção é porque fazer isso provavelmente viola a Responsabilidade única e Inversão de Dependência . Vamos usar um exemplo para demonstrar.

Digamos que tenhamos um método que busca dados de persistência e que a persistência seja um conjunto de arquivos. Como estamos lidando com arquivos, podemos ter um FileNotFoundException :

public String getData(int id) throws FileNotFoundException

Agora, temos uma mudança nos requisitos e nossos dados vêm de um banco de dados. Em vez de um FileNotFoundException (já que 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 temos que verificar, senão o código não será compilado. Se o nosso método for chamado longe, isso pode ser muito para mudar / mudar os outros. Demora muito tempo e as pessoas não vão ser felizes.

A inversão de dependência diz que realmente não devemos lançar nenhuma dessas exceções porque elas expõem os detalhes da 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 estar preocupado 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 alterarmos a implementação interna, podemos simplesmente agrupar essa exceção em um InvalidRecordException e passá-la (ou não envolvê-la e lançar um novo InvalidRecordException ). Código externo não sabe ou não importa qual tipo de persistência está sendo usado. Está tudo encapsulado.

Quanto à responsabilidade única, precisamos pensar no código que gera várias exceções não relacionadas. Digamos que tenhamos 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 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 interromper 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 qualquer dado String para os dados de análise de qualquer fonte: na memória, arquivo, rede, etc. Também podemos testar parse mais facilmente agora, porque não precisamos de um arquivo no disco para executar testes contra.

Às vezes, existem duas (ou mais) exceções que podemos usar em um método, mas se nos ativermos ao SRP e ao DIP, as vezes que nos deparamos com essa situação se tornam mais raras.

    
por 29.11.2014 / 06:56
fonte
3

Eu me lembro de brincar um pouco com isso quando joguei com o Java um tempo atrás, mas eu não estava realmente consciente da distinção entre marcada e não marcada até eu ler sua pergunta. Eu encontrei este artigo no Google rapidamente, e entra em parte da aparente controvérsia:

link

Dito isso, um dos problemas que esse cara estava mencionando com exceções verificadas é que (e eu pessoalmente encontrei isso desde o começo com Java) se você continuar adicionando um monte de exceções verificadas em cláusulas throws em suas declarações de método, você não só precisa incluir muito mais código padrão para suportá-lo, mas também faz uma grande bagunça e quebra a compatibilidade quando você tenta introduzir mais tipos de exceção para diminuir métodos de nível. Se você adicionar um tipo de exceção verificada a um método de nível inferior, será necessário executar novamente o 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 cláusulas throws para usá-lo apenas e, em seguida, apenas criar subclasses dele internamente. Dessa forma, você pode criar novos tipos de exceção verificados sem precisar percorrer todo o 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 todas as subclasses de qualquer maneira), e aposto que é por isso que você está sendo dado é limitar tudo para um tipo de exceção verificada cada. O que é mais importante é que o conselho que você mencionou realmente permite vários tipos de exceção verificados em métodos não públicos, o que faz muito sentido se esse é o motivo deles (ou até mesmo de outra forma). Se é apenas um método privado ou algo similar de qualquer maneira, você não vai ter metade da sua base de código quando você muda uma pequena coisa.

Você perguntou em grande parte se este é um padrão aceito, mas entre sua pesquisa que você mencionou, este artigo razoavelmente pensado, e apenas falando da experiência de programação pessoal, certamente não parece se destacar de qualquer forma. / p>     

por 26.11.2014 / 22:40
fonte
-1

O lançamento de 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, mas faz sentido.

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

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

Aqui há pouca chance de o chamador fazer algo específico com base no tipo de exceção. Então, se queremos empurrá-lo para perceber que esses métodos podem e, por vezes, dar errado, jogando apenas SomethingMightGoWrongException é enogh e melhor.

Por isso, regra no máximo uma exceção verificada.

Mas, se o design do seu projeto usar várias exceções selecionadas significativas, essa regra não deverá se aplicar.

Sidenote: Algo realmente pode dar errado em quase todos os lugares, então pode-se pensar em usar? estende RuntimeException, mas há diferença entre "todos nós cometemos erros" e "isso fala sistema externo e às vezes será baixo, lidar com isso".

    
por 29.11.2014 / 15:06
fonte