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 null
nessa 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, null
mudar o programa causando problemas em outros lugares.
Agora, você está certo. Nesse caso, null
isso simplesmente causaria uma NullPointerException
imediata, 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 null
como 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 null
referê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?
fonte
Respostas:
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.
fonte
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: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 '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.PersonArgumentException
não é tão claro quantoIllegalArgumentException
. Sabe-se universalmente que este último é lançado quando um argumento ilegal é passado. Na verdade, eu esperaria que o primeiro fosse lançado se umPerson
estivesse em um estado inválido para uma chamada (semelhante aoInvalidOperationException
C #).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,
NullReferenceException
ela 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
assert
instruções que:São mais curtos para escrever e ler:
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:
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.
fonte
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
mais sólido, porque
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.registerPerson()
não faz suposições sobre quais outras funções acabarão usando operson
argumento e como elas o usarão: a decisão quenull
é um erro é tomada e implementada localmente.Portanto, especialmente se o código for bastante complexo, eu tendem a preferir essa segunda abordagem.
fonte
Em geral, sim, é uma boa ideia "falhar cedo". No entanto, no seu exemplo específico, o explícito
IllegalArgumentException
não fornece uma melhoria significativa em relação aNullReferenceException
- porque os dois objetos operados já são entregues como argumentos para a função.Mas vamos ver um exemplo um pouco diferente.
Se não houvesse argumento de verificação no construtor, você receberia um
NullReferenceException
ao chamarCalculate
.Mas o trecho de código quebrado não era a
Calculate
função, nem o consumidor daCalculate
função. O trecho de código quebrado foi o código que tenta construí-loPersonCalculator
com um nuloPerson
- e é aí que queremos que a exceção ocorra.Se removermos essa verificação explícita de argumento, você terá que descobrir por que
NullReferenceException
ocorreu quando aCalculate
chamada foi chamada. E rastrear por que o objeto foi construído com umanull
pessoa pode ser complicado, especialmente se o código que constrói a calculadora não estiver próximo do código que realmente chama aCalculate
função.fonte
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.
fonte
Na sua função de exemplo, eu preferiria que você não fizesse verificações e apenas permitisse
NullReferenceException
que 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.
fonte
RuntimeException
não é necessariamente uma suposição válida.