Excepciones: ¿Por qué tirar temprano? ¿Por qué atrapar tarde?

156

Existen muchas mejores prácticas bien conocidas sobre el manejo de excepciones de forma aislada. Sé lo que se debe y no se debe hacer, pero las cosas se complican cuando se trata de mejores prácticas o patrones en entornos más grandes. "Lanzar temprano, atrapar tarde" - He escuchado muchas veces y todavía me confunde.

¿Por qué debería lanzar temprano y atrapar tarde, si en una capa de bajo nivel se produce una excepción de puntero nulo? ¿Por qué debería atraparlo en una capa superior? No tiene sentido para mí detectar una excepción de bajo nivel en un nivel superior, como una capa empresarial. Parece violar las preocupaciones de cada capa.

Imagine la siguiente situación:

Tengo un servicio que calcula una cifra. Para calcular la cifra, el servicio accede a un repositorio para obtener datos sin procesar y algunos otros servicios para preparar el cálculo. Si algo salió mal en la capa de recuperación de datos, ¿por qué debería lanzar una DataRetrievalException a un nivel superior? Por el contrario, preferiría incluir la excepción en una excepción significativa, por ejemplo, una CalculationServiceException.

¿Por qué tirar temprano, por qué atrapar tarde?

shylynx
fuente
104
La idea detrás de "atrapar tarde" es, en lugar de atrapar lo más tarde posible, atrapar tan pronto como sea útil. Si tiene un analizador de archivos, por ejemplo, no tiene sentido manejar un archivo que no se encuentra. ¿Qué se supone que debes hacer con eso, cuál es tu camino de recuperación? No hay una, así que no la atrapes. Ve a tu lexer, ¿qué haces allí? ¿Cómo te recuperas de esto para que tu programa pueda continuar? No puede, deje pasar la excepción. ¿Cómo puede su escáner manejar esto? No puede, déjalo pasar. ¿Cómo puede el código de llamada manejar esto? Puede intentar con otra ruta de archivo o alertar al usuario, así que captura.
Phoshi
16
Hay muy pocos casos en los que una excepción NullPointerException (supongo que eso es lo que significa NPE) alguna vez debería detectarse; si es posible, debe evitarse en primer lugar. Si tiene NullPointerExceptions, entonces tiene algún código roto que necesita ser reparado. Es muy probable que también sea una solución fácil.
Phil
66
Por favor, muchachos, antes de sugerir cerrar esta pregunta como un duplicado, verifique que la respuesta a la otra pregunta no responda muy bien a esta pregunta.
Doc Brown
1
(Citebot) today.java.net/article/2003/11/20/… Si este no es el origen de la cita, proporcione una referencia a la fuente que cree que es la cita original más probable.
rwong
1
Solo un recordatorio para cualquiera que llegue a esta pregunta y desarrolle Android. En Android, las excepciones deben detectarse y manejarse localmente, en la misma función donde se detecta por primera vez. Esto se debe a que las excepciones no se propagan a través de los manejadores de mensajes: su aplicación será eliminada si sucede. Por lo tanto, no debe citar este consejo al hacer el desarrollo de Android.
rwong

Respuestas:

118

En mi experiencia, es mejor lanzar excepciones en el punto donde ocurren los errores. Haces esto porque es el punto donde sabes más sobre por qué se activó la excepción.

A medida que la excepción se recupera, las capturas y repeticiones son una buena forma de agregar contexto adicional a la excepción. Esto puede significar lanzar un tipo diferente de excepción, pero incluya la excepción original cuando haga esto.

Finalmente, la excepción llegará a una capa en la que podrá tomar decisiones sobre el flujo de código (por ejemplo, un aviso al usuario para la acción). Este es el punto donde finalmente debe manejar la excepción y continuar la ejecución normal.

Con práctica y experiencia con su base de código, resulta bastante fácil juzgar cuándo agregar contexto adicional a los errores, y dónde es más sensato realmente, finalmente manejar los errores.

Captura → Rethrow

Haga esto donde pueda agregar más información útilmente para evitar que un desarrollador tenga que trabajar en todas las capas para comprender el problema.

Captura → Manija

Haga esto donde pueda tomar decisiones finales sobre lo que es un flujo de ejecución apropiado pero diferente a través del software.

Captura → Error Volver

Si bien hay situaciones en las que esto es apropiado, se deben considerar las excepciones de captura y devolver un valor de error a la persona que llama para refactorizar en una implementación de Captura → Relanzar.

Michael Shaw
fuente
Sí, ya sé y está fuera de discusión, que debería lanzar excepciones en el punto donde se origina el error. Pero, ¿por qué no debería atrapar el NPE y dejar que suba por el Stacktrace? Siempre atrapaba el NPE y lo envolvía en una excepción significativa. Tampoco veo ninguna ventaja de por qué debería lanzar una excepción DAO al servicio o la capa de interfaz de usuario. Siempre lo captaría en la capa de servicio y lo envolvería en una excepción de servicio con información detallada adicional, por qué falló la llamada al servicio.
shylynx
8
@shylynx Atrapar una excepción y luego volver a lanzar una excepción más significativa es algo bueno. Lo que no debería estar haciendo es detectar una excepción demasiado pronto y luego no volver a lanzarla. El error sobre el que advierte el dicho es detectar la excepción demasiado pronto y luego intentar manejarla en la capa incorrecta del código.
Simon B
Hacer que el contexto sea obvio en el momento de recibir la excepción facilita la vida de los desarrolladores de su equipo. Un NPE requiere más investigación para entender el problema
Michael Shaw
44
@shylynx Uno podría hacer la pregunta: "¿Por qué tiene un punto en su código que puede arrojar un NullPointerException? ¿Por qué no verificar nully lanzar una excepción (tal vez una IllegalArgumentException) antes para que la persona que llama sepa exactamente dónde nullse pasó el mal ?" Creo que eso sería lo que sugeriría la parte "tirar temprano" del dicho.
jpmc26
2
@jpmc Tomé el NPE solo como un ejemplo para enfatizar las preocupaciones en torno a las capas y las excepciones. Podría reemplazarlo con una IllegalArgumentException también.
shylynx
56

Desea lanzar una excepción lo antes posible porque eso hace que sea más fácil encontrar la causa. Por ejemplo, considere un método que podría fallar con ciertos argumentos. Si valida los argumentos y falla al principio del método, inmediatamente sabrá que el error está en el código de llamada. Si espera hasta que se necesiten los argumentos antes de fallar, debe seguir la ejecución y determinar si el error está en el código de llamada (argumento incorrecto) o si el método tiene un error. Cuanto antes arrojes la excepción, más se acercará a su causa subyacente y más fácil será descubrir dónde salieron las cosas.

La razón por la cual las excepciones se manejan en los niveles más altos es porque los niveles más bajos no saben cuál es el curso de acción apropiado para manejar el error. De hecho, podría haber múltiples formas apropiadas de manejar el mismo error dependiendo de cuál sea el código de llamada. Tome la apertura de un archivo, por ejemplo. Si está tratando de abrir un archivo de configuración y no está allí, ignorar la excepción y continuar con la configuración predeterminada puede ser una respuesta adecuada. Si está abriendo un archivo privado que es vital para la ejecución del programa y de alguna manera falta, su única opción es probablemente cerrar el programa.

Ajustar las excepciones en los tipos correctos es una preocupación puramente ortogonal.

Doval
fuente
1
+1 para explicar claramente por qué importan los diferentes niveles. Excelente ejemplo sobre el error del sistema de archivos.
Juan Carlos Coto
24

Otros han resumido bastante bien por qué lanzar temprano . Permítanme concentrarme en el por qué tomar parte tardía , para lo cual no he visto una explicación satisfactoria para mi gusto.

Entonces, ¿por qué las excepciones?

Parece haber una gran confusión acerca de por qué existen excepciones en primer lugar. Permítanme compartir el gran secreto aquí: la razón de las excepciones y el manejo de excepciones es ... ABSTRACCIÓN .

¿Has visto un código como este?

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.");
    }
}

No es así como deberían usarse las excepciones. Existen códigos como los anteriores en la vida real, pero son más una aberración y son realmente la excepción (juego de palabras). La definición de división, por ejemplo, incluso en matemática pura, es condicional: siempre es el "código de la persona que llama" quien debe manejar el caso excepcional de cero para restringir el dominio de entrada. Es feo Siempre es dolor para la persona que llama. Aún así, para tales situaciones, el patrón de verificar y luego hacer es el camino 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.");
    }
}

Alternativamente, puede ir al comando completo en el estilo OOP como este:

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 puede ver, el código de la persona que llama tiene la carga de la verificación previa, pero no hace ningún manejo de excepción después. Si alguna ArithmeticExceptionvez viene de llamar divideo eval, entonces es USTED quien tiene que hacer el manejo de excepciones y corregir su código, porque olvidó el check(). Por las mismas razones, atrapar a NullPointerExceptioncasi siempre es algo incorrecto.

Ahora hay algunas personas que dicen que quieren ver los casos excepcionales en la firma del método / función, es decir, extender explícitamente el dominio de salida . Ellos son los que favorecen las excepciones marcadas . Por supuesto, cambiar el dominio de salida debería forzar la adaptación de cualquier código de llamada directa, y eso se lograría con excepciones comprobadas. ¡Pero no necesitas excepciones para eso! Es por eso que tiene Nullable<T> clases genéricas , clases de casos , tipos de datos algebraicos y tipos de unión . Algunas personas de OO incluso podrían preferir regresar null por simples casos de error 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.");
    }
}

Técnicamente, las excepciones se pueden usar para el propósito anterior, pero este es el punto: no existen excepciones para dicho uso . Las excepciones son pro abstracción. Las excepciones son sobre indirección. Las excepciones permiten extender el dominio de "resultado" sin romper los contratos directos del cliente y diferir el manejo de errores a "otro lugar". Si su código arroja excepciones que se manejan en llamadas directas del mismo código, sin ninguna capa de abstracción en el medio, entonces lo está haciendo INCORRECTAMENTE

¿CÓMO TOMAR TARDE?

Aqui estamos. He discutido para mostrar que el uso de excepciones en los escenarios anteriores no es la forma en que las excepciones deben ser utilizadas. Sin embargo, existe un caso de uso genuino, donde la abstracción y la indirección ofrecidas por el manejo de excepciones es indispensable. Comprender dicho uso ayudará a comprender la recomendación de atrapar también.

Ese caso de uso es: Programación contra abstracciones de recursos ...

Sí, la lógica de negocios debe programarse contra abstracciones , no implementaciones concretas. El código de "cableado" IOC de nivel superior instanciará las implementaciones concretas de las abstracciones de recursos y las transmitirá a la lógica empresarial. Nada nuevo aquí. Pero las implementaciones concretas de esas abstracciones de recursos pueden estar lanzando sus propias excepciones específicas de implementación , ¿no?

Entonces, ¿quién puede manejar esas excepciones específicas de implementación? ¿Es posible manejar alguna excepción específica de recursos en la lógica de negocios entonces? No, no lo es. La lógica de negocios está programada contra abstracciones, lo que excluye el conocimiento de los detalles de excepción específicos de la implementación.

"¡Ajá!", Podrías decir: "pero es por eso que podemos subclasificar excepciones y crear jerarquías de excepciones" (¡mira el Sr. Spring !). Déjame decirte que es una falacia. En primer lugar, cada libro razonable sobre OOP dice que la herencia concreta es mala, pero de alguna manera este componente central de JVM, el manejo de excepciones, está estrechamente relacionado con la herencia concreta. Irónicamente, Joshua Bloch no pudo haber escrito su libro Effective Java antes de que pudiera obtener la experiencia con una JVM en funcionamiento, ¿verdad? Es más un libro de "lecciones aprendidas" para la próxima generación. En segundo lugar, y lo que es más importante, si detecta una excepción de alto nivel, ¿cómo la MANEJARÁ?PatientNeedsImmediateAttentionException: ¿tenemos que darle una pastilla o amputarle las piernas? ¿Qué tal una declaración de cambio sobre todas las subclases posibles? Ahí va tu polimorfismo, ahí va la abstracción. Tienes el punto.

Entonces, ¿quién puede manejar las excepciones específicas de recursos? ¡Debe ser quien conoce las concreciones! ¡El que instancia el recurso! El código de "cableado", por supuesto! Mira esto:

Lógica de negocios codificada contra abstracciones ... ¡SIN MANEJO DE ERRORES DE RECURSOS DE HORMIGÓN!

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

Mientras tanto, en otro lugar, las implementaciones 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);
    }
}

Y finalmente el código de cableado ... ¿Quién maneja las excepciones de recursos concretos? ¡El que sabe de ellos!

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

Ahora ten paciencia conmigo. El código anterior es simplista. Puede decir que tiene una aplicación empresarial / contenedor web con múltiples ámbitos de recursos gestionados por contenedor IOC, y necesita reintentos automáticos y reinicialización de recursos de ámbito de sesión o solicitud, etc. La lógica de cableado en los ámbitos de nivel inferior puede recibir fábricas abstractas para crear recursos, por lo tanto, no estar al tanto de las implementaciones exactas. Solo los ámbitos de nivel superior realmente sabrían qué excepciones pueden arrojar esos recursos de nivel inferior. Ahora espera!

Desafortunadamente, las excepciones solo permiten la indirección sobre la pila de llamadas, y los diferentes ámbitos con sus diferentes cardinalidades generalmente se ejecutan en múltiples subprocesos diferentes. No hay forma de comunicarse a través de eso con excepciones. Necesitamos algo más poderoso aquí. Respuesta: mensaje asíncrono pasando . Capture todas las excepciones en la raíz del alcance de nivel inferior. No ignores nada, no dejes pasar nada. Esto cerrará y eliminará todos los recursos creados en la pila de llamadas del alcance actual. Luego propague los mensajes de error a los ámbitos más altos utilizando colas / canales de mensajes en la rutina de manejo de excepciones, hasta que alcance el nivel donde se conocen las concreciones. Ese es el tipo que sabe cómo manejarlo.

SUMMA SUMMARUM

Entonces, según mi interpretación, atrapar tarde significa atrapar excepciones en el lugar más conveniente DONDE NO ESTÁS ROMPIENDO LA ABSTRACCIÓN MÁS . ¡No atrapes demasiado temprano! Capture excepciones en la capa donde crea la excepción concreta lanzando instancias de las abstracciones de recursos, la capa que conoce las concreciones de las abstracciones. La capa de "cableado".

HTH ¡Feliz codificación!

Daniel Dinnyes
fuente
Tiene razón en que el código que suministra la interfaz sabrá más sobre lo que puede salir mal que el código que usa la interfaz, pero ¿y si un método usa dos recursos del mismo tipo de interfaz y las fallas deben manejarse de manera diferente? ¿O si uno de esos recursos internamente, como detalle de implementación desconocido para su creador, utiliza otros recursos anidados del mismo tipo? Hacer que la capa empresarial se lance WrappedFirstResourceExceptiono WrappedSecondResourceExceptionrequiera que la capa de "cableado" mire dentro de esa excepción para ver la causa raíz del problema ...
supercat
... puede ser asqueroso, pero parecería mejor que asumir que cualquier FailingInputResourceexcepción será el resultado de una operación con in1. En realidad, creo que en muchos casos el enfoque correcto sería hacer que la capa de cableado pase un objeto de manejo de excepciones y que la capa de negocios incluya uno catchque luego invoque el handleExceptionmétodo de ese objeto . Ese método podría volver a lanzar, o suministrar datos predeterminados, o colocar un mensaje "Abortar / Reintentar / Fallar" y permitir que el operador decida qué hacer, etc., dependiendo de lo que requiera la aplicación.
supercat
@ Supercat Entiendo lo que estás diciendo. Yo diría que una implementación de recursos concretos es responsable de conocer las excepciones que puede lanzar. No tiene que especificar todo (existe el llamado comportamiento indefinido ), pero debe asegurarse de que no haya ambigüedad. Además, las excepciones de tiempo de ejecución no verificadas deben documentarse. Si contradice la documentación, entonces es un error. Si se espera que el código de la persona que llama haga algo sensato acerca de una excepción, lo mínimo es que el recurso los envuelve en algunos UnrecoverableInternalException, similar a un código de error HTTP 500.
Daniel Dinnyes
@supercat Acerca de su sugerencia sobre controladores de errores configurables: ¡exactamente! En mi último ejemplo, la lógica de manejo de errores está codificada, llamando al doMyBusinessmétodo estático . Esto fue en aras de la brevedad, y es perfectamente posible hacerlo más dinámico. Dicha Handlerclase se instanciaría con algunos recursos de entrada / salida, y tendría un handlemétodo que recibe una clase que implementa a ReusableBusinessLogicInterface. Luego, podría combinar / configurar para usar diferentes implementaciones de manejador, recurso y lógica de negocios en la capa de cableado en algún lugar por encima de ellas.
Daniel Dinnyes
10

Para responder esta pregunta correctamente, demos un paso atrás y hagamos una pregunta aún más fundamental.

¿Por qué tenemos excepciones en primer lugar?

Lanzamos excepciones para que la persona que llama de nuestro método sepa que no podemos hacer lo que se nos pidió. El tipo de excepción explica por qué no pudimos hacer lo que queríamos hacer.

Echemos un vistazo a algunos códigos:

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

Este código obviamente puede arrojar una excepción de referencia nula si PropertyBes nulo. Hay dos cosas que podríamos hacer en este caso para "corregir" esta situación. Podríamos:

  • Crear automáticamente PropertyB si no lo tenemos; o
  • Deje que la excepción suba al método de llamada.

Crear PropertyB aquí podría ser muy peligroso. ¿Qué razón tiene este método para crear PropertyB? Seguramente esto violaría el principio de responsabilidad única. Con toda probabilidad, si PropertyB no existe aquí, indica que algo ha salido mal. Se está llamando al método en un objeto parcialmente construido o PropertyB se estableció en nulo incorrectamente. Al crear PropertyB aquí, podríamos estar ocultando un error mucho más grande que podría mordernos más adelante, como un error que causa la corrupción de datos.

Si, en cambio, dejamos que la referencia nula brote, estamos informando al desarrollador que llamó a este método, tan pronto como podamos, que algo salió mal. Se ha pasado por alto una condición previa vital para llamar a este método.

En efecto, estamos tirando temprano porque separa nuestras preocupaciones mucho mejor. Tan pronto como se haya producido un error, estamos informando a los desarrolladores de nivel superior al respecto.

Por qué nos "atrapamos tarde" es una historia diferente. Realmente no queremos atrapar tarde, realmente queremos atrapar tan pronto como sepamos cómo manejar el problema correctamente. Algunas veces esto será quince capas de abstracción más tarde y algunas veces será en el momento de la creación.

El punto es que queremos capturar la excepción en la capa de abstracción que nos permite manejar la excepción en el punto donde tenemos toda la información que necesitamos para manejar la excepción correctamente.

Stephen
fuente
Creo que está utilizando desarrolladores ascendentes en el sentido equivocado. Además, usted dijo que viola el principio de responsabilidad única, pero en realidad muchas inicializaciones bajo demanda y almacenamiento en caché de valores se implementan de esa manera (con controles de concurrencia adecuados en su lugar, por supuesto)
Daniel Dinnyes
En el ejemplo dado, ¿qué pasa con la comprobación de nula antes de la operación de resta, como esteif(PropertyB == null) return 0;
user1451111
1
¿Puedes también explicar tu último párrafo, específicamente qué quieres decir con ' capa de abstracción '?
user1451111
Si estamos haciendo un trabajo de IO, la capa de abstracción para detectar una excepción de IO sería donde estamos haciendo el trabajo. En ese momento, tenemos toda la información que necesitamos para decidir si queremos volver a intentar o lanzar un cuadro de mensaje al usuario o crear un objeto utilizando un conjunto de valores predeterminados.
Stephen
"En su ejemplo dado, ¿qué pasa con la comprobación de nulo antes de la operación de sustracción, así si (PropertyB == null) devuelve 0;" Yuck Eso sería decirle al método de llamada que tengo cosas válidas para restar. Por supuesto, esto es contextual, pero sería una mala práctica hacer mi comprobación de errores aquí en la mayoría de los casos.
Stephen
6

Lanza tan pronto como veas algo por lo que valga la pena tirar para evitar poner objetos en un estado no válido. Lo que significa que si se pasó un puntero nulo, lo verificará antes y lanzará un NPE antes de que tenga la posibilidad de llegar al nivel bajo.

Detecte tan pronto como sepa qué hacer para corregir el error (generalmente no es donde lo arroja, de lo contrario, podría usar un if-else), si se pasó un parámetro no válido, la capa que proporcionó el parámetro debería lidiar con las consecuencias .

monstruo de trinquete
fuente
1
Escribiste: Lanza pronto, ... ¡Atrapa pronto ...! ¿Por qué? Es un enfoque completamente contrario en contraste con "lanzar temprano, atrapar tarde".
shylynx
1
@shylynx No sé de dónde viene "lanzar temprano, atrapar tarde", pero su valor es cuestionable. ¿Qué significa exactamente atrapar "tarde"? Donde tiene sentido atrapar una excepción (si es que lo hace) depende del problema. Lo único que está claro es que desea detectar problemas (y lanzarlos) lo antes posible.
Doval
2
Supongo que "atrapar tarde" está destinado a contrastar la práctica de atrapar antes de que pueda saber qué hacer para corregir el error, por ejemplo, a veces ve funciones que capturan todo solo para que puedan imprimir un mensaje de error y luego volver a lanzar la excepción.
@Hurkyl: Un problema con "atrapar tarde" es que si una excepción aparece a través de capas que no saben nada al respecto, puede ser difícil para el código que pueda hacer algo sobre la situación saber que las cosas realmente son como esperado. Como ejemplo simple, suponga que si un analizador para un archivo de documento de usuario necesita cargar un CODEC desde el disco y se produce un error de disco al leerlo, el código que llama al analizador puede actuar de manera inapropiada si cree que hubo un error de disco mientras lee al usuario documento.
supercat
4

Una regla comercial válida es 'si el software de nivel inferior no puede calcular un valor, entonces ...'

Esto solo se puede expresar en el nivel superior, de lo contrario el software de nivel inferior está tratando de cambiar su comportamiento en función de su propia corrección, que solo terminará en un nudo.

soru
fuente
2

En primer lugar, las excepciones son para situaciones excepcionales. En su ejemplo, no se puede calcular ninguna cifra si los datos sin procesar no están presentes porque no se pudieron cargar.

Desde mi experiencia, es una buena práctica resumir excepciones mientras camina por la pila. Por lo general, los puntos donde desea hacer esto es cuando una excepción cruza el límite entre dos capas.

Si hay un error al recopilar sus datos sin procesar en la capa de datos, inicie una excepción para notificar a quien solicitó los datos. No intente solucionar este problema aquí abajo. La complejidad del código de manejo puede ser muy alta. Además, la capa de datos solo es responsable de solicitar datos, no de manejar los errores que se producen al hacer esto. Esto es lo que se entiende por "tirar temprano" .

En su ejemplo, la capa de captura es la capa de servicio. El servicio en sí es una nueva capa, ubicada sobre la capa de acceso a datos. Entonces quieres atrapar la excepción allí. Quizás su servicio tenga una infraestructura de conmutación por error e intente solicitar los datos de otro repositorio. Si esto también falla, envuelva la excepción dentro de algo que la persona que llama del servicio entienda (si se trata de un servicio web, esto podría ser un error de SOAP). Establezca la excepción original como excepción interna para que las capas posteriores puedan registrar exactamente lo que salió mal.

El error de servicio puede ser detectado por la capa que llama al servicio (por ejemplo, la IU). Y esto es lo que significa "atrapar tarde" . Si no puede manejar la excepción en una capa inferior, vuelva a lanzarla. Si la capa superior no puede manejar la excepción, ¡trátela! Esto podría incluir iniciar sesión o presentarlo.

La razón por la que debería volver a generar excepciones (como se describió anteriormente envolviéndolas en excepciones más generales) es que es muy probable que el usuario no pueda entender que hubo un error porque, por ejemplo, un puntero apunta a una memoria no válida. Y a él no le importa. Solo le importa que el servicio no pueda calcular la cifra y esta es la información que se le debe mostrar.

En el futuro, puede (en un mundo ideal) omitir trypor completo / catchcódigo de la interfaz de usuario. En su lugar, use un controlador de excepciones global que pueda comprender las excepciones que posiblemente generan las capas inferiores, las escriba en algún registro y las envuelva en objetos de error que contengan información significativa (y posiblemente localizada) del error. Esos objetos se pueden presentar fácilmente al usuario en cualquier forma que desee (cuadros de mensaje, notificaciones, mensajes brindis, etc.).

Aschratt
fuente
1

Lanzar excepciones desde el principio en general es una buena práctica porque no desea que los contratos incumplidos fluyan a través del código más de lo necesario. Por ejemplo, si espera que un determinado parámetro de función sea un número entero positivo, debe aplicar esa restricción en el punto de la llamada a la función en lugar de esperar hasta que esa variable se use en otro lugar de la pila de códigos.

Al llegar tarde, realmente no puedo comentar porque tengo mis propias reglas y cambia de proyecto a proyecto. Sin embargo, lo único que intento hacer es separar las excepciones en dos grupos. Uno es solo para uso interno y el otro es solo para uso externo. Las excepciones internas son capturadas y manejadas por mi propio código y las excepciones externas están destinadas a ser manejadas por cualquier código que me llame. Esto es básicamente una forma de atrapar cosas más tarde, pero no del todo porque me ofrece la flexibilidad de desviarse de la regla cuando es necesario en el código interno.

davidk01
fuente