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?
fuente
Respuestas:
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.
fuente
NullPointerException
? ¿Por qué no verificarnull
y lanzar una excepción (tal vez unaIllegalArgumentException
) antes para que la persona que llama sepa exactamente dóndenull
se pasó el mal ?" Creo que eso sería lo que sugeriría la parte "tirar temprano" del dicho.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.
fuente
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?
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:
Alternativamente, puede ir al comando completo en el estilo OOP como este:
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
ArithmeticException
vez viene de llamardivide
oeval
, entonces es USTED quien tiene que hacer el manejo de excepciones y corregir su código, porque olvidó elcheck()
. Por las mismas razones, atrapar aNullPointerException
casi 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 regresarnull
por simples casos de error como este: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!
Mientras tanto, en otro lugar, las implementaciones concretas ...
Y finalmente el código de cableado ... ¿Quién maneja las excepciones de recursos concretos? ¡El que sabe de ellos!
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!
fuente
WrappedFirstResourceException
oWrappedSecondResourceException
requiera que la capa de "cableado" mire dentro de esa excepción para ver la causa raíz del problema ...FailingInputResource
excepción será el resultado de una operación conin1
. 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 unocatch
que luego invoque elhandleException
mé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.UnrecoverableInternalException
, similar a un código de error HTTP 500.doMyBusiness
método estático . Esto fue en aras de la brevedad, y es perfectamente posible hacerlo más dinámico. DichaHandler
clase se instanciaría con algunos recursos de entrada / salida, y tendría unhandle
método que recibe una clase que implementa aReusableBusinessLogicInterface
. 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.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:
Este código obviamente puede arrojar una excepción de referencia nula si
PropertyB
es nulo. Hay dos cosas que podríamos hacer en este caso para "corregir" esta situación. Podríamos: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.
fuente
if(PropertyB == null) return 0;
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 .
fuente
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.
fuente
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
try
por completo /catch
có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.).fuente
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.
fuente