
Compartir:
Max es un defensor de los desarrolladores de Python e ingeniero de software interesado en las API de comunicaciones, el aprendizaje automático, la experiencia de los desarrolladores y el baile. Su formación es en Física, pero ahora trabaja en proyectos de código abierto y hace cosas para mejorar la vida de los desarrolladores.
Mejore su proyecto de software - Segunda parte: Realización de cambios
Tiempo de lectura: 15 minutos
¿Alguna vez te has hecho cargo de una base de código y te has dado cuenta de que no estás contento con cómo está escrito u organizado? Es una historia común, pero que puede causar muchos dolores de cabeza. La deuda técnica puede convertirse en una bola de nieve que dificulte exponencialmente la comprensión del código y la incorporación de nuevas funciones.
En esta serie de tres partes, voy a recorrer algunas de las cosas clave que usted querrá hacer para ser más feliz con su brillante (viejo) proyecto. Para dar algunos ejemplos concretos, voy a atar todo junto explicando cómo he refactorizado y mejorado el código abierto Vonage Python SDKuna biblioteca que realiza llamadas HTTP a las API de Vonage, pero los principios se aplican a cualquier tipo de proyecto de software.
Los ejemplos de este post estarán escritos en Python, pero estos principios se aplican a proyectos en cualquier lenguaje. También hay una práctica lista de comprobación si estás intentando arreglar específicamente un proyecto en Python.
La serie, por secciones
Segunda parte: Hacer cambios (este artículo)
¿Qué cubre la segunda parte?
En la segunda parte hablaremos de:
Adquirir confianza para realizar cambios y abordar la deuda técnica
Generar confianza con su jefe, equipo y usuarios
Al final del artículo, estarás preparado para cambiar las cosas a mejor en tu proyecto, al tiempo que mantienes contentas a las personas pertinentes.
Empecemos.
Fijación de la estructura
Si has seguido Primera parte de esta serie, ya tendrás una buena comprensión del proyecto que ahora te pertenece, además de saber cómo está estructurada la base de código (y las pruebas, si tienes la suerte de contar con ellas), y para qué sirve esa estructura. Ahora es un buen momento para pensar en la arquitectura general y empezar a reestructurar el proyecto de forma más lógica si crees que podría mejorarse.
Un ejemplo concreto
El proyecto que estoy utilizando como ejemplo es el Vonage Python SDKque permite al usuario llamar a diferentes API de Vonage. En mi caso, la macroestructura de mi proyecto tenía este aspecto:

Puedes ver en la figura anterior que el código se dividió en seis módulos principales (archivos individuales, ¡sí, Python es muy compacto!):
Un módulo
__init__módulo, normalmente un archivo mínimo utilizado para empaquetar el códigoUn módulo
_internalque contenía métodos internos (y también una de las APIs que soportamos, por alguna razón)sms,voiceyverifycada uno de los cuales contenía código para llamar a una única APIUn módulo
errorsque contenía todos los errores personalizados
Mi primera pregunta al ver esto fue: ¿dónde están las otras API? Cuando llegué a Vonage, el SDK admitía 12 API diferentes (¡hemos añadido más desde entonces!), pero solo había 3 módulos relacionados con estas API.
Resultó que alrededor del 90% del código estaba dentro del archivo __init__.py incluyendo implementaciones de muchas APIs, métodos obsoletos, la lógica para tareas como la generación de JWT, las peticiones API base y otras configuraciones opcionales... demasiado para un archivo que debería ser mínimo. Decidí refactorizarlo creando una clase Client que se encargaría de la lógica central, y luego clases con nombres de APIs (con una pequeña agrupación lógica).
Acabamos con una estructura parecida a esta:

Hágalo usted mismo
Observa la estructura de tu proyecto. Pregúntate: ¿podría hacerlo mejor? Agrupa las funciones similares en módulos lo suficientemente pequeños como para que se entiendan con facilidad, pero no más pequeños.
También recomendaría crear clases para contener métodos similares, así como errores personalizados para cada clase para darte a ti y a tus usuarios más visibilidad sobre lo que está mal cuando las cosas se rompen.
Oficinas
Cuando empiezas con un nuevo proyecto, probablemente no lo habrás dejado en un estado "terminado". Los proyectos de software son siempre un trabajo en curso y las constantes mejoras iterativas son una gran parte de la razón por la que utilizamos el control de versiones. En esta situación, es posible que encuentres ramas en tu sistema de control de versiones que contengan características, mejoras o correcciones de errores.
En muchos casos, la situación ideal es la siguiente: una única rama principal, con características que se desarrollan en ramas secundarias y que luego se fusionan con la rama principal.

En algunos proyectos, puede tener sentido tener una rama beta de mayor duración, si hay una característica beta que deseas probar y mantener separada de tu código base principal. En el SDK Python de Vonage, actualmente tenemos una rama rama beta con código que llama a nuestra Video API de Vonage, que está en versión beta. No queremos fusionar esta rama ya que la API a la que llama no está oficialmente disponible. En este caso, la estructura de tu rama podría parecerse más a esto:

En el caso de que tengas que desarrollar características por separado, puede ser aconsejable volver a basar periódicamente la rama beta para extraer los cambios de la rama principal - esto es lo que hago con el SDK Python de Vonage.
Esos son los casos "buenos". Pero cuando llegas a un proyecto, puede que se parezca más a esto:

Es decir, demasiadas ramas y falta de coherencia. Normalmente, las cosas acaban así cuando varios desarrolladores estaban trabajando en muchas funciones simultáneamente, aunque también podría darse el caso de que un ingeniero anterior utilizara nuevas ramas para planificar varias tareas y probar cosas nuevas, pero nunca llegara a terminarlas.
En esta situación, estas ramas pueden ser una gran fuente de información sobre la forma en que el anterior propietario del proyecto estaba gestionando el proyecto, así como una fuente de inspiración - algunas de las características o correcciones podrían ser lo suficientemente buenas como para implementarlas tú mismo. Sin embargo, una vez que entiendes lo que contiene una rama adicional, suele ser una buena idea cerrarla: el objetivo es tener la mayor cantidad de espacio en blanco posible para poner en práctica tus ideas.
Valla de Chesterton - ¡Entienda lo que está quitando!
Ahora parece un buen momento para recordar a todos (yo incluido) sobre La valla de Chesterton - el aforismo que afirma que no se debe quitar algo sin entender por qué se puso ahí en primer lugar. Esfuérzate por comprender el propósito de una rama o parte del código antes de eliminarla. Y lo más importante: no elimines algo sólo porque no sabes por qué está ahí, intenta entenderlo primero.
Elegir las dependencias adecuadas
Casi todos los proyectos de software dependen del código de otras personas. Lo bueno del código abierto es que muchos problemas y retos han sido resueltos antes de que apareciera tu código, por gente muy inteligente que está dispuesta a compartir su trabajo contigo. Sin embargo, es importante entender las dependencias que tiene tu código, para qué se utilizan y si son las mejores herramientas para el trabajo.
Una dependencia que fue adecuada en el pasado también puede dejar de serlo con el tiempo, lo que nos lleva a plantearnos la siguiente pregunta:
¿Qué es una buena dependencia?
Cuando decida mantener (o sustituir) una dependencia, esta sección debería ayudarle a evaluar si merece la pena utilizarla.
En primer lugar, ten en cuenta la licencia. Asegúrate de que puedes utilizar la dependencia en tu proyecto. Si no puedes, eso es un no. Si no estás seguro de lo que estoy hablando cuando digo esto, este artículo de Snyk debería ayudar a explicar qué buscar al evaluar la licencia de una dependencia.
A continuación, ¿se mantiene activamente la dependencia? Puedes ver el historial de commits de los proyectos de código abierto. Algunos son tan sencillos que necesitan muy poco mantenimiento, y eso está bien. Los proyectos más complejos deben ser mantenidos activamente, es decir, debe haber commits frecuentes, y los issues y PRs no son ignorados durante mucho tiempo. Es importante, ya que quieres que la dependencia sea compatible con nuevas versiones del lenguaje y funcione bien con las nuevas características y otras dependencias que estés utilizando.
Merece la pena tener en cuenta la popularidad de la dependencia. Todas las dependencias pueden tener vulnerabilidades de seguridad, y una dependencia más utilizada tiene más probabilidades de que estas vulnerabilidades sean señaladas por herramientas automatizadas, investigadores de seguridad y otros usuarios, lo que aumenta la probabilidad de que cualquier vulnerabilidad sea parcheada rápidamente, manteniendo a tus usuarios a salvo. Elegir una opción ampliamente utilizada también significa que un mayor número de personas podrán responder a tus preguntas si te quedas atascado.
Por último, considere los activos de apoyo: ¿es clara la documentación? ¿Hay ejemplos fiables en línea que utilicen esta dependencia? Estas cosas pueden ayudarte a empezar o a depurar problemas rápidamente, así que merece la pena tenerlas en cuenta.
¿Así que has encontrado el conjunto perfecto de dependencias? Buenas noticias... por ahora. Una biblioteca bien mantenida que se adapte a tus necesidades podría no seguir siéndolo: los responsables podrían abandonar el proyecto o tus requisitos podrían cambiar, haciéndola inadecuada. El mejor consejo que puedo darte es: ¡continúa evaluando tus dependencias a lo largo del tiempo! Asegúrate de que siguen ajustándose a tus necesidades. Y nunca se sabe, puede que en el futuro aparezca una nueva biblioteca que se adapte aún mejor a tus necesidades.
Probar código ajeno
Si tu proyecto tiene que comunicarse con otro código, aparece el espinoso problema de cómo probar la funcionalidad de tu código. En nuestro ejemplo, la tarea principal del SDK Python de Vonage es llamar a muchas API diferentes. No tenemos control sobre las API en sí, o sobre cómo se comportan.
Un ejemplo de esto es cuando usas el SDK para llamar a la SMS API de Vonage. Muchas cosas pueden suceder aquí, dependiendo de tus necesidades exactas, pero daré un ejemplo específico. Enviar un SMS con el SDK de Python es un proceso bastante simple:
import vonage
client = vonage.Client(key="API_KEY", secret="API_SECRET")
client.sms.send_message(
{
"from": "SENDER_NAME"
"to": "RECIPIENT_PHONE_NUMBER",
"text": "A text message sent using the Vonage SMS API",
}
)Pero después de ejecutar este código, ocurren muchas cosas. Aquí tienes una versión (muy) simplificada:
El SDK envía una solicitud a un servidor de Vonage
El servidor analiza la solicitud y envía una respuesta al SDK.
Si la solicitud es correcta, el servidor envía un SMS al número de teléfono del destinatario.
La información de estado (por ejemplo, recibo de entrega) se envía al servidor de Vonage
Opcionalmente, esta información se envía desde un servidor de Vonage a un webhook especificado por el usuario para que pueda ver información como, por ejemplo, si el SMS se entregó correctamente.

En esta situación, la ejecución de nuestro código hace que se ejecute un montón de otro código. No podemos probar este otro código desde el SDK, así que tenemos que hacer una suposición cuando escribimos nuestras pruebas: si enviamos la información correcta, haremos que ocurran las cosas correctas y obtendremos las respuestas correctas de vuelta.
En nuestro ejemplo, la única parte del código que podemos probar es ésta:

Por tanto, las únicas cuestiones sobre las que se puede evaluar nuestro código son: 1. ¿están bien formadas las peticiones que enviamos? y 2. ¿se tratan adecuadamente las respuestas?
En resumen, limita tus pruebas a comprobar cómo tu código envía y recibe datos, y asume que el otro software hace su trabajo correctamente, enviando respuestas de éxito y error para que tu código las procese. Cuando realices pruebas, considera la posibilidad de capturar solicitudes de API y devolver respuestas simuladas en la forma correcta para la solicitud, de modo que tu código pueda consumir y procesar estos datos simulados cuando ejecutes tus pruebas sin llamar realmente a los servicios externos que no puedes controlar.
Recuerda que tus pruebas demuestran que tu código cumple con las especificaciones de tu conjunto de pruebas, no necesariamente con el comportamiento real de la API. Cuando añadas una nueva función, es una buena idea crear una aplicación de demostración que realmente utilice tu código para realizar solicitudes en tiempo real, además de ejecutar tus pruebas (este enfoque también te proporciona una gran respuesta para almacenar en caché y utilizarla con tus pruebas en el futuro).
Hacer su primer lanzamiento
Así que has limpiado el código base, hecho algunas reestructuraciones, mejorado tus pruebas y quizás incluso añadido nuevas funcionalidades a tu proyecto. Es hora de publicar una nueva versión.
El objetivo de este proceso es generar confianza: que los usuarios y el equipo sepan que sabes lo que haces. El objetivo es demostrar a los usuarios que pueden confiar en tus cambios y que la actualización no romperá ninguna parte de su flujo de trabajo sin su conocimiento. La transparencia es la clave: no querrás dar a tus usuarios sorpresas ocultas.
Utilizar el versionado semántico
La recomendación más importante sería la siguiente: utilizar versionado semántico¡! Con el versionado semántico, se sigue una estructura x.y.z para los números de versión, donde:
x es la versión principal, que debe aumentar cuando realice cambios de última hora,
y es la versión menor, que debe aumentar cuando añada funciones compatibles con versiones anteriores, y
z es la versión del parche, que debe subir cuando realice una corrección de errores, actualización de dependencias, etc., de forma compatible con versiones anteriores.
Para dar un ejemplo de la vida real, al añadir soporte para la Mensajes API de Vonage (pero sin cambiar nada que pudiera hacer que la nueva versión fuera incompatible con versiones anteriores), hice una versión menor, de v2.7.0 -> v2.8.0. Cuando quise eliminar algunos métodos obsoletos y cambiar cómo se instancian los objetos en el SDK, sabía que esto rompería la funcionalidad existente, así que hice una versión mayor, v2.8.0 -> v3.0.0. Cuando encontré un error en el nuevo código, lo corregí y actualicé el SDK con un parche, v3.0.0 -> v3.0.1. Siguiendo estas reglas, el usuario sabe exactamente qué esperar cuando se actualiza a la última versión. Si haces una versión mayor, saben que pueden esperar un cambio importante.
Actualizar el material complementario
Siempre que publiques una nueva versión, actualiza el registro de cambios para que el usuario sepa qué esperar. Suele ser el primer lugar en el que mira un usuario, ya que suele explicar todas las diferencias de la nueva versión y puede ayudarle a decidir si debe actualizar. Si no actualizas el registro de cambios, los usuarios no sabrán qué esperar de una nueva versión y, en consecuencia, serán mucho más cautelosos.
Del mismo modo, cuando hagas una nueva versión, mantén la documentación, los README y los ejemplos de código actualizados con la última versión. Si no lo haces, tus usuarios no sabrán necesariamente cómo utilizar todas tus nuevas y geniales funciones, y si has hecho un cambio de última hora, tus documentos y ejemplos de código podrían dejar de funcionar por completo.
Recuerde que la transparencia es la clave: sea predecible, para que los usuarios veteranos del proyecto puedan confiar en que el código en el que confían está en buenas manos, y para que los nuevos usuarios puedan aprender a utilizar su software.
Reducción de las amortizaciones
Cuando empieces a cambiar el código, es probable que quieras reestructurar/refactorizar gran parte de él. Siempre y cuando esto no cambie la forma en que el código es llamado por un usuario, esto se puede hacer a gusto. Sin embargo, si quieres cambiar cómo se llama a algo, tendrás que eliminar el método antiguo y añadir el nuevo en una versión menor, y luego eliminar el antiguo en una versión mayor más adelante.
Daré un ejemplo específico aquí. En el SDK Python de Vonage, quería cambiar la forma en que el método get_standard_number_insight era llamado por un usuario. Originalmente, este era un método asociado con la clase Client clase. Quería cambiar la estructura por tener una NumberInsight que contuviera este método, que la clase Client clase instanciado y utilizado. Esto haría que esta forma de llamar a la Number Insight API fuera igual a la forma en que un usuario enviaría mensajes SMS o haría llamadas de voz.

En primer lugar, he creado una clase NumberInsight y le añadí una versión del método get_standard_number_insight a la misma.

A continuación, he obsoleto el método en la clase Client clase. En Python, esto se puede hacer mediante la adición de un decorador que imprime una advertencia de desaprobación para el usuario cuando se utiliza el método.

Después de esto, he actualizado los fragmentos de código y los documentos para reflejar el cambio.

Ahora he podido hacer una versión menor. Después de una versión en la que se desaprueba una funcionalidad, es una buena práctica dejar las partes desaprobadas durante un tiempo antes de eliminarlas, para dar tiempo suficiente a la gente a cambiar. Yo sugeriría dejar la funcionalidad obsoleta durante al menos una versión o un mes, lo que sea más largo. Una vez más, se trata de generar confianza.
Dejé este código antiguo durante unos meses, y luego hice una versión mayor en la que eliminé las antiguas formas de llamar a estos métodos. Ser metódico y transparente sobre cuáles eran mis intenciones significaba que mis usuarios sabían qué esperar cuando actualizaban a la última versión.

Equilibrio entre mejoras y nuevos trabajos
Lo último que hay que tener en cuenta cuando se empieza a trabajar en una base de código heredada es que probablemente no se dispondrá de tiempo infinito para pulir el código como a uno le gusta antes de tener que añadir nuevas funcionalidades, corregir errores, etc. Es probable que tengas que hacer malabarismos con otras prioridades, por ejemplo, plazos para añadir nuevas funcionalidades, al mismo tiempo que intentas hacer grandes cambios en el código antiguo.
En este caso, tienes que sentirte cómodo defendiéndote a ti mismo y a tu trabajo. Establece la expectativa de que parte de tu tiempo debe dedicarse a mejorar la base de código heredada, lo que significa que no serás tan rápido en el desarrollo de nuevas funciones como a tu jefe le gustaría. Haz hincapié en que el tiempo que dediques ahora a refactorizar te ayudará a comprender el código base y te reportará beneficios más adelante, ya que será mucho más fácil de mantener.
En última instancia, su trabajo en un proyecto heredado debería mejorar la experiencia del usuario y su facilidad de uso y mantenimiento a largo plazo. Puede que no reciba elogios inmediatos por ocuparse de la deuda técnica, ya que el trabajo no es visible para sus usuarios o su equipo de inmediato, pero la gestión de la deuda técnica puede evitar que las cosas sean mucho peores para todos más adelante: ¡este trabajo merece mucho la pena!
Algo que me ayudó mucho en este proceso fue crear tickets de trabajo para el descubrimiento y el aprendizaje, para demostrar que estaba invirtiendo en la eficiencia futura. También creé tickets para la deuda técnica, para dar a mi equipo una visión del trabajo que estaba haciendo, junto con los tickets para el desarrollo de nuevas características. Este enfoque me ayudó a generar confianza en mi empresa y a que la gente entendiera cómo empleaba mi tiempo.
¿Y ahora qué?
Si has seguido las sugerencias de este artículo, ¡tu proyecto ya tendrá mucho mejor aspecto! Los lanzamientos que hagas sentarán las bases para todas las grandes cosas que quieras hacer con tu proyecto.
Haga clic aquí para leer la tercera parte de la seriedonde explicaré cómo llevar tu proyecto al siguiente nivel. Hablaremos de mejoras que beneficien a tus usuarios, así como de cosas que faciliten el trabajo con el código base. Por último, explicaremos las mejores prácticas para entregar un proyecto.
¿Tiene alguna pregunta o quiere compartir su opinión? Puedes comunicarte con nosotros en nuestro Slack de la comunidad de Vonage o envíanos un mensaje en Twitter.
Compartir:
Max es un defensor de los desarrolladores de Python e ingeniero de software interesado en las API de comunicaciones, el aprendizaje automático, la experiencia de los desarrolladores y el baile. Su formación es en Física, pero ahora trabaja en proyectos de código abierto y hace cosas para mejorar la vida de los desarrolladores.