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 ArithmeticException
vez viene de llamar divide
o 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 NullPointerException
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 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!