Cómo realizar la validación de entrada sin excepciones o redundancia

11

Cuando intento crear una interfaz para un programa específico, generalmente trato de evitar lanzar excepciones que dependen de entradas no validadas.

Entonces, lo que sucede a menudo es que he pensado en un fragmento de código como este (este es solo un ejemplo por el bien de un ejemplo, no importa la función que realiza, ejemplo en Java):

public static String padToEvenOriginal(int evenSize, String string) {
    if (evenSize % 2 == 1) {
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

Bien, digamos que en evenSizerealidad se deriva de la entrada del usuario. Así que no estoy seguro de que sea uniforme. Pero no quiero llamar a este método con la posibilidad de que se produzca una excepción. Entonces hago la siguiente función:

public static boolean isEven(int evenSize) {
    return evenSize % 2 == 0;
}

pero ahora tengo dos comprobaciones que realizan la misma validación de entrada: la expresión en la ifdeclaración y la comprobación explícita isEven. Código duplicado, no agradable, así que refactoricemos:

public static String padToEvenWithIsEven(int evenSize, String string) {
    if (!isEven(evenSize)) { // to avoid duplicate code
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, eso lo resolvió, pero ahora nos metemos en la siguiente situación:

String test = "123";
int size;
do {
    size = getSizeFromInput();
} while (!isEven(size)); // checks if it is even
String evenTest = padToEvenWithIsEven(size, test);
System.out.println(evenTest); // checks if it is even (redundant)

ahora tenemos una verificación redundante: ya sabemos que el valor es par, pero padToEvenWithIsEvenaún así realiza la verificación de parámetros, que siempre devolverá verdadero, como ya llamamos a esta función.

Ahora, por isEvensupuesto, no plantea un problema, pero si la verificación de parámetros es más engorrosa, esto puede generar un costo excesivo. Además de eso, realizar una llamada redundante simplemente no se siente bien.

A veces podemos solucionar este problema introduciendo un "tipo validado" o creando una función donde este problema no pueda ocurrir:

public static String padToEvenSmarter(int numberOfBigrams, String string) {
    int size = numberOfBigrams * 2;
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append('x');
    }
    return sb.toString();
}

pero esto requiere un pensamiento inteligente y un refactor bastante grande.

¿Hay alguna forma (más) genérica en la que podamos evitar las llamadas redundantes isEveny realizar una verificación de doble parámetro? Me gustaría que la solución no llame realmente padToEvencon un parámetro no válido, desencadenando la excepción.


Sin excepciones, no me refiero a la programación sin excepciones, quiero decir que la entrada del usuario no desencadena una excepción por diseño, mientras que la función genérica en sí misma contiene la verificación de parámetros (aunque solo sea para proteger contra errores de programación).

Maarten Bodewes
fuente
¿Estás intentando eliminar la excepción? Puede hacerlo suponiendo cuál debería ser el tamaño real. Por ejemplo, si pasa 13, pase a 12 o 14 y simplemente evite el control por completo. Si no puede hacer una de esas suposiciones, entonces está atascado con la excepción porque el parámetro no se puede usar y su función no puede continuar.
Robert Harvey
@RobertHarvey Eso, en mi opinión, va directamente contra el principio de la menor sorpresa y contra el principio del fracaso rápido. Es muy parecido a devolver nulo si el parámetro de entrada es nulo (y luego olvidarse de manejar el resultado correctamente, por supuesto, hola NPE inexplicable).
Maarten Bodewes
Ergo, la excepción. ¿Derecho?
Robert Harvey
2
¿Esperar lo? Viniste aquí por consejo, ¿verdad? Lo que estoy diciendo (en mi forma aparentemente no tan sutil), es que esas excepciones son por diseño, por lo que si desea eliminarlas, probablemente debería tener una buena razón. Por cierto, estoy de acuerdo con amon: probablemente no debería tener una función que no pueda aceptar números impares para un parámetro.
Robert Harvey
1
@MaartenBodewes: debe recordar que padToEvenWithIsEven no realiza la validación de la entrada del usuario. Realiza una verificación de validez en su entrada para protegerse contra errores de programación en el código de llamada. La extensión que debe tener esta validación depende de un análisis de costo / riesgo en el que se compara el costo de la verificación con el riesgo de que la persona que escribe el código de llamada pase el parámetro incorrecto.
Bart van Ingen Schenau

Respuestas:

7

En el caso de su ejemplo, la mejor solución es usar una función de relleno más general; Si la persona que llama quiere rellenar a un tamaño uniforme, puede comprobarlo por sí mismo.

public static String padString(int size, String string) {
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

Si realiza repetidamente la misma validación en un valor, o desea permitir solo un subconjunto de valores de un tipo, los microtipos / tipos pequeños pueden ser útiles. Para utilidades de uso general como el relleno, esta no es una buena idea, pero si su valor juega un papel particular en su modelo de dominio, usar un tipo dedicado en lugar de valores primitivos puede ser un gran paso adelante. Aquí, puedes definir:

final class EvenInteger {
  public final int value;

  public EvenInteger(int value) {
    if (!(value % 2 == 0))
      throw new IllegalArgumentException("EvenInteger(" + value + ") is not even");
    this.value = value;
  }
}

Ahora puedes declarar

public static String padStringToEven(EvenInteger evenSize, String string)
    ...

y no tiene que hacer ninguna validación interna. Para una prueba tan simple, envolver un int dentro de un objeto probablemente sea más costoso en términos de rendimiento de tiempo de ejecución, pero usar el sistema de tipos a su favor puede reducir errores y aclarar su diseño.

El uso de estos tipos pequeños incluso puede ser útil incluso cuando no realizan ninguna validación, por ejemplo, para desambiguar una cadena que representa a FirstNamede a LastName. Frecuentemente uso este patrón en lenguajes estáticamente escritos.

amon
fuente
¿Su segunda función todavía no arrojó una excepción en algunos casos?
Robert Harvey
1
@RobertHarvey Sí, por ejemplo, en caso de punteros nulos. Pero ahora la verificación principal, que el número es par, se ve obligada a dejar la función a la responsabilidad de la persona que llama. Luego pueden lidiar con la excepción de la manera que mejor les parezca. Creo que la respuesta se centra menos en deshacerse de todas las excepciones que deshacerse de la duplicación de código en el código de validación.
amon
1
Gran respuesta. Por supuesto, en Java, la penalización por crear clases de datos es bastante alta (mucho código adicional), pero eso es más un problema del lenguaje que la idea que ha presentado. Supongo que no usaría esta solución para todos los casos de uso en los que se produciría mi problema, pero es una buena solución contra el uso excesivo de primitivas o para casos extremos donde el costo de la verificación de parámetros es excepcionalmente difícil.
Maarten Bodewes
1
@MaartenBodewes Si quieres hacer una validación, no puedes deshacerte de algo como las excepciones. Bueno, la alternativa es usar una función estática que devuelva un objeto validado o nulo en caso de falla, pero que básicamente no tiene ventajas. Pero al mover la validación fuera de la función al constructor, ahora podemos llamar a la función sin la posibilidad de un error de validación. Eso le da a la persona que llama todo el control. Por ejemplo:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
amon
1
@MaartenBodewes La validación duplicada ocurre todo el tiempo. Por ejemplo, antes de intentar cerrar una puerta, puede verificar si ya está cerrada, pero la puerta en sí misma tampoco permitirá que se vuelva a cerrar si ya está cerrada. Simplemente no hay forma de salir de esto, excepto si siempre confía en que la persona que llama no realiza ninguna operación no válida. En su caso, es posible que tenga una unsafePadStringToEvenoperación que no realice ninguna verificación, pero parece una mala idea solo para evitar la validación.
plalx
9

Como una extensión de la respuesta de @ amon, podemos combinar la suya EvenIntegercon lo que la comunidad de programación funcional podría llamar un "Constructor inteligente", una función que envuelve al constructor tonto y se asegura de que el objeto esté en un estado válido (hacemos que el tonto clase de constructor o en módulo / paquete de idiomas no basados ​​en clases privado para asegurarse de que solo se usen los constructores inteligentes). El truco consiste en devolver un Optional(o equivalente) para que la función sea más composible.

public final class EvenInteger {
    private final int value;

    private EvenInteger(value) {
        this.value = value;
    }

    public static Optional<EvenInteger> of(final int value) {
        if (value % 2 == 0) {
            return Optional.of(new EvenInteger(value));
        }
        return Optional.empty();
    }

    public int getValue() {
        return this.value;
    }
}

Entonces podemos usar fácilmente Optionalmétodos estándar para escribir la lógica de entrada:

class GetEvenInput {
    public Optional<EvenInteger> askOnce() {
        int size = getSizeFromInput();
        return EvenInteger.of(size);
    }

    public EvenInteger keepAsking() {
        return askOnce().orElseGet(() -> keepAsking());
    }
}

También podría escribir keepAsking()en un estilo Java más idiomático con un bucle do-while.

Optional<EvenInteger> result;
do {
    result = askOnce();
} while (!result.isPresent());

return result.get();

Luego, en el resto de su código, puede confiar EvenIntegercon la certeza de que realmente será uniforme y nuestro cheque par solo se escribió una vez, en EvenInteger::of.

Walpen
fuente
Opcional en general representa bastante bien los datos potencialmente inválidos. Esto es análogo a una gran cantidad de métodos TryParse, que devuelven tanto datos como un indicador que indica la validez de la entrada. Con controles de tiempo completos.
Basilevs
1

Hacer la validación dos veces es un problema si el resultado de la validación es el mismo Y la validación se realiza en la misma clase. Ese no es tu ejemplo. En su código refactorizado, la primera comprobación de IsEven que se realiza es una validación de entrada, la falla da como resultado que se solicite una nueva entrada. La segunda comprobación es completamente independiente de la primera, ya que se encuentra en un método público padToEvenWithEven que se puede llamar desde fuera de la clase y tiene un resultado diferente (la excepción).

Su problema es similar al problema del código accidentalmente idéntico que se confunde por no seco. Estás confundiendo la implementación, con el diseño. No son lo mismo, y solo porque tenga una o una docena de líneas que sean iguales, no significa que estén cumpliendo el mismo propósito y que siempre puedan considerarse intercambiables. Además, es probable que tu clase haga demasiado, pero omite eso, ya que esto probablemente sea solo un ejemplo de juguete ...

Si se trata de un problema de rendimiento, puede resolver el problema creando un método privado, que no haga ninguna validación, que su padToEvenWithEven público llamó después de hacer su validación y que su otro método llamaría en su lugar. Si no se trata de un problema de rendimiento, deje que sus diferentes métodos hagan las comprobaciones que requieren para realizar sus tareas asignadas.

jmoreno
fuente
OP declaró que la validación de entrada se realiza para evitar el lanzamiento de la función. Entonces los controles son completamente dependientes e intencionalmente iguales.
D Drmmr
@DDrmmr: no, no son dependientes. La función se lanza porque eso es parte de su contrato. Como dije, la respuesta es crear un método privado que hace todo excepto lanzar la excepción y luego hacer que el método público llame al método privado. El método público retiene la verificación, donde tiene un propósito diferente: manejar la entrada no validada.
jmoreno