December 2011 - Artículos

Hoy se cumplen 10 años del estallido y no puedo dejarlo pasar porque yo estuve ahí, no me lo contó nadie. Vi comer de la basura, vi gente dormir en las veredas, vi saquear los supermercados y vi muchos pero muchos muertos pero lo que me causaba más dolor era ver luchar día a día de mi vieja. Como para no largarse a llorar. Argentinos, no lo olvidemos nunca.

Publicado 20/12/2011 1:34 por Lucas Ontivero | con no comments
Archivado en:

Les dejo la tercera entrega de la serie sobre excepciones.

Saludos

Luego de revisar los requerimientos de la aplicación en la que vengo trabajado estos últimos 6 meses,  tuve que pensar cómo plantear la arquitectura de la misma teniendo en cuenta lo siguiente:

  1. La aplicación es muy muy muy grande, eso era lo primero que saltaba a la vista en los requerimientos. Al día de hoy, esta cuenta con 1.119 vistas, 797 controladores y un número similar de modelos, entre otras cosas.
  2. El equipo iba a ser grande y la idea era que trabajaran lo más focalizados posible en sus casos de uso y luego integrarlos para evitar fricción de dependencias entre los diferentes casos de uso.
  3. La aplicación tiene módulos que agrupan sub-módulos y estos a su vez agrupan casos de uso. Los casos de uso eran bastante similares por lo que era también de esperarse que tuviesen una estructura similar entre ellos. Esto es: Vistas con idénticos o similares nombres como así también controladores y modelos similares los cuales podían también tener el mismo nombre.
  4. El esquema de seguridad se basa en perfiles de usuarios los cuales tienen permisos sobre distintas operaciones sobre cada uno de esos casos de uso.

Con eso en mente, era evidente que el esquema por defecto de MVC (usando Razor) no era el adecuado ya que, por defecto, este agrupa las vistas en la carpeta Views, controladores en la carpeta Controllers y modelos en la carpeta Models. Es muy difícil de trabajar en un sistema grande con esta estructura; es a todas luces insuficiente ya tener 1.119 vistas en una sola carpeta presenta muchos problemas no solo a la hora de encontrar un archivo sino que además obliga a que no existan dos vista con el mismo nombre y eso era algo que el proyecto necesitaba tener realmente.

De no plantear una estructura de módulos, sub-módulos y casos de uso adecuada a las necesidades del proyecto este iba a ser un desastre, así que así lo hice. Para esto extendí dos componentes: ControllerFactory y RazorViewEngine.

Antes de comenzar, y para que vayan dándose una idea, nuestro esquema de mapeo en el Global.asax es el siguiente:

image

Y nuestra estructura de carpetas es la que sigue:

image

El ControllerFactory (simplificado para este blog) se ve más o menos como figura abajo y nos permite tener clases controladores con el mismo nombre pero con distinto namespace:

image

Por otro lado, la organización de las vistas en carpetas módulo/sub-módulo/casodeuso/ se logró extendiendo la clase RazorViewEngine que es la encargada de resolver la ruta donde se deben buscar las vistas cuando desde un controllador se lo solicita. El código de la clase puede verse abajo:
image

Con esto no solo se logra que las vistas se busques en la carpeta propia del caso de uso al cual pertenecen sino que además el orden de búsqueda permite que si una vista, layout o template (EditorTemplate o DisplayTemplate) no se encuentra localmente en la carpeta del caso de uso, este busca luego en las vistas del sub-módulo y luego en las vistas del módulo.

 

Conclusión

Dado que ni el ControllerFactory del framework de MVC permite tener controladores con el mismo nombre ni que el esquema de carpetas que por defecto que utiliza RazorViewEngine es suficiente para proyectos realmente grandes, creo que todo proyecto necesita contemplar la conveniencia de, antes de comenzar a desarrollar sus casos de uso, estudiar esta o alguna otra alternativa.

Yo recomiendo esta solución fuertemente ya que viene dándonos excelentes resultados día tras día y nos ha vuelto la vida increíblemente más sencilla.

Les dejo la segunda entrega de la serie que estoy desarrollando para el equipo de desarrollo en el cual trabajo.

 

Saludos Guiño

Les dejo el primero de una serie de videos sobre excepciones que estoy creando para el equipo de desarrollo al que pertenezco. Espero les guste.

Saludos Guiño

Muchas veces uno cree que el código que .Net Reflector muestra es fiel reflejo de lo que el desarrollador escribió, pero obviamente eso no puede ser cierto ya que esta herramienta toma el IL de un ensamblado y trata de mostrar su equivalente en los lenguajes que se le pida (C#, VB.NET, F# entre otros). Claro que muchas veces hacen tan buen trabajo que uno se olvida de eso.

Como a ILSpy le falta una vueltita de rosca en este aspecto, esto se hace más evidente ya que uno termina viendo código menos pulido como instrucciones GOTOs y otras yerbas en el propio código que uno escribió. Ahora bien, ¿que sucede cuando uno escribe 2 métodos con distintas instrucciones de un lenguaje pero equivalentes en funcionamiento? ¿Pueden estas herramientas hacer un buen trabajo?

Para responder a esta pregunta escribí tres métodos que hacen exactamente lo mismo: imprimen 100 líneas en la consola con los números del 0 al 99:

image
Quería ver si .Net Reflector y ILSpy podían ver la diferencia y la respuesta es: NO, NO PUEDEN. La razón es obvia ya que los tres métodos generan exactamente el mismo código intermedio (IL):

image

Dado que los tres métodos son idénticos bit a bit, ninguna herramienta podría determinar si corresponden a una instrucción for, while o a un enredo de gotos. Tampoco podríamos inferir cual fue el lenguaje con que se escribieron estos métodos (aunque eso poco importa). No obstante, esta es la estructura típica de un bucle for y por lo eso es que tanto .Net Reflector como ILSpy, cuando se les pide que muestren el código C# equivalente, muestran el siguiente fragmento para los tres métodos:

image

Ahora bien, el código IL es idéntico cuando se lo compila en modo Release pero no así cuando se lo compila en modo Debug ya que en este último caso, entre cada instrucción del lenguaje (digamos C#) se coloca una instrucción nop para habilitarnos a poner puntos de interrupción en partes de nuestro código que no se traducen a IL (no son instrucciones ejecutables). Por ejemplo, gracias a estos nops es que podemos poner un breakpoint al inicio de un bloque en modo Debug pero no así en modo Release:

image
En modo Debug esto este breakpoint es válido mientras que en modo Release el depurador pone automáticamente el breakpoint en la primera línea ejecutable despues de la llave {.

image
Ahora bien, dado que cada compilador agrega distinto número de instrucciones nop (esto podría ser falso ya que no probé todos los compiladores, obvio!) a los ensamblados compilador en modo Debug, herramientas como .Net Reflector y ILSpy podrían determinar cual es el lenguaje (en realidad, cual es el compilador) con que se generó ese ensamblado y también saber que un mismo algoritmo originalmente se desarrolló a partir de distintos conjuntos de instrucciones. Veamos el IL de los tres métodos originales en Modo Debug:

image

Como se ve estos tres ahora son diferentes y son aún más diferentes si los compilamos en VB.Net por esto es que digo que sería posible que cuando un ensamblado ha sido compilado en modo debug, estas herramientas cuentan con más información para inferir el código original. Y ya sé que el mundo no es solo C# y VB.Net y que esto quizás no vale la pena, solo digo que si estas herramientas quieren mostrar el código equivalente al IL en algunos de estos lenguajes, y el ensamblado está compilado en modo debug (casi nunca lo está), entonces podrían usar esta info para mostrar el código más parecido al original.

Introducción

La historia del software está plagada de ejemplos de proyectos que fracasaron estrepitosamente, estos por lo general han excedido el presupuesto estimado, el esfuerzo estimado y el  tiempo estimado en muchas veces y si bien parece que la industria viene haciendo avances importantes en estos problemas, aún hoy es común ver u oír de proyectos que fallan de manera espectacular.

Voy a introducir solo algunas de las razones de por qué esto sigue sucediendo, de cómo puede solucionarse y de cómo afecta a los equipos. Pero el foco voy a centrarme en la gente, en cómo los afecta, en sus reacciones y motivaciones (o desmotivaciones) y para ello voy a usar un ejemplo real de un proyecto en el que participé.

Costos

Todos los proyectos que fracasan tienen una característica en común: superan los costos. Sea porque se renegocian o porque pierden dinero o porque no ganan lo esperado, o porque se extienden en el tiempo (también representa un costo) o por el motivo que fuera, los costos están siempre relacionados al fracaso de un proyecto.

Uno de estos costos, y probablemente uno de los que más afectan a la moral de los equipos es el llamado costo de calidad probre, costo de mala calidad o costo de no calidad, esto es, cuanto cuesta (dinero) desarrollar software con defectos (bugs) y luego tener que arreglarlos. Mientras más alto es este costo, más probabilidades existen de que un proyecto fracase.

Una introducción a La Historia

La historia con la que quiero ejemplificar se basa en el desarrollo de una aplicación gubernamental en la que trabajé y a la cual me referiré con el nombre ficticio de “pyjs”. En este proyecto, el costo de calidad pobre era una aberración, cada caso de uso requería un esfuerzo de entre 2,5 y 4 días-hombre de desarrollo y se le hallaban un promedio de 12 bugs. Así, el equipo, formado por alrededor de 13 desarrolladores desarrollaba algo así como 40 casos de uso por sprint de 2 semanas.

Lo interesante de esto es que con esos 40 casos de uso, venían también unos 480 bugs! Digamos que si arreglar cada uno de eso  bugs costara tan solo una hora de desarrollo, esa bolsa de bugs costaría 480 horas-hombre, a digamos algo así como 10 u$/hs serían u$ 4.800 tirados por la ventana cada 2 semanas.

Un poco más sobre este costo

Una de las razones por la que deben atacarse rápidamente las causas de este costo es porque este crece de manera exponencial a medida que la detección de los defectos se aleja de la fase de desarrollo. Así, llevándolo a lo cotidiano, cuesta mucho menos (menos dinero) encontrar un defecto en una revisión de código antes de subirlo al repositorio que encontrarlo durante la etapa de pruebas ya que aquí se debió utilizar tiempo del equipo de pruebas, hay más reportos, más administración, etc. Ni que hablar si en lugar de encontrarlo el equipo, lo encuentra el cliente.

Volvamos a La Historia

En el pyjs la relación testers/desarrolladores era muy baja, no por algún número teórico sino por la simple razón que el equipo de pruebas no daba a basto y por ende se retrasaba cada vez más hasta llegar el punto de que probaban funcionalidad que se había terminado más de un mes atrás. Así las cosas, para cuando reportaban los defectos, el equipo ya estaba desarrollando otras funcionalidades (muchas que dependían del correcto funcionamiento de las anteriores) y una vez acumulados estos bugs se repartían muchas veces a “el equipo de los bugs”, esto es, a miembros del equipo que no habían estado involucrados en el desarrollo de esos casos de uso y que por lo tanto no los conocían, no sabían qué debía hacer esos casos de uso, cómo se relacionaban con otros casos de uso ni nada en lo absoluto. Esto convertía a estos desarrolladores en los “mártires de turno” (digo “de turno” porque alguna vez los cambiaban).

Una cosa curiosa es que la líder de proyecto comentaba que se requería el mismo esfuerzo para desarrollar los casos de uso que para arreglarlos y por esta razón en algunas ocasiones se alternó un sprint para desarrollo y el siguiente para corrección de bugs. Esto quiere decir que a menos que al cliente se le presupuestara 3 o más veces el costo de desarrollo, el proyecto era inviable.

Otra cosa a tener en cuanta es que si los casos de uso no tuvieran defectos, el proyecto podría haberse terminado en quizás en la mitad del tiempo.

Reacciones del management

Ante semejante situación, las reacciones del management fueron las siguientes:

  1. Remover a aquellos desarrolladores que habían participado en el desarrollo de las funcionalidades con mayor número de defectos.
  2. Incrementar el número de desarrolladores e incrementar el “seniority” del equipo incorporando personas con mayor experiencia (incrementando los costos). Esto llevó a la necesidad de “distribuir” al equipo ya que estos nuevos desarrolladores estaban en otras ciudades.
  3. Asignar más presupuesto al proyecto para posibilitar que desarrolladores “externos” al equipo participaran en la creación de casos de uso de manera independiente (desde sus casas y en horarios extralabolares)
  4. Pedir a los desarrolladores que por favor entendieran la situación del proyecto y que no generaran más defectos.
  5. Pedírselo de nuevo
  6. … y de nuevo…. y otra vez más.

Reunión de retrospectiva tras reunión de retrospectiva los líderes de proyecto (digo los porque en realidad eran 2!) repetían que el mismo sermón a los desarrolladores: “Entindan! Les pedimos que por favor entiendan que tienen que generar menos bugs a la vez que necesitamos que incrementen la velocidad!”, “A esta situación llegamos por la falta de compromiso de los desarrolladores”, “Necesitaban más memoria y se las dimos, pidieron cursos y se los estamos gestionando, ahora recapaciten!”

Cómo afectó al equipo

Si esto no bastaba para bajarle la moral a alguien, si alguno con una psiquis de acero todavía permanecía en pie, los reportes de estado del proyecto realizados por teléfono a los superiores argumentando que el proyecto estaba como estaba por una “falta de compromiso”, “por los descuidos”, “por no leer la documentación correctamente” más otras actitudes desmotivantes como los mails pidiendo al equipo que “piensen en las cosas que están haciendo mal”, acabaron con las fuerzas del equipo.

El mismo equipo que reunión de retrospectiva tras reunión de retrospectiva decía que los tiempos estaban muy comprimidos, que los bugs se detectaban muy tarde y que se sentían presionados a entregar aun a sabiendas de que estaban entregando con baja calidad.

Por otro lado, la remoción de prácticamente la mitad del equipo de un plumazo, personas que quizás se desempeñaban algo por debajo del resto pero que hacían valiosos aportes al proyecto (recuerdo que muchos de ellos trabajaban muchas horas de más e incluso cuando se les pidió trabajar un fin de semana lo hicieron de buena gana), no ayudó a mejorar en lo absoluto sino todo lo contrario. De hecho esta fue la primera acción que se tomó.

La moral de un equipo es algo que debe cuidarse celosamente y protegerla porque marca una diferencia enorme en los resultados. Por eso, luego de que la moral bajara al nivel más bajo que recuerdo, el equipo comenzó a realmente desempeñarse por debajo de sus posibilidades, a importarle menos sus resultados, a ser lo que todos los días se les decía que eran: descuidados y poco comprometidos.

Costos de calidad

Existen otros costos de calidad divididos según las tareas específicas que requieren como costo de inspección, costos de prevención y así, pero básicamente lo que se busca es invertir (dinero) en actividades claves que reduzcan el número y severidad de los defectos, idealmente identificándolos en las etapas más tempranas posibles donde el costo de un defecto es menor.

Costos de calidad en esta Historia

En el pyjs la inversión en actividades de “prevención” era prácticamente $ 0,00. La justificación era que no había tiempo, que las entregas al cliente requerían que se utilizara todo el tiempo disponible en el desarrollo de las funcionalidades pactadas. Por esa razón, cuando se pidió al TL que no se siguieran desarrollando las pruebas unitaria y luego cuando propuso volver a realizar las revisiones de código obtuvo un “no hay tiempo para esas cosas”.

Por fortuna, digo que la inversión fue de “prácticamente” $ 0,00 porque se autorizó que el arquitecto desarrollase un generador de código y un set de componentes reutilizables en 4 días, lo que ayudó al proyecto a salir solo un poco de apuros. Esto se autorizó gracias al poder seductor del pensamiento mágico que todo “generador de código” despierta en algunas mentes.

Mi reacción

Como esto estaba muy claro para mi, retrospectiva tras retrospectiva planteaba la necesidad de incorporar aquellas prácticas que todo el mundo sabe que dan resultado: revisiones de código, pruebas unitarias (aunque sea en aquellas funcionalidades más delicadas), reuniones técnicas para aunar criterios o despejar dudas (el equipo se dividía entre 4 ciudades distintas).

Por otro lado, con el fin de obtener feedback más rápido por parte de testing, propuse incrementar el equipo de testing y agilizar las pruebas, filtrar aquellos defectos que no eran realmente defectos o que no se querían o podían arreglar en ese momento (los reportes de defectos en crudo a los desarrolladores es una perdida de tiempo descomunal), no enviar funcionalidades sin probar al cliente (una práctica habitual que encarecía el proyecto y le causaba muchísimo daño).

Por último, no desmotivar al equipo!

Fin de la Historia

Mis recomendaciones cayeron en oídos sordos todas la veces, ni siquiera formaban parte de las minutas de las reuniones de retrospectiva ya que la razón de los problemas no eran ningunas de esas estupideces que yo decía sino que era un tema de falta de compromiso, descuidos y falta de profesionalidad.

A todo esto, la pregunta obvia era: ¿si existe falta de compromiso en algunos miembros entonces por qué no se los remueve?. Y la respuesta fue que no se iba a modificar más el equipo porque la falta de compromiso era general! Yo creo que la razón era otra: si luego de cambiar a la mitad del equipo se cambia a la otra mitad y todo sigue igual…. ¿que se le reporta a los jefes? ¿que los 13 nuevos desarrolladores son tan indolentes como los anteriores?

Conclusión

Yo tenía una hipótesis de que los proyectos sí fallaban la mayoría de las veces por cuestiones técnicas, esto era porque he visto algunos pocos proyectos fallar por problemas técnicos pero ahora está claro que los grandes y monumentales fracasos son en verdad por un management deficiente.

Quiero mostrar cómo es posible modificar nuestros assemblies ya compilador (no no no, antes que los procese el JIT) para agregarles o quitarles código IL. En este caso, voy a ilustrar esto mediante la creación de un sencillísimo Profiler.

Trabajos previos

Este está inspirado en un artículo muy interesante, aunque algo viejo, de Gabriel Schenker sobre como crear un profiler con Mono.Cecil para aplicaciones Silverlight (parte 1 y parte 2 – nos debe la tercera parte). Me gustó bastante pero por desgracia no puso el código (y odio cuando hacen eso!), usa una versión vieja de Mono.Cecil y me pareció buena idea cambiar algunas partes del enfoque original.

Paso a Paso

Objetivo

Lo primero es tomar un ensamblado .net y modificarlo inyectándole algún código. Lo que queremos lograr es, dado un método cualquiera como el que se ve abajo en la Fig.1 insertarle dos llamadas a dos métodos estáticos como se ve en la Fig.2:

image
Fig.1 – Método original

image
Fig.2 – Método instrumentado

Como puede verse, las dos llamadas extras, una al inicio y otra al final del método original, son métodos estáticos que recolectan información del tiempo de ejecución mediante un Stopwatch. Aquí hay una diferencia con la versión de Gabriel Schenker en cuanto a que yo le paso la firma del método mientras que el toma esa información del StackFrame dentro del método WhenEnteringMethod. Esto es más rápido y reduce mucho el código (aunque mi objetivo no fue “mejorar” el código sino simplemente jugar un rato). Otra diferencia es en la forma de medir el tiempo.

Instrumentando

Lo primero que hacemos es tomar la ruta de un ensamblado, leerlo con AssemblyDefinition.ReadAssembly, y luego por cada método encontrado llamamos a InstrumentMethod. Por último guardamos el ensamblado en la misma ruta (lo sobreescribimos).

image

Si el método tiene código, esto es: no es abstracto, no es un método de una interface, no es un método parcial, etc. Le inyectamos nuestras llamadas:

image

Aquí es donde insertamos la primera llamada y le pasamos por parámetro el nombre completo de método.

image

Recolectando información

Entonces ahora todos nuestros métodos, lo primero que hacen es invocar al método WhenEnteringMethod que muestro abajo:

image

Esto básicamente determina cual es el método que se estaba ejecutando y desde el cual se realiza la llamada al método que se está ejecutando en este momento. El método Enter es el que incrementa el número de llamados e inicia el stopwatch. Por último, cuando se sale del método se ejecuta WhenLeavingMethod el cual detiene el stopwatch.

image

Esto genera un árbol de llamadas que podemos ver con nuestro depurador. Por ejemplo, el método EntryPoint (método ficticio que representa al llamador de mas alto nivel) realiza dos llamadas (Calls): la primera al constructor de la clase AClass (AClass.ctor())  una solo vez la cual no tarda nada (0.0 milisegundos) y luego una llamada al método PublicVoidFunction la cual tarda 400 milisegundos. Esta última a su vez invoca 50 veces al método MethodWithAFunctionParameter.

Verlo en acción

image

Para visualizarlo con mayor facilidad podemos bajar esto a un xml y verlo así:

image

Conclusión

Hacer un buen profiler no es algo sencillo en lo absoluto y existen miles de consideraciones a tener en cuenta, no obstante este ejemplo es útil para demostrar lo sencillo que es manipular los ensamblados mediante Mono.Cecil lo que nos abre un universo inmenso de posibilidades para realizar herramientas como profilers, herramientas de coverage, herramientas para implementar AOP, ect.

El código puede descargarse desde aquí