Existem exceções como fluxo de controle considerado um antipadrão sério? Se sim, por quê?

107

De volta ao final dos anos 90, trabalhei bastante com uma base de código que usava exceções como controle de fluxo. Implementou uma máquina de estados finitos para direcionar aplicativos de telefonia. Ultimamente eu me lembro daqueles dias porque eu tenho feito aplicativos web MVC.

Ambos têm Controller s que decidem para onde ir em seguida e fornecem os dados para a lógica de destino. As ações do usuário do domínio de um telefone antigo, como tons DTMF, tornaram-se parâmetros para métodos de ação, mas em vez de retornar algo como ViewResult , eles lançaram um StateTransitionException .

Acho que a principal diferença foi que os métodos de ação eram void functions. Não me lembro de todas as coisas que fiz com esse fato, mas hesitei em percorrer a estrada de lembrar muito, porque desde aquele trabalho, há 15 anos atrás, eu nunca vi isso em código de produção em qualquer outro trabalho . Eu assumi que isso era um sinal de que era um chamado anti-padrão.

Este é o caso e, em caso afirmativo, por quê?

Atualização: quando fiz a pergunta, eu já tinha em mente a resposta do @MasonWheeler, então fui com a resposta que mais me chamou a atenção. Eu acho que a resposta dele também é boa.

    
por Aaron Anodide 04.03.2013 / 23:27
fonte

9 respostas

92

Há uma discussão detalhada sobre isso em Wiki de Ward . Geralmente, o uso de exceções para o fluxo de controle é um antipadrão, com notáveis exceções tosse específicas à situação e à linguagem tosse .

Como um resumo rápido do motivo, geralmente, é um antipadrão:

  • As exceções são, em essência, sofisticadas instruções GOTO
  • A programação com exceções, portanto, leva a mais dificuldade de leitura e ao entendimento do código
  • A maioria dos idiomas possui estruturas de controle existentes projetadas para resolver seus problemas sem o uso de exceções
  • Argumentos de eficiência tendem a ser discutíveis para os compiladores modernos, que tendem a otimizar com a suposição de que as exceções não são usadas para o fluxo de controle.

Leia a discussão na wiki do Ward para obter informações mais detalhadas.

    
por 04.03.2013 / 23:40
fonte
95

O caso de uso para o qual as exceções foram projetadas é "Acabei de encontrar uma situação com a qual não posso lidar corretamente neste momento, porque não tenho contexto suficiente para lidar com isso, mas a rotina que me chamou (ou algo mais a pilha de chamadas) deve saber como lidar com isso. "

O caso de uso secundário é "Acabei de encontrar um erro sério, e agora sair desse fluxo de controle para evitar corrupção de dados ou outros danos é mais importante do que tentar continuar adiante."

Se você não está usando exceções por um desses dois motivos, provavelmente há uma maneira melhor de fazer isso.

    
por 04.03.2013 / 23:41
fonte
26

As exceções são tão poderosas quanto as Continuações e GOTO . Eles são uma construção de fluxo de controle universal.

Em alguns idiomas, eles são a única construção de fluxo de controle universal somente . JavaScript, por exemplo, não tem nem continuações nem GOTO , nem sequer tem chamadas adequadas. Então, se você quiser implementar um fluxo de controle sofisticado em JavaScript, você tem para usar exceções.

O projeto Microsoft Volta foi um projeto de pesquisa (agora descontinuado) para compilar código .NET arbitrário para JavaScript. O .NET tem exceções cuja semântica não mapeia exatamente para JavaScript, mas mais importante, ele tem Threads , e você tem que mapeá-los de alguma forma para JavaScript. Volta fez isso implementando Volta Continuations usando JavaScript Exceptions e, em seguida, implementou todas as construções de fluxo de controle .NET em termos de Continuações Volta. Eles tinham para usar Exceções como fluxo de controle, porque não há outra construção de fluxo de controle poderosa o suficiente.

Você mencionou as máquinas estatais. SMs são triviais para implementar com Tail Tail Calls: todo estado é uma sub-rotina, cada transição de estado é uma chamada de subrotina. Os SMs também podem ser facilmente implementados com GOTO ou Coroutines ou Continuações. No entanto, Java não possui nenhum desses quatro, mas tem exceções. Então, é perfeitamente aceitável usá-los como fluxo de controle. (Bem, na verdade, a escolha correta provavelmente seria usar uma linguagem com a construção de fluxo de controle adequada, mas às vezes você pode ficar preso em Java.)

    
por 05.03.2013 / 02:07
fonte
17

Como outros já mencionaram numerosamente, ( por exemplo, nesta pergunta do Stack Overflow ), o princípio de menos espanto proíbe o uso excessivo de exceções apenas para fins de fluxo de controle. Por outro lado, nenhuma regra é 100% correta, e há sempre aqueles casos em que uma exceção é "apenas a ferramenta certa" - muito parecido com o próprio goto , que vem na forma de break e continue em linguagens como Java, que geralmente são a maneira perfeita de saltar de loops muito aninhados, que nem sempre são evitáveis.

A postagem do blog a seguir explica um caso de uso bastante complexo, mas também bastante interessante, para um não local ControlFlowException :

Explica como dentro de jOOQ (uma biblioteca de abstração de SQL para Java) (disclaimer: Eu trabalho para o fornecedor), tais exceções são ocasionalmente usado para abortar o processo de renderização SQL no início, quando alguma condição "rara" é atendida.

Exemplos de tais condições são:

  • Muitos valores de ligação são encontrados. Alguns bancos de dados não suportam números arbitrários de valores de ligação em suas instruções SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). Nesses casos, o jOOQ aborta a fase de renderização do SQL e renderiza novamente a instrução SQL com valores de ligação embutidos. Exemplo:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }
    

    Se extraíssemos explicitamente os valores de ligação da consulta AST para contá-los todas as vezes, perderíamos valiosos ciclos de CPU para essas 99,9% das consultas que não sofrem desse problema.

  • Algumas lógicas estão disponíveis apenas indiretamente por meio de uma API que queremos executar apenas "parcialmente". O método UpdatableRecord.store() gera uma declaração INSERT ou UPDATE , dependendo do os sinalizadores internos de Record . Do lado de fora, não sabemos que tipo de lógica está contida em store() (por exemplo, bloqueio otimista, manipulação de ouvinte de evento, etc.), por isso não queremos repetir essa lógica quando armazenamos vários registros em um instrução em lote, em que gostaríamos de ter store() apenas gerar a instrução SQL, na verdade, não executá-la. Exemplo:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }
    

    Se tivéssemos externalizado a lógica store() na API "reutilizável" que pode ser personalizada para, opcionalmente, não executar o SQL, estaríamos procurando criar um bastante difícil de manter, API dificilmente reutilizável.

Conclusão

Em essência, nosso uso desses goto s não locais é exatamente o mesmo que Mason Wheeler disse em sua resposta:

"I just encountered a situation that I cannot deal with properly at this point, because I don't have enough context to handle it, but the routine that called me (or something further up the call stack) ought to know how to handle it."

Ambos os usos de ControlFlowExceptions foram bastante fáceis de implementar em comparação com suas alternativas, permitindo-nos reutilizar uma ampla gama de lógica sem refatorá-la das partes internas relevantes.

Mas o sentimento de ser um pouco de surpresa para os futuros mantenedores permanece. O código parece bastante delicado e, embora tenha sido a escolha certa nesse caso, sempre preferimos não usar exceções para o fluxo de controle local , onde é fácil evitar o uso de ramificações comuns por meio de if - else .

    
por 11.01.2015 / 09:16
fonte
9

Usar exceções para o fluxo de controle é geralmente considerado um antipadrão, mas há exceções (sem trocadilhos).

Foi dito milhares de vezes que as exceções são destinadas a condições excepcionais. Uma conexão de banco de dados quebrada é uma condição excepcional. Um usuário digitando letras em um campo de entrada que só deve permitir números não é .

Um bug no seu software que faz com que uma função seja chamada com argumentos ilegais, por exemplo, null onde não permite, é uma condição excepcional.

Ao usar exceções para algo que não é excepcional, você está usando abstrações inadequadas para o problema que está tentando resolver.

Mas também pode haver uma penalidade de desempenho. Algumas linguagens têm uma implementação de manipulação de exceções mais ou menos eficiente, portanto, se o seu idioma de escolha não tiver tratamento eficiente de exceções, isso pode ser muito caro, em termos de desempenho *.

Mas outras linguagens, por exemplo, Ruby, possuem uma sintaxe semelhante à exceção para o fluxo de controle. Situações excepcionais são tratadas pelos operadores raise / rescue . Mas você pode usar throw / catch para construções de fluxo de controle semelhantes a exceções **.

Assim, embora as exceções geralmente não sejam usadas para o fluxo de controle, sua linguagem preferida pode ter outras expressões idiomáticas.

* Exemplo de desempenho de uso dispendioso de exceções: certa vez, fui definido para otimizar um aplicativo de Formulário da Web do ASP.NET com desempenho insatisfatório. Acontece que a renderização de uma tabela grande estava chamando int.Parse() em aprox. mil strings vazias em uma página média, resultando em aprox. mil exceções sendo tratadas. Substituindo o código por int.TryParse() , cortei um segundo! Para cada pedido de página única!

** Isso pode ser muito confuso para um programador vindo do Ruby de outros idiomas, já que throw e catch são palavras-chave associadas a exceções em muitos outros idiomas.

    
por 20.10.2014 / 14:25
fonte
8

É completamente possível lidar com condições de erro sem o uso de exceções. Algumas linguagens, mais notadamente C, nem mesmo possuem exceções, e as pessoas ainda conseguem criar aplicações bastante complexas com ele. A razão pela qual as exceções são úteis é que elas permitem que você especifique sucintamente dois fluxos de controle essencialmente independentes no mesmo código: um se ocorrer um erro e outro se não ocorrer. Sem eles, você acaba com código em todo o lugar que se parece com isso:

status = getValue(&inout);
if (status < 0)
{
    logError("message");
    return status;
}

doSomething(*inout);

Ou equivalente no seu idioma, como retornar uma tupla com um valor como um status de erro, etc. Muitas vezes, as pessoas que apontam como o tratamento "caro" de exceções é, negligenciam todas as instruções if extras, como acima, de que são necessárias para adicionar se você não usar exceções.

Embora esse padrão ocorra com mais frequência ao lidar com erros ou outras "condições excepcionais", na minha opinião, se você começar a ver código clichê como esse em outras circunstâncias, você tem um bom argumento para usar exceções. Dependendo da situação e da implementação, posso ver exceções sendo usadas validamente em uma máquina de estados, porque você tem dois fluxos de controle ortogonais: um que está alterando o estado e outro para os eventos que ocorrem nos estados.

No entanto, essas situações são raras, e se você vai fazer uma exceção à regra, é melhor estar preparado para mostrar sua superioridade a outras soluções. Um desvio sem tal justificação é corretamente chamado de antipadrão.

    
por 05.03.2013 / 00:22
fonte
5

No Python, exceções são usadas para terminação de gerador e iteração. O Python tem uma tentativa / exceção muito eficiente, mas, na verdade, aumentar uma exceção tem alguma sobrecarga.

Devido à falta de quebras de vários níveis ou de uma instrução goto no Python, às vezes usei exceções:

class GOTO(Exception):
  pass

try:
  # Do lots of stuff
  # in here with multiple exit points
  # each exit point does a "raise GOTO()"
except GOTO:
  pass
except Exception as e:
  #display error
    
por 05.03.2013 / 00:36
fonte
4

Vamos esboçar esse uso de exceção:

O algoritmo pesquisa recursivamente até que algo seja encontrado. Então, voltando da recursão, é preciso verificar o resultado para ser encontrado e retornar, caso contrário, continue. E isso repetidamente voltando de alguma profundidade de recursão.

Além de precisar de um% booleano extrafound (para ser empacotado em uma classe, onde de outra forma talvez apenas um int seria retornado), e para a profundidade de recursão o mesmo postlude acontece.

Um desenrolar de uma pilha de chamadas é apenas uma exceção. Então, parece-me um meio de codificação não-giro, mais imediato e apropriado. Não é necessário, uso raro, talvez um estilo ruim, mas direto ao ponto. Comparável com a operação de corte Prolog.

    
por 20.10.2014 / 14:49
fonte
4

Programação é sobre trabalho

Acho que a maneira mais fácil de responder isso é entender o progresso que a OOP fez ao longo dos anos. Tudo feito em OOP (e a maioria dos paradigmas de programação, por exemplo) é modelado em torno de precisar de trabalho feito .

Toda vez que um método é chamado, o chamador está dizendo "Eu não sei como fazer este trabalho, mas você sabe como, então você faz isso por mim."

Isso apresentou uma dificuldade: o que acontece quando o método chamado geralmente sabe como fazer o trabalho, mas nem sempre? Nós precisávamos de uma maneira de nos comunicar "Eu queria te ajudar, eu realmente fiz, mas eu simplesmente não posso fazer isso."

Uma metodologia inicial para comunicar isso foi simplesmente retornar um valor "lixo". Talvez você espere um inteiro positivo, então o método chamado retorna um número negativo. Outra maneira de conseguir isso era definir um valor de erro em algum lugar. Infelizmente, os dois caminhos resultaram em código clichê let-me-check-over-here-to-make-sure-tudo-kosher . Conforme as coisas ficam mais complicadas, esse sistema se desfaz.

Uma Analogia Excepcional

Digamos que você tenha um carpinteiro, um encanador e um eletricista. Você quer encanador para consertar sua pia, então ele dá uma olhada nisso. Não é muito útil se ele disser apenas a você, "Desculpe, não posso consertá-lo. Está quebrado." Inferno, é ainda pior se ele der uma olhada, sair e mandar um carta dizendo que ele não poderia consertá-lo. Agora você precisa checar seu e-mail antes mesmo de saber que ele não fez o que queria.

O que você preferiria é que ele lhe dissesse: "Olha, eu não consegui consertar porque parece que você não está funcionando."

Com essas informações, você pode concluir que deseja que o eletricista analise o problema. Talvez o eletricista encontre algo relacionado ao carpinteiro, e você precisará que o carpinteiro o conserte.

Heck, você pode até não saber que você precisa de um eletricista, você pode não saber quem você precisa. Você é apenas uma empresa de médio porte em um negócio de consertos em casa e seu foco é o encanamento. Então você diz a seu chefe sobre o problema e, em seguida, ele diz ao eletricista para corrigi-lo.

Isto é o que as exceções estão modelando: modos de falha complexos de maneira dissociada. O encanador não precisa saber sobre eletricista - ele nem precisa saber que alguém na cadeia pode consertar o problema. Ele apenas relata o problema que encontrou.

Então ... um anti-padrão?

Ok, entender o ponto das exceções é o primeiro passo. O próximo é entender o que é um antipadrão.

Para se qualificar como um antipadrão, é necessário

  • resolva o problema
  • tem consequências negativas definitivas

O primeiro ponto é facilmente encontrado - o sistema funcionou, certo?

O segundo ponto é mais complicado. A principal razão para usar exceções como o fluxo de controle normal é ruim é porque esse não é seu objetivo. Qualquer peça de funcionalidade em um programa deve ter um propósito relativamente claro, e a cooptação desse propósito leva a uma confusão desnecessária.

Mas isso não é um dano definitivo . É uma maneira pobre de fazer as coisas e é estranho, mas um anti-padrão? Não. Apenas ... estranho.

    
por 29.06.2016 / 01:51
fonte