domingo, 29 de junio de 2008

No hagáis esto en casa (II)

Los que no han leído la primera parte del artículo deberían comenzar por aquí.

¡Bien por Juan y Sebastián! Era eso, exactamente. Recapitulemos muy rápidamente.

Traduje desde VB.Net a C#

Public Function Resultado() as Integer ...(etc)... If Me.SumaDeAlgo()=xxx And Me.SumaDeOtraCosa()=yyy And Me.SumaDeAlgoMas()=zzz Then ...(etc)... End Function

como

public int Resultado() { ...(etc)... if( this.SumaDeAlgo()==xxx && this.SumaDeOtraCosa()==yyy && this.SumaDeAlgoMas()==zzz ) ...(etc)... }

El problema es la diferencia entre el "And" del VB.Net y el "&&" de C# (no me hubiese pasado de usar una herramienta automática). El "&&" del C# es equivalente al "AndAlso" del VB.Net. Es decir, cuando tengo varios términos booleanos unidos con un "And" primero se evalúan todos y luego se evalúa la expresión más general (en nuestro ejemplo eso implica que siempre se ejecuten las funciones SumaDeAlgo, SumaDeOtraCosa y SumaDeAlgoMas). En cambio, si tengo esos mismos términos unidos con un "AndAlso" o un "&&", se dice que la expresión es cortocircuitada (el término un poco raro lo saqué hace años de la ayuda traducida por Microsoft, igualmente no me desagrada). Si el primer término da false los siguientes no se evalúan, ya que su resultado es irrelevante para el curso de acción a seguir (en nuestro ejemplo, si SumaDeAlgo no es igual a xxx, SumaDeOtraCosa y SumaDeAlgoMas jamás se ejecutan).

O sea que la traducción exacta del código sería:

public int Resultado() { ...(etc)... double sumaDeAlgo = this.SumaDeAlgo(); double sumaDeOtraCosa = this.SumaDeOtraCosa(); double sumaDeAlgoMas = this.SumaDeAlgoMas(); if( sumaDeAlgo==xxx && sumaDeOtraCosa==yyy && sumaDeAlgoMas==zzz ) ...(etc)... }

Así nos aseguramos de que las tres funciones se ejecuten antes de llegar a la condición.

Muy divertido. Un error de distracción. Pero... pensemos un poco en cómo estaba escrito este código en VB y qué era lo que se supone que hacía. Un punteo.

1) Obviamente mi traducción no funcionaba porque las funciones, aparte de devolver un resultado hacían algo más. Concretamente modificaban variables de nivel de clase. Pero... si la función se llama "SumaDeAlgo" ¿cómo puede suponerse que la función modifica algo sin mirar el código? Es una convención de nombres básica el hecho de que si un procedimiento hace algo (en el sentido de modificar el estado del sistema, en este caso el de la clase a la que pertenece) debe tener un verbo en su nombre, o no devolver valor (así por lo menos sabemos que el resultado se establece en algún otro lado, aunque igual no sería muy divertido de leer).

Personalmente mi criterio es que si un procedimiento hace algo y por otro lado devuelve un resultado, el nombre debe referirse a lo que hace porque eso altera el estado del sistema. Si no, se supone que la función llega a su resultado sólo a partir de sus parámetros. En todo caso, si el resultado que devuelve es importante y su significado no es obvio (por lo cual tenemos quue documentarlo o hacerlo explícito) una variable "out" es bastante clara:

void HagoAlgo( out double sumaDeAlgo )

Así queda en primer lugar lo importante para el que lee, lo que no debe pasársele por alto, que es que al invocarla pasa algo, se altera el estado del sistema y que no es lo mismo no invocarla o invocarla dos veces seguidas.

2) Entonces, es un tema de nombres. Si, pero también es un problema de lógica "humana". No es un problema de codificación, sino un problema de semántica. Se ve que, funcionalmente, la tarea a realizar se divide en la ejecución de tres funciones y la posterior evaluación de sus resultados para determinar un curso de acción. ¿Por qué no escribir exactamente eso? El código más claro no siempre es el código más corto.

En clase es muy común que los alumnos pregunten si tal cosa se entiende mejor codificándola de esta manera o de esta otra. Yo siempre contesto: "La mejor manera de codificar es la que más se parezca a cómo pensaste el problema". Si el código sale intrincado, lo que hay que emprolijar es lo que tenemos en la cabeza. Una vez que tengamos el problema resuelto de una forma sencilla y prolija, lo escribiremos sencillamente sin problemas.

3) El punto 2) es el que a mi criterio justifica mis maldiciones. En la primera parte escribí que quería saber "quién fue el retorcido que escribió eso". Sutileza: no escribí "tonto", "estúpido", "inútil" o algo por el estilo, sino "retorcido" porque creo que escribió lo que tenía en la cabeza. Justifico la agresión por las tres horas que perdí tratando de entender algo simple, creo que me gané el derecho a descargar mi frustración. Pero más allá de eso, una vez calmados los ánimos, vale la pena sentarse a charlar un poco con ese programador para llegar a una visión más simple del problema, que no es otra cosa que decir "del negocio".

Estos problemas denotan siempre una pobre comprensión del negocio, que muy pocas veces es culpa del que escribe el código. El que escribe el código es un programador, que será bueno, malo, con mucha o poca experiencia, pero un programador al fin y al cabo. Usualmente no es un experto en facturación, ni en logística, ni en aquello a lo que está enfocado el negocio, y no tiene por qué serlo.

El problema de negocio está en la cabeza de analistas y líderes de proyecto. Tal vez el analista no le explicó bien el problema al programador (tal vez su análisis fue igual de retorcido), tal vez el líder de proyecto lo dejó sólo en la codificación de una funcionalidad demasiado grande, cuando debería haberle asignado un compañero de trabajo (de a dos estas cosas no pasan por el simple hecho de que la solución debe ser lo suficientemente simple como para explicársela al compañero de equipo). En ultimísima instancia el programador podría haberse dado cuenta de que estaba enrevesado y pedir ayuda pero... ¿no es pedir demasiado? ¿Cuántas veces nos damos cuenta solos de lo retorcido de nuestra propia lógica? No creo que muchas.

Para ir cerrando, creo este artículo en dos partes encierra un proceso muy común en cualquier desarrollo: en un primer momento, ante un problema de diseño, codificación o entendimiento, el escarmiento (mis maldiciones) truena sobre el que escribe el código. Pero a medida que pasa el tiempo se calman los ánimos, se avanza un poco en la reflexión y vemos que el problema rara vez está ahí. Cuando se cae un avión primero se le apunta al piloto. ¿Un avión puede caerse por el error de una sola persona? ¿Un bug puede llegar a producción o un sistema puede volverse inentendible sólo por que alguien lo escribió?

2 comentarios:

Sebastián Streiger dijo...

Este problema está analizado por Bertrand Meyer bajo el nombre de "Command-query separation".
http://en.wikipedia.org/wiki/Command-query_separation

Respecto del nombre "cortocircuito" para describir el comportamiento de && y || en los lenguajes C y sus derivados, proviene de la (mala)traducción de "shortcut", en castellano lo mejor sería decirle "atajo", que es otro de los signifacados de la palabra "shortcut" y el que más se aproxima al fenomeno descripto.

Sebastián

Unknown dijo...

Como aporte, pese a que el código sea incorrecto, creo que se puede usar la expresión '&' en vez de '&&' para ejecutar el mismo funcionamiento que el "AND" de VB.


saludos!