Validación del parámetro de entrada en el llamante: ¿duplicación de código?

13

¿Cuál es el mejor lugar para validar los parámetros de entrada de la función: en el llamador o en la propia función?

Como me gustaría mejorar mi estilo de codificación, trato de encontrar las mejores prácticas o algunas reglas para este problema. Cuándo y qué es mejor.

En mis proyectos anteriores, solíamos verificar y tratar cada parámetro de entrada dentro de la función (por ejemplo, si no es nulo). Ahora, he leído aquí en algunas respuestas y también en el libro Pragmatic Programmer, que la validación del parámetro de entrada es responsabilidad de quien llama.

Eso significa que debo validar los parámetros de entrada antes de llamar a la función. En todas partes se llama la función. Y eso plantea una pregunta: ¿no crea una duplicación de la condición de verificación en todos los lugares a los que se llama la función?

No me interesan solo las condiciones nulas, sino la validación de cualquier variable de entrada (valor negativo para la función sqrt , división por cero, combinación incorrecta de estado y código postal, o cualquier otra cosa)

¿Existen algunas reglas sobre cómo decidir dónde verificar la condición de entrada?

Estoy pensando en algunos argumentos:

  • cuando el tratamiento de la variable no válida puede variar, es bueno validarlo en el lado de la persona que llama (por ejemplo, función sqrt() ; en algunos casos es posible que desee trabajar con un número complejo, por lo que trato la condición en el que llama)
  • cuando la condición de verificación es la misma en todas las personas que llaman, es mejor verificarla dentro de la función, para evitar duplicaciones
  • la validación del parámetro de entrada en el llamante tiene lugar solo una antes de llamar a muchas funciones con este parámetro. Por lo tanto, la validación de un parámetro en cada función no es efectiva
  • la solución correcta depende del caso particular

Espero que esta pregunta no sea duplicada de ninguna otra, busqué este problema y encontré preguntas similares, pero no mencionan exactamente este caso.

    
pregunta srnka 20.02.2013 - 14:21

6 respuestas

13

Depende. Decidir dónde colocar la validación debe basarse en la descripción y la fuerza del contrato implícita (o documentada) por el método. La validación es una buena manera de reforzar el cumplimiento de un contrato específico. Si por alguna razón el método tiene un contrato muy estricto, entonces sí, depende de usted verificarlo antes de llamar.

Este es un concepto especialmente importante cuando creas un método público , porque básicamente estás anunciando que algún método realiza alguna operación. ¡Será mejor que hagas lo que dices!

Tome el siguiente método como ejemplo:

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

¿Cuál es el contrato implícito en DeletePerson ? El programador solo puede asumir que si se pasa cualquier Person , se eliminará. Sin embargo, sabemos que esto no siempre es cierto. ¿Qué sucede si p es un valor null ? ¿Qué pasa si p no existe en la base de datos? ¿Qué pasa si la base de datos está desconectada? Por lo tanto, DeletePerson no parece cumplir bien su contrato. Algunas veces, elimina una persona y otras veces lanza una NullReferenceException, o una DatabaseNotConnectedException, o otras veces no hace nada (como si la persona ya estuviera eliminado).

Las API como esta son muy difíciles de usar, porque cuando llamas a esto "caja negra" de un método, pueden ocurrir todo tipo de cosas terribles.

Aquí hay un par de maneras en que puede mejorar el contrato:

  • Agregue validación y agregue una excepción al contrato. Esto hace que el contrato sea más fuerte , pero requiere que la persona que llama realice la validación. La diferencia, sin embargo, es que ahora ellos conocen sus requerimientos. En este caso, lo comunico con un comentario XML de C #, pero en su lugar, podría agregar un throws (Java), usar un Assert , o usar una herramienta de contrato como Contratos de Código.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        _database.Delete(p);
    }
    

    Nota al margen: el argumento en contra de este estilo es a menudo que causa una validación previa excesiva de todos los códigos de llamada, pero en mi experiencia, a menudo no es así. Piense en un escenario en el que está intentando eliminar una persona nula. ¿Cómo ocurrió eso? ¿De dónde viene la Persona nula? Si se trata de una interfaz de usuario, por ejemplo, ¿por qué se manejó la tecla Eliminar si no hay una selección actual? Si ya se eliminó, ¿no debería haberse eliminado de la pantalla? Obviamente, hay excepciones a esto, pero a medida que un proyecto crece, a menudo se agradecerá el código como este para evitar que los errores penetren profundamente en el sistema.

  • Agregue la validación y el código a la defensiva. Esto hace que el contrato sea más flexible , porque ahora este método hace más que solo eliminar a la persona. Cambié el nombre del método para reflejar esto, pero podría no ser necesario si eres consistente en tu API. Este enfoque tiene sus pros y sus contras. El pro es que ahora puedes llamar a TryDeletePerson pasando todo tipo de entradas inválidas y nunca te preocupes por las excepciones. La desventaja, por supuesto, es que los usuarios de su código probablemente llamarán demasiado a este método, o podría dificultar la depuración en los casos en que p sea nulo. Esto podría considerarse una violación leve del Principio de Responsabilidad Única , así que mantén esa mente si surge una guerra de llamas.

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • Combine enfoques. A veces desea un poco de ambos, donde desea que las personas que llaman externas sigan las reglas de cerca (para obligarlos a que sean responsables del código), pero desea que su código privado sea flexible.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        TryDeletePerson(p);
    }
    
    internal void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    

En mi experiencia, concentrarme en los contratos que implicaste en lugar de una regla difícil funciona mejor. La codificación defensiva parece funcionar mejor en los casos en que es difícil o difícil para la persona que llama determinar si una operación es válida. Los contratos estrictos parecen funcionar mejor donde usted espera que la persona que llama solo realice llamadas de método cuando realmente tienen sentido.

    
respondido por el Kevin McCormick 20.02.2013 - 17:10
7

Es una cuestión de convención, documentación y caso de uso.

No todas las funciones son iguales. No todos los requisitos son iguales. No toda la validación es igual.

Por ejemplo, si su proyecto Java intenta evitar los punteros nulos siempre que sea posible (consulte recomendaciones de estilo de guayaba , por ejemplo), ¿sigue validando cada argumento de función para asegurarse de que no sea nulo? Probablemente no sea necesario, pero es probable que aún lo hagas, para que sea más fácil encontrar errores. Pero puede usar una aserción donde previamente lanzó una NullPointerException.

¿Qué pasa si el proyecto está en C ++? La convención / tradición en C ++ es documentar las condiciones previas, pero solo verificarlas (si las hay) en las versiones de depuración.

En cualquier caso, tiene una condición documentada en su función: ningún argumento puede ser nulo. En su lugar, podría extender el dominio de la función para incluir valores nulos con comportamiento definido, por ejemplo. msgstr "si algún argumento es nulo, lanza una excepción". Por supuesto, esa es mi herencia de C ++ aquí, en Java, es bastante común documentar las condiciones previas de esta manera.

Pero no todas las condiciones previas, incluso pueden se pueden verificar razonablemente. Por ejemplo, un algoritmo de búsqueda binario tiene la condición previa de que la secuencia a buscar debe estar ordenada. Pero la verificación de que definitivamente es así es una operación O (N), por lo que al hacerlo en cada llamada, se anula un poco el uso de un algoritmo O (log (N)) en primer lugar. Si está programando a la defensiva, puede realizar verificaciones menores (por ejemplo, verificando que para cada partición que busca, los valores de inicio, medio y final están ordenados), pero eso no detecta todos los errores. Normalmente, solo tendrá que confiar en que se cumpla la condición previa.

El único lugar real donde necesita verificaciones explícitas está en los límites. ¿Entrada externa a tu proyecto? Validar, validar, validar. Un área gris es los límites de la API. Realmente depende de cuánto desea confiar en el código del cliente, de la cantidad de daño que hace la entrada no válida y de cuánta asistencia desea proporcionar para encontrar errores. Por supuesto, cualquier límite de privilegio debe contar como externo: syscalls, por ejemplo, se ejecuta en un contexto de privilegio elevado y, por lo tanto, debe ser muy cuidadoso de validar. Cualquier validación de este tipo, por supuesto, debe ser interna a la syscall.

    
respondido por el Sebastian Redl 20.02.2013 - 16:33
5

La validación de parámetros debe ser la preocupación de la función que se está llamando. La función debe saber qué se considera entrada válida y qué no. Las personas que llaman pueden no saber esto, especialmente cuando no saben cómo se implementa la función internamente. Se debe esperar que la función maneje cualquier combinación de valores de parámetros de las personas que llaman.

Debido a que la función es responsable de validar los parámetros, puede escribir pruebas de unidad contra esta función para asegurarse de que se comporte como se pretende con los valores de los parámetros válidos e inválidos.

    
respondido por el Bernard 20.02.2013 - 14:47
4

Dentro de la propia función. Si la función se usa más de una vez, no querrá verificar el parámetro para cada llamada a función.

Además, si la función se actualiza de tal manera que afectará la validación del parámetro, debe buscar cada aparición de la validación de la persona que llama para actualizarlos. No es encantador :-).

Puede consultar Cláusula de protección

Actualizar

Vea mi respuesta para cada escenario que haya proporcionado.

  • cuando el tratamiento de la variable no válida puede variar, es bueno validarlo en el lado de la persona que llama (por ejemplo, función sqrt() ; en algunos casos es posible que desee trabajar con un número complejo, por lo que trato la condición en el que llama)

    Responder

    La mayoría de los lenguajes de programación admite números enteros y reales de forma predeterminada, no un número complejo, por lo que su implementación de sqrt solo acepta números no negativos. El único caso en el que tiene una función sqrt que devuelve un número complejo es cuando usa un lenguaje de programación orientado a las matemáticas, como Mathematica

    Además, sqrt para la mayoría de los lenguajes de programación ya está implementado, por lo tanto, no podría modificarlo, y si intenta reemplazar la implementación (ver parches de mono), sus colaboradores se sorprenderán por qué sqrt De repente acepta números negativos.

    Si quería uno, puede ajustarlo alrededor de su función personalizada sqrt que maneja un número negativo y devuelve un número complejo.

  • cuando la condición de verificación es la misma en todas las personas que llaman, es mejor verificarla dentro de la función, para evitar duplicaciones

    Responder

    Sí, esta es una buena práctica para evitar dispersar la validación de parámetros en su código.

  • la validación del parámetro de entrada en el llamante tiene lugar solo una antes de llamar a muchas funciones con este parámetro. Por lo tanto, la validación de un parámetro en cada función no es efectiva

    Responder

    Estaría bien si la persona que llama es una función, ¿no crees?

    Si las funciones dentro de la persona que llama son utilizadas por otra persona que llama, ¿qué le impide validar el parámetro dentro de las funciones llamadas por la persona que llama?

  • la solución correcta depende del caso particular

    Responder

    Apunta a un código mantenible. Mover su validación de parámetros garantiza una fuente de verdad sobre lo que la función puede aceptar o no.

respondido por el OnesimusUnbound 20.02.2013 - 14:30
2

Una función debe indicar sus condiciones previas y posteriores.
Las condiciones previas son las condiciones que debe cumplir la persona que llama antes de que pueda usar correctamente la función y puede (y con frecuencia sí) incluir la validez de los parámetros de entrada.
Las condiciones posteriores son las promesas que la función hace a sus llamadores.

Cuando la validez de los parámetros de una función es parte de las condiciones previas, es responsabilidad de la persona que llama asegurarse de que esos parámetros sean válidos. Pero eso no significa que todas las personas que llaman tengan que verificar explícitamente cada parámetro antes de la llamada. En la mayoría de los casos, no se necesitan pruebas explícitas porque la lógica interna y las condiciones previas de la persona que llama ya garantizan que los parámetros sean válidos.

Como medida de seguridad contra errores de programación (errores), puede verificar que los parámetros pasados a una función realmente cumplan con las condiciones previas indicadas. Como estas pruebas pueden ser costosas, es una buena idea poder desactivarlas para compilaciones de lanzamiento. Si estas pruebas fallan, entonces el programa debería terminarse, ya que probablemente se ha encontrado con un error.

Aunque a primera vista la comprobación de la persona que llama parece invitar a la duplicación de códigos, en realidad es al revés. La verificación en el beneficiario de la llamada da como resultado la duplicación de código y un montón de trabajo innecesario que se está realizando. Solo piénselo, con qué frecuencia pasa los parámetros a través de varias capas de funciones, haciendo solo pequeños cambios en algunos de ellos en el camino. Si aplica constantemente el método check-in-callee , cada una de esas funciones intermedias tendrá que volver a realizar la verificación de cada uno de los parámetros.
Y ahora imagine que uno de esos parámetros se supone que es una lista ordenada. Con la verificación de la persona que llama, solo la primera función tendría que asegurarse de que la lista esté realmente ordenada. Todos los demás saben que la lista ya está ordenada (ya que eso es lo que indicaron en su condición previa) y pueden pasarla sin más comprobaciones.

    
respondido por el Bart van Ingen Schenau 20.02.2013 - 16:53
0

La mayoría de las veces no puede saber quién, cuándo y cómo llamará a la función que escribió. Lo mejor es asumir lo peor: se llamará a su función con parámetros no válidos. Así que definitivamente deberías cubrir eso.

Sin embargo, si el idioma que usa admite excepciones, es posible que no compruebe ciertos errores y esté seguro de que se lanzará una excepción, pero en este caso debe asegurarse de describir el caso en la documentación (debe tener documentación). La excepción le dará a la persona que llama información suficiente sobre lo que sucedió y también dirigirá la atención a los argumentos no válidos.

    
respondido por el superM 20.02.2013 - 14:57

Lea otras preguntas en las etiquetas