código de contratos / afirma: ¿qué pasa con los controles duplicados?

10

Soy un gran fanático de escribir afirmaciones, contratos o cualquier tipo de cheques disponibles en el idioma que estoy usando. Una cosa que me molesta un poco es que no estoy seguro de cuál es la práctica común para hacer frente a los controles duplicados.

Situación de ejemplo: primero escribo la siguiente función

void DoSomething( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  //code using obj
}

luego, unas horas más tarde, escribo otra función que llama a la primera. Como todo está fresco en la memoria, decido no duplicar el contrato, ya que sé que ya DoSomethingcomprobará si hay un objeto nulo:

void DoSomethingElse( object obj )
{
  //no Requires here: DoSomething will do that already
  DoSomething( obj );
  //code using obj
}

El problema obvio: DoSomethingElseahora depende DoSomethingde verificar que obj no sea nulo. Por lo tanto, debería DoSomethingdecidir no verificar más, o si decido usar otra función, obj podría no verificarse más. Lo que me lleva a escribir esta implementación después de todo:

void DoSomethingElse( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  DoSomething( obj );
  //code using obj
}

Siempre seguro, no se preocupe, excepto que si la situación crece, el mismo objeto podría verificarse varias veces y es una forma de duplicación y todos sabemos que eso no es tan bueno.

¿Cuál es la práctica más común para situaciones como estas?

stijn
fuente
3
ArgumentBullException? Esa es una nueva :)
un CVn
jajaja @ mis habilidades de escritura ... Lo editaré.
stijn

Respuestas:

13

Personalmente, verificaría si hay nulo en cualquier función que fallará si se obtiene un nulo, y no en ninguna función que no lo haga.

Entonces, en su ejemplo anterior, si doSomethingElse () no necesita desreferenciar obj, entonces no comprobaría obj para nulo allí.

Si DoSomething () no hace referencia a obj, entonces debería verificar nulo.

Si ambas funciones lo desreferencian, ambos deberían verificar. Entonces, si DoSomethingElse desreferencia obj, entonces debería verificar si es nulo, pero DoSomething también debería verificar si es nulo, ya que puede llamarse desde otra ruta.

De esta manera, puede dejar el código bastante limpio y aún así garantizar que los cheques estén en el lugar correcto.

Luke Graham
fuente
1
Estoy completamente de acuerdo. Las condiciones previas de cada método deben ser independientes. Imagine que reescribe de manera DoSomething()tal que la condición previa ya no sea necesaria (poco probable en este caso particular, pero podría suceder en una situación diferente), y elimine la verificación de la condición previa. Ahora, algún método aparentemente no relacionado está roto debido a la condición previa que falta. Tomaré un poco de duplicación de código para aclarar sobre fallas extrañas como esa por el deseo de guardar algunas líneas de código, cualquier día.
un CVn
2

¡Excelente! Veo que te enteraste de los contratos de código para .NET. Code Contracts va mucho más allá de sus afirmaciones promedio, de las cuales el verificador estático es el mejor ejemplo. Es posible que esto no esté disponible para usted si no tiene instalado Visual Studio Premium o superior, pero es importante comprender la intención detrás de esto si va a usar contratos de código.

Cuando aplica un contrato a una función, literalmente es un contrato . Esa función garantiza comportarse de acuerdo con el contrato y se garantiza que solo se utilizará según lo definido por el contrato.

En su ejemplo dado, la DoSomethingElse()función no cumple con el contrato según lo especificado por DoSomething(), ya que se puede pasar nulo, y el verificador estático indicará este problema. La forma de resolver esto es agregando el mismo contrato a DoSomethingElse().

Ahora, esto significa que habrá duplicación, pero esta duplicación es necesaria ya que elige exponer la funcionalidad en dos funciones. Estas funciones, aunque privadas, también se pueden invocar desde diferentes lugares de su clase, por lo que la única forma de garantizar que de una llamada dada el argumento nunca sea nulo es duplicar los contratos.

Esto debería hacer que reconsidere por qué divide el comportamiento en dos funciones en primer lugar. Siempre ha sido mi opinión ( contrario a la creencia popular ) que no debe dividir funciones que solo se llaman desde un solo lugar . Al exponer la encapsulación aplicando los contratos, esto se vuelve aún más evidente. ¡Parece que encontré una argumentación adicional para mi causa! ¡Gracias! :)

Steven Jeuris
fuente
con respecto a su último párrafo: en el código real, ambas funciones eran miembros de dos clases diferentes, razón por la cual están divididas. Aparte de eso, me encontraba en la siguiente situación muchas veces: escribir una función larga, decidir no dividirla. Más tarde descubra que parte de la lógica está duplicada en otro lugar, así que divídala de todos modos. O un año después, léelo nuevamente y lo encuentre ilegible, así que divídalo de todos modos. O al depurar: funciones divididas = menos golpes en la tecla F10. Hay más razones, por lo que personalmente prefiero dividirme, aunque eso significa que a veces puede ser demasiado extremo.
stijn
(1) "Más tarde descubra que parte de la lógica está duplicada en otro lugar" . Es por eso que me parece más importante siempre "desarrollar hacia una API" que simplemente dividir funciones. Piensa constantemente en la reutilización, no solo dentro de la clase actual. (2) "O un año después, léelo nuevamente y lo encuentre ilegible" ¿ Porque las funciones tienen nombres, es mejor? Usted tiene aún más legibilidad si usa lo que un comentarista en mi blog describió como "párrafos de código". (3) "funciones divididas = menos golpes en la tecla F10" ... No veo por qué.
Steven Jeuris
(1) acordado (2) la legibilidad es una preferencia personal, por lo que no es realmente algo que se debata para mí. (3) pasar por una función de 20 líneas requiere golpear F10 20 veces. Pasar por una función que tiene 10 de esas líneas en una función dividida me da la opción de solo tener que presionar F10 11 veces. Sí, en el primer caso puedo poner puntos de interrupción o seleccionar 'saltar al cursor', pero eso sigue siendo un esfuerzo mayor que el segundo caso.
stijn
@stijn: (2) de acuerdo; p (3) ¡gracias por aclarar!
Steven Jeuris