14 de octubre de 2009

El antipatrón flecha y el idioma "Dí, no preguntes"

Porque a un objeto nunca se le deben preguntar las cosas... ¡y menos sobre sus partes íntimas!

Hace algún tiempo tenía una conversación con mi amigo Alcides, respecto a unos métodos que él estaba implementando para la capa de datos de un proyecto en el que estaba trabajando. De inmediato noté una configuración familiar: el antipatrón flecha.

Básicamente lo que vemos en un antipatrón flecha es algo así:

   if foo then begin
     if bar then begin
       if baz then begin
         DoSomething()
       end{if}
     end{if}
   end;{if}

Como pueden ver, la disposición del código es algo parecido a la forma de una flecha y de ahí su nombre.

Ahora bien, ¿porqué se considera un antipatrón? Hay varias razones para ello. En primer lugar, obscurece el propósito del código. Es realmente difícil entender que es lo que se está tratando de hacer en medio de esa maraña de if's anidados (si piensan que esto no esta tan mal, agreguen los else's correspondientes). Lo que es más, los seres humanos solo tenemos una capacidad limitada para seguir mentalmente los detalles de lo que estamos haciendo. Por lo general, más allá de 3 niveles de anidación las cosas se vuelven muy difíciles de seguir.

Las causas de que este patrón son muchas y variadas:

  • Seguir ciegamente la regla de "un solo punto de entrada y un solo punto de salida por función".
  • Combinar condiciones complejas y ciclos.
  • Lenguajes de programación sin evaluación booleana por circuito corto.
  • Programadores que no saben que significa el punto anterior... ejem... but I digress...
  • Simple y llana "cultura".

No voy a entrar en una controversia teológica sobre el punto uno, ya que para muchas personas es tan intocable como la divinidad de Elvis Presley.

En código escrito en lenguajes estructurados (sin soporte para objetos), este código es muy común y realmente es difícil llegar a una alternativa para el mismo, fuera de la factorización continua y cuidadosa. Sin embargo, una de las causas de raíz por las que siguen surgiendo incluso en código orientado a objetos, es porque seguimos pensando en términos estructurados.

En lenguajes estructurados, normalmente pensamos de la siguiente manera: llamar-función, obtener-resultado, evaluar-resultado, decisión. Esto está bien en un modelo que favorece el control centralizado de todo lo que sucede en mi programa, sin embargo no están adecuado para un modelo orientado a objetos.

Cuando pensamos en objetos, debemos recordar que estos forman una "red" de entidades que colaboran para lograr un objetivo. Cuando un objeto necesita realizar una acción y para ello requiere del servicio de otro objeto, simplemente va y le pide a aquel objeto que realice la acción necesaria de su parte.

Esto es similar a lo que podría suceder en una oficina cualquiera:

  • Si el jefe es una persona que anima a sus subalternos a tomar responsabilidad de su trabajo, es más probable que delegue en su equipo las actividades para las que están mejor calificados, dejando libre tiempo para que él se concentre en la visión más amplia de las cosas y la planeación estratégica.
  • En cambio, si el jefe es del tipo "controlador", querrá seguir paso a paso cada una de las acciones que llevan a cabo sus empleados, literalmente "respirando sobre sus hombros", para "asegurarse que no cometan errores".

Ok con la nota social, pero ¿Qué tiene que ver eso con los objetos?. Bueno, veamos un caso típico.

Cuando programamos operaciones en una base de datos, es común el manejo de transacciones lógicas, como por ejemplo, al guardar un pedido, se crea una entrada en la tabla de pedidos y una en la tabla de detalle por cada elemento en el pedido. Si por alguna razón alguna de las partes no puede guardarse, toda la transacción debiera abortarse. Ok, una forma típica de codificar esto es:

Conexion.Begin();
...
if Orden.GuardaNueva() then begin
  if Orden.GuardaDetalle() then begin
    if Cliente.IncrementaSaldo(Order.Total()) then begin
      Conexion.Commit()
    end{if}
    else begin
      MensajeError := "No se pudo actualizar el saldo del" +
                      " cliente";
      Conexion.Rollback()
    end{else}
  end{if}
  else begin
    MensajeError := "No se pudo guardar el detalle de la" +
                    " orden";
    Conexion.Rollback()
  end{else}
end;{if}
else begin
  MensajeError := "No se pudo crear la orden";
  Conexion.Rollback()
end{else}

Nada bonito, ¿verdad? Sin embargo, lo de menos es la apariencia. Estas cosas tienen la pésima costumbre de crecer. Es difícil seguir la lógica. Y ¿qué tal si falla la primera condición? ¡Miren hasta donde está su else!

Una opción puede ser aplicar el consejo de Brian Kernighan en The Practice of Programming e invertir el sentido de los if's y unirlos mediante else's:

Conexion.Begin();
...
if not Orden.GuardaNueva() then begin
  MensajeError := "No se pudo actualizar el saldo del" + 
                  "cliente";
  Conexion.Rollback()
end{if}
else if not Orden.GuardaDetalle() then begin
  MensajeError := "No se pudo guardar el detalle de la" +
                  "orden";
  Conexion.Rollback()
end{if}
else if not Cliente.IncrementaSaldo(Order.Total()) then begin
  MensajeError := "No se pudo crear la orden";
  Conexion.Rollback()
end;{if}
else begin
  Conexion.Commit()
end{if}

esto es definitivamente una mejora. Sin embargo, ¿qué pasa si hacemos esto mismo a la usanza de la POO? Tendremos lo siguiente:

Conexion.Begin();
...
Orden.GuardaNueva();
Orden.GuardaDetalle();
Cliente.IncrementaSaldo(Order.Total());
Conexion.Commit();

Esta es la esencia del idioma "tell, don't ask". No necesitamos estar micro-administrando a cada objeto que participa en nuestro código. No debemos hacerlo.

Está bien usar funciones y propiedades para obtener el estado de un objeto, siempre y cuando no se utilize el resultado para tomar decisiones «fuera» de dicho objeto. Cualquier decisión basada enteramente en el estado de un objeto debe hacerse «dentro» del objeto en sí.

Cada objeto tiene sus propias responsabilidades, incluyendo la de verificar que las cosas hayan salido bien y notificar en caso contrario—la forma en que normalmente se hace esto es mediante el uso de “signals” o excepciones, pero eso es material de otro post.

Saludos

No hay comentarios:

Publicar un comentario