Creo que leí la misma entrevista de Bruce Eckel que tú hiciste, y siempre me molestó. De hecho, el entrevistado hizo el argumento (si esta es realmente la publicación de la que está hablando) Anders Hejlsberg, el genio de MS detrás de .NET y C #.
http://www.artima.com/intv/handcuffs.html
Aunque soy fanático de Hejlsberg y su trabajo, este argumento siempre me ha parecido falso. Básicamente se reduce a:
"Las excepciones marcadas son malas porque los programadores simplemente abusan de ellas al atraparlas siempre y descartarlas, lo que lleva a que se oculten e ignoren problemas que de otro modo se presentarían al usuario".
Por "presentado de otra manera al usuario" quiero decir que si usa una excepción de tiempo de ejecución, el programador perezoso simplemente lo ignorará (en lugar de atraparlo con un bloque catch vacío) y el usuario lo verá.
El resumen del resumen del argumento es que "los programadores no los usarán correctamente y no usarlos correctamente es peor que no tenerlos" .
Hay algo de verdad en este argumento y, de hecho, sospecho que la motivación de Goslings para no poner anulaciones de operador en Java proviene de un argumento similar: confunden al programador porque a menudo se abusa de ellos.
Pero al final, me parece un argumento falso de Hejlsberg y posiblemente uno post hoc creado para explicar la falta en lugar de una decisión bien pensada.
Yo diría que si bien el uso excesivo de las excepciones marcadas es algo malo y tiende a conducir a un manejo descuidado por parte de los usuarios, pero el uso adecuado de ellas permite que el programador API brinde un gran beneficio al programador cliente API.
Ahora, el programador de la API debe tener cuidado de no lanzar excepciones comprobadas por todas partes, o simplemente molestarán al programador del cliente. El programador de clientes muy flojo recurrirá a la captura (Exception) {}
como advierte Hejlsberg y se perderán todos los beneficios y se producirá el infierno. Pero en algunas circunstancias, simplemente no hay sustituto para una buena excepción comprobada.
Para mí, el ejemplo clásico es la API de archivo abierto. Cada lenguaje de programación en la historia de los lenguajes (al menos en los sistemas de archivos) tiene una API en algún lugar que le permite abrir un archivo. Y cada programador cliente que use esta API sabe que tiene que lidiar con el caso de que el archivo que está intentando abrir no existe. Permítanme reformular eso: cada programador cliente que use esta API debe saber que tiene que lidiar con este caso. Y ahí está el problema: ¿puede el programador de API ayudarlos a saber que deben lidiar con eso solo comentando o pueden insistir en que el cliente lo haga ?
En C el idioma es algo así como
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
donde fopen
indica falla al devolver 0 y C (tontamente) te permite tratar 0 como un booleano y ... Básicamente, aprendes este idioma y estás bien. Pero qué pasa si eres un novato y no aprendiste el idioma. Entonces, por supuesto, comienzas con
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
y aprender de la manera difícil.
Tenga en cuenta que aquí solo estamos hablando de lenguajes fuertemente tipados: hay una idea clara de lo que es una API en un lenguaje fuertemente tipado: es una mezcla heterogénea de funcionalidades (métodos) que puede usar con un protocolo claramente definido para cada uno.
Ese protocolo claramente definido generalmente se define mediante una firma de método. Aquí fopen requiere que le pase una cadena (o un char * en el caso de C). Si le das algo más, obtienes un error en tiempo de compilación. No siguió el protocolo, no está utilizando la API correctamente.
En algunos idiomas (oscuros), el tipo de retorno también forma parte del protocolo. Si intentas llamar al equivalente defopen()
en algunos idiomas sin asignarlo a una variable, también obtendrá un error en tiempo de compilación (solo puede hacerlo con las funciones nulas).
El punto que estoy tratando de hacer es que: en un lenguaje de tipo estático, el programador de API alienta al cliente a usar la API correctamente al evitar que se compile su código de cliente si comete algún error obvio.
(En un lenguaje escrito dinámicamente, como Ruby, puede pasar cualquier cosa, digamos un flotante, como el nombre del archivo, y se compilará. ¿Por qué molestar al usuario con excepciones marcadas si ni siquiera va a controlar los argumentos del método? los argumentos que se hacen aquí se aplican solo a los idiomas de tipo estático).
Entonces, ¿qué pasa con las excepciones marcadas?
Bueno, aquí está una de las API de Java que puede usar para abrir un archivo.
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
¿Ves esa trampa? Aquí está la firma para ese método API:
public FileInputStream(String name)
throws FileNotFoundException
Tenga en cuenta que FileNotFoundException
es una excepción marcada .
El programador de API le dice esto: "Puede usar este constructor para crear un nuevo FileInputStream pero usted
a) debe pasar el nombre del archivo como una Cadena
b) debe aceptar la posibilidad de que el archivo no se encuentre en tiempo de ejecución "
Y ese es todo el punto en lo que a mí respecta.
La clave es básicamente lo que la pregunta dice como "Cosas que están fuera del control del programador". Mi primer pensamiento fue que él / ella quiere decir cosas que están fuera de la API control de los programadores . Pero, de hecho, las excepciones comprobadas cuando se usan correctamente deberían ser para cosas que están fuera del control del programador cliente y del programador API. Creo que esta es la clave para no abusar de las excepciones marcadas.
Creo que el archivo abierto ilustra muy bien el punto. El programador de API sabe que puede darles un nombre de archivo que no existe en el momento en que se llama a la API, y que no podrán devolverle lo que desea, pero tendrá que lanzar una excepción. También saben que esto sucederá con bastante regularidad y que el programador del cliente puede esperar que el nombre del archivo sea correcto en el momento en que escribió la llamada, pero también puede ser incorrecto en tiempo de ejecución por razones que están fuera de su control.
Entonces, la API lo hace explícito: habrá casos en los que este archivo no exista en el momento en que me llames y es mejor que lo trates.
Esto sería más claro con un caso contrario. Imagina que estoy escribiendo una tabla API. Tengo el modelo de tabla en algún lugar con una API que incluye este método:
public RowData getRowData(int row)
Ahora, como programador de API, sé que habrá casos en los que algún cliente pasa un valor negativo para la fila o un valor de fila fuera de la tabla. Por lo tanto, podría sentir la tentación de lanzar una excepción marcada y obligar al cliente a lidiar con ella:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(Realmente no lo llamaría "marcado", por supuesto).
Este es un mal uso de las excepciones marcadas. El código del cliente estará lleno de llamadas para recuperar datos de fila, cada uno de los cuales tendrá que usar un try / catch, ¿y para qué? ¿Van a informar al usuario que se buscó la fila incorrecta? Probablemente no, porque cualquiera que sea la interfaz de usuario que rodea mi vista de tabla, no debería permitir que el usuario entre en un estado donde se solicita una fila ilegal. Entonces es un error por parte del programador del cliente.
El programador de API aún puede predecir que el cliente codificará dichos errores y debería manejarlo con una excepción de tiempo de ejecución como un IllegalArgumentException
.
Con una excepción marcada getRowData
, este es claramente un caso que llevará al programador perezoso de Hejlsberg simplemente agregando capturas vacías. Cuando eso suceda, los valores de fila ilegales no serán obvios incluso para la depuración del probador o del desarrollador del cliente, sino que conducirán a errores que son difíciles de identificar la fuente. Los cohetes Arianne explotarán después del lanzamiento.
Bien, así que aquí está el problema: estoy diciendo que la excepción marcada FileNotFoundException
no es solo algo bueno, sino una herramienta esencial en la caja de herramientas de programadores de API para definir la API de la manera más útil para el programador del cliente. Pero CheckedInvalidRowNumberException
es un gran inconveniente, lo que lleva a una mala programación y debe evitarse. Pero cómo notar la diferencia.
Supongo que no es una ciencia exacta y supongo que subyace y quizás justifica en cierta medida el argumento de Hejlsberg. Pero no estoy contento de tirar al bebé con el agua del baño aquí, así que permítanme extraer algunas reglas aquí para distinguir las excepciones comprobadas de las malas:
Fuera del control del cliente o Cerrado vs Abierto:
Las excepciones marcadas solo deben usarse cuando el caso de error está fuera de control tanto de la API como del programador del cliente. Esto tiene que ver con qué tan abierto o cerrado es el sistema. En una IU restringida donde el programador del cliente tiene control, por ejemplo, sobre todos los botones, comandos de teclado, etc. que agregan y eliminan filas de la vista de tabla (un sistema cerrado), es un error de programación del cliente si intenta obtener datos de Una fila inexistente. En un sistema operativo basado en archivos donde cualquier número de usuarios / aplicaciones puede agregar y eliminar archivos (un sistema abierto), es concebible que el archivo que solicita el cliente se haya eliminado sin su conocimiento, por lo que se espera que lo manejen. .
Ubicuidad:
Las excepciones marcadas no deben usarse en una llamada API que el cliente realiza con frecuencia. Con frecuencia me refiero a muchos lugares en el código del cliente, no con frecuencia en el tiempo. Por lo tanto, un código de cliente no tiende a intentar abrir mucho el mismo archivo, pero mi vista de tabla se RowData
extiende desde diferentes métodos. En particular, voy a escribir mucho código como
if (model.getRowData().getCell(0).isEmpty())
y será doloroso tener que envolver en try / catch cada vez.
Informar al usuario:
Deben usarse excepciones marcadas en los casos en que pueda imaginar que se presentará un mensaje de error útil al usuario final. Este es el "¿y qué harás cuando ocurra?" pregunta que planteé arriba. También se relaciona con el elemento 1. Dado que puede predecir que algo fuera de su sistema de API de cliente podría hacer que el archivo no esté allí, puede informarle razonablemente al usuario al respecto:
"Error: could not find the file 'goodluckfindingthisfile'"
Dado que su número de fila ilegal fue causado por un error interno y sin culpa del usuario, realmente no hay información útil que pueda proporcionarle. Si su aplicación no permite que las excepciones de tiempo de ejecución caigan en la consola, probablemente terminará dándoles un mensaje feo como:
"Internal error occured: IllegalArgumentException in ...."
En resumen, si no cree que su programador cliente pueda explicar su excepción de una manera que ayude al usuario, entonces probablemente no debería estar usando una excepción marcada.
Entonces esas son mis reglas. Algo artificial, y sin duda habrá excepciones (por favor, ayúdenme a refinarlas si lo desean). Pero mi argumento principal es que hay casos FileNotFoundException
en los que la excepción marcada es una parte tan importante y útil del contrato de API como los tipos de parámetros. Por lo tanto, no debemos prescindir de él solo porque se usa incorrectamente.
Lo siento, no quise hacer esto tan largo y ondulado. Déjame terminar con dos sugerencias:
R: Programadores de API: use excepciones marcadas con moderación para preservar su utilidad. En caso de duda, use una excepción no marcada.
B: Programadores de clientes: habitúese a crear una excepción envuelta (google it) al principio de su desarrollo. JDK 1.4 y posterior proporcionan un constructor RuntimeException
para esto, pero también puede crear fácilmente el suyo propio. Aquí está el constructor:
public RuntimeException(Throwable cause)
Luego, habitúese cuando tenga que manejar una excepción marcada y se sienta perezoso (o cree que el programador de API fue demasiado celoso al usar la excepción marcada en primer lugar), no se trague la excepción, envuélvala y volver a tirarlo.
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
Ponga esto en una de las pequeñas plantillas de código de su IDE y úsela cuando se sienta flojo. De esta manera, si realmente necesita manejar la excepción marcada, se verá obligado a volver y tratarla después de ver el problema en tiempo de ejecución. Porque, créeme (y Anders Hejlsberg), nunca volverás a ese TODO en tu
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}