He oído que se recomienda validar argumentos de métodos públicos:
- ¿Se debe verificar si es nulo si no espera nulo?
- ¿Debe un método validar sus parámetros?
- MSDN - CA1062: Validar argumentos de métodos públicos (tengo antecedentes .NET pero la pregunta no es específica de C #)
La motivación es comprensible. Si un módulo se usará de manera incorrecta, queremos lanzar una excepción de inmediato en lugar de cualquier comportamiento impredecible.
Lo que me molesta es que los argumentos incorrectos no son el único error que se puede cometer al usar un módulo. Aquí hay algunos escenarios de error en los que necesitamos agregar lógica de verificación si seguimos las recomendaciones y no queremos una escalada de errores:
- Llamada entrante - argumentos inesperados
- Llamada entrante: el módulo está en un estado incorrecto
- Llamada externa: resultados inesperados devueltos
- Llamada externa - efectos secundarios inesperados (doble entrada a un módulo de llamada, rompiendo otros estados de dependencias)
Intenté tener en cuenta todas estas condiciones y escribir un módulo simple con un método (lo siento, no C-chicos):
public sealed class Room
{
private readonly IDoorFactory _doorFactory;
private bool _entered;
private IDoor _door;
public Room(IDoorFactory doorFactory)
{
if (doorFactory == null)
throw new ArgumentNullException("doorFactory");
_doorFactory = doorFactory;
}
public void Open()
{
if (_door != null)
throw new InvalidOperationException("Room is already opened");
if (_entered)
throw new InvalidOperationException("Double entry is not allowed");
_entered = true;
_door = _doorFactory.Create();
if (_door == null)
throw new IncompatibleDependencyException("doorFactory");
_door.Open();
_entered = false;
}
}
Ahora es seguro =)
Es bastante espeluznante. Pero imagine lo espeluznante que puede ser en un módulo real con docenas de métodos, estado complejo y muchas llamadas externas (¡hola, amantes de la inyección de dependencia!). Tenga en cuenta que si está llamando a un módulo cuyo comportamiento se puede anular (clase no sellada en C #), entonces está haciendo una llamada externa y las consecuencias no son predecibles en el alcance de la persona que llama.
En resumen, ¿cuál es la forma correcta y por qué? Si puede elegir entre las opciones a continuación, responda preguntas adicionales, por favor.
Verifique el uso completo del módulo. ¿Necesitamos pruebas unitarias? ¿Hay ejemplos de tal código? ¿Debe la inyección de dependencia tener un uso limitado (ya que provocará más lógica de comprobación)? ¿No es práctico mover esos controles al tiempo de depuración (no incluir en la versión)?
Marque solo argumentos. Según mi experiencia, la verificación de argumentos, especialmente la verificación nula, es la verificación menos efectiva, porque el error de argumento rara vez conduce a errores complejos y escalas de errores. La mayoría de las veces obtendrá un NullReferenceException
en la siguiente línea. Entonces, ¿por qué las verificaciones de argumentos son tan especiales?
No verifique el uso del módulo. Es una opinión bastante impopular, ¿puedes explicar por qué?
Respuestas:
TL; DR: Validar el cambio de estado, confiar en [validez del] estado actual.
A continuación solo considero las verificaciones habilitadas para el lanzamiento. Las afirmaciones activas solo de depuración son una forma de documentación, que es útil a su manera y está fuera del alcance de esta pregunta.
Considere los siguientes principios:
Definiciones
Estado mutable
Problema
En idiomas imperativos, el síntoma de error y su causa pueden estar separados por horas de trabajo pesado. La corrupción estatal puede ocultarse y mutar para dar como resultado una falla inexplicable, ya que la inspección del estado actual no puede revelar el proceso completo de corrupción y, por lo tanto, el origen del error.
Solución
Cada cambio de estado debe ser cuidadosamente elaborado y verificado. Una forma de lidiar con el estado mutable es mantenerlo al mínimo. Esto se logra mediante:
Al extender el estado de un componente, considere hacerlo permitiendo que el compilador imponga la inmutabilidad de los nuevos datos. Además, aplique cada restricción de tiempo de ejecución razonable, limitando los posibles estados resultantes a un conjunto bien definido lo más pequeño posible.
Ejemplo
Repetición y cohesión de responsabilidad
Problema
La verificación de las condiciones previas y las condiciones posteriores de las operaciones conduce a la duplicación del código de verificación tanto en el cliente como en el componente. La validación de la invocación de componentes a menudo obliga al cliente a asumir algunas de las responsabilidades del componente.
Solución
Confíe en el componente para realizar la verificación de estado cuando sea posible. Los componentes deben proporcionar una API que no requiera una verificación de uso especial (verificación de argumentos o aplicación de secuencia de operación, por ejemplo) para mantener el estado del componente bien definido. Obligan a verificar los argumentos de invocación de API según sea necesario, informan fallas por los medios necesarios y se esfuerzan por evitar la corrupción de su estado.
Los clientes deben confiar en los componentes para verificar el uso de su API. No solo se evita la repetición, el cliente ya no depende de detalles adicionales de implementación del componente. Considere el marco como un componente. Solo escriba código de verificación personalizado cuando los invariantes de los componentes no sean lo suficientemente estrictos o para encapsular la excepción de los componentes como detalle de implementación.
Si una operación no cambia de estado y no está cubierta por las verificaciones de cambio de estado, verifique cada argumento en el nivel más profundo posible.
Ejemplo
Responder
Cuando los principios descritos se aplican al ejemplo en cuestión, obtenemos:
Resumen
El estado del cliente consta de valores de campos propios y partes del estado del componente que no están cubiertos por sus propios invariantes. La verificación solo debe hacerse antes del cambio de estado real de un cliente.
fuente
Una clase es responsable de su propio estado. Por lo tanto, valide en la medida en que mantenga o ponga las cosas en un estado aceptable.
No, no arrojes una excepción, en su lugar, ofrece un comportamiento predecible. El corolario de la responsabilidad estatal es hacer que la clase / aplicación sea tan tolerante como práctica. Por ejemplo, pasando
null
aaCollection.Add()
? Simplemente no agregue y continúe. ¿Obtienenull
información para crear un objeto? Cree un objeto nulo o un objeto predeterminado. Arriba, eldoor
ya esopen
? Y qué, sigue adelante.DoorFactory
argumento es nulo? Crea uno nuevo. Cuando creo unenum
siempre tengo unUndefined
miembro. Hago un uso liberal deDictionary
syenums
para definir las cosas explícitamente; y esto contribuye en gran medida a ofrecer un comportamiento predecible.Sí, aunque camino a través de la sombra del valle de parámetros, no temeré discusiones. Para lo anterior, también uso los parámetros predeterminados y opcionales tanto como sea posible.
Todo lo anterior permite que el procesamiento interno continúe. En una aplicación particular, tengo docenas de métodos en varias clases con un solo lugar donde se lanza una excepción. Incluso entonces, no es por argumentos nulos o porque no pude continuar procesando, es porque el código terminó creando un objeto "no funcional" / "nulo".
editar
citando mi comentario en su totalidad. Creo que el diseño no debería simplemente "darse por vencido" al encontrar 'nulo'. Especialmente usando un objeto compuesto.
fin editar
fuente
encapsulation
&single responsibility
. Prácticamente no haynull
verificación después de la primera capa que interactúa con el cliente. El código es <strike> tolerante </strike> robusto. Las clases están diseñadas con estados predeterminados y, por lo tanto, funcionan sin estar escritas como si el código de interacción estuviera lleno de errores, basura no autorizada. Un padre compuesto no tiene que alcanzar las capas secundarias para evaluar la validez (y por implicación, verificarnull
en todos los rincones y grietas). El padre sabe lo que significa el estado predeterminado de un niño