Product Crafter Emilio Carrión

Mantén la complejidad del software bajo control

La clave para un desarrollo de software mantenible

Desarrollar software es caro. El software puede degenerar rápidamente y los costes pueden aumentar sin que nos demos cuenta, y el principal culpable de estos costes es la complejidad. La mayoría de los problemas a los que nos enfrentamos desarrollando vienen de la complejidad inherente de nuestras creaciones.

El coste puede venir de muchas fuentes, pero todas ellas están relacionadas con cuanto nos cuesta cambiar nuestro software. Si nos cuesta mucho tiempo introducir un cambio es que es demasiado complejo. Si nos cuesta mantener o actualizar el código existente es que es demasiado complejo. Si no encontramos una forma sencilla de testear parte de nuestras funcionalidades, es que están programadas de forma demasiado compleja.

Deformando un poco el principio de Pareto, me atrevo a decir que el desarrollo del día a día en la mayoría de los equipos se reparte en un 80% lidiar con la complejidad del sistema y el otro 20% para implementar de verdad el cambio.

En este artículo intentaré dar algunos consejos para darle la vuelta a esos porcentajes y que consigamos invertir el tiempo en lo que verdad importa: aportarles valor a nuestros usuarios.

La mayoría del tiempo lo gastamos leyendo código

Como ya he comentado, invertimos mucho tiempo en entender nuestro propio código. Como dijo una vez muy sabiamente Uncle Bob:

De hecho, la proporción de tiempo dedicado a la lectura frente a la escritura es de más de 10 a 1. Constantemente leemos código antiguo como parte del esfuerzo por escribir código nuevo. … [Por lo tanto,] hacer que sea fácil de leer hace que sea más fácil escribir.

Por lo tanto, el primer paso es escribir código fácil de leer, que se entienda, escrito para seres humanos.

Nombres claros

Para que nuestro código sea fácil de leer primero hemos de llamar a las cosas como son. Dejarnos de nombrar variables como letras o con nombres indescifrables. Hay que llamar a las cosas con nombres sencillos y descriptivos que indiquen su contenido o su función. No hay mejor documentación que un buen naming.

Tendemos a hacer las cosas más complejas de lo que deberían ser

Como desarrolladores muchas veces estamos tentados a montarnos una feria. Tenemos la necesidad de usar las últimas tecnologías, la última estructura de datos o patrón de diseño que hemos aprendido y, muchas veces, hacemos las cosas de una manera más compleja de lo que podría ser.

KISS (Keep It Simple, Stupid!)

El principio KISS o de mantenerlo simple es básicamente un toque de atención sobre este fenómeno. El software simple es más fácil de mantener, de evolucionar y es menos propenso a errores. Por lo tanto, cuando implementemos una nueva funcionalidad siempre hemos de abogar por hacer las cosas de la manera más sencilla posible. No hemos de irnos por las ramas y hemos de aguantar la tentación de hacer sobre-ingeniería, algo que a medio plazo sale muy caro.

Single Responsability Principle

Una forma fácil de aplicar un diseño simple es hacer que las cosas solo tengan una función. El SRP o principio de responsabilidad única hace plantearnos esta restricción que hace que nuestros artefactos de software sean más simples por naturaleza. Si tenemos una clase que se encarga de enviar datos a un tercero, no hagamos que también escriba en nuestra base de datos, deleguemos esa funcionalidad a otra entidad que esté especializada en hacer eso y solo eso. Manteniendo a raya las responsabilidades de cada entidad de nuestro código conseguiremos que todo sea mucho más sencillo. Al fin y al cabo, es fácil hacer las cosas simples cuando solo tienen un objetivo.

YAGNI (You Aren’t Gonna Need It)

Como también he mencionado, como desarrolladores tendemos a hacer las cosas más complejas de lo que deberían ser. Y eso no siempre es debido a que nos embelese la sobre-ingeniería. A veces hacemos los sistemas más complejos por si acaso. Por si acaso tenemos más de 100.000 usuarios algún día, por si acaso luego nos piden esta otra funcionalidad, por si acaso cae un meteorito sobre el centro de datos. La vida, como el desarrollo, está lleno de por si acasos y en nuestro software debemos aprender a limitarlos. No digo que no haya que tener en cuenta el futuro, pero tenemos que desarrollar nuestro software sin estar tan pendientes de lo que pueda pasar y hacer foco en lo que queremos conseguir ahora. Si además hacemos un software flexible donde sea fácil añadir y quitar cosas, esos por si acasos no serán un problema cuando lleguen (enfatizo, cuando lleguen).

Tenemos que saber acotar nuestro problema

Otro gran causante de la complejidad es que muchas veces no sabemos acotar bien los problemas que estamos solucionando ni nuestro propio software. Fallamos muchas veces al separar que cosas no van con otras cosas y causamos que nuestro código esté acoplado entre sí y sea difícil de mantener y cambiar.

Contextos de dominio

Un buen primer paso para reducir el acoplamiento y aumentar la cohesión en nuestro software es hacer una buena separación del dominio en los diferentes contextos que lo componen. Por ejemplo, si estas desarrollando un e-commerce es conveniente separar todo el contexto del catálogo del contexto de pagos. Al final son problemas que requieren soluciones muy diferentes y separándolas y diluyendo todo lo posible su acoplamiento hará que desarrollarlas e iterarlas sea mucho más sencillo.

En micro: a nivel del software y el código; y en macro: si diferentes equipos son responsables de diferentes contextos, el posible bloqueo que haya entre ellos será menor que si comparten contextos: la ley de Conway en todo su esplendor.

Reducir el acoplamiento

Como hemos comentado, reducir el acoplamiento entre diferentes contextos de nuestro dominio es esencial para evitar bloqueos o posibles problemas a la hora de desarrollar. Una buena herramienta para ello es desarrollar siempre usando interfaces para interactuar con otras partes de nuestro dominio o de nuestra organización. Acordar contratos con las partes implicadas y desarrollar siguiendo esas interfaces definidas sin preocuparnos de la implementación detrás de ellas. Eso nos permitirá trabajar de una manera asíncrona y desacoplada que acelerará el desarrollo y reducirá los problemas.

Aumentar la cohesión

Por otro lado, a la hora de desarrollar y mantener software es conveniente tener una buena cohesión. Esto es hacer que las cosas que funcionan juntas permanezcan juntas. En este sentido hay herramientas como la arquitectura vertical que nos pueden ayudar a mantener el código relacionado con una funcionalidad en el mismo sitio. Podemos verlo también desde un punto de vista horizontal al separar por capas nuestra aplicación, usando el patrón repositorio, por ejemplo, hacemos que la lógica de la capa de persistencia permanezca en un mismo lugar y desacoplándola del resto de nuestra lógica de negocio facilitando su fácil modificación.

Esconder la complejidad (cuando no pueda ser evitada)

Otra forma de acotar la complejidad de nuestro software (siempre que no sea evitable) es usar capas de abstracción. Usando interfaces como ya hemos comentado antes podemos ocultar la complejidad de ciertas partes del sistema detrás de contratos simples que nos ayudan a centrarnos más en nuestra lógica de negocio y no tanto en la complejidad inherente de ciertas partes del sistema.

En la función book_any_available_slot delegamos la complejidad inherente del servicio de slots en una interfaz sencilla de usar y entender.

Tenemos que saber qué problema estamos resolviendo

He dejado esta sección para el final, pero considero que es la más importante. Cuando desarrollamos un producto digital es muy importante tener claro qué estamos construyendo. Al igual que si no puedes explicar una cosa es que no la entiendes, si no sabes cuáles son las necesidades de tu producto acabarás construyendo algo más complejo de lo necesario y que puede que no cumpla siquiera con los requisitos.

Obtén requerimientos claros

El primer paso para empezar a desarrollar un producto es tener siempre super claras las necesidades que está cubriendo y los requisitos que se te piden. Si haces foco en cumplir solo lo estrictamente necesario será más fácil evitar complejidad accidental y desarrollar tu software de la forma más simple posible (aquí entra de nuevo evitar los por si acasos).

En esta parte es imprescindible una buena comunicación y un buen trabajo en el día a día con el resto de los roles de tu equipo (diseñadores, product managers, stakeholders). Si os entendéis entre vosotros, esta recopilación de requisitos se hará de manera orgánica sin que te des cuenta.

Usa TDD

Este consejo puede parecer muy dogmático y es que lo es. Una de las herramientas que más uso en mi día a día es TDD (Test-Driven Development). Y es que hacer TDD es más que empezar a desarrollar escribiendo los tests. Hacer TDD te permite hacer foco primero en lo más importante: cuál es el problema que estás resolviendo. Escribiendo primero los tests te permite pensar en el requisito que estamos cubriendo antes de pensar en cómo vamos a implementarlo. Después, el intentar poner el test en verde lo antes posible nos ayuda a no irnos por las ramas, lo que normalmente lleva a un diseño más simple. Y finalmente, la fase de refactor nos da espacio para ver el problema en su completitud y simplificar la implementación sabiendo que seguimos cubriendo las necesidades (si nuestros tests siguen en verde claro). No puedo recomendarlo más.

TL;DR

La complejidad inherente al desarrollo de software puede aumentar los costos, disminuir la eficiencia y dificultar la entrega de valor a los usuarios. La clave para revertir esta situación radica en abordar la complejidad desde múltiples ángulos:

Simplicidad en el diseño: Enfocarse en escribir código claro y comprensible para los humanos es el primer paso para facilitar el desarrollo futuro.

Principios de diseño: Conceptos como KISS (Keep It Simple, Stupid!), SRP (Single Responsibility Principle) y YAGNI (You Aren’t Gonna Need It) resaltan la importancia de la simplicidad, la claridad y el rechazo por la sobre-ingeniería.

División del dominio: Separar los distintos contextos del software permite reducir el acoplamiento y aumentar la cohesión, facilitando el desarrollo e iteración.

Abstracción y contratos claros: El uso de interfaces y capas de abstracción ayuda a ocultar la complejidad cuando sea necesario, permitiendo enfocarse en la lógica de negocio.

Enfoque en requisitos claros: Entender profundamente los requerimientos del producto desde el principio ayuda a evitar la complejidad innecesaria. Para ello trabaja estrechamente con tu equipo.

Usar TDD: El Test-Driven Development no solo asegura la calidad del software, sino que también dirige la atención hacia la resolución del problema antes de pensar en su implementación, fomentando un diseño más simple y una comprensión más profunda del problema.

En resumen, simplificar el proceso de desarrollo y mantener el enfoque en las necesidades del usuario son elementos clave para reducir la complejidad y maximizar el valor entregado en el desarrollo de software.