La nefasta obsesión por “entregar valor al cliente”

Y uno al leer el título podría pensar que se esta incurriendo en un desprecio hacia el cliente que nos ha encargado el desarrollo de un software, pero nada más lejos de la realidad, la reflexión viene a algo que aun sigue siendo mal entendido por quienes estamos involucrados en el desarrollo de software.

No cuento nada nuevo que tradicionalmente (modelo “cascada”) el desarrollo de software tiene un historial de fracasos totales o parciales, ya sea en funcionalidad incompleta, sobrecostos de tiempo y dinero, lo cual lleva como consecuencia en que los aun medianamente exitosos tengan la presión por conseguir entregar “lo que se pide” “a tiempo”, y claro muchas veces sin importar el “cómo” que es a lo que nos lleva el título de este post, el cómo la obsesión por entregar algo que el cliente pueda usar hace que se pierda la perspectiva global del problema.

Y el problema (deuda técnica) ya es conocido: métodos de más de 400 líneas de código, código muerto y/o duplicado, lo cual deriva en poca mantenibilidad, bugs como consecuencia de la extensión de la funcionalidad, pobre performance, etc; síntomas que hacen que el que hereda un código desee ir corriendo tras el que hizo el código originalmente, (y un cliente pagando resignado los costos de implementar un parche o una mejora menor).

En este punto se nos podría decir que esto ocurre porque el equipo de desarrollo no aplicó un buen patrón de diseño, que no siguió los principios S.O.L.I.D., lo cual si bien tiene algo de cierto no es sino la segunda capa de un problema mayor que veremos en breve.

Se pensaría que con la irrupción de las metodologías ágiles la situación ha estado cambiando, pero no, la realidad no es tan hermosa, pues si bien ahora tenemos más visibilidad sobre los elementos (historias de usuario) que se están desarrollando no debemos de olvidar que las metodologías imponen un proceso de priorización, en el cual lamentablemente las historias funcionales (si, las que “dan valor”) toman precedencia siempre sobre las técnicas, las cuales tienen que rogar para hacerse un lugar en el sprint o en el flujo del Kanban, y muchas veces nunca logran entrar por la sencilla razón de que “no hay como justificar ante el cliente algo que no se ve que le reporte valor”. Así pues, se cumple con la entrega de funcionalidad continuamente, pero seguimos acumulando deuda técnica pues, a pesar de seguir perfectamente el proceso, sólo nos estamos enfocando en entregar un software que funcione sin fijarnos en qué es lo que hay detrás de este software que se entregó perfectamente en plazos pues lo importante es que funcione, ¿no? ¿qué hay deuda técnica? tranquilos, eso lo solucionamos con una refactorización circunstancial (léase: arreglamos cuando podamos y no nos quite mucho tiempo).

Recomiendo leer este interesante articulo de Lucas Ontivero donde nos cuenta como las cosas han seguido yendo mal en el lado técnico a pesar del agilísimo.

Adicionalmente tenemos el factor del QA (Quality Assurance), uno podría creer que en empresas grandes este equipo tendría una visión integral de la calidad del producto, lo cual debería incluir tanto la verificación de la mantenibilidad y calidad del código como de la validación de la arquitectura, pero no, al parecer su visión va sólo al lado funcional, o sea a lo “visible” del gráfico anterior, siendo que temas como la mantenibilidad “lo ve el equipo de desarrollo”, así pues por un lado se exige un cumplimiento funcional (muchas veces sin entender las razones por las que “la gente de desarrollo” opto por implementar de una manera diferente a como QA lo pensaba) estricto pero por otro lado se omite la validación de la calidad de lo técnico, pues claro… no es lo que ve el cliente de manera directa, por más que este sea el que luego tenga que pagar los “intereses” de la deuda técnica(*). No se ustedes pero si un equipo que se supone se encarga de la “calidad” durante el ciclo de desarrollo ignora la parte técnica de la implementación (¿alguien dijo auditorías?) , le esta haciendo un flaco favor al proyecto y al cliente.

Así pues mucho del problema viene dado por la importancia que se le quiere dar al elemento técnico del proyecto, el cual viene tradicionalmente es relegado por debajo del administrativo, siendo que muchos profesionales hacen gala de haber tocado muy poco los elementos correspondientes a la programación en su formación profesional, lo cual en mi opinión generalmente (he conocido excepciones) ocasiona que no se sepa (debido a que a veces no se entiende) el impacto de una decisión técnica que se tome en cualquier etapa del desarrollo, razón por la cual prime el componente funcional/visible a la hora de definir prioridades en el desarrollo.

Afortunadamente hay caminos de solución, uno de ellos es tratar de entender qué es lo que ha derivado en la creación del Manifesto for Software Craftsmanship como una evolución del Manifiesto Agile (esto lo explica muy bien Edson en esta presentación); y por otro lado, dentro del marco de las metodologías ágiles, reconocer de manera visible el rol que deben de jugar los componentes técnicos, en ese sentido me sorprendió muy gratamente como en su reciente conferencia "Lean from the Trenches" Henrik Kniberg indicó claramente cómo dentro de su proceso ágil (basado en Kanban) había reservado, dentro de su tablero, el espacio necesario para las historias técnicas, de esta manera

Es que como Henrik dijo, en un mundo ideal sólo habría historias de negocio, pero no estamos en un mundo ideal, los problemas técnicos existen y no podemos pretender seguir ignorándolos, o creer ingenuamente que efectivamente lo “solucionaremos después”, o ya en el lado extremo seguir en la ilusión de que cualquier programador podrá implementar algo si le damos una especificación funcional completa y detallada.

Entonces, regresando al título de este post, creo que debe de quedar claro que no es que no tengamos como meta el entregar funcionalidad y software, sino también validar el cómo llegamos a ese entregable, procurando (entre otras cosas):

  • Dar el espacio a las historias técnicas cuando corresponda, y no relegándolas a cuando “haya tiempo”.
  • Sospechar del programador que defiende una mala implementación con un “pero funciona”.
  • Ser consciente de los riesgos que significa ir por el camino rápido en aras de entregar la funcionalidad pedida.
  • Que todo el equipo (y no sólo el que implementa) sea consciente de lo que hay en la implementación (arquitectura, algoritmos, etc) y de como eso condiciona los tiempos de desarrollo, especialmente cuando se trata de agregar una nueva funcionalidad.
  • Valorar el factor técnico, procurar la mejora de las habilidades técnicas de los desarrolladores: mentorship, revisión por pares, dojos, etc.

Claro que mucho de esto tendría que ir de la mano con un reconocimiento dentro de las organizaciones al rol que juegan las TI en su crecimiento como negocio, pero por ese lado la cosa esta bien complicada.

(*) Se entiende como “intereses” de la deuda técnica al hecho de que conforme pasa el tiempo será más difícil corregir las malas decisiones o malas implementaciones de código, ya sea porque el desarrollador que implementó algo de manera críptica ya no se encuentra en el equipo, porque nueva funcionalidad superpuesta generó más dependencias o simplemente porque por el tiempo que pasó ya es más difícil acordarse el porque se tomó esa decisión que “parecía buena en su momento”.

En defensa de Visual Basic.Net y las “buenas practicas”

Primero el Disclaimer: La herramienta a la que le tengo mas cariño y con la que he sido mas productivo es Borland Delphi (ahora Embarcadero, pero la que yo conocí era de Borland), dicho esto pasemos al grano.

El titulo del post proviene a razón de la ultima sesión de Mad.Nug en la que Jorge Serrano compartió su experiencia sobre las posibles acciones a tomar cuando se tienen aplicaciones en Visual Basic 6 y existe la necesidad/presion/requerimiento de migrarlas a .Net, la sesion fue muy explicativa, recordandonos cuales son los puntos donde hay mas incompatibilidades y las posibles opciones:

– Herramienta de migración automática
– Entorno mixto (Microsoft Interop Forms Toolkit 2.1 y wrappers)
– Empezar todo desde cero.

De acuerdo a su experiencia (corrígeme si te estoy referenciando mal, Jorge) debería procurarse por la ultima opción (en ese sentido Jorge facilito un buen enfoque para acometer esos proyectos), y estar alertas ante el riesgo de que una solución puntual como un entorno mixto crezca y pierdas el control.

Si bien comparto la idea de que hay casos en que no queda otra que proceder de esa manera, creo que esa decisión tiene que ser sopesada teniendo en cuenta criterios como: cantidad de lineas de código existente, dependencia de terceros, disponibilidad de conocimiento funcional, etc… en ese sentido se sintió la falta de cubrir posibles estrategias de cuando se ha decidido ir por el camino de la migración (que igual se hace para algunas aplicaciones, dejando el resto a un rehacer desde cero).

Bueno, a lo que iba, como parte del camino de un rehacer desde 0, Jorge planteo que había que procurar migrar hacia C# en lugar de hacia VB.Net, ¿por qué? es lo que nunca me queda claro, salvo por un hecho (mencionado en la reunión) que es la mayor disponibilidad de recursos en Internet para C# que para VB.Net, pero fuera de ello el acceso hacia el Framework de .Net es básicamente el mismo (salvo excepción que contare luego), por lo que también en la conversación con los asistentes se dejo caer que con C# se es mas ordenado y se encamina hacia las buenas practicas, por lo que ya entramos a un tema de preferencias y es a donde quería llegar.

Admitamoslo, por sus propias características en Visual Basic 6 era muy muy fácil desarrollar con malas practicas y que el código spaghetti generado sea muy difícil de mantener, pero si una cosa tiene Visual Basic .Net es que significo una ruptura sin pasos intermedios con respecto a VB6 a fin de volverlo OOP y compatible con el nuevo Framework (*), y por lo tanto mucha de la laxitud inherente al viejo VB6 ya no existe, es un buen lenguaje OOP y muy valido para aprovechar los conocimientos de sintaxis propios de Basic: Dim, as, Next, End…, el problema gordo es la herencia de mala imagen que lastra el lenguaje, asi que explicar a un manager que seria menos traumatico continuar con VB.Net en vez de C#…. complicado, triste realidad.

Siendo así creo que solo hay dos razones no personales para optar por desarrollar un proyecto en C#:
– Es muy critico contar con todos los recursos de documentación disponibles para un tema en concreto, que lamentablemente en su mayoría están en C# (p.ej. hay casi tres veces mas info para Generics en C# que para Visual Basic).
– Te ha tocado acceder a una parte del Framework que expone justamente un tipo de datos existente en C# pero no en Visual Basic.Net, eso me paso con la versión 1.1, no se si lo habrán solucionado, pero … queda como alerta.

En contraparte tengo algunas razones de productividad para seguir usando VB.Net siempre que puedo:
– Detección automática de algunos errores y de su corrección, esa potencia de mostrarte un subrayado en algunos errores de sintaxis es de veras de agradecerse, y lo mismo con el hecho de que si compilas y saltan errores no tienes que recompilar para ver si has corregido bien o no, conforme vas corrigiendo tu pila de errores se reduce.
– No sensible a las mayúsculas/minúsculas, esta característica va de la mano con lo anterior, si defines bien una variable (usando Camel y Pascal case adecuadamente) y luego la invocas, el hecho de que se te auto corrijan las mayúsculas y minúsculas es una señal inmediata de que no te has equivocado al invocar el nombre de dicha variable.
– Las instrucciones (begin..end..next..) son mas claras que las llaves ({}) para poder hacerse una idea del ámbito de un bloque de código.

Dicho esto, como desarrollador he venido a conocer ambos lenguajes, pero lo fundamental es el Framework, siendo que el saber C# me ha facilitado acceder a ciertos recursos no existentes para VB.Net, pero que luego a la hora de implementar no he vacilado en aplicarlos sobre proyectos en VB.Net, así que es bueno saber ambos, pero uno no debería de inhibirse y proponer el uso de VB.Net en los proyectos.

Pero algo se me quedo dando vueltas ya desde la sesión a propósito de eso de que C# te ayuda a ser mas ordenado (o algo así), y a esto se sumo el comentario de uno de los asistentes comento que ellos tenían una aplicación grande en VB6, basada en COM+/MTS, hecha según las recomendaciones que daba Microsoft por entonces para el desarrollo en COM, lo cual me hizo retroceder a la época en que este diagrama salia en toda presentación de arquitectura y diseño de aplicaciones alla por el final de los 90s:

Pongámonos en contexto, para el lanzamiento de Visual Basic 6, ya había un gran parque de aplicaciones en VB4 y VB5, pero la fama del “DLL Hell” ya era muy fuerte, así que Microsoft emprendió una campaña muy intensiva para explicar el concepto de desarrollo de aplicaciones distribuidas, las ventajas del multicapas en la mantenibilidad de las aplicaciones, etc etc… conceptos que ahora son el pan de cada día en los objetivos de los diversos proyectos actuales, fueron recien conocidos para muchos gracias a Microsoft, y la verdad es que en ese entonces Microsoft se preocupo mucho en hacer pedagogía de las buenas practicas, tan así que en los cursos de certificación se hacia énfasis en lo que hay detras de la instanciación de objetos en COM para de esa manera impulsar formas mas ordenadas de programar, y lo bueno de todo es que esa pedagogía era de manera no agresiva, planteándola como una evolución de las aplicaciones que habia en desarrollo entonces, a fin de que los programadores mejoraran sus practicas frente a la fama de caos, o sea se pugnaba (por entonces) un cambio evolutivo en las formas de hacer las cosas apoyándose en lo que los desarrolladores ya conocían, lo cual condujo a que los partners serios si que hicieran aplicaciones buenas y solidas usando VB6.

Para bien o para mal todo cambia con la introducción de .Net, que como ya hemos indicado en este y otros posts vino a ser un ruptura muy fuerte, en la que sobrevivía el que tenia mejor las bases de OOP y/o Windows DNA. Claro, ya lo peor paso (salvo las app VB6 que se resisten a morir) y la cosa esta mas o menos estabilizada, salvo por la nueva tendencia del famoso patron SOLID, sobre el cual ya comente que tenia mis dudas sobre si facilitaban la mantenibilidad de las aplicaciones, pero que en todo caso hay que entenderlas y saber como y cuando aplicarlas, siempre sin dogmatismos.

Y es justamente que ahora los dogmatismos se hacen presentes, pues en lugar de introducirnos progresivamente a las nuevas “buenas practicas” se nos pide que ya no usemos ni Windows Forms (aunque si, podríamos pasar a WPF) ni Windows Forms, diciendonos que “no es testeable” “no permite xxx principio” de buenas a primeras sin mostrar evolutivamente como mejorar el código que estamos desarrollando, de ahi a lo que dije en un twitt reciente: Si nos explicasen las pruebas unitarias con ejemplos basados en multicontroles, eventos y mantenimientos, igual las adoptariamos #digonomas (o este otro), digo eso porque la mayoría de las aplicaciones corporativas de uso interno (no portales de contenido) terminan siendo eso y no los clásicos ejemplos de calculadoras o números perfectos, así que seria bueno que los planteamientos de difusión sobre TDD o SOLID partan de un enfoque evolutivo y no rupturista (dando cobertura tanto a VB.Net como C#) a fin de lograr una mejor adopción.

A ver si no llega alguien por aquí diciendo que he “perdido el punto”… xD

(*) Y si, sigo creyendo que muchos traumas se hubieran evitado si al hacer .Net (y C#) se hubiera mirado lo que tenia VB6 a fin de hacer la ruptura menos dolorosa.

¿Los nuevos patrones ayudan a la mantenibilidad de aplicaciones?

Si tuviera que decir que funcionalidad del Visual Studio (y de VB6 ya puestos) he estado usando mas ultimante, la respuesta seria “Go to Definition” (seguida de “Find All References”).

La razón de esto es debido a que ahora me corresponde estar en mantenimiento de aplicaciones, lo cual implica que cuando hay un error tratar de encontrar que metodo o clase fallo y ver que hacer para solucionar el error, y en otras ocasiones añadirle una funcionalidad a cualquiera de las aplicaciones ya existentes (en cuya creación yo no intervine).

Como ya explique anteriormente esta es una situación en la que no se empieza de cero, y se requiere tratar de entender de manera rápida el como esta montado todo a fin de encontrar la raiz del problema, por lo cual usualmente tengo dos caminos:

– Ubicar la aplicación e ir haciendo una “depuración sin ejecutar” mediante el “Go to definition” tratando de ubicar los puntos de paso críticos que hay durante la cascada de llamadas de los sucesivos métodos involucrados en la funcionalidad.
– Buscar el texto con el mensaje de error reportado, y de esa manera mediante el “Find all references” identificar de abajo hacia arriba la posible pila de llamadas y en el camino encontrar lo que ocasiono el problema.

Pero llegados a este punto vemos que se ha incrementado la demanda por desacoplamiento y pruebas unitariasdentro de nuestros codigos, todo como parte de los patrones SOLID:

* SRP: The Single Responsibility Principle (Principio de Responsabilidad Única)
* OCP: The Open/Closed Principle (Principio Abierto / Cerrado)
* LSP: The Liskov Substitution Principle (Principio de Sustitución de Liskov)
* ISP: Interface Segregation Principle (Principio de Segregación de Interfaces)
* DIP: The Dependency Inversion Principle (Principio de Inversión de Dependencias)

Principios muy interesantes, que valen la pena revisar, pero que introducen tendencias como la Inyección de Dependencias y la Inversión de Control, para lo cual recomiendo este muy bien explicado articulo de Gisela Torres de donde extraigo esta explicacion;

Generalmente, cuando tenemos una clase que depende de otras para ciertas acciones, necesitamos inicializar instancias de las mismas para poder utilizarlas posteriormente. En ocasiones, una de las alternativas puede ser crear un objeto de dichas clases de forma privada e inicializarlas, utilizando el constructor de la clase principal.

Si vemos estas acciones desde el punto de vista de la Inyección de Dependencias y la Inversión de Control, no sería la forma más óptima debido a que la clase que sufre estas dependencias no debería ser la responsable de la creación de las mismas.

¿QUÉ CONSEGUIMOS?

  • Desacoplamiento.
  • Mejora la posibilidad de testeo de la aplicación.
  • Mejor mantenimiento a la hora de realizar cambios de los componentes, gracias a la modularidad.
  • […]

Ambos conceptos están tan ligados que, en ocasiones, no se hace distinción. Se utiliza el concepto Inversión de Control para delegar en otro componente, un framework por ejemplo, la responsabilidad de crear las instancias necesarias en lugar de crearlas nosotros mismos. Por otro lado, la Inyección de Dependencias es el término utilizado cuando una clase depende de otra y, a través del constructor generalmente acepta un parámetro del tipo del cual depende. 

Siguiendo el ejemplo, podemos ver que en lugar de el tradicional mecanismo de instanciación de la clase que necesitamos, por detras existe un interesante mecanismo de Configuración y Factoria de Objetos que nos permiten que en lugar de:
public class HomeController : Controller
{
private readonly ITwitterService _twitterService;
public HomeController()
{
_twitterService = new TwitterService();
}

public ActionResult Index()
{
return View(_twitterService.FetchTweets());
}
}

Tengamos:
public class HomeController : Controller
{
private readonly ITwitterService _twitterService;

public HomeController(ITwitterService twitterService)
{
_twitterService = twitterService;
}

public ActionResult Index()
{
return View(_twitterService.FetchTweets());
}
}

Nada mal según parece, usamos interfaces, y se logra una mayor facilidad para pruebas unitarias, lo cual es uno de los objetivos a lograr, pero oh sorpresa… si ahora le hacemos “Go To Definition” al FetchTweets notaremos que ya no vamos a la implementación del método en cuestión (definido en la clase TwitterService), sino al cuerpo de la Interfaz ITwitterService, lo cual detendría nuestra búsqueda en cascada, al no tener mucha idea de cual es la implementación real que se esta invocando, por lo cual nos quedan dos potenciales soluciones:

– Correr la aplicación y en tiempo de ejecución ver la implementación real para así tomar las medidas de análisis necesarias a fin de resolver el problema.
– Tratar de entender la configuración de las dependencias y así inferir cual es la clase que se esperaba instanciar.

Como se comprenderá esta circunstancia puede hacer complicado una tarea de mantenimiento, y la idea de que esas tendencias pueden significar un problema, no en el tiempo de desarrollo, sino en posteriores mantenimientos me han dado vueltas en la cabeza durante un buen tiempo, hasta que encontré este interesante articulo, del cual cito y traduzco:

Yo lo he experimentado de primera mano, en muchos diferentes proyectos donde he tenido que hacerme cargo de codigo despues de que un desarrollador dejo la compañia, y puede ser una experiencia exasperante. Una mejora es solicitada, o un bug es reportado, digamos que se trata de un bug. Se me puede haber dado una excepcion en la pila, y eso al menos me lleva al codigo fuente en donde empezar. Lo que usualmente encuentro es un bonito set de pruebas unitarias que puedo correr y comprobar que todas se ejecutan correctamente, lo cual no es una gran sorpresa; pero, todos los tests usan “mocks” por lo que no puedo reproducir inmediatamente el bug como no sea escribiendo un lindo mock test para simular como el código debería responder ante un particular error de entrada, puesto que yo puedo “mockear” facilmente el error. Eso esta bien, pero no es util si mi objetivo principal es evitar que el error ocurra.

¿Que clase en concreto es la responsable del bug? Aqui es donde se pone complicado, en la mayoria de los casos, la pila no te da ninguna pista. Leer el codigo y examinar la configuración puede ser una enorme perdida de tiempo y termina siendo una tediosa búsqueda en amplitud.

Como el autor apunta “la capacidad para obtener información rápidamente y entender un programa al leer el código es degradada por IoC debido al desacoplamiento”, y así.. esa seria la alerta con la que nos deberíamos quedar, ver cual es el lugar de estos patrones, y si la búsqueda de objetivos en este momento como el desacoplamiento y las pruebas unitarias, no vendrían a significar un lastre en el largo plazo, cuando la aplicación tenga que ser mantenida por un equipo diferente.

El paso por mantenimiento de aplicaciones te hace tener en cuenta estas otras consideraciones.

Otro articulo interesante:
Dependency Injection makes code unintelligible