La falsa seguridad del tipado estático y los IDEs.

Photo by Markus Spiske on Unsplash


Este post trata sobre algunas observaciones que he hecho sobre mi trabajo como desarrollador de software, específicamente como codificador y que de alguna forma he visto que estas observaciones se aplican al trabajo de otros desarrolladores también.

Antes de dar comienzo a la idea principal, sería bueno formalizar los conceptos siguientes:

  • Lenguaje de programación (LP).
  • IDE (Integrate Development Enviromnent o Entorno de Desarrollo Integrado, ej: Visual Studio, PyCharm, Eclipse)

Lo que hay que dejar claro es que existen muchos lenguajes de programación y cada uno de ellos tienen características las cuales nos ayudan a clasificarlos y utilizarlo en dependencia del problema que tengamos que resolver. De forma general podemos decir que las principales características son:

  • Paradigma(s): las abstracciones que te brinda para modelar un problema el lenguaje, ejemplos: Progamación Orientada a Objectos (POO), Programación Funcional (PF), etc.
  • Sintaxis: Es el código que escribimos, dependiendo de las reglas de cada lenguaje (no escribes igual un ciclo for en C# que en Python).
  • Semántica: Son las reglas bajo las cuales el lenguaje establece los límites para la interpretación de la sintaxis.
  • Sistema de Tipado: Esta característica se refiere a la rigurosidad del LP en el momento de plasmar explícitamente los tipos de datos que intervienen en una operación. Dentro de este tópico hay varias divisiones:
  • Tipado vs No Tipado: (todos los lenguajes que usamos hoy día son tipados, porque hacen algún control sobre el tipo de los datos en las operaciones para garantizar cierta estabilidad del software).
  • Tipado Estático vs Tipado Dinámico: 
    • Tipado Estático: Todas las expresiones tienen sus tipos determinados antes de la ejecución del programa (ej LP: C#, Java, C, Golang), esto permite que los errores de tipado sean capturados durante el tiempo de compilación (esto brinda como facilidad que el lenguaje advierte al programador de los errores de tipado sin necesidad de caer en una excepción o un estado de pánico durante la ejecución). Se puede argumentar como desventaja de este sistema de tipado el aumento del tamaño del código declarando el tipo de cada dato a priori.
    • Tipado Dinámico:  El tipo de cada dato es determinado en tiempo de ejecución (ej LP: Perl, Python, Javascript, Ruby). Esto quiere decir, que el tipo de una expresión no se determina de la syntaxis como en el tipado estático, sino que se debe ejecutar el programa para que una vez que se crea en memoria los datos se pueda hacer el chequeo de tipos. Los pros y contras se pueden inferir de lo mencionado en el tipado estático.
  • Tipado Débil vs Tipado Fuerte: 
    • Tipado Débil: Permite un valor de un tipo se tratado como otro. Ejemplo de cierto tipado débil lo tenemos en Javascript cuando hacemos 2*X, implicítamente X se convierte en entero, incluso si es una cadena de caracteres. Aunque esto tiene sus ventajas, porque permite evitar una función para convertir de cadena a entero y así evitar que nuestro código aumente, por lo general los errores que se obtienen como consecuencia de está conversión implícita son peores, sobre todo porque se detectan solamente en tiempo de ejecución.
    • Tipado Fuerte (o Tipado Seguro): No te permite realizar una operación en un tipo incorrecto de dato, en el ejemplo anterior 2*X, la variable X solo puede ser entera o decimal, dependiendo del lenguaje.

Una vez formalizados los conceptos anteriores podemos regresar al tema principal de este artículo, que sería: ¿Cómo un LP con un sistema de tipado fuerte y estático con la ayuda de un buen IDE puede convertir mi código en un monstruo inmantenible y que puede ser mi peor pesadilla?

Para desarrollar un poco la respuesta a la pregunta anterior tengo que hacer la historia que me llevó a semejante conclusión, prometo que no será muy larga. Comencé desarrollando para una empresa en C# utilizando Visual Studio como IDE, en el equipo que estaba era muy chico, diría 5 personas, constantemente llegaban diferentes tipos de proyectos, algunos tenían que ver con hacer un sitio web, otros con hacer RPC (Remote Procedure Calls), etc. Lo que estaba claro es que iba a trabajar con diferentes tecnologías y enfrentaría diferentes retos, lo cual es muy bueno. Inicialmente me propuse a aprender C# y el Framework .Net a fondo, para eso me leí "C# in Depth" por Jon Skeet (lectura obligatoria para un desarrollador C#), "Concurrency in C# CookBook" por Stephen Cleary, "Pro C# 7" de Philip Japikse y otros como "Pro ASP.NET MVC 5" de Adam Freeman. Poco a poco tenía mayor entendimiento del lenguaje y los frameworks principales, los proyectos llegaban y cada vez era más fácil convertir los requerimientos en código, me gustaba mirar a mi código y ver que estaba utilizando todos los recursos del lenguaje, mientras más técnico se veía mi código mejor. Trataba por todos los medios de lograr generalidad a través de la genericidad y utilizar la reflexión para crear lo que yo eventualmente llamaría microframeworks para resolver algunas tareas, como llamadas a la Base de Datos SQL utilizando consultas y procedimientos almacenados. 

Sentía que todo iba viento en popa, el nivel técnico que iba ganando era indiscutible y todo parecía ir a mejor hasta que un día tuve una reunión donde me informaban que había que agregar algunas funcionalidades a un proyecto que se había codificado varios años atrás por desarrolladores que ya no estaban en el equipo. El jefe de equipo me describió brevemente la idea de lo que estaba implementado, y me dijo que no demoraría mucho agregar las funcionalidades, a lo sumo 2 días. En efecto, las funcionalidades eran sencillas y el sentido común dictaba que no debería ser difícil, y bueno, al final el Jefe de Equipo tenía razón, me llevó dos días agregar las funcionalidades, lo que olvidó decirme que los días incluían las noches también. El reto estaba en que por primera vez me encontraba ante una solución gigantesca que nunca antes había visto, y no tenía idea donde empezar a revisar para agregar las funcionalidades.

El primer problema que encontré cuando descargué la solución fue que mirando su estructura no sugería mucho, todos los módulos o proyectos estaban en la carpeta principal, no seguían un estándar a la hora de nombrarlo, existían proyectos con nombres como "backend" o "bussinesslogic" los cuales eran buenos candidatos para darles una mirada, recuerdo haber abierto el proyecto "bussinesslogic" (BL) y encontrarme con que solo había una clase llamada Manager, que era singleton y tenía cientos de líneas. No existían modelos en ese proyecto, de hecho, como estaba estructurada la solución en si, la lógica de negocios no manejaba modelos propios. La realidad fue que estuve bastante tiempo navegando entre el código de la solución, utilizando las facilidades del IDE para encontrar declaraciones de clases e interfaces. Era complicado saber los límites dentro de los cuales un modelo se encontraba, lo más que se podía hacer era ver las referencias en el código a ese tipo (facilidad que también da el IDE y que es gracias al tipado estático de C#). Una observación curiosa que encontré fue que las nuevas funcionalidades a implementar utilizaban los modelos existentes, como consecuencia debía añadir campos nuevos que solo se utilizaban en lugares muy específicos, pero debido al libre tránsito de los modelos por la solución de un extremo a otro, rápidamente entendí que estos campos nuevos estarían en todos lados, eran pasajeros en un tren que se tomaba en la capa de presentación y llegarían hasta la capa de persistencia, estaban obligados a realizar el viaje completo, independientemente de si era necesario o no. Muchas veces me encontraba comentarios en las propiedades o campos de los modelos diciendo que ya no eran necesarios, sin embargo, no los habían borrado, veía las referencias al campo o propiedad (estado en adelante) brindadas por el IDE para ver si era cierto que no se utilizaba, "0 referencia, lo borro" pensé, nada que ver, estaban referenciados, entonces me di a la tarea de ver en que se utilizaban algunos de estos estados candidatos a ser borrados, la gran mayoría en algún momento del tiempo de vida del modelo se asignaba o cambiaba algún valor, pero no se hacía nada con estos valores a nivel de lógica de negocio. En algún momento del tiempo de vida del software fueron parte de los requeriemientos y se utilizaron, pero después se dejaron de utilizar, entonces: ¿por qué no se borraron?, me animé a preguntar esto en el equipo de desarrolladores, las respuestas variaban, pero todas convergían al miedo de romper algo.

El miedo a romper algo (aunque pareciera que no era así según mi observación anterior) se refería a que era demasiado laborioso seguir el estado de un campo durante el tiempo de vida del modelo en toda la solución para asegurar que no se utilizaba en la lógica de negocio, otro sub argumento dentro de este era relacionado con el jefe de equipo, él tendía a expresar los requerimientos a niveles de código, no dejando mucho a la imaginación del programador, y si durante esa explicación no se hablaba de eliminar un campo de los modelos, era mejor no hacerlo, porque quizás él tenía algo en mente para eso después, o él sabía que se estaba utilizando de alguna manera que todos desconocían. 

Otro elemento negativo para estos estados no utilizados por la lógica de negocio era la reutilización para diferentes ideas semánticas permaneciendo con el mismo nombre. Por poner un ejemplo, existía una propiedad llamada Nombre que inicialmente almacenaba el nombre de un cliente, con el tiempo esa propiedad quedó obsoleta pero no se borró, en un momento determinado, comenzó a mandarse en este campo el nombre de la compañía a la que pertenecía el cliente. Debido a como se trabajaba en el equipo no se documentaba nada prácticamente, y como la estructura de la solución y los nombres no sugerían mucho, si se quería saber que valor aportaba para el negocio alguna clase, tendrías que seguir el código de principio a fin para saber en qué caso(s) de uso era utilizada, o si tenías suerte podías preguntarle al creador del código (si se acordaba).

Otro elemento que ralentizaba mi comprensión de la solución era la falta de información explícita en las clases. Un ejemplo de esto ocurría cuando abría el código de una clase y el constructor no recibía ningún parámetro, sin embargo, cuando empezaba a revisar los métodos veía que dependía de otras clases, pero como estas clases seguían el patrón Singleton a la hora de ser instanciadas era difícil hacer una imagen visual rápida de las dependencias a ese nivel, tenías que revisar todos los métodos (era complicado hasta para hacer un diagrama).

Más allá de todas estas observaciones, se lograba hacer el trabajo en tiempo, a costa de tu tiempo personal y tu salud mental. Lo relatado anteriormente me pasó algunas veces más, y de cierta forma había asumido que era la vida del desarrollador de software, todos estaban estresados a mi alrededor ¿por qué yo no? En ocasiones los desarrolladores del equipo bromeaban diciendo que te pagaban 60% por tu nivel de comprometimiento (un adjetivo hermoso que todos sabíamos que quería decir estrés realmente) y un 40% por programar, "porque programar y hacer un producto medianamente viable puede cualquiera, sino eres tú, contratan a otro(s) que cobre(n) menos y ya está". Pero bueno, volviendo al tema que nos ocupa, después de tener que mantener varias veces código que no había creado yo, un día me informan que tengo como tarea dar mantenimiento a un código mío que hacía varios meses estaba en producción y del cual no recordaba nada. "Es pan comido", pensé, después de todo lo creé yo, por increíble que parezca, cuando abrí la solución creada por mi, me encontré con muchos problemas similares de los que comentaba anteriormente, era mi solución y parecía que la había escrito otra persona, en ese momento solo te queda aceptar tu responsabilidad en el desorden que has creado. 

Como había dicho, a estas alturas ya había asumido que el estrés y por tanto la regla del 60/40 era inevitable, más que nada porque todos a mi alrededor lo habían asumido conciente o inconcientemente y me repetían que no había solución a ese problema. Algunos bromeaban diciendo que solo utilizando drogas se podría entender facilmente el código de otra persona o el negocio que se modelaba en una solución. Lo que cambió un poco mi percepción y ayudó a formalizar ciertos problemas en mi proceso de codificación fue cuando cierto día que tenía que hacer un cambio mínimo en un método de una clase, mi laptop estaba con la RAM casi al 100% porque tenía abierto otros procesos de monitoreos que no podía cerrar en ese momento, no podía iniciar un IDE porque consumiría demasiados recursos, así que fui directamente por el explorador de ficheros y abrí el archivo de la clase en VIM (un editor de textos, que permite instalar plugins), las palabras claves y sintaxis estaban resaltadas como si fuera cualquier IDE, así que todo bien, me dirigí al método hacer el cambio mínimo. El primer problema fue que no tenía auto completamiento (aunque puede ser instalado), que normalmente uso para orientarme al utilizar métodos o variables. Sabía que variables tenía que utilizar, o que proveedores intervenían en la funcionalidad, pero los nombres eran tan poco sugerentes que solo utilizando auto completamiento podía relacionarlo a lo que quería, o recorrer nuevamente el código en búsqueda de esas variables. Otra cosa que me chocó fue lo complicado para saber que método debía llamar, porque muchos se llamaban iguales con argumentos diferentes, o algunos se llamaban diferentes con argumentos iguales, y los nombres te daban una idea muy vaga de lo que hacían. Normalmente el IDE te permite navegar a estos métodos y darle una ojeada rápida y utilizar el que necesites (aunque ya has incurrido en una pérdida de tiempo), de esta manera entendía, no solo la fricción que generaba mantener mi código, sino que si no fuera por el IDE y el tipado estático del lenguaje que brindan las ventajas anteriores fuera imposible para mi crear un producto viable en un tiempo requerido y garantizar su mantenibilidad.

Lo que me ayudó a cambiar mi mentalidad acerca de la regla 60/40 fue observar a desarrolladores que al parecer no eran muy "técnicos" a nivel de tecnología o LP pero tenían un acercamiento más olístico hacia la codificación. Y no solo cambiaron la regla 60/40 a 5/95, sino que dejaban una evidente separación entre el término "programador (o codificador)" y "desarrollador de software". Lecturas recomendadas en este aspecto: "Clean Code" y "Clean Architecture" por Robert Cecil Martin, "Get your hands dirty on Clean Architecture" por Tom Hombergs, "Practical Test-Driven Development using C# 7" por John Callaway, Clayton Hunt. Otro libro que me ayudó a desmistificar el proceso de desarrollo de software en grandes compañías como Google y de cierta forma salir del sesgo profesional en el que me encontraba fue el siguiente: "Software Engineering at Google" por Titus Winters, Tom Manshreck, Hyrum Wright. 

Y de esto se trata este post, de reflexionar acerca de como podemos facilitar nuestro trabajo como desarrolladores de software y como disminuir la fricción que generamos cuando codificados sin ser cuidadosos en nombres de variables, métodos, estructura de una solución, nombre de paquetes, módulos, etc., buscando no tener que vernos obligados a trabajar horas extras porque nuestro código no es limpio o expresivo. Espero que este post ayude a uno que otro junior como yo y les deseo, Happy Coding!!!.


Comments