Programar sin condicionales
Desde el momento en el que comenzamos a programar, nos enseñan a utilizar el IF. Eso es porque utilizar condiciones es fácil.
Lo difícil es no usarlas.
Y eso es lo que quiero ver aquí.
NOTA: Voy a utilizar Python, pero podría utilizarse cualquier lenguaje. No es necesario tener conocimientos previos de Python.
Actualización 2012-05-12: Transformo referencias en links y añado el apartado de “reflexiones”.
¿Por qué?
Pero… ¿Por qué molestarnos? Ya somos felices utilizando condicionales. Y nos va bien. ¿Merece la pena un sobreesfuerzo?
A medida que vayamos descubriéndolo, mencionaré las razones.
Estética
Aunque cuando lo estamos programando parece obvio, cuando de verdad queremos utilizarlo, los condicionales ensucian nuestro código. Veamos un ejemplo:
|
|
Expresividad
Resulta confuso, ¿no? Y, además resulta que he olvidado, a propósito un else. Así que si se cumple la condicion1 pero no la condicion2… ¿qué? ¿Es un error? ¿Es como debe funcionar?
Una posible solución es añadir un comentario. Puede parecer una buena solución, pero…:
- Los comentarios no se prueban.
- Los comentarios no son formales y pueden resultar más difíciles de comprender que el propio código.
- Si alguien realiza un cambio, hay que confiar en que modificará el comentario.
Esto nos lleva a pensar que no debemos tener dos niveles de condicionales. Y eso es bueno.
Dificultad para pruebas
No somos conscientes de ello, pero cada condicional duplica el número de pruebas necesarias en nuestro código para mantener el mismo porcentaje de cobertura. Cada condicional anidado, los eleva al cuadrado.
Tener un código largo con numerosas condiciones anidadas hace que el código sea imposible de probar e imposible de seguir. Siempre se nos escapará algún caso de uso.
Para probar el código de arriba, necesitaré las siguientes pruebas:
Condicion1 | Condicion2 | Condicion3 | Esperado |
---|---|---|---|
True | True | True | accion1 |
True | True | False | accion1 |
True | False | True | – |
True | False | False | – |
False | True | True | accion3 |
False | True | False | accion4 |
False | False | True | accion3 |
False | False | False | accion4 |
Y alguien puede decir: Bueno, puedes ahorrarte el caso “True True False”, ya que es igual que el “True True True”… Sí, puedo. Pero estoy saltándome pruebas. Al fin y al cabo, el condicional está en el código.
Mi opinión es que 8 casos de uso sólo para 2 condiciones… es demasiado.
Condicionales encubiertos
Hay que tener cuidado: no sólo los IFs son condicionales; también lo son los “FOR” y “WHILE”, que incluyen una condición para el bucle. Todos ellos entran dentro de la misma categoría: condicionales.
Soluciones
Así que puede que no esté tan mal evitar los condicionales, después de todo. Pero… ¿cómo lo hacemos?
Siguiendo una técnica de divide y vencerás. Resulta más sencillo de lo que parece, y el código resultará mucho más expresivo.
Aislando
La primera acción será mandar nuestro código a una función propia. Así que, a partir de ahora, puedo asumir que no hay código ni antes ni después de nuestro bloque.
|
|
Evitando “ELSEs”
Una vez aislado, evitaremos los ELSE. La mejor manera es abandonar la función cuando ya no nos interesa nada más:
|
|
Solo con esta acción habremos mejorado la legibilidad de nuestro código.
Buscando contratos
En muchas ocasiones hay condiciones que hacen que nuestra función no haga nada. En esos casos, decimos que “no se cumple el contrato”. Lo mejor es dejar claros esos casos al comienzo de las funciones:
|
|
Vemos en este caso que, además, hemos podido reducir una de las condiciones, ya que el otro caso no puede darse.
Simplificar condiciones
Pero eso ha implicado crear condiciones más complejas. Podemos evitarlas creando funciones:
|
|
Si los nombres de las funciones están bien elegidos, habremos eliminado también algunos comentarios. Resulta interesante evitar que las funciones que sólo comprueban la condición contentan el nombre de la condición en su propio nombre. Ah, y es indispensable evitar por todos los medios, los “AND” en los nombres de las funciones. Parece una tontería, pero a veces éste es el paso más difícil.
Ejemplo
Veamos un ejemplo: FizzBuzz. Es un ejemplo tremendamente sencillo que se puede implementar en menos de 100 caracteres, pero vamos a intentar que quede algo legible. Consiste en imprimir números, pero sustituiremos los múltiplos de 3 por “FIZZ”, los de 5 por “BUZZ” y los de ambos por “FIZZBUZZ”:
|
|
No se ven, pero tenemos 3 condiciones anidadas, así que dividimos funciones:
|
|
Como veis, esta simple acción me ha permitido reducir todos los “i+1” que tenía. Eliminemos los ELSEs:
|
|
Esto ya va pareciendo otra cosa… Pero tengo demasiados “prints”. Como véis, el proceso no es automático o no lo contaría: haría un script. Así que voy a redicir un poco todo ese mejunje:
|
|
Busco contratos:
|
|
Y ahora voy a quitarme las condiciones del medio. Acordaos de que no puedo utilizar AND y que no puedo usar cosas como “isMultiploOf5”, ya que incluiría la condición en el nombre de mi función. Es mucho mejor usar la acción que voy a aplicar para describir la función:
|
|
Y, como véis, el código resultante es mucho más sencillo y no necesito comentar nada, porque queda perfectamente explicado.
Comparadlo con el código inicial:
|
|
Ventajas
A parte de la más obvia, que es la estética, podemos ver otra serie de ventajas.
Como he podido reducirlo tanto, al realizar las pruebas puedo probar cada función por separado y, después probar la función principal. Resultará mucho más fiable, y me será más sencillo identificar los posibles caminos.
Además, con un poco de suerte encuentro código que puedo reutilizar. En el mejunje inicial, hacer esto no era difícil: era imposible.
Reflexiones
Hay quien puede criticarme diciendo que no merece la pena dedicar tanto tiempo a un programa que ya funcionaba. Y tienen razón. A medias.
Claro, hay quien dirá que soy un fanático del agilismo y que presto demasiada atención al código. “El código no es lo importante, sino la arquitectura”.
Tan solo veamos lo que ocurriría en un entorno real, en el que el cliente se replantea las cosas a medida que el desarrollador va programando. Suponeros que, en este punto, el cliente dice que quiere que los múltiplos de 7 se reemplacen por “TOZZ”, los de 3 y 7 por “FIZZTOZZ”, los de 5 y 7 por “BUZZTOZZ” y los de 3, 5 y 7 por “FIZZBUZZTOZZ”. ¿Cómo os sentiríais si tenéis que modificar cada uno de los códigos?
Este supuesto no me lo he sacado de la manga. Es la razón de fracaso de la mayor parte de los proyectos software.
El ejemplo sirve también para demostrar por qué hay que hacer pruebas. Todo lo expuesto en este artículo es irrealizable sin una batería de pruebas. Si no, ¿quién me asegura que, ante una modificación sencilla, el código sigue comportándose de la misma manera? Fijáos en el nuevo entorno, en el que el cliente ha solicitado el “TOZZ”. ¿Quién me asegura que el algoritmo inicial sigue funcionando? Pues únicamente mis tests.
Y hay quien puede criticarme diciendo que hay soluciones mejores. Y tienen razón. No sé quién dijo que “todo es posible con tiempo infinito y recursos ilimitados”. En el punto en el que lo he dejado considero que es suficientemente aceptable como para poder parar.
Y también hay quien puede decirme que, después de todo, sigo usando condicionales. Sí, elegí mal el título del artículo :D pero la intención de deshacerme de estos condicionales es la que me ha llevado a dejar el código más limpio.
¡Quiero más!
Llegar hasta aquí ya es un éxito. Ahí arriba había mucho código y entiendo que puede resultar difícil de seguir. Si lo has conseguido, ¡enhorabuena!
Si te has quedado diciendo: “Joder, cómo mola. ¡Quiero más!”, debo decir que no tengo ningún mérito. Todo está en el Clean Code , de Robert C. Martin (AKA @unclebobmartin o en el ‘Diseño Ágil con TDD’ , de Carlos Blé (AKA @carlosble ). Te recomiendo que comiences por ahí y continúes por sus propias bibliografías.