Tener una bandera para indicar si debemos lanzar errores


64

Recientemente comencé a trabajar en un lugar con algunos desarrolladores mucho más antiguos (más de 50 años). Han trabajado en aplicaciones críticas relacionadas con la aviación donde el sistema no puede fallar. Como resultado, el programador anterior tiende a codificar de esta manera.

Tiende a poner un booleano en los objetos para indicar si se debe lanzar una excepción o no.

Ejemplo

public class AreaCalculator
{
    AreaCalculator(bool shouldThrowExceptions) { ... }
    CalculateArea(int x, int y)
    {
        if(x < 0 || y < 0)
        {
            if(shouldThrowExceptions) 
                throwException;
            else
                return 0;
        }
    }
}

(En nuestro proyecto, el método puede fallar porque estamos tratando de usar un dispositivo de red que no puede estar presente en ese momento. El ejemplo del área es solo un ejemplo del indicador de excepción)

Para mí, esto parece un olor a código. Escribir pruebas unitarias se vuelve un poco más complejo ya que debe probar la marca de excepción cada vez. Además, si algo sale mal, ¿no le gustaría saberlo de inmediato? ¿No debería ser responsabilidad de quien llama determinar cómo continuar?

Su lógica / razonamiento es que nuestro programa necesita hacer 1 cosa, mostrar datos al usuario. Cualquier otra excepción que no nos impida hacerlo debe ser ignorada. Estoy de acuerdo en que no deberían ignorarse, sino que deberían burbujear y ser manejados por la persona adecuada, y no tener que lidiar con banderas para eso.

¿Es esta una buena forma de manejar las excepciones?

Editar : solo para dar más contexto sobre la decisión de diseño, sospecho que es porque si este componente falla, el programa aún puede funcionar y hacer su tarea principal. Por lo tanto, no querríamos lanzar una excepción (¿y no manejarla?) Y hacer que elimine el programa cuando para el usuario funcione bien

Edición 2 : para dar aún más contexto, en nuestro caso se llama al método para restablecer una tarjeta de red. El problema surge cuando la tarjeta de red se desconecta y se vuelve a conectar, se le asigna una dirección IP diferente, por lo que Restablecer arrojará una excepción porque estaríamos intentando restablecer el hardware con la antigua IP.


22
c # tiene una convención para este patrón Try-Parse. Más información: docs.microsoft.com/en-us/dotnet/standard/design-guidelines/… La bandera no coincide con este patrón.
Peter

18
Esto es básicamente un parámetro de control y cambia la forma en que se ejecutan los métodos internos. Esto es malo, independientemente del escenario. martinfowler.com/bliki/FlagArgument.html , softwareengineering.stackexchange.com/questions/147977/… , medium.com/@amlcurran/…
bic

1
Además del comentario de Try-Parse de Peter, aquí hay un buen artículo sobre las excepciones de Vexing: blogs.msdn.microsoft.com/ericlippert/2008/09/10/…
Linaith

2
"Por lo tanto, no querríamos lanzar una excepción (¿y no manejarla?) Y hacer que elimine el programa cuando para el usuario funcione bien", ¿sabe que puede detectar excepciones, verdad?
user253751

1
Estoy bastante seguro de que esto se ha cubierto antes en otro lugar, pero dado el simple ejemplo de Área, me inclinaría más a preguntarme de dónde vendrían esos números negativos y si podría manejar esa condición de error en otro lugar (por ejemplo, lo que sea que estaba leyendo el archivo que contenía largo y ancho, por ejemplo); Sin embargo, el "estamos tratando de usar un dispositivo de red que no puede estar presente en ese momento". punto podría merecer una respuesta totalmente diferente, ¿es una API de terceros o algo estándar de la industria como TCP / UDP?
Jrh

Respuestas:


73

El problema con este enfoque es que, si bien nunca se lanzan excepciones (y, por lo tanto, la aplicación nunca se bloquea debido a excepciones no detectadas), los resultados devueltos no son necesariamente correctos y el usuario puede nunca saber que hay un problema con los datos (o cuál es ese problema y cómo corregirlo).

Para que los resultados sean correctos y significativos, el método de llamada debe verificar el resultado en busca de números especiales, es decir, valores de retorno específicos utilizados para denotar problemas que surgieron al ejecutar el método. Los números negativos (o cero) que se devuelven para cantidades positivas definidas (como área) son un buen ejemplo de esto en el código anterior. Sin embargo, si el método de llamada no sabe (¡u olvida!) Verificar estos números especiales, el procesamiento puede continuar sin darse cuenta de un error. Luego, los datos se muestran al usuario mostrando un área de 0, que el usuario sabe que es incorrecta, pero no tiene ninguna indicación de qué salió mal, dónde o por qué. Luego se preguntan si alguno de los otros valores está mal ...

Si se lanzara la excepción, el procesamiento se detendría, el error (idealmente) se registraría y el usuario podría ser notificado de alguna manera. El usuario puede arreglar lo que está mal y volver a intentarlo. El manejo adecuado de las excepciones (¡y las pruebas!) Asegurará que las aplicaciones críticas no se bloqueen o terminen en un estado no válido.


1
@Quirk Es impresionante cómo Chen logró violar el Principio de responsabilidad única en solo 3 o 4 líneas. Ese es el verdadero problema. Además, el problema del que habla (el programador no piensa en las consecuencias de los errores en cada línea) siempre es una posibilidad con excepciones no verificadas y solo a veces una posibilidad con excepciones marcadas. Creo que he visto todos los argumentos presentados contra las excepciones comprobadas, y ninguno de ellos es válido.
No U

@TKK personalmente, hay algunos casos en los que me he encontrado en los que me hubiera gustado realmente las excepciones marcadas en .NET. Sería bueno si hubiera algunas herramientas de análisis estático de gama alta que pudieran asegurarse de que lo que documenta una API como sus excepciones arrojadas sea preciso, aunque eso probablemente sería prácticamente imposible, especialmente al acceder a recursos nativos.
jrh

1
@jrh Sí, sería bueno si algo incluyera alguna seguridad de excepción en .NET, similar a cómo TypeScript excluye la seguridad de tipo en JS.
No U

47

¿Es esta una buena forma de manejar las excepciones?

No, creo que esta es una práctica bastante mala. Lanzar una excepción versus devolver un valor es un cambio fundamental en la API, cambiar la firma del método y hacer que el método se comporte de manera bastante diferente desde una perspectiva de interfaz.

En general, cuando diseñamos clases y sus API, debemos considerar que

  1. puede haber varias instancias de la clase con diferentes configuraciones flotando en el mismo programa al mismo tiempo, y

  2. Debido a la inyección de dependencia y a muchas otras prácticas de programación, un cliente consumidor puede crear los objetos y entregarlos a otro para que los use, por lo que a menudo tenemos una separación entre los creadores de objetos y los usuarios de objetos.

Considere ahora qué debe hacer el llamador del método para hacer uso de una instancia que le han entregado, por ejemplo, para llamar al método de cálculo: la persona que llama tendría que verificar si el área es cero y detectar excepciones, ¡ay! Las consideraciones de prueba van no solo a la clase en sí, sino también al manejo de errores de quienes llaman ...

Siempre debemos hacer las cosas lo más fácil posible para el cliente consumidor; Esta configuración booleana en el constructor que cambia la API de un método de instancia es lo opuesto a hacer que el programador cliente consumidor (tal vez usted o su colega) caiga en el pozo del éxito.

Para ofrecer ambas API, es mucho mejor y más normal proporcionar dos clases diferentes: una que siempre arroja un error y otra que siempre devuelve 0 en caso de error, o bien, proporciona dos métodos diferentes con una sola clase. De esta manera, el cliente consumidor puede saber fácilmente cómo verificar y manejar los errores.

Usando dos clases diferentes o dos métodos diferentes, puede usar los IDE para encontrar usuarios de métodos y funciones de refactorización, etc. mucho más fácilmente ya que los dos casos de uso ya no se combinan. La lectura, escritura, mantenimiento, revisiones y pruebas de código también es más simple.


En otra nota, personalmente siento que no deberíamos tomar parámetros de configuración booleanos, donde los llamadores reales simplemente pasan una constante . Dicha parametrización de la configuración combina dos casos de uso separados sin beneficio real.

¡Eche un vistazo a su base de código y vea si alguna vez se usa una variable (o expresión no constante) para el parámetro de configuración booleana en el constructor! Lo dudo.


Y una consideración adicional es preguntar por qué la computación del área puede fallar. Lo mejor podría ser lanzar el constructor, si no se puede hacer el cálculo. Sin embargo, si no sabe si el cálculo se puede hacer hasta que el objeto se inicialice más, entonces quizás considere usar diferentes clases para diferenciar esos estados (no está listo para calcular el área frente a listo para calcular el área).

Leí que su situación de falla está orientada hacia la comunicación remota, por lo que es posible que no se aplique; solo algo de comida para pensar.


¿No debería ser responsabilidad de quien llama determinar cómo continuar?

Sí estoy de acuerdo. Parece prematuro que la persona que llama decida que el área de 0 es la respuesta correcta en condiciones de error (especialmente porque 0 es un área válida, por lo que no hay forma de distinguir entre el error y el 0 real, aunque puede no aplicarse a su aplicación).


Realmente no tiene que verificar la excepción porque debe verificar los argumentos antes de llamar al método. Verificar el resultado contra cero no distingue entre los argumentos legales 0, 0 y los negativos ilegales. La API es realmente horrible en mi humilde opinión.
BlackJack

El anexo K MS empuja para iostreams C99 y C ++ son ejemplos de API donde un gancho o bandera cambia radicalmente la reacción a las fallas.
Deduplicador

37

Han trabajado en aplicaciones críticas relacionadas con la aviación donde el sistema no puede fallar. Como resultado ...

Esa es una introducción interesante, que me da la impresión de que la motivación detrás de este diseño es evitar arrojar excepciones en algunos contextos "porque el sistema podría fallar" . Pero si el sistema "puede fallar debido a una excepción", esta es una clara indicación de que

  • las excepciones no se manejan adecuadamente , al menos ni de manera rígida.

Entonces, si el programa que usa el AreaCalculatorestá defectuoso, su colega prefiere no hacer que el programa se "bloquee temprano", sino devolver algún valor incorrecto (esperando que nadie lo note, o esperando que nadie haga algo importante con él). En realidad, eso está enmascarando un error y, en mi experiencia, tarde o temprano conducirá a errores de seguimiento para los que se hace difícil encontrar la causa raíz.

En mi humilde opinión, escribir un programa que no se bloquee bajo ninguna circunstancia, pero muestre datos incorrectos o resultados de cálculo, generalmente no es mejor que dejar que el programa se bloquee. El único enfoque correcto es darle a la persona que llama la oportunidad de notar el error, lidiar con él, dejarle decidir si el usuario debe ser informado sobre el comportamiento incorrecto y si es seguro continuar con el procesamiento o si es más seguro para detener el programa por completo. Por lo tanto, recomendaría uno de los siguientes:

  • hace que sea difícil pasar por alto el hecho de que una función puede generar una excepción. La documentación y los estándares de codificación son su amigo aquí, y las revisiones periódicas de código deben respaldar el uso correcto de los componentes y el manejo adecuado de las excepciones.

  • capacite al equipo para que espere y maneje excepciones cuando usen componentes de "caja negra" y tenga en mente el comportamiento global del programa.

  • si por alguna razón cree que no puede obtener el código de llamada (o los desarrolladores que lo escriben) para usar el manejo de excepciones correctamente, entonces, como último recurso, puede diseñar una API con variables de salida de error explícitas y sin excepciones, como

    CalculateArea(int x, int y, out ErrorCode err)

    por lo que es muy difícil para la persona que llama pasar por alto la función que podría fallar. Pero esto es en mi humilde opinión extremadamente feo en C #; Es una vieja técnica de programación defensiva de C donde no hay excepciones, y normalmente no debería ser necesario trabajar en eso hoy en día.


3
"escribir un programa que no se cuelgue bajo ninguna circunstancia, pero muestre datos incorrectos o resultados de cálculo, por lo general no es mejor que dejar que el programa se cuelgue". Estoy totalmente de acuerdo en general, aunque podría imaginar que en la aviación probablemente preferiría tener el avión sigue con los instrumentos que muestran valores incorrectos en comparación con el apagado de la computadora del avión. Para todas las aplicaciones menos críticas, definitivamente es mejor no enmascarar errores.
Trilarion

18
@Trilarion: si un programa para computadora de vuelo no contiene el manejo adecuado de excepciones, "solucionar esto" haciendo que los componentes no arrojen excepciones es un enfoque muy equivocado. Si el programa falla, debería haber algún sistema de respaldo redundante que pueda hacerse cargo. Si el programa no se bloquea y muestra una altura incorrecta, por ejemplo, los pilotos pueden pensar "todo está bien" mientras el avión se precipita hacia la próxima montaña.
Doc Brown

77
@Trilarion: si la computadora de vuelo muestra una altura incorrecta y el avión se estrellará debido a esto, tampoco lo ayudará (especialmente cuando el sistema de respaldo está allí y no se informa que necesita hacerse cargo). Los sistemas de respaldo para computadoras de avión no son una idea nueva, google para "sistemas de respaldo de computadora de avión", estoy bastante seguro de que los ingenieros de todo el mundo siempre construyeron sistemas redundantes en cualquier sistema crítico de la vida real (y si fuera solo por no perder) seguro).
Doc Brown

44
Esta. Si no puede permitirse que el programa se bloquee, tampoco puede permitirse que silenciosamente dé respuestas incorrectas. La respuesta correcta es tener un manejo de excepciones apropiado en todos los casos. Para un sitio web, eso significa un controlador global que convierte errores inesperados en 500. También puede tener controladores adicionales para situaciones más específicas, como tener un try/ catchdentro de un ciclo si necesita continuar el procesamiento si falla un elemento.
jpmc26

2
Obtener el resultado incorrecto es siempre el peor tipo de falla; eso me recuerda la regla sobre la optimización: "hazlo correctamente antes de optimizar, porque obtener la respuesta incorrecta más rápido aún no beneficia a nadie".
Toby Speight

13

Escribir pruebas unitarias se vuelve un poco más complejo ya que debe probar la marca de excepción cada vez.

Cualquier función con n parámetros será más difícil de probar que una con parámetros n-1 . Extienda eso hasta lo absurdo y el argumento se convierte en que las funciones no deberían tener parámetros porque eso las hace más fáciles de probar.

Si bien es una gran idea escribir código que sea fácil de probar, es una idea terrible poner la simplicidad de prueba por encima de la escritura de código útil para las personas que tienen que llamarlo. Si el ejemplo en la pregunta tiene un interruptor que determina si se produce o no una excepción, es posible que las personas que llaman que desean ese comportamiento merezcan agregarlo a la función. Donde está la línea entre lo complejo y lo demasiado complejo es un juicio; cualquier persona que trate de decirle que hay una línea brillante que se aplica en todas las situaciones debe mirar con desconfianza.

Además, si algo sale mal, ¿no le gustaría saberlo de inmediato?

Eso depende de tu definición de error. El ejemplo en la pregunta define mal como "dada una dimensión de menos de cero y shouldThrowExceptionses verdadera". Recibir una dimensión si menos de cero no está mal cuando shouldThrowExceptionses falso porque el interruptor induce un comportamiento diferente. Eso no es, simplemente, una situación excepcional.

El verdadero problema aquí es que el conmutador tenía un nombre incorrecto porque no es descriptivo de lo que hace que la función haga. Si se le hubiera dado un nombre mejor treatInvalidDimensionsAsZero, ¿habría hecho esta pregunta?

¿No debería ser responsabilidad de quien llama determinar cómo continuar?

La persona que llama determina cómo continuar. En este caso, lo hace con anticipación configurando o borrando shouldThrowExceptionsy la función se comporta de acuerdo con su estado.

El ejemplo es patológicamente simple porque hace un solo cálculo y devuelve. Si lo hace un poco más complejo, como calcular la suma de las raíces cuadradas de una lista de números, lanzar excepciones puede dar problemas a las personas que llaman que no pueden resolver. Si paso una lista de [5, 6, -1, 8, 12]y la función arroja una excepción sobre el -1, no tengo forma de decirle a la función que continúe porque ya habrá abortado y tirado la suma. Si la lista es un conjunto de datos enorme, generar una copia sin ningún número negativo antes de llamar a la función podría no ser práctico, por lo que me veo obligado a decir de antemano cómo se deben tratar los números no válidos, ya sea en forma de "simplemente ignórelos". "cambiar o tal vez proporcionar una lambda que sea llamada para tomar esa decisión.

Su lógica / razonamiento es que nuestro programa necesita hacer 1 cosa, mostrar datos al usuario. Cualquier otra excepción que no nos impida hacerlo debe ser ignorada. Estoy de acuerdo en que no deberían ignorarse, sino que deberían burbujear y ser manejados por la persona adecuada, y no tener que lidiar con banderas para eso.

Una vez más, no existe una solución única para todos. En el ejemplo, la función fue, presumiblemente, escrita con una especificación que dice cómo lidiar con las dimensiones negativas. Lo último que quiere hacer es reducir la relación señal / ruido de sus registros llenándolos con mensajes que digan "normalmente, se lanzaría una excepción aquí, pero la persona que llama dijo que no se molestara".

Y como uno de esos programadores mucho más viejos, le pediría que amablemente se aleje de mi césped. ;-)


Estoy de acuerdo en que la denominación y la intención son extremadamente importantes y, en este caso, el nombre del parámetro adecuado realmente puede cambiar las tornas, por lo que es +1, pero 1. la our program needs to do 1 thing, show data to user. Any other exception that doesn't stop us from doing so should be ignoredmentalidad puede llevar al usuario a tomar decisiones basadas en los datos incorrectos (porque en realidad el programa debe hacerlo 1 cosa: ayudar al usuario a tomar decisiones informadas) 2. casos similares, como por ejemplo, bool ExecuteJob(bool throwOnError = false)son extremadamente propensos a errores y conducen a un código difícil de razonar con solo leerlo.
Eugene Podskal

@EugenePodskal Creo que la suposición es que "mostrar datos" significa "mostrar datos correctos". El interlocutor no dice que el producto terminado no funciona, solo que podría estar escrito "incorrecto". Tendría que ver algunos datos duros sobre el segundo punto. Tengo un puñado de funciones muy utilizadas en mi proyecto actual que tienen un interruptor de lanzar / no lanzar y no son más difíciles de razonar que cualquier otra función, pero ese es un punto de datos.
Blrfl

Buena respuesta, creo que esta lógica se aplica a una gama mucho más amplia de circunstancias que solo los OP. Por cierto, el nuevo cpp tiene una versión de lanzamiento / no lanzamiento por exactamente estos motivos. Sé que hay algunas pequeñas diferencias, pero ...
drjpizzle

8

El código de seguridad crítico y 'normal' puede conducir a ideas muy diferentes de cómo se ve la 'buena práctica'. Hay mucha superposición: algunas cosas son arriesgadas y deben evitarse en ambas, pero aún existen diferencias significativas. Si agrega un requisito para garantizar que responda, estas desviaciones se vuelven bastante sustanciales.

Estos a menudo se relacionan con cosas que esperarías:

  • Para git, la respuesta incorrecta podría ser muy mala en relación con: tomar demasiado tiempo / abortar / colgar o incluso estrellarse (que efectivamente no son problemas en relación con, digamos, alterar accidentalmente el código facturado).

    Sin embargo: para un panel de instrumentos que tiene un cálculo de la fuerza g se estanca y evita que se haga un cálculo de la velocidad del aire puede ser inaceptable.

Algunos son menos obvios:

  • Si ha probado mucho , los resultados de primer orden (como las respuestas correctas) no son una preocupación tan grande en términos relativos. Sabes que tus pruebas habrán cubierto esto. Sin embargo, si hubo un estado oculto o flujo de control, no sabe que esto no será la causa de algo mucho más sutil. Esto es difícil de descartar con las pruebas.

  • Ser demostrablemente seguro es relativamente importante. No muchos clientes pensarán si la fuente que están comprando es segura o no. Si estás en el mercado de la aviación por otro lado ...

¿Cómo se aplica esto a su ejemplo?

No lo sé. Hay una serie de procesos de pensamiento que pueden tener reglas principales como "Nadie arroja código de producción" que se adopta en un código crítico de seguridad que sería bastante tonto en situaciones más habituales.

Algunos se relacionarán con la integración, algunos de seguridad y tal vez otros ... Algunos son buenos (se necesitaban límites de rendimiento / memoria ajustados) algunos son malos (no manejamos las excepciones correctamente, así que mejor no arriesgarnos). La mayoría de las veces, incluso sabiendo por qué lo hicieron, realmente no responderá la pregunta. Por ejemplo, si tiene que ver con la facilidad de auditar el código más que mejorarlo, ¿es una buena práctica? Realmente no puedes decirlo. Son animales diferentes y necesitan un trato diferente.

Dicho todo esto, me parece un poco sospechoso PERO :

El software crítico de seguridad y las decisiones de diseño de software probablemente no deberían ser tomadas por extraños en el intercambio de pila de ingeniería de software. Puede haber una buena razón para hacerlo, incluso si es parte de un mal sistema. No leas demasiado a nada de esto más que como "alimento para el pensamiento".


7

A veces, lanzar una excepción no es el mejor método. No menos debido al desbobinado de la pila, sino a veces porque atrapar una excepción es problemático, particularmente a lo largo del lenguaje o las costuras de la interfaz.

Una buena manera de manejar esto es devolver un tipo de datos enriquecido. Este tipo de datos tiene suficiente estado para describir todas las rutas felices y todas las rutas infelices. El punto es que si interactúa con esta función (miembro / global / de otro modo) se verá obligado a manejar el resultado.

Dicho esto, este tipo de datos enriquecido no debería forzar la acción. Imagine en su área, por ejemplo, algo así var area_calc = new AreaCalculator(); var volume = area_calc.CalculateArea(x, y) * z;. Parece útil volumedebe contener el área multiplicada por la profundidad, que podría ser un cubo, un cilindro, etc.

¿Pero qué pasa si el servicio area_calc no funciona? Luego area_calc .CalculateArea(x, y)devolvió un rico tipo de datos que contiene un error. ¿Es legal multiplicar eso por z? Es una buena pregunta. Puede obligar a los usuarios a manejar la verificación de inmediato. Sin embargo, esto rompe la lógica con el manejo de errores.

var area_calc = new AreaCalculator();
var area_result = area_calc.CalculateArea(x, y);
if (area_result.bad())
{
    //handle unhappy path
}
var volume = area_result.value() * z;

vs

var area_calc = new AreaCalculator();
var volume = area_calc.CalculateArea(x, y) * z;
if (volume.bad())
{
    //handle unhappy path
}

La lógica esencialmente se extiende sobre dos líneas y se divide por manejo de errores en el primer caso, mientras que el segundo caso tiene toda la lógica relevante en una línea seguida por el manejo de errores.

En ese segundo caso volumees un tipo de datos rico. No es solo un número. Esto hace que el almacenamiento sea más grande y volumeaún deberá investigarse para detectar una condición de error. Además, volumepuede alimentar otros cálculos antes de que el usuario decida manejar el error, permitiéndole manifestarse en varias ubicaciones dispares. Esto puede ser bueno o malo dependiendo de los detalles de la situación.

Alternativamente, volumepodría ser solo un tipo de datos simple, solo un número, pero ¿qué sucede con la condición de error? Podría ser que el valor se convierta implícitamente si está en una condición feliz. Si está en una condición infeliz, puede devolver un valor predeterminado / de error (para el área 0 o -1 puede parecer razonable). Alternativamente, podría arrojar una excepción en este lado del límite de la interfaz / idioma.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}
var volume = foo();
if (volume <= 0)
{
    //handle error
}

vs.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}

try { var volume = foo(); }
catch(...)
{
    //handle error
}

Al pasar un valor incorrecto o posiblemente incorrecto, el usuario tiene una gran responsabilidad para validar los datos. Esta es una fuente de errores, porque en lo que respecta al compilador, el valor de retorno es un entero legítimo. Si no se revisó algo, lo descubrirá cuando las cosas salgan mal. El segundo caso combina lo mejor de ambos mundos al permitir excepciones para manejar rutas infelices, mientras que las rutas felices siguen el procesamiento normal. Desafortunadamente, obliga al usuario a manejar sabiamente las excepciones, lo cual es difícil.

Para ser claros, una ruta infeliz es un caso desconocido para la lógica de negocios (el dominio de la excepción), no validar es una ruta feliz porque sabes cómo manejar eso mediante las reglas de negocio (el dominio de las reglas).

La solución definitiva sería una que permita todos los escenarios (dentro de lo razonable).

  • El usuario debe poder consultar por una mala condición y manejarla de inmediato
  • El usuario debe poder operar en el tipo enriquecido como si se hubiera seguido la ruta feliz y propagar los detalles del error.
  • El usuario debe poder extraer el valor de la ruta feliz a través de la conversión (implícita / explícita, como es razonable), generando una excepción para las rutas infelices.
  • El usuario debe poder extraer el valor de la ruta feliz o usar un valor predeterminado (suministrado o no)

Algo como:

Rich::value_type value_or_default(Rich&, Rich::value_type default_value = ...);
bool bad(Rich&);
...unhappy path report... bad_state(Rich&);
Rich& assert_not_bad(Rich&);
class Rich
{
public:
   typedef ... value_type;

   operator value_type() { assert_not_bad(*this); return ...value...; }
   operator X(...) { if (bad(*this)) return ...propagate badness to new value...; /*operate and generate new value*/; }
}

//check
if (bad(x))
{
    var report = bad_state(x);
    //handle error
}

//rethrow
assert_not_bad(x);
var result = (assert_not_bad(x) + 23) / 45;

//propogate
var y = x * 23;

//implicit throw
Rich::value_type val = x;
var val = ((Rich::value_type)x) + 34;
var val2 = static_cast<Rich::value_type>(x) % 3;

//default value
var defaulted = value_or_default(x);
var defaulted_to = value_or_default(x, 55);

@TobySpeight Bastante justo, estas cosas son sensibles al contexto y tienen su rango de alcance.
Kain0_0

Creo que el problema aquí son los bloques 'afirmar_no_bajas'. Creo que estos terminarán en el mismo lugar que el código original intentó resolver. Sin embargo, en las pruebas, deben tenerse en cuenta, si realmente son afirmaciones, deben eliminarse antes de la producción en un avión real. de lo contrario algunos grandes puntos.
drjpizzle

@drjpizzle Diría que si fuera lo suficientemente importante como para agregar un protector para la prueba, es lo suficientemente importante como para dejar el protector en su lugar cuando se ejecuta en producción. La presencia del guardia en sí implica duda. Si duda del código lo suficiente como para protegerlo durante las pruebas, lo duda por una razón técnica. es decir, la condición podría / ocurre de manera realista. Las pruebas de ejecución no prueban que la condición nunca se alcanzará en la producción. Eso significa que hay una condición conocida, que podría ocurrir, que debe manejarse de alguna manera. Creo que cómo se maneja, es el problema.
Kain0_0

3

Contestaré desde el punto de vista de C ++. Estoy bastante seguro de que todos los conceptos básicos son transferibles a C #.

Parece que su estilo preferido es "siempre lanzar excepciones":

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

Esto puede ser un problema para el código C ++ porque el manejo de excepciones es pesado : hace que el caso de falla se ejecute lentamente y hace que el caso de falla asigne memoria (que a veces ni siquiera está disponible), y generalmente hace que las cosas sean menos predecibles. El peso pesado de EH es una de las razones por las que escuchas a la gente decir cosas como "No uses excepciones para controlar el flujo".

Entonces, algunas bibliotecas (como <filesystem>) usan lo que C ++ llama una "API dual", o lo que C # llama el Try-Parsepatrón (¡gracias Peter por el consejo!)

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

bool TryCalculateArea(int x, int y, int& result) {
    if (x < 0 || y < 0) {
        return false;
    }
    result = x * y;
    return true;
}

int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
    // use a2
}

Puede ver el problema con las "API duales" de inmediato: mucha duplicación de código, sin orientación para los usuarios sobre qué API es la "correcta" para usar, y el usuario debe tomar una decisión difícil entre mensajes de error útiles ( CalculateArea) y speed ( TryCalculateArea) porque la versión más rápida toma nuestra útil "negative side lengths"excepción y la aplana en un inútil false: "algo salió mal, no me preguntes qué o dónde". (Algunas API duales utilizan un tipo de error más expresivo, como int errnoo C ++ 's std::error_code, pero que todavía no le dice dónde se produjo el error - sólo que lo hizo aparecer en alguna parte.)

Si no puede decidir cómo debe comportarse su código, ¡siempre puede pasar la decisión a la persona que llama!

template<class F>
int CalculateArea(int x, int y, F errorCallback) {
    if (x < 0 || y < 0) {
        return errorCallback(x, y, "negative side lengths");
    }
    return x * y;
}

int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });

Esto es esencialmente lo que está haciendo su compañero de trabajo; excepto que está factorizando el "controlador de errores" en una variable global:

std::function<int(const char *)> g_errorCallback;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorCallback("negative side lengths");
    }
    return x * y;
}

g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);

Mover parámetros importantes de parámetros de funciones explícitas al estado global es casi siempre una mala idea. No lo recomiendo. (El hecho de que no sea un estado global en su caso, sino simplemente un estado miembro de toda la instancia mitiga un poco la maldad, pero no mucho).

Además, su compañero de trabajo está limitando innecesariamente el número de posibles comportamientos de manejo de errores. En lugar de permitir cualquier lambda de manejo de errores, decidió solo dos:

bool g_errorViaException;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorViaException ? throw Exception("negative side lengths") : 0;
    }
    return x * y;
}

g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);

Este es probablemente el "punto amargo" de cualquiera de estas posibles estrategias. Le ha quitado toda la flexibilidad al usuario final al obligarlo a usar una de sus dos devoluciones de llamada para el manejo de errores; y tienes todos los problemas del estado global compartido; y todavía estás pagando por esa rama condicional en todas partes.

Finalmente, una solución común en C ++ (o cualquier lenguaje con compilación condicional) sería forzar al usuario a tomar la decisión de todo su programa, globalmente, en tiempo de compilación, para que la ruta de código no tomada se pueda optimizar por completo:

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
        return 0;
#else
        throw Exception("negative side lengths");
#endif
    }
    return x * y;
}

// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);

Un ejemplo de algo que funciona de esta manera es la assertmacro en C y C ++, que condiciona su comportamiento en la macro del preprocesador NDEBUG.


Si se devuelve un std::optionaldesde TryCalculateArea(), es sencillo unificar la implementación de ambas partes de la interfaz dual en una única plantilla de función con un indicador de tiempo de compilación.
Deduplicador

@Dupuplicator: Tal vez con un std::expected. Con solo std::optional, a menos que malinterprete su solución propuesta, aún sufriría por lo que dije: el usuario debe tomar una decisión difícil entre mensajes de error útiles y velocidad, porque la versión más rápida toma nuestra "negative side lengths"excepción útil y la aplana en un inútil false- " algo salió mal, no me preguntes qué o dónde ".
Quuxplusone

Es por eso que libc ++ <filesystem> en realidad hace algo muy cercano al patrón del compañero de trabajo de OP: se canaliza std::error_code *eca través de todos los niveles de la API, y luego en la parte inferior hace el equivalente moral de if (ec == nullptr) throw something; else *ec = some error code. (Resume lo real ifen algo llamado ErrorHandler, pero es la misma idea básica.)
Quuxplusone

Bueno, esa sería una opción para mantener la información de error extendida sin tirar. Puede ser apropiado o no valer el costo adicional potencial.
Deduplicador

1
Tantos buenos pensamientos contenidos en esta respuesta ... Definitivamente necesita más votos a favor :-)
cmaster

1

Creo que debería mencionarse de dónde obtuvo su patrón su colega.

Hoy en día, C # tiene el patrón TryGet public bool TryThing(out result). Esto le permite obtener su resultado, sin dejar de saber si ese resultado es incluso un valor válido. (Por ejemplo, todos los intvalores son resultados válidos para Math.sum(int, int), pero si el valor se desbordara, este resultado particular podría ser basura). Sin embargo, este es un patrón relativamente nuevo.

Antes de la outpalabra clave, tenía que lanzar una excepción (costosa, y la persona que llama tenía que atraparla o matar todo el programa), crear una estructura especial (clase antes de clase o genéricos eran realmente una cosa) para que cada resultado representara el valor y posibles errores (toma tiempo y tiempo para crear e inflar el software), o devolver un valor predeterminado de "error" (que podría no haber sido un error).

El enfoque que utiliza su colega les brinda la posibilidad de fallar al principio de las excepciones al probar / depurar nuevas características, al tiempo que les brinda la seguridad y el rendimiento en tiempo de ejecución (el rendimiento fue un problema crítico todo el tiempo ~ 30 años atrás) de solo devolver un valor de error predeterminado. Ahora, este es el patrón en el que se escribió el software y el patrón esperado para avanzar, por lo que es natural seguir haciéndolo de esta manera, aunque ahora hay mejores formas. Lo más probable es que este patrón se haya heredado de la edad del software, o un patrón que sus universidades nunca crecieron (los viejos hábitos son difíciles de romper).

Las otras respuestas ya cubren por qué esto se considera una mala práctica, por lo que terminaré recomendando que lea sobre el patrón TryGet (tal vez también la encapsulación de lo que promete un objeto a su llamador).


Antes de la outpalabra clave, escribiría una boolfunción que tome un puntero al resultado, es decir, un refparámetro. Podría hacerlo en VB6 en 1998. La outpalabra clave simplemente le compra la seguridad en tiempo de compilación de que el parámetro se asigna cuando la función regresa, eso es todo. Sin embargo, es un patrón agradable y útil.
Mathieu Guindon

@MathieuGuindon Yeay, pero Get Try aún no era un patrón bien conocido / establecido, e incluso si lo fuera, no estoy completamente seguro de que se hubiera utilizado. Después de todo, parte del avance hasta Y2K fue que almacenar algo más grande que 0-99 era inaceptable.
Tezra

0

Hay momentos en que desea hacer su enfoque, pero no consideraría que sean la situación "normal". La clave para determinar en qué caso se encuentra es:

Su lógica / razonamiento es que nuestro programa necesita hacer 1 cosa, mostrar datos al usuario. Cualquier otra excepción que no nos impida hacerlo debe ser ignorada.

Consulta los requisitos. Si sus requisitos realmente dicen que tiene un trabajo, que es mostrar datos al usuario, entonces tiene razón. Sin embargo, en mi experiencia, la mayoría de las veces al usuario también le importa qué datos se muestran. Quieren los datos correctos. Algunos sistemas solo quieren fallar en silencio y dejar que un usuario descubra que algo salió mal, pero los consideraría la excepción a la regla.

La pregunta clave que haría después de una falla es "¿Está el sistema en un estado donde las expectativas del usuario y los invariantes del software son válidos?" Si es así, entonces, por supuesto, solo regrese y continúe. Prácticamente hablando, esto no es lo que sucede en la mayoría de los programas.

En cuanto al indicador en sí, el indicador de excepciones generalmente se considera olor de código porque un usuario necesita saber de alguna manera en qué modo se encuentra el módulo para comprender cómo funciona la función. Si está en !shouldThrowExceptionsel modo, el usuario tiene que saber que ellos son responsables de la detección de errores y el mantenimiento de las expectativas e invariantes cuando se producen. También son responsables en ese mismo momento, en la línea donde se llama la función. Una bandera como esta suele ser muy confusa.

Sin embargo, sucede. Considere que muchos procesadores permiten cambiar el comportamiento del punto flotante dentro del programa. Un programa que desea tener estándares más relajados puede hacerlo simplemente cambiando un registro (que es efectivamente una bandera). El truco es que debes ser muy cauteloso para evitar pisar accidentalmente los dedos de los demás. El código a menudo verificará el indicador actual, lo configurará en la configuración deseada, realizará operaciones y luego lo restablecerá. De esa manera, nadie se sorprende con el cambio.


0

Este ejemplo específico tiene una característica interesante que puede afectar las reglas ...

CalculateArea(int x, int y)
{
    if(x < 0 || y < 0)
    {
        if(shouldThrowExceptions) 
            throwException;
        else
            return 0;
    }
}

Lo que veo aquí es una verificación de precondición . Una comprobación de precondición fallida implica un error más arriba en la pila de llamadas. Por lo tanto, la pregunta es si este código es responsable de informar errores ubicados en otro lugar.

Parte de la tensión aquí es atribuible al hecho de que esta interfaz exhibe una obsesión primitiva , xy yse supone que representa mediciones reales de longitud. En un contexto de programación donde los tipos específicos de dominios son una opción razonable, en efecto, acercaríamos la verificación de precondición a la fuente de los datos; en otras palabras, dejaríamos la responsabilidad de la integridad de los datos más arriba en la pila de llamadas, donde tenemos un mejor sentido para el contexto.

Dicho esto, no veo nada fundamentalmente malo en tener dos estrategias diferentes para administrar un cheque fallido. Mi preferencia sería usar la composición para determinar qué estrategia está en uso; el indicador de característica se usaría en la raíz de la composición, en lugar de en la implementación del método de la biblioteca.

// Configurable dependencies
AreaCalculator(PreconditionFailureStrategy strategy)

CalculateArea(int x, int y)
{
    if (x < 0 || y < 0) {
        return this.strategy.fail(0);
    }
    // ...
}

Han trabajado en aplicaciones críticas relacionadas con la aviación donde el sistema no puede fallar.

La Junta Nacional de Tráfico y Seguridad es realmente buena; Podría sugerir técnicas de implementación alternativas a las barbas grises, pero no estoy dispuesto a discutir con ellas sobre el diseño de mamparos en el subsistema de informe de errores.

En términos más generales: ¿cuál es el costo para el negocio? Es mucho más barato bloquear un sitio web que un sistema vital.


Me gusta la sugerencia de una forma alternativa de retener la flexibilidad deseada mientras se moderniza.
drjpizzle

-1

Los métodos manejan excepciones o no, no hay necesidad de banderas en lenguajes como C #.

public int Method1()
{
  ...code

 return 0;
}

Si algo sale mal en ... el código, entonces la persona que llama tendrá que manejar esa excepción. Si nadie maneja el error, el programa terminará.

public int Method1()
{
try {  
...code
}
catch {}
 ...Handle error 
}
return 0;
}

En este caso, si sucede algo malo en ... el código, el Método 1 está manejando el problema y el programa debe continuar.

Donde maneja las excepciones depende de usted. Ciertamente puedes ignorarlos atrapando y sin hacer nada. Pero, me aseguraría de que solo esté ignorando ciertos tipos específicos de excepciones que puede esperar que ocurran. Ignorar ( exception ex) es peligroso porque algunas excepciones que no desea ignorar, como las excepciones del sistema con respecto a la falta de memoria, etc.


3
La configuración actual que OP publicó pertenece a la decisión de lanzar voluntariamente una excepción. El código de OP no conduce a la ingestión no deseada de cosas como excepciones de memoria insuficiente. En todo caso, la afirmación de que las excepciones bloquean el sistema implica que la base del código no captura excepciones y, por lo tanto, no se tragará ninguna excepción; los que fueron y no fueron arrojados por la lógica comercial de OP intencionalmente.
Flater

-1

Este enfoque rompe la filosofía de "falla rápido, falla duro".

Por qué quieres fallar rápido:

  • Cuanto más rápido falle, más cerca estará el síntoma visible de la falla de la causa real de la falla. Esto hace que la depuración sea mucho más fácil: en el mejor de los casos, tiene la línea de error justo en la primera línea de su seguimiento de pila.
  • Cuanto más rápido falle (y detecte el error apropiadamente), menos probable es que confunda el resto de su programa.
  • Cuanto más difícil falle (es decir, arroje una excepción en lugar de simplemente devolver un código "-1" o algo así), es más probable que la persona que llama realmente se preocupe por el error y no solo siga trabajando con valores incorrectos.

Inconvenientes de no fallar rápido y duro:

  • Si evita fallas visibles, es decir, finge que todo está bien, tiende a hacer que sea increíblemente difícil encontrar el error real. Imagine que el valor de retorno de su ejemplo es parte de una rutina que calcula la suma de 100 áreas; es decir, llamar a esa función 100 veces y sumar los valores de retorno. Si silenciosamente suprime el error, no hay forma de encontrar dónde ocurre el error real; y todos los siguientes cálculos serán silenciosamente incorrectos.
  • Si retrasa la falla (al devolver un valor de retorno imposible como "-1" para un área), aumenta la probabilidad de que la persona que llama de su función simplemente no se preocupe por eso y se olvide de manejar el error; a pesar de que tienen la información sobre la falla en la mano.

Finalmente, el manejo real de errores basado en excepciones tiene el beneficio de que puede dar un mensaje u objeto de error "fuera de banda", puede enganchar fácilmente el registro de errores, alertas, etc. sin escribir una sola línea adicional en su código de dominio .

Por lo tanto, no solo hay razones técnicas simples, sino también razones de "sistema" que hacen que sea muy útil fallar rápidamente.

Al final del día, no fallar duro y rápido en nuestros tiempos actuales, donde el manejo de excepciones es liviano y muy estable es solo medio criminal. Entiendo perfectamente de dónde viene el pensamiento de que es bueno suprimir las excepciones, pero ya no es aplicable.

Especialmente en su caso particular, donde incluso da la opción de lanzar excepciones o no: esto significa que la persona que llama tiene que decidir de todos modos . Por lo tanto, no hay inconveniente en absoluto para que la persona que llama detecte la excepción y la maneje adecuadamente.

Un punto que aparece en un comentario:

Se ha señalado en otras respuestas que fallar rápido y duro no es deseable en una aplicación crítica cuando el hardware en el que se está ejecutando es un avión.

Fallar rápido y duro no significa que toda la aplicación se bloquee. Significa que en el punto donde ocurre el error, está fallando localmente. En el ejemplo del OP, el método de bajo nivel que calcula alguna área no debe reemplazar silenciosamente un error por un valor incorrecto. Debería fallar claramente.

Obviamente, alguien que llama en la cadena tiene que detectar ese error / excepción y manejarlo adecuadamente. Si este método se usó en un avión, esto probablemente debería provocar que se encienda un LED de error, o al menos para mostrar "área de cálculo de error" en lugar de un área incorrecta.


3
Se ha señalado en otras respuestas que fallar rápido y duro no es deseable en una aplicación crítica cuando el hardware en el que se está ejecutando es un avión.
HAEM

1
@HAEM, entonces eso es un malentendido sobre lo que significa fallar rápido y difícil. He agregado un párrafo sobre esto a la respuesta.
AnoE

Incluso si la intención no es fallar 'tan duro', todavía se considera arriesgado jugar con ese tipo de fuego.
drjpizzle

Ese es el punto, @drjpizzle. Si estás acostumbrado a fallar, falla rápido, no es "arriesgado" o "jugar con ese tipo de fuego" en mi experiencia. Au contraire. Significa que te acostumbras a pensar "qué pasaría si obtuviera una excepción aquí", donde "aquí" significa en todas partes , y tiendes a ser consciente de si el lugar donde estás programando actualmente va a tener un problema importante ( accidente aéreo, lo que sea en ese caso). Jugar con fuego sería esperar que todo
salga
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.