Diseño de declaraciones if: las cláusulas de protección pueden ser todo lo que necesita

Existe un gran debate sobre cómo se deben usar las declaraciones if para una mejor claridad y legibilidad del código. La mayoría de ellos se reduce a una opinión que es completamente subjetiva y estética. Y generalmente eso es cierto, pero a veces todavía podemos encontrar un terreno común. Esta publicación tiene como objetivo derivar un pequeño conjunto de consejos para algunos de estos casos. Con suerte, esto reducirá la cantidad de pensamiento y debate al escribir algo tan simple como una declaración if.

Cláusulas de guardia

Probablemente el caso más documentado es la cláusula de guardia, también conocida como afirmación o condición previa. La idea es que cuando tenga algo que afirmar al comienzo de un método, haga esto con un retorno rápido.

ejecución nula pública (temporizador) {
  if (! timer.isEnabled ())
    regreso;
  
  if (! timer.valid ())
    lanzar nuevo InvalidTimerException ();
  timer.run ();
}

A pesar de parecer obvio, a menudo se deja atrás debido al hecho de que nuestros cerebros funcionan de manera diferente: tendemos a pensar "ejecutar el temporizador si está habilitado", no "ejecutar el temporizador, pero si no está habilitado no hacer nada". Sin embargo, en el código, este último conduce a una mejor separación de los casos de esquina de la lógica del método principal, si seguimos estrictamente que solo algo afirmado al principio puede considerarse una cláusula de protección.

Como parece, la cláusula de guardia no es una idea nueva. Sin embargo, lo veo como uno de los más directos y simples, que puede aceptarse sin mucho debate. Pero la mayor revelación para mí fue que la mayoría de los casos en los que se usa la declaración if se pueden transformar para usar una cláusula de guardia, y esto es lo que voy a explicar.

Bloques if grandes

Es bastante común encontrar algo como lo siguiente en una base de código:

if (something.isEnabled ()) {
  // bonita
  // largo
  // lógica
  // de
  // corriendo
  // alguna cosa
}

Por lo general, no está diseñado de esta manera desde el principio. En cambio, tales bloques aparecen cuando aparece un nuevo requisito que dice "deberíamos ejecutar ese algo solo en el caso de X". La solución más directa, por supuesto, sería envolver la parte "ejecutar ese algo" en una declaración if. Fácil.

El problema es que en realidad solo es fácil escribir, pero no leer. Vea, para comprender el código, uno necesita derivar la imagen completa de él, un concepto con el que trabajar aún más, porque los humanos son mucho mejores para operar conceptos, no algoritmos. La introducción de este tipo de condición al principio crea un contexto adicional, que debe tenerse en cuenta al trabajar con la lógica del método principal. Obviamente, debemos esforzarnos por reducir la cantidad de contexto mental requerido en cualquier momento dado.

Otro gran problema surge cuando la lógica de dicho código se vuelve lo suficientemente larga como para no encajar la pantalla verticalmente. En algún momento, incluso puede olvidar que está allí, lo que rompería completamente su imagen.

Para evitar todo esto, solo necesita invertir la declaración if para que la condición se convierta en una cláusula de protección:

if (! something.isEnabled ())
  regreso;
// mismo
// largo
// lógica
// de
// corriendo
// alguna cosa

Una vez más, hay una separación de preocupaciones: primero te deshaces de las condiciones previas al principio (también las descartas de tu mente, lo cual es importante para enfocarte mejor en la lógica principal), y luego simplemente haces lo que el método requiere hacer.

Volviendo en el medio

Los retornos múltiples se consideran una mala idea por razones, y este es probablemente uno de los más grandes. Contrariamente a un retorno en una cláusula de guardia, un retorno o lanzamiento en el medio del método no se detecta tan fácilmente a primera vista. Como cualquier otra lógica condicional, complica el proceso de construir una imagen mental del método. Además, requiere que tenga en cuenta que, en algunas circunstancias, la lógica principal de este método no se ejecutará por completo, y cada vez que tenga que decidir: ¿debe poner el nuevo código antes o después de esta devolución?

Aquí hay un ejemplo de un código real:

public void executeTimer (String timerId) {
  logger.debug ("Ejecución de temporizador con ID {}", timerId);
  TimerEntity timerEntity = timerRepository.find (timerId);
  logger.debug ("Found TimerEntity {} para timer ID {}", timerEntity, timerId);
  if (timerEntity == null)
    regreso;
  Temporizador = Timer.fromEntity (timerEntity);
  timersInvoker.execute (temporizador);
}

Parece una condición previa, ¿no? Pongámoslo donde debe ser una condición previa:

public void executeTimer (String timerId) {
  logger.debug ("Ejecución de temporizador con ID {}", timerId);
  TimerEntity timerEntity = timerRepository.find (timerId);
  logger.debug ("Found TimerEntity {} para timer ID {}", timerEntity, timerId);
  executeTimer (timerEntity);
}
private void executeTimer (TimerEntity timerEntity) {
  if (timerEntity == null)
    regreso;
  Temporizador = Timer.fromEntity (timerEntity);
  timersInvoker.execute (temporizador);
}

Este código refactorizado es mucho más fácil de leer, ya que no hay condiciones en absoluto en la lógica principal, solo hay una mera secuencia de acciones simples. Lo que quería mostrar es que casi siempre es posible dividir un método que tiene un retorno en el medio para que las condiciones previas estén bien separadas de la lógica principal.

Pequeñas acciones condicionales

También es una práctica común tener muchas declaraciones condicionales pequeñas como esta:

if (timer.getMode ()! = TimerMode.DRAFT)
  timer.validate ();

Esto es totalmente legítimo en el caso general, pero a menudo es solo una condición previa oculta que debería colocarse mejor en un método en sí mismo. Probablemente notará esto más adelante, cuando cada vez que invoque el método necesite agregar esta instrucción if porque la lógica de negocios lo dice. Considerando eso, la solución adecuada sería:

public void validate () {
  if (modo == TimerMode.DRAFT)
    regreso;
  
  // lógica de validación
}

Y el uso ahora es tan simple como:

timer.validate ();

Conclusión

El uso de cláusulas de protección es una buena práctica para evitar ramificaciones innecesarias y, por lo tanto, hacer que su código sea más sencillo y legible. Creo que si estos pequeños consejos se consideran sistemáticamente, disminuirá en gran medida el costo de mantenimiento de su software.

Y, por último, en general, no hay nada malo en tener una declaración condicional aquí o allá en su código. Aunque mi experiencia muestra que si nunca te importa tratar de evitar eso, en algún momento terminarás desperdiciando horas construyendo una imagen mental de un código complejo altamente ramificado mientras intentas poner lo siguiente que una empresa quiere. .