Existem muitas práticas recomendadas conhecidas sobre o tratamento de exceções isoladamente. Conheço bem o que fazer e o que não fazer, mas as coisas ficam complicadas quando se trata de práticas recomendadas ou padrões em ambientes maiores. "Jogue cedo, pegue tarde" - já ouvi muitas vezes e ainda me confunde.
Por que devo jogar cedo e atrasar se, em uma camada de baixo nível, uma exceção de ponteiro nulo é lançada? Por que eu deveria pegá-lo em uma camada superior? Não faz sentido capturar uma exceção de baixo nível em um nível superior, como uma camada de negócios. Parece violar as preocupações de cada camada.
Imagine a seguinte situação:
Eu tenho um serviço que calcula uma figura. Para calcular a figura, o serviço acessa um repositório para obter dados brutos e alguns outros serviços para preparar o cálculo. Se algo deu errado na camada de recuperação de dados, por que devo lançar uma DataRetrievalException para um nível superior? Por outro lado, eu preferiria agrupar a exceção em uma exceção significativa, por exemplo, uma CalculationServiceException.
Por que jogar cedo, por que pegar tarde?
fonte
Respostas:
Na minha experiência, é melhor lançar exceções no ponto em que os erros ocorrem. Você faz isso porque é o ponto em que você sabe mais sobre o motivo pelo qual a exceção foi acionada.
À medida que a exceção se desenrola no backup das camadas, capturar e reconfigurar é uma boa maneira de adicionar contexto adicional à exceção. Isso pode significar lançar um tipo diferente de exceção, mas inclua a exceção original ao fazer isso.
Eventualmente, a exceção chegará a uma camada na qual você poderá tomar decisões sobre o fluxo de código (por exemplo, solicitar ao usuário uma ação). Este é o ponto em que você finalmente deve lidar com a exceção e continuar a execução normal.
Com a prática e a experiência com sua base de código, fica muito fácil julgar quando adicionar contexto adicional aos erros e, onde é mais sensato, finalmente, finalmente lidar com os erros.
Captura → Rethrow
Faça isso onde for útil adicionar mais informações para evitar que um desenvolvedor precise trabalhar em todas as camadas para entender o problema.
Prender → Alça
Faça isso onde você puder tomar decisões finais sobre o que é um fluxo de execução apropriado, mas diferente através do software.
Captura → Retorno de erro
Embora existam situações em que isso seja apropriado, capturar exceções e retornar um valor de erro ao chamador devem ser consideradas para refatoração em uma implementação Catch → Rethrow.
fonte
NullPointerException
? Por que não procurarnull
e lançar uma exceção (talvez umaIllegalArgumentException
) antes, para que o chamador saiba exatamente onde o malnull
foi transmitido?" Acredito que seria o que sugeriria a parte "jogar mais cedo" do ditado.Você deseja lançar uma exceção o mais rápido possível, pois isso facilita a localização da causa. Por exemplo, considere um método que poderia falhar com certos argumentos. Se você validar os argumentos e falhar no início do método, você saberá imediatamente que o erro está no código de chamada. Se você esperar até que os argumentos sejam necessários antes de falhar, você deverá seguir a execução e descobrir se o erro está no código de chamada (argumento incorreto) ou se o método possui um erro. Quanto mais cedo você lança a exceção, mais próxima ela está de sua causa subjacente e mais fácil é descobrir onde as coisas deram errado.
A razão pela qual as exceções são tratadas em níveis mais altos é porque os níveis mais baixos não sabem qual é o curso de ação apropriado para lidar com o erro. De fato, pode haver várias maneiras apropriadas de lidar com o mesmo erro, dependendo do código de chamada. Por exemplo, abra um arquivo. Se você estiver tentando abrir um arquivo de configuração e ele não estiver lá, ignorar a exceção e prosseguir com a configuração padrão pode ser uma resposta apropriada. Se você estiver abrindo um arquivo particular que é vital para a execução do programa e está de alguma forma ausente, sua única opção é provavelmente fechar o programa.
Quebrar as exceções nos tipos certos é uma preocupação puramente ortogonal.
fonte
Outros resumiram muito bem por que jogar cedo . Em vez disso, deixe-me concentrar no porquê pegar uma parte tardia , para a qual não vi uma explicação satisfatória para o meu gosto.
POR QUE EXCEÇÕES?
Parece haver uma grande confusão sobre por que as exceções existem em primeiro lugar. Deixe-me compartilhar o grande segredo aqui: o motivo das exceções e o tratamento das exceções são ... ABSTRACÇÃO .
Você já viu código assim:
Não é assim que as exceções devem ser usadas. Código como o acima existe na vida real, mas são mais uma aberração e são realmente a exceção (trocadilho). A definição de divisão, por exemplo, mesmo em matemática pura, é condicional: é sempre o "código do chamador" que deve lidar com o caso excepcional de zero para restringir o domínio de entrada. É feio. É sempre uma dor para quem liga. Ainda assim, para tais situações, o padrão de verificação e execução é o caminho natural a seguir:
Como alternativa, você pode executar um comando completo no estilo OOP desta maneira:
Como você vê, o código de chamada tem o ônus da pré-verificação, mas não realiza nenhuma manipulação de exceção depois. Se alguma
ArithmeticException
vez surgir uma chamadadivide
oueval
, então é VOCÊ quem deve executar a exceção e corrigir seu código, porque você esqueceu acheck()
. Pelas razões semelhantes, pegar umNullPointerException
quase sempre é a coisa errada a se fazer.Agora, algumas pessoas dizem que querem ver casos excepcionais na assinatura do método / função, ou seja, estender explicitamente o domínio de saída . Eles são os que favorecem as exceções verificadas . Obviamente, alterar o domínio de saída deve forçar a adaptação de qualquer código de chamada direta, o que seria realmente alcançado com exceções verificadas. Mas você não precisa de exceções para isso! É por isso que você tem
Nullable<T>
classes genéricas , classes de casos , tipos de dados algébricos e tipos de união . Algumas pessoas OO podem até preferir retornarnull
para casos de erro simples como este:Tecnicamente, as exceções podem ser usadas para a finalidade como a acima, mas aqui está o ponto: as exceções não existem para esse uso . Exceções são pró abstração. Exceção são sobre indireção. As exceções permitem estender o domínio "resultado" sem violar os contratos diretos dos clientes e adiar o tratamento de erros para "outro lugar". Se o seu código gerar exceções que são tratadas em chamadores diretos do mesmo código, sem nenhuma camada de abstração no meio, você estará fazendo ERRADO
COMO PEGAR O TARDE?
Então aqui estamos nós. Argumentei meu caminho para mostrar que o uso de exceções nos cenários acima não é como as exceções devem ser usadas. Porém, existe um caso de uso genuíno, em que a abstração e a indireção oferecidas pelo tratamento de exceções são indispensáveis. Entender esse uso também ajudará a entender a recomendação de captura tardia .
Esse caso de uso é: Programação contra abstrações de recursos ...
Sim, a lógica de negócios deve ser programada em abstrações , não em implementações concretas. O código de "fiação" do COI de nível superior instancia as implementações concretas das abstrações de recursos e as transmite à lógica de negócios. Nada de novo aqui. Mas as implementações concretas dessas abstrações de recursos podem potencialmente lançar suas próprias exceções específicas de implementação , não podem?
Então, quem pode lidar com essas exceções específicas de implementação? É possível lidar com alguma exceção específica de recurso na lógica de negócios? Não, não é. A lógica de negócios é programada em abstrações, o que exclui o conhecimento desses detalhes de exceção específicos da implementação.
"Aha!", Você pode dizer: "mas é por isso que podemos subclassificar exceções e criar hierarquias de exceção" (confira Mr. Spring !). Deixe-me dizer, isso é uma falácia. Em primeiro lugar, todo livro razoável sobre OOP diz que a herança concreta é ruim, mas, de alguma forma, esse componente principal da JVM, tratamento de exceções, está intimamente ligado à herança concreta. Ironicamente, Joshua Bloch não poderia ter escrito seu livro Java efetivo antes de obter a experiência com uma JVM em funcionamento, poderia? É mais um livro de "lições aprendidas" para a próxima geração. Em segundo lugar, e mais importante, se você pegar uma exceção de alto nível, como vai lidar com isso?
PatientNeedsImmediateAttentionException
: temos que lhe dar um comprimido ou amputar as pernas !? Que tal uma instrução switch sobre todas as subclasses possíveis? Lá se vai o seu polimorfismo, lá se vai a abstração. Você entendeu.Então, quem pode lidar com as exceções específicas do recurso? Deve ser quem conhece as concreções! Aquele que instanciado o recurso! O código "fiação", é claro! Veja isso:
Lógica comercial codificada contra abstrações ... NENHUM MANUSEIO DE ERROS DE RECURSO DE CONCRETO!
Enquanto isso, em algum outro lugar, as implementações concretas ...
E finalmente o código de fiação ... Quem lida com exceções concretas de recursos? Quem sabe sobre eles!
Agora tenha paciência comigo. O código acima é simplista. Você pode dizer que possui um contêiner de aplicativos / Web corporativo com vários escopos de recursos gerenciados por contêineres IOC e precisa de tentativas e reinicialização automáticas de sessão ou solicitar recursos de escopo, etc. A lógica de fiação nos escopos de nível inferior pode receber fábricas abstratas para crie recursos, portanto, não esteja ciente das implementações exatas. Somente escopos de nível superior saberiam realmente quais exceções esses recursos de nível inferior podem gerar. Agora espere!
Infelizmente, as exceções permitem apenas a indireção sobre a pilha de chamadas, e diferentes escopos com suas cardinalidades diferentes geralmente são executados em vários segmentos diferentes. Não há como se comunicar com isso, com exceções. Precisamos de algo mais poderoso aqui. Resposta: passagem de mensagem assíncrona . Capte todas as exceções na raiz do escopo de nível inferior. Não ignore nada, não deixe escapar nada. Isso fechará e descartará todos os recursos criados na pilha de chamadas do escopo atual. Em seguida, propague mensagens de erro para escopos mais altos, usando filas / canais de mensagens na rotina de tratamento de exceções, até atingir o nível em que as concreções são conhecidas. Esse é o cara que sabe como lidar com isso.
SUMMA SUMMARUM
Então, de acordo com minha interpretação, pegar tarde significa pegar exceções no lugar mais conveniente ONDE VOCÊ NÃO ESTÁ QUEBRANDO A ABSTRACÇÃO MAIS . Não pegue muito cedo! Capture exceções na camada em que você cria a exceção concreta, lançando instâncias das abstrações de recursos, a camada que conhece as concreções das abstrações. A camada "fiação".
HTH. Feliz codificação!
fonte
WrappedFirstResourceException
ouWrappedSecondResourceException
e exigindo a "fiação" camada de olhar para dentro essa exceção para ver a causa raiz do problema ...FailingInputResource
exceção será o resultado de uma operação comin1
. Na verdade, acho que em muitos casos a abordagem correta seria fazer com que a camada de fiação passasse por um objeto de tratamento de exceções e a camada de negócios incluísse umacatch
que invocasse ohandleException
método desse objeto . Esse método pode reconfigurar ou fornecer dados padrão ou exibir um prompt "Abortar / Repetir / Falhar" e permitir que o operador decida o que fazer, etc., dependendo do que o aplicativo requer.UnrecoverableInternalException
, semelhante a um código de erro HTTP 500.doMyBusiness
método estático . Isso foi por uma questão de brevidade, e é perfeitamente possível torná-lo mais dinâmico. EssaHandler
classe seria instanciada com alguns recursos de entrada / saída e teria umhandle
método que recebe uma classe implementando aReusableBusinessLogicInterface
. Em seguida, você pode combinar / configurar para usar diferentes implementações de manipulador, recurso e lógica de negócios na camada de fiação em algum lugar acima deles.Para responder a essa pergunta corretamente, vamos dar um passo atrás e fazer uma pergunta ainda mais fundamental.
Por que temos exceções em primeiro lugar?
Lançamos exceções para permitir que o chamador de nosso método saiba que não poderíamos fazer o que nos foi pedido. O tipo da exceção explica por que não conseguimos fazer o que queríamos.
Vamos dar uma olhada em algum código:
Esse código obviamente pode gerar uma exceção de referência nula se
PropertyB
for nulo. Há duas coisas que poderíamos fazer nesse caso para "corrigir" essa situação. Poderíamos:Criar PropertyB aqui pode ser muito perigoso. Qual o motivo desse método para criar o PropertyB? Certamente isso violaria o princípio da responsabilidade única. Com toda a probabilidade, se PropertyB não existir aqui, isso indica que algo deu errado. O método está sendo chamado em um objeto parcialmente construído ou o PropertyB foi definido como nulo incorretamente. Ao criar o PropertyB aqui, poderíamos estar escondendo um bug muito maior que poderia nos morder mais tarde, como um bug que causa corrupção de dados.
Se, em vez disso, deixarmos a referência nula aparecer, informaremos o desenvolvedor que chamou esse método, assim que possível, que algo deu errado. Uma condição prévia vital para chamar esse método foi perdida.
Com efeito, estamos jogando cedo porque separa nossas preocupações muito melhor. Assim que ocorrer uma falha, avisaremos os desenvolvedores do upstream.
Por que "atrasamos" é uma história diferente. Realmente não queremos nos atrasar, realmente queremos nos encontrar assim que soubermos como lidar com o problema corretamente. Algumas vezes, serão quinze camadas de abstração mais tarde e outras, no ponto da criação.
O ponto é que queremos capturar a exceção na camada de abstração que nos permite lidar com a exceção no ponto em que temos todas as informações necessárias para lidar adequadamente com a exceção.
fonte
if(PropertyB == null) return 0;
Jogue assim que vir algo que vale a pena jogar para evitar colocar objetos em um estado inválido. Significando que se um ponteiro nulo foi passado, você o verifica mais cedo e lança um NPE antes que ele tenha a chance de chegar ao nível mais baixo.
Capture assim que você souber o que fazer para corrigir o erro (geralmente não é o local onde você lança, caso contrário, você pode usar um if-else), se um parâmetro inválido for passado, a camada que forneceu o parâmetro deve lidar com as conseqüências .
fonte
Uma regra de negócios válida é 'se o software de nível inferior falhar ao calcular um valor, então ...'
Isso só pode ser expresso no nível superior; caso contrário, o software de nível inferior está tentando alterar seu comportamento com base em sua própria correção, que só terminará em um nó.
fonte
Antes de tudo, as exceções são para situações excepcionais. No seu exemplo, nenhum valor pode ser calculado se os dados brutos não estiverem presentes porque não puderam ser carregados.
Pela minha experiência, é uma boa prática abstrair exceções enquanto subimos a pilha. Normalmente, os pontos em que você deseja fazer isso são sempre que uma exceção cruza o limite entre duas camadas.
Se houver um erro ao coletar seus dados brutos na camada de dados, lance uma exceção para notificar quem solicitou os dados. Não tente solucionar esse problema aqui em baixo. A complexidade do código de manipulação pode ser muito alta. Além disso, a camada de dados é responsável apenas pela solicitação de dados, não pelo tratamento de erros que ocorrem ao fazer isso. É isso que significa "jogar cedo" .
No seu exemplo, a camada de captura é a camada de serviço. O serviço em si é uma nova camada, localizada sobre a camada de acesso a dados. Então você quer pegar a exceção lá. Talvez seu serviço tenha alguma infraestrutura de failover e tente solicitar os dados de outro repositório. Se isso também falhar, envolva a exceção em algo que o chamador do serviço entenda (se for um serviço da Web, isso pode ser uma falha SOAP). Defina a exceção original como exceção interna para que as camadas posteriores possam registrar exatamente o que deu errado.
A falha de serviço pode ser detectada pela camada que está chamando o serviço (por exemplo, a interface do usuário). E é isso que significa "pegar tarde" . Se você não conseguir lidar com a exceção em uma camada inferior, repita-a novamente. Se a camada superior não puder lidar com a exceção, lide com isso! Isso pode incluir o registro ou a apresentação.
A razão pela qual você deve repetir as exceções (como descrito acima, agrupando-as em exceções mais gerais) é que o usuário provavelmente não é capaz de entender que houve um erro porque, por exemplo, um ponteiro apontava para a memória inválida. E ele não se importa. Ele se preocupa apenas com o fato de o número não poder ser calculado pelo serviço e essa é a informação que deve ser exibida para ele.
Vai futher você pode (em um mundo ideal) completamente deixar de fora
try
/catch
código do UI. Em vez disso, use um manipulador de exceções global capaz de entender as exceções que são possivelmente lançadas pelas camadas inferiores, as grave em algum log e as agrupe em objetos de erro que contêm informações significativas (e possivelmente localizadas) do erro. Esses objetos podem ser facilmente apresentados ao usuário da forma que você desejar (caixas de mensagens, notificações, brindes e assim por diante).fonte
Lançar exceções no início é geralmente uma boa prática, porque você não deseja que contratos quebrados fluam pelo código além do necessário. Por exemplo, se você espera que um determinado parâmetro de função seja um número inteiro positivo, imponha essa restrição no ponto da chamada da função, em vez de esperar até que essa variável seja usada em outro lugar na pilha de códigos.
Chegando tarde, não posso comentar porque tenho minhas próprias regras e isso muda de projeto para projeto. A única coisa que tento fazer é separar as exceções em dois grupos. Um é apenas para uso interno e o outro é apenas para uso externo. As exceções internas são capturadas e tratadas pelo meu próprio código, e as exceções externas devem ser tratadas por qualquer código que esteja me chamando. Essa é basicamente uma forma de capturar as coisas mais tarde, mas não exatamente porque me oferece a flexibilidade de me desviar da regra quando necessário no código interno.
fonte