Validación del parámetro de entrada en la persona que llama: ¿duplicación de código?

16

¿Dónde está el mejor lugar para validar los parámetros de entrada de la función: en la persona que llama o en la función misma?

Como me gustaría mejorar mi estilo de codificación, trato de encontrar las mejores prácticas o algunas reglas para este problema. Cuando y que 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.

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

No estoy interesado solo en condiciones nulas, sino en la validación de cualquier variable de entrada (valor negativo para sqrtfuncionar, dividir por cero, combinación incorrecta de estado y código postal, o cualquier otra cosa)

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

Estoy pensando en algunos argumentos:

  • cuando el tratamiento de una variable no válida puede variar, es bueno validarlo en el lado de la persona que llama (por ejemplo, la sqrt()función; en algunos casos, es posible que desee trabajar con un número complejo, por lo que trato la condición en la persona 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 la persona que llama 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 un duplicado de ninguna otra, busqué este problema y encontré preguntas similares, pero no mencionan exactamente este caso.

srnka
fuente

Respuestas:

15

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

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

Tome el siguiente método como ejemplo:

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

¿Qué implica el contrato DeletePerson? El programador solo puede suponer que si Personse pasa alguno , se eliminará. Sin embargo, sabemos que esto no siempre es cierto. ¿Qué pasa si pes un nullvalor? Y sip 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. A veces, elimina a una persona, y a veces arroja una NullReferenceException, o una DatabaseNotConnectedException, o a veces no hace nada (como si la persona ya está eliminada).

Las API como esta son notoriamente difíciles de usar, porque cuando llama a este "recuadro negro" de un método, pueden ocurrir todo tipo de cosas terribles.

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

  • Agregue validación y agregue una excepción al contrato. Esto fortalece el contrato , pero requiere que la persona que llama realice la validación. La diferencia, sin embargo, es que ahora conocen sus requisitos. En este caso, comunico esto con un comentario XML de C #, pero en su lugar podría agregar un throws(Java), usar una Assertherramienta de contrato o una herramienta de contrato como Code Contracts.

    ///<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 por parte de todos los códigos de llamada, pero en mi experiencia, este no suele ser el caso. Piense en un escenario en el que está tratando de eliminar una Persona nula. ¿Cómo pasó eso? ¿De dónde vino la Persona nula? Si se trata de una IU, por ejemplo, ¿por qué se manejó la tecla Eliminar si no hay una selección actual? Si ya se hubiera eliminado, ¿no debería haberse eliminado ya de la pantalla? Obviamente, hay excepciones a esto, pero a medida que crezca un proyecto, a menudo agradecerá a un código como este por evitar que los errores penetren profundamente en el sistema.

  • Agregue validación y código a la defensiva. Esto hace que el contrato sea más flexible , porque ahora este método hace más que simplemente eliminar a la persona. Cambié el nombre del método para reflejar esto, pero podría no ser necesario si eres coherente en tu API. Este enfoque tiene sus pros y sus contras. La ventaja es que ahora puede llamar TryDeletePersonpasar todos los tipos de entradas no válidas y nunca preocuparse 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 tenga en cuenta si estalla una guerra de llamas.

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • Combinar enfoques. A veces desea un poco de ambos, donde desea que las personas que llaman externas sigan las reglas de cerca (para obligarlos a codificar de manera responsable), 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, concentrarse en los contratos que implicó en lugar de una regla estricta funciona mejor. La codificación defensiva parece funcionar mejor en casos donde 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 cuando se espera que la persona que llama solo haga llamadas a métodos cuando realmente tienen sentido.

Kevin McCormick
fuente
Gracias por muy buena respuesta con ejemplo. Me gusta el punto de enfoque "defensivo" y de "contrato estricto".
srnka
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 punteros nulos siempre que sea posible (consulte las recomendaciones de estilo Guava , por ejemplo), ¿aún valida todos los argumentos de función para asegurarse de que no sea nulo? Probablemente no sea necesario, pero es probable que aún lo haga para que sea más fácil encontrar errores. Pero puede usar una afirmació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 es que las hay) en las compilaciones de depuración.

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

Pero no todas las condiciones previas ni siquiera se pueden verificar razonablemente. Por ejemplo, un algoritmo de búsqueda binaria tiene la condición previa de que la secuencia a buscar debe ser ordenada. Pero verificar que definitivamente es así es una operación O (N), por lo que hacer eso en cada llamada anula el punto de usar un algoritmo O (log (N)) en primer lugar. Si está programando a la defensiva, puede hacer comprobaciones menores (por ejemplo, verificar que para cada partición que busca, se ordenan los valores inicial, medio y final), pero eso no detecta todos los errores. Por lo general, solo tendrá que confiar en que se cumpla la condición previa.

El único lugar real donde necesita verificaciones explícitas es en los límites. ¿Entrada externa a su proyecto? Validar, validar, validar. Un área gris son los límites de la API. Realmente depende de cuánto desea confiar en el código del cliente, cuánto daño hace la entrada no válida y cuánta asistencia desea proporcionar para encontrar errores. Cualquier límite de privilegio debe contar como externo, por supuesto, las llamadas al sistema, por ejemplo, se ejecutan en un contexto de privilegio elevado y, por lo tanto, deben tener mucho cuidado de validar. Cualquier validación de este tipo debe ser interna a la llamada al sistema.

Sebastian Redl
fuente
Gracias por tu respuesta. ¿Puede, por favor, dar el enlace a la recomendación de estilo Guava? No puedo googlear y averiguar qué has querido decir con eso. +1 para la validación de los límites.
srnka
Enlace agregado. En realidad no es una guía de estilo completa, solo una parte de la documentación de las utilidades no nulas.
Sebastian Redl
6

La validación de parámetros debería ser la preocupación de la función que se llama. 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 internamente la función. 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 unitarias contra esta función para asegurarse de que se comporta según lo previsto con valores de parámetros válidos e inválidos.

Bernardo
fuente
Gracias por responder. Entonces, usted piensa que esa función debería verificar los parámetros de entrada válidos e inválidos en todos los casos. Algo diferente de la afirmación del libro del Programador Pragmático: "la validación del parámetro de entrada es responsabilidad de quien llama". Es bueno pensar "La función debe saber lo que se considera válido ... Las personas que llaman pueden no saber esto" ... ¿Entonces no le gusta usar condiciones previas?
srnka
1
Puede usar condiciones previas si lo desea (vea la respuesta de Sebastian ), pero prefiero estar a la defensiva y manejar cualquier tipo de información posible.
Bernard
4

Dentro de la función misma. Si la función se usa más de una vez, no querrá verificar el parámetro para cada llamada a la 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 la cláusula de guardia

Actualizar

Vea mi respuesta para cada escenario que haya proporcionado.

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

    Responder

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

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

    Si quería uno, puede ajustarlo a su sqrtfunción personalizada 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 la persona que llama 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

    Sería bueno si la persona que llama es una función, ¿no te parece?

    Si otra persona utiliza las funciones dentro de la 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 al código mantenible. Mover su validación de parámetros asegura una fuente de verdad sobre lo que la función puede aceptar o no.

Onésimo Sin consolidar
fuente
Gracias por responder. El sqrt () fue solo un ejemplo, el mismo comportamiento con el parámetro de entrada puede ser utilizado por muchas otras funciones. "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" - No estoy de acuerdo con esto. Entonces podemos decir lo mismo para el valor de retorno: si la función se actualiza de tal manera que afectará el valor de retorno, debe corregir cada persona que llama ... Creo que la función debe tener una tarea bien definida que hacer ... De lo contrario El cambio en la persona que llama es necesario de todos modos.
srnka
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 pueda (y a menudo lo haga) incluir la validez de los parámetros de entrada.
Las condiciones posteriores son las promesas que la función hace a sus llamantes.

Cuando la validez de los parámetros de una función es parte de las condiciones previas, es responsabilidad del llamante asegurarse de que esos parámetros sean válidos. Pero eso no significa que cada persona que llama tiene 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 establecidas. Como estas pruebas pueden ser costosas, es una buena idea poder desactivarlas para las versiones de lanzamiento. Si estas pruebas fallan, entonces el programa debe finalizar, ya que probablemente se ha encontrado con un error.

Aunque a primera vista el cheque en la persona que llama parece invitar a la duplicación de código, en realidad es al revés. La verificación en la persona que llama da como resultado la duplicación de código y se realiza un montón de trabajo innecesario.
Solo piense en ello, con qué frecuencia pasa los parámetros a través de varias capas de funciones, realizando 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 hacer la verificación para cada uno de los parámetros. Con la verificación en la persona que llama, solo la primera función debería 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 dijeron en su condición previa) y pueden pasarla sin más controles.
Y ahora imagine que uno de esos parámetros se supone que es una lista ordenada.

Bart van Ingen Schenau
fuente
+1 Gracias por la respuesta. Bonita reflexión: "La verificación en la persona que llama da lugar a la duplicación de código y se está haciendo mucho trabajo innecesario". Y en la oración: "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": ¿qué quiere decir con la expresión "lógica interna"? La funcionalidad DBC?
srnka
@srnka: Con "lógica interna" me refiero a los cálculos y decisiones en una función. Es esencialmente la implementación de la función.
Bart van Ingen Schenau
0

En la mayoría de los casos, no puede saber quién, cuándo y cómo llamará a la función que escribió. Es mejor 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 verifique 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 (necesita tener documentación). La excepción le dará a la persona que llama suficiente información sobre lo que sucedió y también dirigirá la atención a los argumentos no válidos.

superM
fuente
En realidad, puede ser mejor validar el parámetro y, si el parámetro no es válido, lanzar una excepción usted mismo. He aquí por qué: los payasos que llaman a su rutina sin molestarse en asegurarse de que le dieron datos válidos son los mismos que no se molestarán en verificar el código de retorno de error que indica que pasaron datos no válidos. Lanzar una excepción obliga al problema a solucionarse.
John R. Strohm