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:

if condicion1:
  if condicion2:
    accion1
else:
  if condicion3:
    accion3
  else:
    accion4

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 True False False False False True True False False True True False False True False True False True False True False accion1 accion1 -- -- accion3 accion4 accion3 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.

def funcion():
  if condicion1:
    if condicion2:
      accion1
  else:
    if condicion3:
      accion3
    else:
      accion4

Evitando "ELSEs"

Una vez aislado, evitaremos los ELSE. La mejor manera es abandonar la función cuando ya no nos interesa nada más:

def funcion1():
  if condicion1:
    if condicion2:
      accion1
    return

  if condicion3:
    accion3
    return

  accion4

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:

def funcion1():
  if condicion1 and not condicion2:
    return

  if condicion1 and condicion2:
    accion1
    return

  if condicion3:
    accion3
    return

  accion4

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:

def funcion1():
  if shouldExit():
    return

  if shouldApplyAction1():
    accion1
    return

  if condicion3:
    accion3
    return

  accion4

def shouldExit():
  return condicion1 and not condicion2

def shouldApplyAction1():
  return condicion1 and condicion2

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":

def fizzbuzz(n):
  for i in xrange(n):
    if (i+1) % 3 == 0:
      if (i+1) % 5 == 0:
        print 'FIZZBUZZ'
      else:
        print 'FIZZ'
    elif (i+1) % 5 == 0:
      print 'BUZZ'
    else:
      print i+1

No se ven, pero tenemos 3 condiciones anidadas, así que dividimos funciones:

def fizzbuzz(n):
  for i in xrange(n):
    printNumber(i+1)

def printNumber(n):
    if n % 3 == 0:
      if n % 5 == 0:
        print 'FIZZBUZZ'
      else:
        print 'FIZZ'
    elif n % 5 == 0:
      print 'BUZZ'
    else:
      print n

Como veis, esta simple acción me ha permitido reducir todos los "i+1" que tenía. Eliminemos los ELSEs:

def fizzbuzz(n):
  for i in xrange(n):
    printNumber(i+1)

def printNumber(n):
  if n % 3 == 0:
    if n % 5 == 0:
      print 'FIZZBUZZ'
      return
    print 'FIZZ'
    return

  if n % 5 == 0:
    print 'BUZZ'
    return

  print n

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:

def fizzbuzz(n):
  for i in xrange(n):
    print solveNumber(i+1)

def solveNumber(n):
  if n % 3 == 0:
    if n % 5 == 0:
      return 'FIZZBUZZ'
    return 'FIZZ'

  if n % 5 == 0:
    return 'BUZZ'

  return n

Busco contratos:

def fizzbuzz(n):
  for i in xrange(n):
    print solveNumber(i+1)

def solveNumber(n):
  if n % 3 == 0 and n % 5 == 0:
      return 'FIZZBUZZ'

  if n % 3 == 0:
    return 'FIZZ'

  if n % 5 == 0:
    return 'BUZZ'

  return n

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:

def fizzbuzz(n):
  for i in xrange(n):
    print solveNumber(i+1)

def solveNumber(n):
  if isFizzBuzz(n):
    return 'FIZZBUZZ'

  if isFizz(n):
    return 'FIZZ'

  if isBuzz(n) == 0:
    return 'BUZZ'

  return n

def isFizzBuzz(n):
  return n % 3 == 0 and n % 5 == 0

def isFizz(n):
  return n % 3 == 0

def isBuzz(n):
  return n % 5 == 0

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:

def fizzbuzz(n):
  for i in xrange(n):
    if (i+1) % 3 == 0:
      if (i+1) % 5 == 0:
        print 'FIZZBUZZ'
      else:
        print 'FIZZ'
    elif (i+1) % 5 == 0:
      print 'BUZZ'
    else:
      print i+1

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.


Comentarios

Comments powered by Disqus