Solución True-way en Java: analice 2 números de 2 cadenas y luego devuelva su suma

84

Una pregunta bastante estúpida. Dado el código:

public static int sum(String a, String b) /* throws? WHAT? */ {
  int x = Integer.parseInt(a); // throws NumberFormatException
  int y = Integer.parseInt(b); // throws NumberFormatException
  return x + y;
}

¿Podrías decir si es bueno Java o no? De lo que estoy hablando NumberFormatExceptiones de una excepción sin marcar. No tiene que especificarlo como parte de la sum()firma. Además, según tengo entendido, la idea de las excepciones no verificadas es solo para indicar que la implementación del programa es incorrecta, y aún más, detectar excepciones no verificadas es una mala idea, ya que es como arreglar un programa defectuoso en tiempo de ejecución .

¿Alguien podría aclarar si:

  1. Debo especificar NumberFormatExceptioncomo parte de la firma del método.
  2. Debo definir mi propia excepción marcada ( BadDataException), manejar NumberFormatExceptiondentro del método y volver a lanzarlo como BadDataException.
  3. Debo definir mi propia excepción marcada ( BadDataException), validar ambas cadenas de alguna manera como expresiones regulares y lanzar mi BadDataExceptionsi no coincide.
  4. ¿Tu idea?

Actualización :

Imagínese, no es un marco de código abierto, que debería usar por alguna razón. Miras la firma del método y piensas: "Está bien, nunca arroja". Entonces, algún día, tienes una excepción. ¿Es normal?

Actualización 2 :

Hay algunos comentarios que dicen que mi sum(String, String)es un mal diseño. Estoy absolutamente de acuerdo, pero para aquellos que creen que el problema original nunca aparecería si tuviéramos un buen diseño, aquí hay una pregunta adicional:

La definición del problema es la siguiente: tiene una fuente de datos donde los números se almacenan como Strings. Esta fuente puede ser un archivo XML, una página web, una ventana de escritorio con 2 cuadros de edición, lo que sea.

Su objetivo es implementar la lógica que toma estos 2 Strings, los convierte en intsy muestra un cuadro de mensaje que dice "la suma es xxx".

No importa cuál sea el enfoque que utilice para diseñar / implementar esto, tendrá estos 2 puntos de funcionalidad interna :

  1. Un lugar donde se convierte Stringaint
  2. Un lugar donde agregas 2 ints

La pregunta principal de mi publicación original es:

Integer.parseInt()espera que se pase la cadena correcta . Siempre que pase una cadena incorrecta , significa que su programa es incorrecto (no " su usuario es un idiota"). Debe implementar el fragmento de código donde, por un lado, tiene Integer.parseInt () con semántica MUST y, por otro lado, debe estar de acuerdo con los casos en los que la entrada es incorrecta: DEBERÍA semántica .

Entonces, brevemente: ¿cómo implemento la semántica DEBERÍA si solo tengo bibliotecas MUST ?

Andrey Agibalov
fuente
13
Esa no es una pregunta estúpida de ninguna manera. ¡De hecho, es bastante bueno! No estoy completamente seguro de qué opción elegiría.
martin
1
Para su actualización: Sí, de acuerdo con el principio de falla rápida, esto tampoco sería necesariamente algo malo.
Johan Sjöberg
1
Estoy de acuerdo con Martin. Es uno de los aspectos de la programación Java que es el menos comprendido y que requiere algunas mejores prácticas. Esto se puede ver por la falta de páginas sobre el tema y muy pocas, además de O'Reilly, dicen "así es como se debe hacer".
James P.
2
De hecho, esta es una excelente pregunta.
Chris Cudmore
1
¡Gran pregunta! También planteó algunas excelentes respuestas: D
Kheldar

Respuestas:

35

Esta es una buena pregunta. Ojalá más gente pensara en esas cosas.

En mi humilde opinión, lanzar excepciones no marcadas es aceptable si se le han pasado parámetros de basura.

En términos generales, no debe lanzar BadDataExceptionporque no debe usar Excepciones para controlar el flujo del programa. Las excepciones son para los excepcionales. Las personas que llaman a su método pueden saber antes de llamarlo si sus cadenas son números o no, por lo que pasar basura es evitable y, por lo tanto, puede considerarse un error de programación , lo que significa que está bien lanzar excepciones sin marcar.

Con respecto a la declaración throws NumberFormatException, esto no es tan útil, porque pocos lo notarán debido a que NumberFormatException está desmarcado. Sin embargo, los IDE pueden hacer uso de él y ofrecerse a integrarse try/catchcorrectamente. Una buena opción es usar javadoc también, por ejemplo:

/**
 * Adds two string numbers
 * @param a
 * @param b
 * @return
 * @throws NumberFormatException if either of a or b is not an integer
 */
public static int sum(String a, String b) throws NumberFormatException {
    int x = Integer.parseInt(a); 
    int y = Integer.parseInt(b); 
    return x + y;
}

EDITADO :
Los comentaristas han hecho puntos válidos. Debe considerar cómo se utilizará y el diseño general de su aplicación.

Si el método se usará en todas partes, y es importante que todas las personas que llaman manejen los problemas, declare que el método arroja una excepción marcada (obligando a las personas que llaman a lidiar con los problemas), pero saturando el código con try/catchbloques.

Si, por otro lado, estamos usando este método con datos en los que confiamos, entonces declárelo como se indicó anteriormente, porque no se espera que explote nunca y evitará el desorden de código de try/catchbloques esencialmente innecesarios .

Bohemio
fuente
6
El problema es que las excepciones no controladas a menudo se ignoran y pueden filtrarse en su pila de llamadas, causando estragos en partes de su aplicación que no tienen idea de lo que hay debajo. La pregunta es realmente qué bit de código debe verificar si la entrada es válida o no.
G_H
1
@G_H tiene razón. Los tutoriales de Java dicen esto: "Si se puede esperar razonablemente que un cliente se recupere de una excepción, conviértalo en una excepción marcada. Si un cliente no puede hacer nada para recuperarse de la excepción, conviértalo en una excepción no marcada". La pregunta es ¿dónde se debe RuntimeExceptionmanejar? ¿Debería ser la persona que llama o debería dejarse flotar la excepción? Si es así, ¿cómo se debe manejar?
James P.
1
Para responder parcialmente a esto, he visto dos enfoques: el primero es interceptar Exceptionsdesde capas inferiores y envolverlas en Excepciones de nivel superior que son más significativas para el usuario final y el segundo es usar un controlador de excepciones no detectado que se puede inicializar en el lanzador de aplicaciones.
James P.
2
En mi humilde opinión, tener NumberFormatException en javaDoc es mucho más importante. Ponerlo en la throwscláusula es una buena adición, pero no es obligatorio.
Dorus
3
Para "Las personas que llaman a su método pueden saber antes de llamarlo si sus cadenas son números o no, por lo que pasar basura es evitable" : Eso no es realmente cierto. La única forma sencilla de verificar que una cadena se analiza como int es probarla. Aunque es posible que desee realizar algunas comprobaciones por adelantado, la comprobación exacta es un PITA.
maaartinus
49

En mi opinión, sería preferible manejar la lógica de excepción lo más arriba posible. Por eso preferiría la firma

 public static int sum(int a, int b);

Con la firma de su método no cambiaría nada. O eres

  • Utilizando valores incorrectos mediante programación, donde en su lugar podría validar su algoritmo de productor
  • o enviando valores desde, por ejemplo, la entrada del usuario, en cuyo caso ese módulo debe realizar la validación

Por lo tanto, el manejo de excepciones en este caso se convierte en un problema de documentación .

Johan Sjöberg
fuente
4
Me gusta esto. Obliga al usuario a seguir el contrato y hace que el método haga solo una cosa.
Chris Cudmore
No entiendo cómo el uso de este método es diferente al del usuario haciendo a + b. ¿Por qué necesitamos este método?
Swati
4
@Swati: Creo que es un ejemplo , que es una gran técnica para mostrar las cosas de forma sencilla. El método de suma también podría ser myVeryComplicatedMethodWhichComputesPhoneNumberOfMyIdealMatchGirl (int myID, int myLifeExpectancy) ...
Kheldar
3
Totalmente de acuerdo aquí. una sumfunción que espera dos números, debería obtener dos números. La forma en que los obtuvo no está relacionada con la sumfunción. Separe su lógica correctamente . Entonces, relacionaría esto con un problema de diseño.
c00kiemon5ter
5

Número 4. Como se indica, este método no debe tomar cadenas como parámetros, debe tomar números enteros. En cuyo caso (dado que Java se ajusta en lugar de desbordarse) no hay posibilidad de una excepción.

 x = sum(Integer.parseInt(a), Integer.parseInt(b))

es mucho más claro en cuanto a lo que se quiere decir que x = sum(a, b)

Quiere que la excepción ocurra lo más cerca posible de la fuente (entrada).

En cuanto a las opciones 1-3, no define una excepción porque espera que las personas que llaman asuman que, de lo contrario, su código no puede fallar, define una excepción para definir lo que sucede en condiciones de falla conocidas QUE SON ÚNICAS PARA SU MÉTODO. Es decir, si tiene un método que es un envoltorio alrededor de otro objeto y arroja una excepción, páselo. Solo si la excepción es única para su método, debe lanzar una excepción personalizada (frex, en su ejemplo, si se suponía que sum solo devolviera resultados positivos, entonces sería apropiado verificar eso y lanzar una excepción, si por otro lado java arrojó una excepción de desbordamiento en lugar de envolver, luego pasaría eso, no lo definiría en su firma, lo renombraría o lo comería).

Actualización en respuesta a la actualización de la pregunta:

Entonces, brevemente: ¿cómo implemento la semántica DEBERÍA si solo tengo bibliotecas MUST?

La solución a esto es envolver la biblioteca MUST y devolver un valor DEBERÍA. En este caso, una función que devuelve un entero. Escriba una función que tome una cadena y devuelva un objeto Integer, o funciona o devuelve nulo (como Ints.tryParse de guava). Haga su validación por separado de su operación, su operación debe tomar en cuenta. Si se llama a su operación con valores predeterminados cuando tiene una entrada no válida, o si hace otra cosa, dependerá de sus especificaciones; lo más que puedo decir al respecto es que es realmente poco probable que el lugar para tomar esa decisión sea en la operación método.

jmoreno
fuente
¿Cuál es tu idea aquí? Voy a tener el mismo problema incluso si tengo sum(int, int)y parseIntFromString(String). Para obtener la misma funcionalidad, en cualquier caso, tendré que escribir un fragmento de código donde hay 2 llamadas a parseIntFromString()y 1 llamada a sum(). Considere este caso, si tiene sentido para usted, no veo ninguna diferencia desde el punto de vista del problema original.
Andrey Agibalov
@ loki2302: el punto es que la persona que llama decide qué comportamiento es apropiado cuando no es un número. Además, si no realizan la verificación de errores, la falla se produce un paso más cerca de donde se asigna el valor; para fines de depuración, cuanto más cerca de la asignación, más fácil será la depuración. Y es más probable que no escriba la prueba de unidad adecuada para la persona que llama si está haciendo TDD. Básicamente, no desea que se proporcione una cadena en el método x que se supone que es un número y luego 5 clases y 20 llamadas a métodos lanzan una excepción cuando intenta tratarlo como un número.
jmoreno
No lo entiendo. ¿Está tratando de decir que proporcionar una interfaz de int sum(String, String)nunca es posible en realidad?
Andrey Agibalov
1
@ loki2302: Dado el código que se muestra, sería posible, pero una mala idea. Si el número en la cadena pudiera ser una palabra donde "2", "dos", "dos", "deux", "zwo" fueran todos tratados de la misma manera, entonces esa firma sería apropiada. Pero tal como está, el método recibe dos cadenas y las trata como números enteros. ¿Por qué querrías hacer eso? Es una mala idea.
jmoreno
2
@ loki2302: Aceptaste la respuesta diciendo que estaba bien lanzar una excepción sin marcar cuando se pasaba basura, pero el objetivo de un lenguaje fuertemente tipado es EVITAR que se pase basura. Tener un método que espera que una cadena sea siempre un valor entero es solo buscar problemas. Incluso Integer.parseInt no se ocupa de eso bien (una excepción en lo que DEBERÍA esperarse de entrada es mala, el método integer.TryParse de .Net es mucho mejor, aunque la falta de un parámetro de salida en Java lo hace algo poco práctico).
jmoreno
3

1. Debo especificar NumberFormatException como parte de la firma del método.

Creo que sí. Es una buena documentación.

2. Debo definir mi propia excepción marcada (BadDataException), manejar NumberFormatException dentro del método y volver a lanzarlo como BadDataException.

A veces sí. Las excepciones marcadas se consideran mejores en algunos casos, pero trabajar con ellas es todo un PITA. Es por eso que muchos frameworks (por ejemplo, Hibernate) usan solo excepciones en tiempo de ejecución.

3. Debo definir mi propia excepción marcada (BadDataException), validar ambas cadenas de alguna manera como expresiones regulares y lanzar mi BadDataException si no coincide.

Nunca. Más trabajo, menos velocidad (a menos que espere que lanzar la excepción sea una regla) y ninguna ganancia en absoluto.

4. ¿Tu idea?

Ninguno en absoluto.

maaartinus
fuente
2

No 4.

Creo que no cambiaría el método en absoluto. Pondría un intento de captura alrededor del método de llamada o superior en el seguimiento de la pila donde estoy en un contexto en el que puedo recuperarme elegantemente con la lógica empresarial de la excepción.

No haría con certeza el número 3 ya que lo considero exagerado.

Farmor
fuente
2

Suponiendo que lo que está escribiendo va a ser consumido (como una API) por otra persona, entonces debe ir con 1, NumberFormatException es específicamente con el propósito de comunicar tales excepciones y debe usarse.

Ashkan Aryan
fuente
2
  1. Primero debe preguntarse si el usuario de mi método debe preocuparse por ingresar datos incorrectos o si se espera que ingrese los datos correctos (en este caso, String). Esta expectativa también se conoce como diseño por contrato .

  2. y 3. Sí, probablemente debería definir BadDataException o incluso mejor usar algunos de los que eliminan como NumberFormatException, sino dejar que se muestre el mensaje estándar. Capture NumberFormatException en el método y vuelva a lanzarlo con su mensaje, sin olvidar incluir el seguimiento de pila original.

  3. Depende de la situación, pero probablemente volvería a lanzar NumberFormatException con información adicional. Y también debe haber una explicación javadoc de cuáles son los valores esperados paraString a, String b

Ácaro Mitreski
fuente
1

Depende mucho del escenario en el que te encuentres.

Caso 1. Siempre eres tú quien depura el código y nadie más y la excepción no causará una mala experiencia de usuario

Lanzar la NumberFormatException predeterminada

Caso 2: el código debe ser extremadamente fácil de mantener y comprensible

Defina su propia excepción y agregue muchos más datos para depurar mientras la lanza.

No necesita verificaciones de expresiones regulares ya que, de todos modos, irá a la excepción en caso de entrada incorrecta.

Si fuera un código de nivel de producción, mi idea sería definir más de una excepción personalizada, como

  1. Excepción de formato de número
  2. Excepción de desbordamiento
  3. Excepción nula, etc.

y lidiar con todos estos por separado

Adithya Surampudi
fuente
1
  1. Puede hacerlo para dejar claro que esto puede suceder por una entrada incorrecta. Podría ayudar a alguien que usa su código a recordar cómo manejar esta situación. Más específicamente, está dejando en claro que no lo maneja en el código usted mismo, o devuelve algún valor específico en su lugar. Por supuesto, JavaDoc también debería dejar esto en claro.
  2. Solo si desea obligar a la persona que llama a lidiar con una excepción marcada.
  3. Eso parece una exageración. Confíe en el análisis para detectar una entrada incorrecta.

En general, una NumberFormaException no está marcada porque se espera que se proporcione una entrada que se pueda analizar correctamente. La validación de entrada es algo que debe manejar. Sin embargo, analizar la entrada es la forma más sencilla de hacer esto. Simplemente puede dejar su método como está y advertir en la documentación que se espera una entrada correcta y cualquier persona que llame a su función debe validar ambas entradas antes de usarla.

G_H
fuente
1

Cualquier comportamiento excepcional debe aclararse en la documentación. O debe indicar que este método devuelve un valor especial en caso de error (como null, cambiando el tipo de retorno a Integer) o debe usarse el caso 1. Tenerlo explícito en la firma del método permite al usuario ignorarlo si asegura cadenas correctas por otros medios, pero aún es obvio que el método no maneja este tipo de falla por sí mismo.

kapex
fuente
1

Responda a su pregunta actualizada.

Sí, es perfectamente normal tener excepciones "sorpresa". Piense en todos los errores de tiempo de ejecución que uno tiene cuando es nuevo en la programación.

e.g ArrayIndexOutofBound

También es una excepción sorpresa común para cada bucle.

ConcurrentModificationException or something like that
Farmor
fuente
1

Si bien estoy de acuerdo con la respuesta de que se debe permitir filtrar la excepción de tiempo de ejecución, desde una perspectiva de diseño y usabilidad, sería una buena idea envolverla en una IllegalArgumentException en lugar de lanzarla como NumberFormatException. Esto luego hace que el contrato de su método sea más claro mediante el cual declara que se le pasó un argumento ilegal debido al cual lanzó una excepción.

Con respecto a la actualización de la pregunta "Imagina, no es un marco de código abierto, que deberías usar por alguna razón. Miras la firma del método y piensas:" Está bien, nunca arroja ". Luego, algún día, obtuviste una excepción . ¿Es normal?" el javadoc de su método siempre debe derramar el comportamiento de su método (restricciones previas y posteriores). Piense en las líneas de, por ejemplo, las interfaces de colección, donde si no se permite un nulo, el javadoc dice que se lanzará una excepción de puntero nulo, aunque nunca es parte de la firma del método.

Escorpión
fuente
2
NumberFormatException es una subclase de IllegalArgumentException, por lo que este envoltorio no agrega información.
Don Roby
lo que significa que la excepción puede filtrarse tal como está (como en este caso nfe ya es una excepción de argumento ilegal) y puede haber un manejo genérico para tratar con argumentos ilegales en algún lugar de la jerarquía de llamadas. así que si este fuera un ejemplo donde los argumentos fueron pasados ​​por el usuario, podría haber un código genérico que envolvería todo el manejo de argumentos ilegales e informaría al usuario al respecto.
Escorpión
1

Como estás hablando de una buena práctica de Java, en mi opinión, siempre es mejor

  • Para manejar la excepción no marcada, analícela y a través de una excepción personalizada sin marcar.

  • Además, mientras lanza una excepción personalizada sin marcar, puede agregar el mensaje de excepción que su cliente podría entender y también imprimir el seguimiento de la pila de la excepción original

  • No es necesario declarar la excepción personalizada como "lanzamientos", ya que no está marcada.

  • De esta manera, no está violando el uso de las excepciones no verificadas, al mismo tiempo que el cliente del código entendería fácilmente el motivo y la solución de la excepción.

  • Además, documentar correctamente en java-doc es una buena práctica y ayuda mucho.

Samarth
fuente
1

Creo que depende de tu propósito, pero lo documentaría como mínimo:

/**
 * @return the sum (as an int) of the two strings
 * @throws NumberFormatException if either string can't be converted to an Integer
 */
public static int sum(String a, String b)
  int x = Integer.parseInt(a);
  int y = Integer.parseInt(b);
  return x + y;
}

O tome una página del código fuente de Java para la clase java.lang.Integer:

public static int parseInt(java.lang.String string) throws java.lang.NumberFormatException;
Chris Knight
fuente
1

¿Qué tal el patrón de validación de entrada implementado por la biblioteca 'Guava' de Google o la biblioteca 'Validator' de Apache ( comparación )?

En mi experiencia, se considera una buena práctica validar los parámetros de una función al comienzo de la función y lanzar Excepciones cuando corresponda.

Además, consideraría que esta pregunta es en gran medida independiente del idioma. La 'buena práctica' aquí se aplicaría a todos los lenguajes que tienen funciones que pueden tomar parámetros que pueden o no ser válidos.

Spycho
fuente
1

Creo que su primera frase de "Una pregunta bastante estúpida" es muy relevante. ¿Por qué escribirías un método con esa firma en primer lugar? ¿Tiene sentido sumar dos cadenas? Si el método de llamada quiere sumar dos cadenas, es responsabilidad del método de llamada asegurarse de que sean entradas válidas y convertirlas antes de llamar al método.

En este ejemplo, si el método de llamada no puede convertir las dos cadenas en un int, podría hacer varias cosas. Realmente depende en qué capa se produce esta suma. Supongo que la conversión de cadenas estaría muy cerca del código de front-end (si se hizo correctamente), de modo que el caso 1 sería el más probable:

  1. Establecer un mensaje de error y detener el procesamiento o redirigir a una página de error
  2. Devuelve falso (es decir, pondría la suma en algún otro objeto y no sería necesario devolverlo)
  3. Lanza alguna BadDataException como estás sugiriendo, pero a menos que la suma de estos dos números sea muy importante, esto es excesivo y, como se mencionó anteriormente, probablemente sea un mal diseño ya que implica que la conversión se está realizando en el lugar equivocado
GreenieMeanie
fuente
1

Hay muchas respuestas interesantes a esta pregunta. Pero todavía quiero agregar esto:

Para el análisis de cadenas, siempre prefiero usar "expresiones regulares". El paquete java.util.regex está ahí para ayudarnos. Así que terminaré con algo como esto, que nunca arroja ninguna excepción. Depende de mí devolver un valor especial si quiero detectar algún error:

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public static int sum(String a, String b) {
  final String REGEX = "\\d"; // a single digit
  Pattern pattern = Pattern.compile(REGEX);
  Matcher matcher = pattern.matcher(a);
  if (matcher.find()) { x = Integer.matcher.group(); }
  Matcher matcher = pattern.matcher(b);
  if (matcher.find()) { y = Integer.matcher.group(); }
  return x + y;
}

Como se puede ver, el código es un poco más largo, pero podemos manejar lo que queremos (y establecer valores predeterminados para xey, controlar lo que sucede con las cláusulas else, etc.) Incluso podríamos escribir una transformación más general rutina, a la que podemos pasar cadenas, valores de retorno predeterminados, código REGEX para compilar, mensajes de error para lanzar, ...

Espero que haya sido útil.

Advertencia: no pude probar este código, así que disculpe posibles problemas de sintaxis.

Luis
fuente
1
estamos hablando de Java? ¿Qué es Integer.matcher? privatevariable dentro del método? Missing ()Para el IFS, falta mucha ;, x, yno declarada, matcherdeclarado dos veces, ...
user85421
De hecho Carlos, lo hice de prisa, y como dije, no pude probarlo de inmediato. Lo siento. Quería mostrar la forma de hacer las expresiones regulares.
Louis
De acuerdo, pero esto no resuelve el problema con NumberFormatException, la pregunta principal en mi opinión, (suponiendo que Integer.matcher.group()sea ​​así Integer.parseInt(matcher.group()))
user85421
0

Se enfrenta a este problema porque permite que los errores de usuario se propaguen demasiado profundamente en el núcleo de la aplicación y, en parte, también porque abusa de los tipos de datos de Java.

Debería tener una separación más clara entre la validación de entrada del usuario y la lógica empresarial, utilizar la escritura de datos adecuada y este problema desaparecerá por sí solo.

El hecho es que la semántica de Integer.parseInt()es conocida: su propósito principal es analizar números enteros válidos . Falta un paso de análisis / validación de entrada de usuario explícito.

rustyx
fuente