¿Se considera que las excepciones como flujo de control son un antipatrón serio? Si es así, ¿por qué?

102

A finales de los 90, trabajé bastante con una base de código que usaba excepciones como control de flujo. Implementó una máquina de estados finitos para impulsar aplicaciones de telefonía. Últimamente me recuerdan esos días porque he estado haciendo aplicaciones web de MVC.

Ambos tienen Controller s que deciden a dónde ir a continuación y suministran los datos a la lógica de destino. Las acciones del usuario desde el dominio de un teléfono de la vieja escuela, como los tonos DTMF, se convirtieron en parámetros de los métodos de acción, pero en lugar de devolver algo como ViewResult , lanzaron un StateTransitionException .

Creo que la principal diferencia era que los métodos de acción eran funciones void . No recuerdo todas las cosas que hice con este hecho, pero he dudado incluso en ir por el camino de recordar mucho porque desde ese trabajo, hace 15 años, nunca vi esto en el código de producción en ningún otro trabajo . Asumí que esto era una señal de que se trataba de un llamado antipatrón.

Este es el caso, y si es así, ¿por qué?

Actualización: cuando hice la pregunta, ya tenía en mente la respuesta de @MasonWheeler, así que elegí la respuesta que más se agregó a mi conocimiento. Creo que la suya también es una buena respuesta.

    
pregunta Aaron Anodide 04.03.2013 - 23:27

9 respuestas

90

Hay una discusión detallada de esto en Wiki de Ward . En general, el uso de excepciones para el flujo de control es un antipatrónico, con excepciones tos tos específicas para cada situación y lenguaje.

Como resumen rápido de por qué, en general, es un antipatrón:

  • Las excepciones son, en esencia, declaraciones sofisticadas de GOTO
  • La programación con excepciones, por lo tanto, lleva a un código más difícil de entender y entender
  • La mayoría de los idiomas tienen estructuras de control existentes diseñadas para resolver sus problemas sin el uso de excepciones
  • Los argumentos para la eficiencia tienden a ser discutibles para los compiladores modernos, que tienden a optimizarse suponiendo que las excepciones no se usan para el flujo de control.

Lea la discusión en la wiki de Ward para obtener información mucho más detallada.

    
respondido por el blueberryfields 04.03.2013 - 23:40
94

El caso de uso para el que se diseñaron las excepciones es "Me acabo de encontrar una situación que no puedo manejar adecuadamente en este momento, porque no tengo suficiente contexto para manejarlo, pero la rutina que me llamó (o algo más) en la pila de llamadas) debería saber cómo manejarlo ".

El caso de uso secundario es "Me acabo de encontrar un error grave, y en este momento, salir de este flujo de control para evitar la corrupción de datos u otros daños es más importante que tratar de continuar".

Si no estás usando excepciones por una de estas dos razones, probablemente haya una mejor manera de hacerlo.

    
respondido por el Mason Wheeler 04.03.2013 - 23:41
26

Las excepciones son tan poderosas como las continuaciones y GOTO . Son una construcción de flujo de control universal.

En algunos idiomas, son la construcción de flujo de control universal only . JavaScript, por ejemplo, no tiene Continuaciones ni GOTO , ni siquiera tiene llamadas de cola correctas. Por lo tanto, si desea implementar un flujo de control sofisticado en JavaScript, tiene para usar Excepciones.

El proyecto Microsoft Volta fue un proyecto de investigación (ahora suspendido) para compilar código .NET arbitrario para JavaScript. .NET tiene Excepciones cuya semántica no se asigna exactamente a JavaScript, pero lo más importante es que tiene Temas , y usted tiene que asignarlos de alguna manera a JavaScript. Volta hizo esto implementando Continuaciones de Volta utilizando excepciones de JavaScript y luego implementó todas las construcciones de flujo de control .NET en términos de Continuaciones de Volta. Ellos tenían para usar Excepciones como flujo de control, porque no hay otra construcción de flujo de control lo suficientemente poderosa.

Usted mencionó Máquinas de Estado. Los SM son triviales de implementar con las llamadas de cola apropiadas: cada estado es una subrutina, cada transición de estado es una llamada de subrutina. Los SM también se pueden implementar fácilmente con GOTO o Coroutines o Continuaciones. Sin embargo, Java no tiene ninguno de esos cuatro, pero tiene excepciones. Por lo tanto, es perfectamente aceptable utilizarlos como flujo de control. (Bueno, en realidad, la opción correcta probablemente sería usar un lenguaje con la estructura de flujo de control adecuada, pero a veces puede que esté atrapado en Java).

    
respondido por el Jörg W Mittag 05.03.2013 - 02:07
17

Como han mencionado muchos otros, ( por ejemplo, en esta pregunta de desbordamiento de pila ), el principio de menos asombro prohibirá el uso excesivo de excepciones solo para fines de flujo de control. Por otro lado, ninguna regla es 100% correcta, y siempre hay casos en los que una excepción es "la herramienta adecuada", como el propio goto , por cierto, que se envía en forma de break y continue en lenguajes como Java, que a menudo son la manera perfecta de saltar fuera de los bucles anidados, que no siempre se pueden evitar.

La siguiente publicación del blog explica un caso de uso bastante complejo pero también bastante interesante para un no local ControlFlowException :

Explica cómo dentro de jOOQ (una biblioteca de abstracción de SQL para Java) (descargo de responsabilidad: trabajo para el proveedor), tales excepciones son ocasionalmente se utiliza para abortar el proceso de representación SQL temprano cuando se cumple alguna condición "rara".

Ejemplos de tales condiciones son:

  • Se encuentran demasiados valores de enlace. Algunas bases de datos no admiten números arbitrarios de valores de enlace en sus sentencias de SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). En esos casos, jOOQ anula la fase de representación de SQL y vuelve a representar la instrucción SQL con valores de enlace en línea. Ejemplo:

    // 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();
    }
    

    Si extrajáramos explícitamente los valores de enlace de la consulta AST para contarlos cada vez, perderíamos ciclos de CPU valiosos para el 99.9% de las consultas que no sufren este problema.

  • Algunas lógicas solo están disponibles indirectamente a través de una API que queremos ejecutar solo "parcialmente". El método UpdatableRecord.store() genera una declaración INSERT o UPDATE , dependiendo de Las banderas internas de Record . Desde el "exterior", no sabemos qué tipo de lógica está contenida en store() (por ejemplo, bloqueo optimista, manejo del detector de eventos, etc.), por lo que no queremos repetir esa lógica cuando almacenamos varios registros en una declaración por lotes, donde nos gustaría que store() solo genere la instrucción SQL, no la ejecute realmente. Ejemplo:

    // 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());                
      }
    }
    

    Si hubiéramos externalizado la lógica store() en la API "reutilizable" que puede personalizarse para que, opcionalmente, no ejecute el SQL, estaríamos buscando crear un sistema bastante difícil de mantener, API difícilmente reutilizable.

Conclusión

Básicamente, nuestro uso de estos goto s no locales está en la línea de lo que Mason Wheeler dijo en su respuesta:

  

"Me acabo de encontrar con una situación que no puedo manejar adecuadamente en este momento, porque no tengo suficiente contexto para manejarlo, pero la rutina que me llamó (o algo más arriba en la pila de llamadas) debería saber cómo para manejarlo ".

Ambos usos de ControlFlowExceptions fueron bastante fáciles de implementar en comparación con sus alternativas, lo que nos permite reutilizar una amplia gama de lógica sin refactorizarla de las partes internas relevantes.

Pero la sensación de que esto es un poco sorprendente para los futuros mantenedores permanece. El código parece bastante delicado y, si bien fue la elección correcta en este caso, siempre preferiríamos no usar excepciones para el flujo de control local , donde es fácil evitar el uso de bifurcaciones ordinarias a través de if - else .

    
respondido por el Lukas Eder 11.01.2015 - 09:16
9

El uso de excepciones para el flujo de control es generalmente considerado un antipatrón, pero hay excepciones (no se pretende hacer un juego de palabras).

Se ha dicho mil veces, que las excepciones son para condiciones excepcionales. Una conexión de base de datos rota es una condición excepcional. Un usuario que ingresa letras en un campo de entrada que solo debe permitir números no es .

Un error en su software que hace que una función sea llamada con argumentos ilegales, por ejemplo. null donde no se permite, es una condición excepcional.

Al usar excepciones para algo que no es excepcional, estás usando abstracciones inapropiadas para el problema que estás tratando de resolver.

Pero también puede haber una penalización de rendimiento. Algunos idiomas tienen una implementación de manejo de excepciones más o menos eficiente, por lo que si su idioma de elección no tiene un manejo eficiente de excepciones, puede ser muy costoso, en cuanto al rendimiento *.

Pero otros idiomas, por ejemplo, Ruby, tienen una sintaxis similar a la de una excepción para el flujo de control. Las situaciones excepcionales son manejadas por los operadores raise / rescue . Pero puede usar throw / catch para construcciones de flujo de control similares a excepciones **.

Por lo tanto, aunque las excepciones generalmente no se utilizan para el flujo de control, su idioma de elección puede tener otros modismos.

* En el ejemplo de un uso costoso de rendimiento de las excepciones: una vez se configuró para optimizar una aplicación de formulario web ASP.NET de bajo rendimiento. Resultó que la representación de una tabla grande llamaba a int.Parse() en aprox. mil cadenas vacías en una página promedio, lo que resulta en aprox. Se manejan mil excepciones. Al reemplazar el código con int.TryParse() , ¡me afeité un segundo! Para cada solicitud de una sola página!

** Esto puede ser muy confuso para un programador que viene a Ruby desde otros idiomas, ya que tanto throw como catch son palabras clave asociadas con excepciones en muchos otros idiomas.

    
respondido por el Pete 20.10.2014 - 14:25
8

Es completamente posible manejar las condiciones de error sin el uso de excepciones. Algunos idiomas, sobre todo C, ni siquiera tienen excepciones, y las personas aún logran crear aplicaciones bastante complejas con él. La razón por la que las excepciones son útiles es que le permiten especificar de manera sucinta dos flujos de control esencialmente independientes en el mismo código: uno si ocurre un error y otro si no lo hace. Sin ellos, terminas con un código en todo el lugar que se ve así:

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

doSomething(*inout);

O equivalente en su idioma, como devolver una tupla con un valor como estado de error, etc. A menudo, las personas que señalan qué tan "costoso" es el manejo de excepciones, ignoran todas las declaraciones if adicionales como las anteriores. para agregar si no utiliza excepciones.

Si bien este patrón ocurre con mayor frecuencia cuando se manejan errores u otras "condiciones excepcionales", en mi opinión, si comienza a ver un código repetitivo como este en otras circunstancias, tiene un argumento bastante bueno para usar excepciones. Dependiendo de la situación y la implementación, puedo ver que las excepciones se usan de manera válida en una máquina de estados, porque tiene dos flujos de control ortogonales: uno que cambia el estado y otro para los eventos que ocurren dentro de los estados.

Sin embargo, esas situaciones son raras, y si va a hacer una excepción (es decir, el juego de palabras) a la regla, es mejor que esté preparado para mostrar su superioridad a otras soluciones. Una desviación sin tal justificación se llama, con razón, un antipatrón.

    
respondido por el Karl Bielefeldt 05.03.2013 - 00:22
5

En Python, las excepciones se utilizan para el generador y la terminación de iteración. Python tiene bloques try / except muy eficientes, pero en realidad, generar una excepción conlleva algunos gastos generales.

Debido a la falta de interrupciones de múltiples niveles o una declaración de goto en Python, a veces he usado excepciones:

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
    
respondido por el gahooa 05.03.2013 - 00:36
4

Vamos a hacer un bosquejo de tal uso de Excepción:

El algoritmo busca de forma recursiva hasta que se encuentra algo. Así que volviendo de la recursión, uno tiene que verificar el resultado para encontrarlo, y luego regresar, de lo contrario continuar. Y eso vuelve repetidamente de alguna profundidad de recursión.

Además de necesitar un% booleano adicional found (para ser empaquetado en una clase, donde de otro modo tal vez solo se hubiera devuelto un int), y para la profundidad de la recursión, ocurre lo mismo después de la exclusión.

Este desenlace de una pila de llamadas es justamente para lo que es una excepción. Así que me parece un medio de codificación no goto-like, más inmediato y apropiado. No es necesario, uso raro, tal vez mal estilo, pero al punto. Comparable con la operación de corte Prolog.

    
respondido por el Joop Eggen 20.10.2014 - 14:49
4

La programación es sobre el trabajo

Creo que la forma más fácil de responder a esto es entender el progreso que ha logrado OOP a lo largo de los años. Todo lo que se hace en OOP (y la mayoría de los paradigmas de programación, en realidad) se modela en torno a que necesita trabajo realizado .

Cada vez que se llama a un método, la persona que llama dice "No sé cómo hacer este trabajo, pero usted sabe cómo hacerlo, así que lo hace por mí".

Esto presentaba una dificultad: ¿qué sucede cuando el método llamado generalmente sabe cómo hacer el trabajo, pero no siempre? Necesitábamos una forma de comunicarnos "Quería ayudarte, realmente lo hice, pero no puedo hacer eso".

Una metodología temprana para comunicar esto fue simplemente devolver un valor de "basura". Tal vez espere un entero positivo, por lo que el método llamado devuelve un número negativo. Otra forma de lograr esto era establecer un valor de error en algún lugar. Desafortunadamente, ambas formas dieron lugar a un código que me permite verificar-aquí-para-asegurar-todo-es-kosher . A medida que las cosas se complican, este sistema se desmorona.

Una analogía excepcional

Digamos que tienes un carpintero, un fontanero y un electricista. Usted quiere que el fontanero arregle su fregadero, entonces él lo mira. No es muy útil si solo le dice a usted, "Lo siento, no puedo arreglarlo. Está roto". Demonios, es aún peor si tuviera que echarle un vistazo, dejarlo y enviarle un correo. Carta diciendo que no podía arreglarlo. Ahora tienes que revisar tu correo antes de saber que él no hizo lo que querías.

Lo que preferirías es que te diga, "Mira, no pude arreglarlo porque parece que tu bomba no está funcionando".

Con esta información, puede concluir que desea que el electricista revise el problema. Quizás el electricista encuentre algo relacionado con el carpintero, y usted necesitará que el carpintero lo arregle.

Vaya, es posible que ni siquiera sepa que necesita un electricista, o que no sepa quién necesita. Usted solo es de gerencia media en un negocio de reparación de viviendas, y su enfoque es la plomería. Entonces le dice a su jefe sobre el problema y luego él le dice al electricista que lo arregle.

Esto es lo que las excepciones están modelando: los modos de falla complejos de una manera desacoplada. El plomero no necesita saber acerca de un electricista, ni siquiera necesita saber que alguien de la cadena puede solucionar el problema. Él acaba de informar sobre el problema que encontró.

Entonces ... ¿un anti-patrón?

Ok, entonces el primer paso es entender el punto de las excepciones. Lo siguiente es entender qué es un anti-patrón.

Para calificar como un anti-patrón, necesita

  • resuelve el problema
  • tiene consecuencias definitivamente negativas

El primer punto se cumple fácilmente: el sistema funcionó, ¿no?

El segundo punto es más pegajoso. La razón principal para usar excepciones como flujo de control normal es mala porque ese no es su propósito. Cualquier pieza dada de funcionalidad en un programa debe tener un propósito relativamente claro, y la cooptación de ese propósito conduce a una confusión innecesaria.

Pero eso no es un daño definitivo . Es una manera pobre de hacer las cosas, y es raro, ¿pero un anti-patrón? No. Solo ... extraño.

    
respondido por el MirroredFate 29.06.2016 - 01:51

Lea otras preguntas en las etiquetas