Tudo bem usar exceções como ferramentas para "capturar" erros mais cedo?

29

Eu uso exceções para detectar problemas mais cedo. Por exemplo:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

Meu programa nunca deve passar nullnessa função. Eu nunca pretendo. No entanto, como todos sabemos, coisas indesejadas acontecem na programação.

Lançar uma exceção, se esse problema ocorrer, permite que eu o localize e corrija antes que cause mais problemas em outros locais do programa. A exceção interrompe o programa e me diz "coisas ruins aconteceram aqui, corrija-as". Em vez disso, nullmudar o programa causando problemas em outros lugares.

Agora, você está certo. Nesse caso, nullisso simplesmente causaria uma NullPointerExceptionimediata, portanto pode não ser o melhor exemplo.

Mas considere um método como este, por exemplo:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Nesse caso, a nullcomo o parâmetro seria passado ao redor do programa e poderá causar erros muito mais tarde, o que dificilmente será rastreado até sua origem.

Alterando a função da seguinte maneira:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Permite identificar o problema muito antes que ele cause erros estranhos em outros lugares.

Além disso, uma nullreferência como parâmetro é apenas um exemplo. Pode haver muitos tipos de problemas, de argumentos inválidos a qualquer outra coisa. É sempre melhor identificá-los mais cedo.


Portanto, minha pergunta é simples: essa é uma boa prática? Meu uso de exceções como ferramentas de prevenção de problemas é bom? É uma aplicação legítima de exceções ou é problemática?

Aviv Cohn
fonte
2
O downvoter pode explicar o que há de errado com esta pergunta?
Giorgio
2
"falha cedo, falha frequentemente"
Bryan Chen
16
É para isso que existem exceções.
precisa saber é o seguinte
Observe que os contratos de codificação permitem detectar muitos desses erros estaticamente. Isso geralmente é superior a lançar uma exceção no tempo de execução. O suporte e a eficácia dos contratos de codificação variam de acordo com o idioma.
Brian
3
Como você está lançando IllegalArgumentException, é redundante dizer " Pessoa de entrada ".
kevin cline

Respostas:

29

Sim, "falhar cedo" é um princípio muito bom, e essa é simplesmente uma maneira possível de implementá-lo. E nos métodos que precisam retornar um valor específico, não há realmente muito mais que você possa fazer para falhar deliberadamente - é lançar exceções ou disparar afirmações. Exceções devem sinalizar condições 'excepcionais', e detectar um erro de programação certamente é excepcional.

Kilian Foth
fonte
Falhar cedo é o caminho a percorrer, se não houver maneira de se recuperar de um erro ou condição. Por exemplo, se você precisar copiar um arquivo e o caminho de origem ou destino estiver vazio, deverá lançar uma exceção imediatamente.
Michael Shopsin
Ele também é conhecido como "falhar rapidamente"
keuleJ
8

Sim, lançar exceções é uma boa ideia. Jogue-os cedo, jogue-os frequentemente, jogue-os ansiosamente.

Eu sei que há um debate "exceções versus asserções", com alguns tipos de comportamento excepcional (especificamente, aqueles que refletem erros de programação) manipulados por asserções que podem ser "compiladas" para tempo de execução, em vez de compilações de depuração / teste. Mas a quantidade de desempenho consumida em algumas verificações extras de correção é mínima no hardware moderno, e qualquer custo extra é superado pelo valor de ter resultados corretos e não corrompidos. Na verdade, nunca conheci uma base de código de aplicativo para a qual desejaria (a maioria) verificações removidas em tempo de execução.

Fico tentado a dizer que não gostaria de verificações e condicionais extras dentro dos laços apertados do código numericamente intensivo ... mas é aí que muitos erros numéricos são gerados e, se não detectados, se propagam para o exterior. efetuar todos os resultados. Portanto, mesmo lá, vale a pena fazer as verificações. De fato, alguns dos melhores e mais eficientes algoritmos numéricos são baseados na avaliação de erros.

Um último lugar para se ter consciência do código extra é o código muito sensível à latência, onde condicionais extras podem causar paralisações no pipeline. Portanto, no meio do sistema operacional, DBMS e outros kernels de middleware e manipulação de protocolo / comunicação de baixo nível. Mas, novamente, esses são alguns dos locais em que os erros provavelmente serão observados e seus efeitos (segurança, correção e integridade dos dados) serão os mais prejudiciais.

Uma melhoria que encontrei é não lançar apenas exceções no nível base. IllegalArgumentExceptioné bom, mas pode vir de praticamente qualquer lugar. Na maioria dos idiomas, não é preciso muito para adicionar exceções personalizadas. Para o seu módulo de gestão de pessoas, diga:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Então, quando alguém vê um PersonArgumentException, está claro de onde vem. Há um ato de equilíbrio sobre quantas exceções personalizadas você deseja adicionar, porque você não deseja multiplicar entidades desnecessariamente (Navalha de Occam). Muitas vezes, apenas algumas exceções personalizadas são suficientes para sinalizar "este módulo não está obtendo os dados corretos!" ou "este módulo não pode fazer o que deveria fazer!" de uma maneira específica e personalizada, mas não tão precisa quanto à necessidade de reimplementar toda a hierarquia de exceções. Frequentemente chego a esse pequeno conjunto de exceções personalizadas começando com exceções de estoque, depois varrendo o código e percebendo que "esses N locais estão gerando exceções de estoque, mas eles se resumem à ideia de alto nível de que não estão obtendo os dados. eles precisam; vamos '

Jonathan Eunice
fonte
I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Então você não criou um código crítico para o desempenho. No momento, estou trabalhando em algo que faz 37 milhões de operações por segundo com asserções compiladas e 42 milhões sem elas. As asserções não existem validam entrada externa, elas estão lá para garantir que o código esteja correto. Meus clientes estão mais do que felizes em receber o aumento de 13%, uma vez que estou satisfeito que minhas coisas não estão quebradas.
Blrfl 23/09
Deveria evitar uma base de código com exceções personalizadas, pois, embora possa ajudá-lo, não ajuda outras pessoas que precisam se familiarizar com elas. Normalmente, se você se estende estendendo uma exceção comum e não adiciona nenhum membro ou funcionalidade, na verdade não precisa. Mesmo no seu exemplo, PersonArgumentExceptionnão é tão claro quanto IllegalArgumentException. Sabe-se universalmente que este último é lançado quando um argumento ilegal é passado. Na verdade, eu esperaria que o primeiro fosse lançado se um Personestivesse em um estado inválido para uma chamada (semelhante ao InvalidOperationExceptionC #).
Selali Adobor 23/09/14
É verdade que, se você não for cuidadoso, poderá acionar a mesma exceção de outro lugar da função, mas isso é uma falha do código, não da natureza das exceções comuns. E é aí que entram os rastreios de depuração e pilha. Se você estiver tentando "rotular" suas exceções, use os recursos existentes que as exceções mais comuns fornecem, como usar o construtor de mensagens (é exatamente para isso). Algumas exceções até fornecem aos construtores parâmetros adicionais para obter o nome do argumento problemático, mas você pode fornecer ao mesmo recurso uma mensagem bem formada.
Selali Adobor 23/09/14
Admito que apenas mudar o nome de uma exceção comum para uma marca própria basicamente da mesma exceção é um exemplo fraco e raramente vale o esforço. Na prática, tento emitir exceções personalizadas em um nível semântico mais alto, mais intimamente ligado à intenção do módulo.
Jonathan Eunice
Fiz um trabalho sensível ao desempenho nas regiões numérica / HPC e OS / middleware. Um aumento de 13% no desempenho não é algo pequeno. Eu poderia desligar alguns cheques para obtê-lo, da mesma maneira que um comandante militar poderia pedir 105% da produção nominal do reator. Mas eu vi "ele funcionará mais rápido dessa maneira", dado frequentemente como um motivo para desativar verificações, backups e outras proteções. É basicamente trocar resiliência e segurança (em muitos casos, incluindo integridade de dados) por desempenho extra. Se isso vale a pena é uma decisão judicial.
Jonathan Eunice
3

Falhar o mais rápido possível é ótimo ao depurar um aplicativo. Lembro-me de uma falha específica de segmentação em um programa C ++ herdado: o local em que o erro foi detectado não tinha nada a ver com o local em que foi introduzido (o ponteiro nulo foi movido com alegria de um lugar para outro na memória antes de finalmente causar um problema ) Rastreios de pilha não podem ajudá-lo nesses casos.

Então, sim, a programação defensiva é uma abordagem realmente eficaz para detectar e corrigir erros rapidamente. Por outro lado, pode ser exagerado, especialmente com referências nulas.

No seu caso específico, por exemplo: se alguma referência for nula, NullReferenceExceptionela será lançada na próxima instrução, ao tentar obter a idade de uma pessoa. Você realmente não precisa verificar as coisas aqui: deixe o sistema subjacente capturar esses erros e gerar exceções, é por isso que elas existem .

Para um exemplo mais realista, você pode usar assertinstruções que:

  1. São mais curtos para escrever e ler:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. São projetados especificamente para sua abordagem. Em um mundo em que você tem afirmações e exceções, é possível distingui-las da seguinte maneira:

    • As asserções são para erros de programação ("/ * isso nunca deve acontecer * /"),
    • Exceções são para casos extremos ( situações excepcionais, mas prováveis)

Assim, expor suas suposições sobre as entradas e / ou o estado de seu aplicativo com asserções permite que o próximo desenvolvedor entenda um pouco mais o propósito do seu código.

Um analisador estático (por exemplo, o compilador) também pode ser mais feliz.

Por fim, as asserções podem ser removidas do aplicativo implantado usando um único comutador. Mas, de um modo geral, não espere melhorar a eficiência com isso: as verificações de asserções no tempo de execução são insignificantes.

coredump
fonte
1

Tanto quanto sei, programadores diferentes preferem uma solução ou outra.

A primeira solução é geralmente preferida porque é mais concisa, em particular, você não precisa verificar a mesma condição repetidamente em diferentes funções.

Eu encontro a segunda solução, por exemplo

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

mais sólido, porque

  1. Ele captura o erro o mais rápido possível, ou seja, quando registerPerson()é chamado, não quando uma exceção de ponteiro nulo é lançada em algum lugar na pilha de chamadas. A depuração se torna muito mais fácil: todos sabemos até que ponto um valor inválido pode percorrer o código antes que ele se manifeste como um bug.
  2. Ele diminui o acoplamento entre funções: registerPerson()não faz suposições sobre quais outras funções acabarão usando o personargumento e como elas o usarão: a decisão que nullé um erro é tomada e implementada localmente.

Portanto, especialmente se o código for bastante complexo, eu tendem a preferir essa segunda abordagem.

Giorgio
fonte
1
Dar o motivo ("A pessoa de entrada é nula".) Também ajuda a entender o problema, diferente de quando algum método obscuro de uma biblioteca falha.
Florian F
1

Em geral, sim, é uma boa ideia "falhar cedo". No entanto, no seu exemplo específico, o explícito IllegalArgumentExceptionnão fornece uma melhoria significativa em relação a NullReferenceException- porque os dois objetos operados já são entregues como argumentos para a função.

Mas vamos ver um exemplo um pouco diferente.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Se não houvesse argumento de verificação no construtor, você receberia um NullReferenceExceptionao chamar Calculate.

Mas o trecho de código quebrado não era a Calculatefunção, nem o consumidor da Calculatefunção. O trecho de código quebrado foi o código que tenta construí-lo PersonCalculatorcom um nulo Person- e é aí que queremos que a exceção ocorra.

Se removermos essa verificação explícita de argumento, você terá que descobrir por que NullReferenceExceptionocorreu quando a Calculatechamada foi chamada. E rastrear por que o objeto foi construído com uma nullpessoa pode ser complicado, especialmente se o código que constrói a calculadora não estiver próximo do código que realmente chama a Calculatefunção.

Pete
fonte
0

Não nos exemplos que você dá.

Como você diz, jogar explicitamente não está ganhando muito quando você vai receber uma exceção logo em seguida. Muitos argumentam que ter uma exceção explícita com uma boa mensagem é melhor, embora eu não concorde. Em cenários pré-lançados, o rastreamento da pilha é bom o suficiente. Em cenários pós-lançamento, o site de chamada geralmente pode fornecer mensagens melhores do que dentro da função.

O segundo formulário está fornecendo informações demais para a função. Essa função não deve necessariamente saber que as outras funções serão ativadas na entrada nula. Mesmo se eles lançarem uma entrada nula agora , torna-se muito problemático refatorar, caso isso pare de acontecer, uma vez que a verificação nula está espalhada por todo o código.

Mas, em geral, você deve jogar cedo assim que determinar que algo deu errado (respeitando o DRY). Estes talvez não sejam grandes exemplos disso.

Telastyn
fonte
-3

Na sua função de exemplo, eu preferiria que você não fizesse verificações e apenas permitisse NullReferenceExceptionque isso acontecesse.

Por um lado, não faz sentido passar um nulo lá de qualquer maneira, então vou descobrir o problema imediatamente com base no lançamento de um NullReferenceException.

Segundo, é que, se todas as funções lançam exceções ligeiramente diferentes, com base em que tipo de entradas obviamente erradas foram fornecidas, logo você tem funções que podem gerar 18 tipos diferentes de exceções e logo se vê dizendo que é demais muito trabalho para lidar e suprimir todas as exceções de qualquer maneira.

Não há nada que você possa realmente fazer para ajudar na situação de um erro de tempo de design em sua função; deixe a falha acontecer sem alterá-la.

whatsisname
fonte
Trabalho há alguns anos com base em códigos baseados neste princípio: os ponteiros são simplesmente desassistidos quando necessário e quase nunca são verificados no NULL e tratados explicitamente. A depuração desse código sempre foi muito demorada e problemática, pois as exceções (ou despejos principais) ocorrem muito mais tarde, no ponto em que ocorre o erro lógico. Eu perdi literalmente semanas procurando por certos bugs. Por esse motivo, prefiro o estilo de falha o mais rápido possível, pelo menos para códigos complexos, onde não é imediatamente óbvio de onde vem uma exceção de ponteiro nulo.
Giorgio
"Por um lado, não faz sentido passar um nulo para lá de qualquer maneira, então vou descobrir o problema imediatamente com base no lançamento de uma NullReferenceException.": Não necessariamente, como o segundo exemplo de Prog ilustra.
Giorgio
"Segundo, é que, se todas as funções lançam exceções ligeiramente diferentes, com base em que tipo de entradas obviamente erradas foram fornecidas, logo você terá funções que poderão gerar 18 tipos diferentes de exceções ...": Nesse caso, você pode usar a mesma exceção (mesmo NullPointerException) em todas as funções: o importante é lançar mais cedo, não lançar uma exceção diferente de cada função.
Giorgio
É para isso que servem as RuntimeExceptions. Qualquer condição que "não deva acontecer", mas que impeça o funcionamento do seu código, deve gerar uma. Você não precisa declará-los na assinatura do método. Mas você precisa capturá-los em algum momento e relatar que a função da ação requestd falhou.
Florian F
1
A questão não especifica um idioma, portanto, dependendo, por exemplo, RuntimeException não é necessariamente uma suposição válida.