Exceções: Por que jogar cedo? Por que pegar tarde?

156

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?

shylynx
fonte
104
A idéia por trás de "pegar tarde" é, em vez de pegar o mais tarde possível, pegar o mais cedo possível. Se você tiver um analisador de arquivo, por exemplo, não há sentido em lidar com um arquivo não encontrado. O que você pretende fazer com isso, qual é o seu caminho de recuperação? Não há um, então não pegue. Vá para o seu lexer, o que você faz lá, como se recupera disso para que seu programa possa continuar? Não pode deixar passar a exceção. Como o seu scanner pode lidar com isso? Não pode deixar passar. Como o código de chamada pode lidar com isso? Ele pode tentar outro caminho de arquivo ou alertar o usuário, então pegue.
Phoshi #
16
Existem muito poucos casos em que uma NullPointerException (suponho que é isso que NPE significa) deve ser capturada; se possível, deve ser evitado em primeiro lugar. Se você está tendo NullPointerExceptions, tem algum código quebrado que precisa ser corrigido. Provavelmente também é uma solução fácil.
Phil
6
Por favor, pessoal, antes de sugerir fechar esta pergunta como uma duplicata, verifique se a resposta para a outra pergunta não responde muito bem a essa pergunta.
Doc Brown
1
(Citebot) today.java.net/article/2003/11/20/… Se essa não for a origem da cotação, forneça uma referência à fonte que você acha que é a cotação original mais provável.
rwong
1
Apenas um lembrete para quem chegar a essa pergunta e desenvolver o Android. No Android, as exceções devem ser capturadas e tratadas localmente - na mesma função em que são capturadas pela primeira vez. Isso ocorre porque as exceções não se propagam pelos manipuladores de mensagens - seu aplicativo será eliminado se isso acontecer. Portanto, você não deve citar este conselho ao desenvolver o Android.
Rwong #

Respostas:

118

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.

Michael Shaw
fonte
Sim, eu já sei e está fora de questão que devo lançar exceções no ponto em que o erro se origina. Mas por que eu não deveria pegar o NPE e deixá-lo subir no Stacktrace? Eu sempre pegava o NPE e o envolvia em uma exceção significativa. Também não vejo nenhuma vantagem de lançar uma exceção DAO até o serviço ou a camada da interface do usuário. Eu sempre o pegava na camada de serviço e o envolvia em uma exceção de serviço com informações detalhadas adicionais, por que a chamada do serviço falhou.
shylynx
8
@shylynx Capturar uma exceção e, em seguida, relançar uma exceção mais significativa é uma boa coisa a fazer. O que você não deveria estar fazendo é capturar uma exceção muito cedo e depois não a rever novamente. O erro que o ditado está alertando é capturar a exceção muito cedo e, em seguida, tentar manipulá-la na camada errada no código.
B
Tornar o contexto óbvio no momento de receber a exceção facilita a vida dos desenvolvedores da sua equipe. A NPE requer uma investigação mais aprofundada para entender o problema
Michael Shaw
4
@shylynx Alguém pode fazer a pergunta: "Por que você tem um ponto em seu código que pode gerar um NullPointerException? Por que não procurar nulle lançar uma exceção (talvez uma IllegalArgumentException) antes, para que o chamador saiba exatamente onde o mal nullfoi transmitido?" Acredito que seria o que sugeriria a parte "jogar mais cedo" do ditado.
jpmc26
2
@jpmc Tomei o NPE apenas como exemplo para enfatizar as preocupações em torno das camadas e as exceções. Eu poderia substituí-lo por um IllegalArgumentException também.
shylynx
56

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.

Doval
fonte
1
+1 para explicar claramente por que os diferentes níveis são importantes. Excelente exemplo sobre o erro do sistema de arquivos.
Juan Carlos Coto
24

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:

static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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:

static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Como alternativa, você pode executar um comando completo no estilo OOP desta maneira:

static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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 ArithmeticExceptionvez surgir uma chamada divideou eval, então é VOCÊ quem deve executar a exceção e corrigir seu código, porque você esqueceu a check(). Pelas razões semelhantes, pegar um NullPointerExceptionquase 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 retornar null para casos de erro simples como este:

static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

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!

static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}

Enquanto isso, em algum outro lugar, as implementações concretas ...

static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}

E finalmente o código de fiação ... Quem lida com exceções concretas de recursos? Quem sabe sobre eles!

static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}

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!

Daniel Dinnyes
fonte
Você está certo de que o código que fornece a interface saberá mais sobre o que pode dar errado do que o código que usa a interface, mas suponha que um método use dois recursos do mesmo tipo de interface e que as falhas precisem ser tratadas de maneira diferente? Ou se um desses recursos internamente, como um detalhe de implementação desconhecido do criador, usar outros recursos aninhados do mesmo tipo? Tendo o lance camada de negócios WrappedFirstResourceExceptionou WrappedSecondResourceExceptione exigindo a "fiação" camada de olhar para dentro essa exceção para ver a causa raiz do problema ...
supercat
... pode ser nojento, mas parece melhor do que assumir que qualquer FailingInputResourceexceção será o resultado de uma operação com in1. 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 uma catchque invocasse o handleExceptionmé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.
Supercat
@ Supercat Eu entendo o que você está dizendo. Eu diria que uma implementação concreta de recursos é responsável por conhecer as exceções que pode gerar. Ele não precisa especificar tudo (existe o chamado comportamento indefinido ), mas deve garantir que não haja ambiguidade. Além disso, exceções de tempo de execução desmarcadas devem ser documentadas. Se contradizer a documentação, é um bug. Se se espera que o código do chamador faça algo sensato a respeito de uma exceção, o mínimo é que o recurso os agrupe em alguns UnrecoverableInternalException, semelhante a um código de erro HTTP 500.
Daniel Dinnyes
@supercat Sobre sua sugestão sobre manipuladores de erros configuráveis: exatamente! No meu último exemplo, a lógica de tratamento de erros é codificada, chamando o doMyBusinessmétodo estático . Isso foi por uma questão de brevidade, e é perfeitamente possível torná-lo mais dinâmico. Essa Handlerclasse seria instanciada com alguns recursos de entrada / saída e teria um handlemétodo que recebe uma classe implementando a ReusableBusinessLogicInterface. 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.
Daniel Dinnyes
10

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:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

Esse código obviamente pode gerar uma exceção de referência nula se PropertyBfor nulo. Há duas coisas que poderíamos fazer nesse caso para "corrigir" essa situação. Poderíamos:

  • Crie automaticamente o PropertyB, se não o tivermos; ou
  • Deixe a exceção borbulhar até o método de chamada.

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.

Stephen
fonte
Eu acho que você está usando desenvolvedores upstream no sentido errado. Além disso, você disse que viola o princípio da responsabilidade individual, mas na realidade muitos na inicialização demanda e valor cache é implementado dessa maneira (com controles de simultaneidade adequados no lugar, é claro)
Daniel Dinnyés
No seu exemplo, que tal if(PropertyB == null) return 0;
procurar
1
Você também pode elaborar seu último parágrafo, especificamente o que você quer dizer com ' camada de abstração '.
user1451111
Se estivermos fazendo algum trabalho de IO, a camada de abstração para capturar uma exceção de E / S seria onde estamos fazendo o trabalho. Nesse ponto, temos todas as informações necessárias para decidir se queremos tentar novamente ou exibir uma caixa de mensagem para o usuário ou criar um objeto usando um conjunto de padrões.
Stephen
"No seu exemplo, que tal procurar nulo antes da operação de subtração, assim se (PropertyB == null) retornar 0;" Que nojo. Isso seria dizer ao método de chamada que eu tenho coisas válidas para subtrair de e para. É claro que isso é contextual, mas seria uma prática ruim fazer a verificação de erros aqui na maioria dos casos.
Stephen
6

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 .

catraca arrepiante
fonte
1
Você escreveu: Jogue em breve, ... Pegue em breve ...! Por quê? Essa é uma abordagem completamente contrária, em contraste com "jogar cedo, pegar tarde".
precisa saber é o seguinte
1
@ shylynx Não sei de onde vem o "lance cedo, pegue tarde", mas seu valor é questionável. O que exatamente significa "atrasado"? Onde faz sentido capturar uma exceção (se houver) depende do problema. A única coisa que fica clara é que você deseja detectar problemas (e lançar) o mais cedo possível.
Doval
2
Eu suponho que "pegar tarde" tem como objetivo contrastar a prática de pegar antes que você possa saber o que fazer para corrigir o erro - por exemplo, às vezes você vê funções que capturam tudo apenas para que possam imprimir uma mensagem de erro e, em seguida, refazer a exceção.
@Hurkyl: Um problema com "atrasar" é que, se uma exceção surgir através de camadas que não sabem nada sobre isso, pode ser difícil para um código que esteja em condições de fazer algo sobre a situação para saber que as coisas realmente são tão esperado. Como um exemplo simples, suponha que, se um analisador de um arquivo de documento do usuário precisar carregar um CODEC do disco e ocorrer um erro de disco durante a leitura, o código que chama o analisador poderá agir de maneira inadequada se considerar que houve um erro de disco durante a leitura do usuário. documento.
supercat
4

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ó.

soru
fonte
2

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/ catchcó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).

Aschratt
fonte
1

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.

davidk01
fonte