Lançar novas RuntimeExceptions em código inacessível é um estilo ruim?

31

Fui designado para manter um aplicativo escrito há algum tempo por desenvolvedores mais qualificados. Me deparei com este pedaço de código:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
        try {
            return translate(mailManagementService.retrieveUserMailConfiguration(id));
        } catch (Exception e) {
            rethrow(e);
        }
        throw new RuntimeException("cannot reach here");
    }

Estou curioso para saber se jogar RuntimeException("cannot reach here")é justificado. Provavelmente estou perdendo algo óbvio, sabendo que esse código vem de um colega mais experiente.
EDIT: Aqui está o corpo de retrocesso a que algumas respostas se referem. Considero que não é importante nesta questão.

private void rethrow(Exception e) throws MailException {
        if (e instanceof InvalidDataException) {
            InvalidDataException ex = (InvalidDataException) e;
            rethrow(ex);
        }
        if (e instanceof EntityAlreadyExistsException) {
            EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
            rethrow(ex);
        }
        if (e instanceof EntityNotFoundException) {
            EntityNotFoundException ex = (EntityNotFoundException) e;
            rethrow(ex);
        }
        if (e instanceof NoPermissionException) {
            NoPermissionException ex = (NoPermissionException) e;
            rethrow(ex);
        }
        if (e instanceof ServiceUnavailableException) {
            ServiceUnavailableException ex = (ServiceUnavailableException) e;
            rethrow(ex);
        }
        LOG.error("internal error, original exception", e);
        throw new MailUnexpectedException();
    }


private void rethrow(ServiceUnavailableException e) throws
            MailServiceUnavailableException {
        throw new MailServiceUnavailableException();
    }

private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
    throw new PersonNotAuthorizedException();
}

private void rethrow(InvalidDataException e) throws
        MailInvalidIdException, MailLoginNotAvailableException,
        MailInvalidLoginException, MailInvalidPasswordException,
        MailInvalidEmailException {
    switch (e.getDetail()) {
        case ID_INVALID:
            throw new MailInvalidIdException();
        case LOGIN_INVALID:
            throw new MailInvalidLoginException();
        case LOGIN_NOT_ALLOWED:
            throw new MailLoginNotAvailableException();
        case PASSWORD_INVALID:
            throw new MailInvalidPasswordException();
        case EMAIL_INVALID:
            throw new MailInvalidEmailException();
    }
}

private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
    switch (e.getDetail()) {
        case LOGIN_ALREADY_TAKEN:
            throw new MailLoginNotAvailableException();
        case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
            throw new MailEmailAddressAlreadyForwardedToException();
    }
}

private void rethrow(EntityNotFoundException e) throws
        MailAccountNotCreatedException,
        MailAliasNotCreatedException {
    switch (e.getDetail()) {
        case ACCOUNT_NOT_FOUND:
            throw new MailAccountNotCreatedException();
        case ALIAS_NOT_FOUND:
            throw new MailAliasNotCreatedException();
    }
}
Sok Pomaranczowy
fonte
12
é tecnicamente acessível, se rethrownão for realmente throwuma exceção. (o que pode acontecer algum dia, se a implementação for alterada)
njzk2 27/07/2015
34
Como um aparte, um AssertionError pode ser uma escolha semanticamente melhor que RuntimeException.
JVR 27/07
1
O lance final é redundante. Se você ativar o findbugs ou outras ferramentas de análise estática, ele indicaria esses tipos de linhas para remoção.
Bon Ami
7
Agora que vejo o restante do código, estou pensando que talvez você deva mudar "desenvolvedores mais qualificados" para " desenvolvedores mais antigos ". A Code Review ficaria feliz em explicar o porquê.
JVR 28/07
3
Tentei criar exemplos hipotéticos de por que as exceções são terríveis. Mas nada que eu já tenha feito é uma vela para isso.
Paul Draper

Respostas:

36

Primeiro, obrigado por atualizar sua pergunta e nos mostrar o que rethrowfaz. Então, de fato, o que ele faz é converter exceções com propriedades em classes de exceções mais refinadas. Mais sobre isso mais tarde.

Como eu realmente não respondi a pergunta principal originalmente, aqui vai: sim, geralmente é um estilo ruim lançar exceções de tempo de execução em código inacessível; é melhor usar afirmações ou, melhor ainda, evitar o problema. Como já apontado, o compilador aqui não pode ter certeza de que o código nunca sai do try/catchbloco. Você pode refatorar seu código aproveitando que ...

Erros são valores

(Sem surpresa, é bem conhecido em movimento )

Vamos usar um exemplo mais simples, o que eu usei antes da sua edição: imagine que você esteja registrando algo e criando uma exceção de invólucro, como na resposta do Konrad . Vamos chamá-lo logAndWrap.

Em vez de lançar a exceção como um efeito colateral de logAndWrap, você poderia deixá-lo fazer seu trabalho como efeito colateral e fazê-lo retornar uma exceção (pelo menos, a que foi fornecida na entrada). Você não precisa usar genéricos, apenas funções básicas:

private Exception logAndWrap(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    return new CustomWrapperException(exception);
}

Então, você throwexplicitamente, e seu compilador está feliz:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     throw logAndWrap(e);
}

E se você esquecer throw?

Como explicado no comentário de Joe23 , uma maneira defensiva de programação para garantir que a exceção seja sempre lançada consistiria em fazer explicitamente um throw new CustomWrapperException(exception)no final de logAndWrap, como é feito pelo Guava.Throwables . Dessa forma, você sabe que a exceção será lançada e seu analisador de tipos ficará feliz. No entanto, suas exceções personalizadas precisam ser desmarcadas, o que nem sempre é possível. Além disso, eu classificaria o risco de um desenvolvedor ausente gravar throwmuito baixo: o desenvolvedor deve esquecê-lo e o método circundante não deve retornar nada, caso contrário, o compilador detectaria um retorno ausente. Esta é uma maneira interessante de combater o sistema de tipos, e funciona, no entanto.

Rethrow

O real também rethrowpode ser escrito como uma função, mas tenho problemas com sua implementação atual:

  • Existem muitos elencos inúteis como Elencos são de fato necessários (ver comentários):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
    
  • Ao lançar / retornar novas exceções, a antiga é descartada; no código a seguir, a MailLoginNotAvailableExceptionnão me permite saber qual login não está disponível, o que é inconveniente; além disso, os traços de pilha serão incompletos:

    private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
        switch (e.getDetail()) {
            case LOGIN_ALREADY_TAKEN:
                throw new MailLoginNotAvailableException();
            case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
                throw new MailEmailAddressAlreadyForwardedToException();
        }
    }
    
  • Por que o código de origem não lança essas exceções especializadas em primeiro lugar? Eu suspeito que rethrowseja usado como uma camada de compatibilidade entre um subsistema (de correspondência) e a lógica de negócios (talvez a intenção seja ocultar detalhes de implementação, como exceções lançadas, substituindo-as por exceções personalizadas). Mesmo se eu concordar que seria melhor ter um código sem captura, como sugerido na resposta de Pete Becker , não acho que você terá a oportunidade de remover o código catche rethrowaqui sem grandes refatorações.

coredump
fonte
1
Os elencos não são inúteis. Eles são necessários, porque não há despacho dinâmico com base no tipo de argumento. O tipo de argumento deve ser conhecido no momento da compilação para chamar o método correto.
Joe23
No seu logAndWrapmétodo, você pode lançar a exceção em vez de retorná-la, mas mantenha o tipo de retorno (Runtime)Exception. Dessa forma, o bloco catch pode permanecer como você o escreveu (sem o lançamento de código inacessível). Mas tem a vantagem adicional de que você não pode esquecer o throw. Se você retornar a exceção e esquecer o lançamento, isso fará com que a exceção seja silenciosamente engolida. Lançar enquanto tiver um tipo de retorno RuntimeException fará o compilador feliz, além de evitar esses erros. É assim que o Guava Throwables.propagateé implementado.
Joe23
@ Joe23 Obrigado pelo esclarecimento sobre o envio estático. Sobre o lançamento de exceções dentro da função: você quer dizer que o possível erro que você está descrevendo é um bloco catch que chama, logAndWrapmas não faz nada com a exceção retornada? Isso é interessante. Dentro de uma função que deve retornar um valor, o compilador observaria que há um caminho sem nenhum valor retornado, mas isso é útil para métodos que não retornam nada. Obrigado novamente.
Coredump
1
É exatamente isso que eu mentir. E só para enfatizar o ponto novamente: não é algo que eu criei e que tenha sido creditado, mas obtido da fonte da goiaba.
Joe23
@ Joe23 eu adicionei uma referência explícita à biblioteca
coredump
52

Essa rethrow(e);função viola o princípio que afirma que, em circunstâncias normais, uma função retornará, enquanto em circunstâncias excepcionais, uma função lançará uma exceção. Essa função viola esse princípio lançando uma exceção em circunstâncias normais. Essa é a fonte de toda a confusão.

O compilador assume que essa função retornará em circunstâncias normais; portanto, até onde o compilador pode dizer, a execução pode chegar ao final da retrieveUserMailConfigurationfunção; nesse momento, é um erro não ter uma returninstrução. A RuntimeExceptionjogada lá deve aliviar essa preocupação do compilador, mas é uma maneira bastante desajeitada de fazê-lo. Outra maneira de evitar o function must return a valueerro é adicionar uma return null; //to keep the compiler happydeclaração, mas isso é igualmente desajeitado na minha opinião.

Então, pessoalmente, eu substituiria isso:

rethrow(e);

com isso:

report(e); 
throw e;

ou, melhor ainda, (como sugerido pelo coredump ), com isso:

throw reportAndTransform(e);

Assim, o fluxo de controle seria tornado óbvio para o compilador, para que sua final throw new RuntimeException("cannot reach here");se tornasse não apenas redundante, mas na verdade nem compilável, pois seria sinalizada pelo compilador como código inacessível.

Essa é a maneira mais elegante e também mais simples de sair dessa situação feia.

Mike Nakis
fonte
1
-1 A sugestão de dividir a função em duas instruções pode introduzir erros de programação devido a uma cópia / colagem incorreta; Também estou um pouco preocupado com o fato de que você editou sua pergunta para sugerir throw reportAndTransform(e)um par de minutos depois que eu postei a minha própria resposta. Talvez você tenha modificado sua pergunta independentemente, e peço desculpas antecipadamente, se assim for, mas, caso contrário, dar um pouco de crédito é algo que as pessoas costumam fazer.
Coredump
4
@coredump De fato, li sua sugestão e até li outra resposta que atribui essa sugestão a você, e lembrei-me de que realmente empreguei exatamente esse construto no passado, então decidi incluí-lo na minha resposta. Eu pensei que não era tão importante para incluir uma atribuição, porque não estamos falando de uma ideia original aqui, mas se você discordar disso, não quero agravá-lo, então a atribuição está agora lá, complete com um link. Felicidades.
35630 Mike Nakis
Não é que eu tenha uma patente para essa construção, o que é bastante comum; mas imagine que você escreve uma resposta e, pouco tempo depois, ela é "assimilada" por outra. Portanto, a atribuição é muito apreciada. Obrigado.
Coredump
3
Não há nada de errado em si com uma função que não retorna. Isso acontece o tempo todo, por exemplo, em sistemas em tempo real. O problema final é uma falha no java, pois a linguagem analisa e reforça o conceito de correção das linguagens e, no entanto, a linguagem não fornece um mecanismo para anotar de alguma maneira a anotação de que uma função não retorna. No entanto, existem padrões de design que contornam isso, como throw reportAndTransform(e). Muitos padrões de design são patches sem recursos de idioma. Este é um deles.
David Hammen
2
Por outro lado, muitas línguas modernas são complexas o suficiente. Transformar cada padrão de design em um recurso de linguagem principal os inchará ainda mais. Este é um bom exemplo de um padrão de design que não pertence ao idioma principal. Como está escrito, é bem entendido não apenas pelo compilador, mas também por muitas outras ferramentas que precisam analisar o código.
MSalters
7

O throwprovavelmente foi adicionado para contornar o "método deve retornar um valor" erro que ocorreria de outra maneira - os dados Java fluir analisador é bastante inteligente para compreender que não returné necessário depois de um throw, mas não após o seu costume rethrow()método, e não há nenhuma @NoReturnanotação que você poderia usar para corrigir isso.

No entanto, a criação de uma nova exceção em código inacessível parece supérflua. Eu simplesmente escreveria return null, sabendo que, de fato, nunca acontece.

Kilian Foth
fonte
Você está certo. Eu verifiquei o que aconteceria se eu comentar a throw new...linha e ela reclama de nenhuma declaração de retorno. É uma programação ruim? Existe alguma convenção sobre a substituição de uma declaração de retorno inacessível? Deixarei esta pergunta sem resposta por um tempo para incentivar mais comentários. No entanto, obrigado pela sua resposta.
Sok Pomaranczowy
3
@SokPomaranczowy Sim, é uma programação ruim. Esse rethrow()método oculta o fato de que catchestá repetindo a exceção. Eu evitaria fazer algo assim. Além disso, é claro, rethrow()na verdade , pode falhar em repetir a exceção; nesse caso, algo mais acontece.
Ross Patterson
26
Por favor, não substitua a exceção por return null. Com a exceção, se alguém no futuro quebrar o código para que a última linha se torne acessível, ficará imediatamente claro quando a exceção for lançada. Se preferir return null, agora você tem um nullvalor flutuando no aplicativo que não era esperado. Quem sabe onde na aplicação NullPointerExceptionsurgirá? A menos que você tenha sorte e seja imediatamente após a chamada para essa função, será muito difícil rastrear o problema real.
Unholysampler
5
@MatthewRead A execução do código que se pretende inacessível é um bug grave, return nullé provável que oculte o bug porque você não pode distinguir os outros casos em que ele retorna nulldesse bug.
CodesInChaos 28/07
4
Além disso, se a função já retornar nulo em algumas circunstâncias e você também usar um retorno nulo nessa circunstância, os usuários terão agora dois cenários possíveis em que podem estar quando receberem um retorno nulo. Às vezes isso não importa, porque o retorno nulo já significa "não há objeto e não estou especificando o porquê", portanto, adicionar mais um motivo possível para fazer pouca diferença. Mas muitas vezes adicionar ambiguidade é ruim, e certamente significa que os bugs serão tratados inicialmente como "certas circunstâncias" em vez de parecer imediatamente "isso está quebrado". Falhe cedo.
21715 Steve
6

o

throw new RuntimeException("cannot reach here");

A declaração deixa claro para uma PESSOA que está lendo o código, o que está acontecendo, por isso é muito melhor do que retornar nulo, por exemplo. Também facilita a depuração se o código for alterado de maneira inesperada.

No entanto, rethrow(e)apenas parece errado! Então, no seu caso , acho que refatorar o código é uma opção melhor. Veja as outras respostas (eu gosto mais do coredump) para saber como resolver seu código.

Ian
fonte
4

Não sei se existe uma convenção.

De qualquer forma, outro truque seria fazer o seguinte:

private <T> T rethrow(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    throw new CustomWrapperException(exception);
}

Permitindo isso:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     return rethrow(e);
}

E não RuntimeExceptioné mais necessário artificial . Mesmo que rethrownunca realmente retorne qualquer valor, é bom o suficiente para o compilador agora.

Ele retorna um valor na teoria (assinatura do método) e, em seguida, fica isento de fazê-lo, pois gera uma exceção.

Sim, pode parecer estranho, mas, novamente - jogar um fantasma RuntimeExceptionou retornar nulos que nunca serão vistos neste mundo - também não é exatamente uma coisa de beleza.

Buscando a legibilidade, você pode renomear rethrowe ter algo como:

} catch (Exception e) {
     return nothingJustRethrow(e);
}
Konrad Morawski
fonte
2

Se você remover o trybloco completamente, não precisará do rethrowou do throw. Este código faz exatamente a mesma coisa que o original:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
    return translate(mailManagementService.retrieveUserMailConfiguration(id));
}

Não deixe que o fato de ser de desenvolvedores mais experientes o engane. Isso é código podre, e acontece o tempo todo. Apenas conserte.

Edição: Eu li errado rethrow(e)como simplesmente re-lançando a exceção e. Se esse rethrowmétodo realmente faz algo diferente de repetir a exceção, livrar-se dela altera a semântica desse método.

Pete Becker
fonte
As pessoas continuarão acreditando que capturas de caracteres curinga que não lidam com nada são realmente manipuladores de exceção. Sua versão desconsidera essa suposição falha por não fingir lidar com aquilo que não pode. O chamador de retrieveUserMailConfiguration () espera receber algo de volta. Se essa função não puder retornar algo razoável, ela deverá falhar rápida e alta. É como se as outras respostas não tivessem problema com o catch(Exception)cheiro odioso do código.
msw
1
@msw - Codificação de cultos de carga.
Pete Becker
@msw - por outro lado, fornece um local para definir um ponto de interrupção.
Pete Becker
1
É como se você não vê problema em descartar toda uma parte da lógica. Sua versão claramente não faz a mesma coisa que a original, que, a propósito, já falha rapidamente e em voz alta. Eu preferiria escrever métodos de captura livre, como o da sua resposta, mas ao trabalhar com o código existente, não devemos quebrar contratos com outras partes de trabalho. Pode ser que o que é feito rethrowseja inútil (e esse não é o ponto aqui), mas você não pode simplesmente se livrar do código que não gosta e dizer que é equivalente ao original quando claramente não é. Existem efeitos colaterais na rethrowfunção personalizada .
Coredump
1
@ jpmc26 O núcleo da questão é sobre a exceção "não é possível chegar aqui", que pode surgir por outros motivos além do bloco try / catch anterior. Mas eu concordo que o bloco cheira a peixe, e se essa resposta fosse redigida com mais cuidado originalmente, teria ganhado muito mais votos (não diminuí o voto). A última edição é boa.
Coredump