Diseño y simplicidad

Diseño y simplicidad
Sin comentarios Facebook Twitter Flipboard E-mail

Desarrollar software no es una tarea fácil. Aunque estemos desarrollando algo muy sencillo, es posible que nos compliquemos tanto la vida, que acabemos haciendo algo complejo. Muchas veces con la idea de meter tecnologías o técnicas supuestamente mejores, acabamos aumentando la complejidad del desarrollo. Y esto puede ser algo que a la larga nos cueste muy caro. Tanto en tiempo como en dinero. Así que es importante que nuestros desarrollos tiendan a la sencillez. ¿Pero es eso fácil de conseguir?

¿Por qué desarrollamos software? A veces se nos olvida, pero el propósito de todo lo que desarrollamos es ayudar a la gente. Si estamos desarrollando un navegador web, lo que queremos es que la gente pueda ver sus páginas web favoritas. Si estamos desarrollando un programa de hojas de cálculo, lo que queremos es que la gente pueda introducir datos y hacer cálculos. Y esto es así siempre. A veces desarrollamos para parecer muy listos o desarrollamos para hacernos ricos. Pero eso es perder el foco del objetivo final.

Nuestro fin último tiene que ser el de ayudar a la gente con nuestros desarrollos. Pensando de esta manera nos daremos cuenta de qué funcionalidades son interesantes para las personas que van a usar nuestro desarrollo. Podremos priorizar, e incluso eliminar, tareas.

El valor y el esfuerzo

Supongamos que tenemos 50 funcionalidades para implementar en nuestra aplicación ¿Cómo las priorizamos? Primero podemos decir que toda funcionalidad pendiente de desarrollo tiene un valor y necesita de un determinado esfuerzo para finalizarse.

Valor

El valor, de una posible funcionalidad a desarrollar, es más alto cuanto más ayude a la gente. Pero medir el valor de una posible funcionalidad no siempre es fácil. Aunque no podamos pensar en números exactos, siempre podemos hacer aproximaciones. Cuando pensamos en el valor de una funcionalidad debemos pensar en su valor potencial y en su valor probable. Su valor potencial es el cuánto ayudará a los usuarios o clientes. El valor probable será con cuánta frecuencia los ayudará.

Una funcionalidad que se ejecute pocas veces, pero que tenga un alto valor potencial, es una funcionalidad deseable. Más aun, si además de tener un potencial muy alto, se ejecuta frecuentemente. Si su valor potencial no es tan alto, pero se ejecuta muchas veces, también puede ser interesante ejecutarla, aunque quizá algo menos. En el último escalón estarían esas funcionalidades que ni son ejecutadas a menudo (su valor probable es bajo), ni son demasiado útiles (su valor potencial es bajo).

Esfuerzo

El esfuerzo, es el tiempo que nos costará desarrollar esa funcionalidad, y es más fácilmente representable en números. Al final desarrollar algo le lleva X tiempo a una persona determinada. Lo que no es tan fácil es medirlo, ya que depende de muchos factores. Por ejemplo no todos los programadores trabajan igual de rápido. Además podemos encontrar bastantes costes ocultos que incrementen el esfuerzo, como la mantenibilidad del código, tiempo de análisis del problema, comunicación con otros departamentos o desarrolladores etc.

Mantenimiento

Hay un factor importante que también hay que tener en cuenta en todo desarrollo: el mantenimiento. No basta con desarrollar una funcionalidad, si no que hay que mantenerla a lo largo del tiempo. Y dependiendo de las decisiones que tomemos, esto puede ser más sencillo o más complicado. Debemos pensar también, en que una funcionalidad debe seguir ayudando a usuarios en el futuro, por lo que nuestra funcionalidad tendrá también un esfuerzo y un valor en el futuro.

Calidad y futuro

Todas estas variables nos sirven para explicar porque es deseable un software bien diseñado y con calidad. En realidad es relativamente desarrollar software que ayude a una persona ahora mismo, pero es muchísimo más complicado diseñar un software que ayude a muchas personas durante muchos años. Y es que si lo piensas, si tu aplicación va bien, en un futuro tendrás mucho más trabajo que ahora, ya que tendrás muchos más usuarios según pase el tiempo. Si ignoramos el diseño, y no pensamos en el mantenimiento futuro, acabaremos haciendo un software, que no ayude a sus usuarios o que no lo haga a tiempo.

En un futuro tendrás mucho más trabajo que ahora, ya que tendrás muchos más usuarios según pase el tiempo

El problema es que aunque pensemos en el futuro, hay cosas que serán difíciles de predecir. Esto nos lleva a que es importante tener en cuenta el futuro de nuestro desarrollo, pero es una pérdida de tiempo intentar adivinarlo. ¿Habéis estado involucrados en algún proyecto que se haya pasado de previsor? Seguro que sí. Al final se acaban tomando decisiones equivocadas en base a una predicción que no debía haberse hecho, haciendo el desarrollo futuro mucho más lento y complicado.

Probabilidad de cambio

El tema aquí es claro: cuanto más tiempo esté funcionando nuestro software, más probable es que haya que cambiarlo. Es difícil predecir en qué punto y cuándo cambiará nuestro software, pero es bastante probable que si pasa 20 años funcionando, haya cambiado hasta el 100% del mismo.

Esta ley de probabilidad nos hace cometer tres típicos errores:

Escribir código que no es necesario

Hay muchas veces que estamos desarrollando y nos surge una duda: "¿y si el usuario quisiera hacer esto?". La idea en ese momento nos parece buena y decidimos implementarla para no tener que hacerlo en el futuro. El problema es que eso no es algo que el usuario nos haya pedido, si no algo que nosotros hemos pensado que sería buena idea. Trabajo que nos quitamos en el futuro. Lo malo de este enfoque es que cabe la posibilidad de que esa funcionalidad nunca se utilice, habiendo trabajado para nada. Además, cuando intentamos hacer desarrollos abiertos, tendemos a escribir un código más sucio y feo, lo que todavía complica más las cosas. Hay que ser prácticos y aplicar YAGNI (You Ain’t Gonna Need it ).

No hacer el código sencillo de modificar

Si nuestros desarrollos son demasiado rígidos, serán difíciles de mantener. A veces el código es rígido porque hemos asumido demasiadas cosas. Si los requisitos no están claros, y tomamos decisiones con mucha antelación, nuestro código va a ser difícil de modificar, ya que el diseño no será lo suficientemente versátil. Otras veces, el problema está en el diseño del software (o en su ausencia). Si no dedicamos tiempo suficiente a pensar en el diseño, tendremos problemas en el futuro.

En definitiva, tenemos que pensar en el diseño, sin asumir nada, y haciéndolo en base a la información que conocemos ahora y no en lo que podría pasar en el futuro.

Pasarnos de genéricos

Esto nos ha pasado a todos. Como tenemos dudas en cuanto al futuro comportamiento del software, decidimos desarrollarlo intentando controlar todas las posibles situaciones. Obviamente esto es muy difícil, y más en un entorno de incertidumbre. Debido a esto acabamos haciendo un diseño demasiado complejo. Este ejercicio de sobreingeniería, nos complica tanto que el desarrollo, e incluso el inicial, se ve muy penalizado.

Al igual que antes, hay que ser tan genérico como necesitemos en el momento actual, y con la información que tenemos.

Desarrollo iterativo e incremental

Como os habréis fijado, estos problemas pueden paliarse en gran medida evitando los desarrollos en cascada y tomando un enfoque iterativo e incremental. Decidimos cual es la funcionalidad que queremos implementar, la desarrollamos en base a lo que sabemos ahora, y corregimos los problemas que podamos encontrar. Una vez hayamos hecho esto, volvemos a empezar, con otra funcionalidad. Y luego otra, y luego otra.

Es una buena manera de conseguir un código sencillo y mantenible, así que por eso, desarrollar y diseñar de forma incremental está demostrando ser una técnica tan útil.

Bugs y errores

Da igual lo bueno que seas programando, que al final siempre vas a añadir errores en tú código. Es algo que debemos asumir. Cuanto más grande sea el cambio que tenemos que implementar, más fácil es que cometamos un error que provoque bugs que afecten al sistema o al usuario. Esto nos lleva a pensar que es mejor no modificar el software para evitar problemas futuros, pero como ya hemos dicho antes, todo software tiende a cambiar con el paso del tiempo. Así que nuestra misión como programadores es la de crear diseños que nos permitan modificar el entorno con los menos cambios posibles en el software. Y esto no es nada fácil.

El trabajo ya es lo bastante complicado, como para encima perder el tiempo con errores que no existen.

Una cosa importante aquí, es que solo tenemos que arreglar cosas que no funcionen. Tiene que haber evidencias que demuestren que algo está fallando. A veces los usuarios reportan como error algo que en realidad no lo es, bien porque no está fallando realmente, o bien porque desean una funcionalidad que está ya implementada, pero que desconocen. Como programadores también tendemos a arreglar problemas que todavía no existen, por ejemplo cuando caemos en la trampa de la optimización prematura.

El trabajo ya es lo bastante complicado, como para encima perder el tiempo con errores que no existen.

Simplicidad

Si no modificamos nuestro código, no provocamos errores. Pero como es imposible que no modifiquemos en algún momento una aplicación que esté funcionando a lo largo del tiempo, hay que pensar en la mejor manera de hacerlo.

La mejor manera es intentar hacer cambios pequeños. Pero esto no es suficiente, ya que también tenemos que pensar en la facilidad de mantenimiento en el futuro. Y aquí es donde podemos decir que la facilidad de mantenimiento de cualquier software, es proporcional a la simplicidad de sus partes.

Cuanto más sencillas son las partes de nuestro desarrollo, más fácil es el mantenimiento del sistema en conjunto. Además es mucho más fácil hablar de piezas o partes, ya que es muy difícil manejar la complejidad de todo un sistema. Aunque sea un sistema sencillo, es muy difícil comprender todas las partes del mismo, pero es mucho más asequible conocer cada una de sus partes y más si estas son sencillas.

Aquí debemos pensar otra vez en el esfuerzo que nos lleva un desarrollo. Es cierto que hacer un diseño simple y fácilmente mantenible (ahora, sin pensar en el futuro), puede llevar un poco más de esfuerzo. Pero la experiencia nos dice que es mucho más fácil hacer cambios e introducir nuevas funcionalidades en software sencillo y bien diseñado.

Cuanto más sencillas son las partes de nuestro desarrollo, más fácil es el mantenimiento del sistema en conjunto

El problema de la simplicidad, es que no es igual para todos. Algo que a un programador le parece simple, puede parecerle muy complicado a otro. Aquí debemos pensar en alguien que no conoce nada sobre el código que escribimos, pero tiene que intentar entenderlo. Es algo a lo que nos hemos enfrentado todos. Que levante la mano el que no se ha visto frustrado intentando descifrar el código desarrollado por otra persona. Así que desarrollar pensando en alguien que en un futuro tendrá que descifrar nuestro código (quizá nuestro yo futuro), es siempre un acierto.

Como la simplicidad conlleva un esfuerzo, es lógico pensar en cómo de simples tenemos que ser. Aquí la regla es sencilla: cuanto más sencillo sea nuestro código, mejor. Para tontos. Lo contrario significa que posibles cambios en el futuro no se hagan bien, ya que no se entiende bien el diseño, y esto provoque errores y pérdidas de tiempo.

Hay además unas cuantas reglas que podemos utilizar para hacer nuestro código más sencillo:

  • Hay que ser consistente en el código. Si las variables las nombramos utilizando camel case hazlo así durante todo el desarrollo. Si los nombres de los métodos están en inglés, que sea así a lo largo de todo el código. La consistencia ayuda mucho a la hora de leer y entender código fuente.
  • Formateo del código. Con los IDE's y editores es más fácil hoy en día, pero no está de más recordarlo. El código bien formateado, con sus espacios y sus saltos de línea para separar conceptos, ayuda mucho a mejorar la legibilidad.
  • Nombres de variables y métodos. Un clásico del código limpio. Haz que los nombres de variables, métodos y funciones sean explicativos de lo que hacen o representan. Es mucho mejor un CalculadorIVAReducido, que un Calculador. Es mucho mejor un IsVIPCustomer que un b_vip_customer. Los nombres son de gran ayuda a la hora de entender el código así que hay que tomarse un tiempo para pensarlos.
  • Comentarios. Los comentarios ayudan, o entorpecen. Si el código necesita un comentario, quizá es que no es lo suficientemente sencillo. Pero si no se puede simplificar más, deja un comentario de por qué lo haces así. Y siempre, siempre evitar los comentarios obvios que solo ensucian el código. En GenbetaDev ya hablamos sobre esto.

Complejidad

Controlado el tema de la simplicidad, debemos hablar de su antagonista, la complejidad. Lo primero a tener en cuenta es que la complejidad, unida a más complejidad, hace las cosas exponencialmente difícil. Si añadimos una nueva funcionalidad, es probable que esta tenga que coordinarse con otras funcionalidades que ya existían en el sistema. Si la nueva funcionalidad es compleja, esto será complicado, pero es que si además las otras funcionalidades que ya existen también son complicadas, la complejidad se dispara. Seguro que alguna vez has ido a añadir un cambio supuestamente sencillo y has acabado perdiendo horas por intentar integrarlo con el código ya existente.

Aunque normalmente añadimos complejidad al añadir funcionalidades, también se puede añadir de otras maneras: como añadiendo más programadores, ampliando el alcance del proyecto, por malentendidos, por malas tecnologías, por un mal diseño etc.

Manejar la complejidad que ya existe no es tarea sencilla. Lo que mejor suele funcionar es ir cambiando partes pequeñas de nuestro código, para hacerlo más simple de forma progresiva. No es algo rápido, y a veces es frustrante, pero es ideal para mejorar el código sin tener que tirar abajo el sistema existente.

Como esto lleva tiempo, lo podemos entender como parte de añadir funcionalidades. Cuando tengamos que añadir una funcionalidad, podemos definir un tiempo para la tarea de simplificar la instauración de esa funcionalidad en el código existente.

También podemos encontrarnos el problema de que no podamos esquivar la complejidad (por ejemplo cuando trabajamos directamente con hardware). En este caso lo que deberemos hacer es esconder la complejidad. Creamos un wrapper para llamar a esas funciones que interactúan con el hardware, y así desacoplamos una parte de la otra, haciendo las cosas mucho más sencillas.

Testing

Para estar seguros de que nuestros programas siguen funcionando en el futuro, tenemos que hacer tests. Y punto. Cuanto más testees un sistema, mayor seguridad tendremos de que ese sistema funciona correctamente. No quiero profundizar mucho más en los tests, pero por muy listo que seas, hay que probar el código que escribes, porque no hay nadie infalible y siempre se cometen errores.

Conclusión

Que nuestro código sea sencillo y mantenible es garantía de éxito. Conseguirlo no es tarea fácil, pero es algo que con la suficiente experiencia y con un buen diseño, es factible de conseguir.

Imagen | Mark Skippe

Comentarios cerrados
Inicio