Cláusulas de guarda (guard clauses)
Abro un método que se encarga de sacar mercancía del almacén y me encuentro con esto.
public function withdraw(Order $order, int $units): void
{
if ($order->isConfirmed()) {
if ($this->hasStockFor($units)) {
if (!$order->isCancelled()) {
$this->available -= $units;
$this->registerMovement($order, $units);
}
}
}
}
La lógica que de verdad importa, la que reduce las existencias y apunta el movimiento, vive cuatro niveles hacia dentro. Para llegar hasta ella tengo que ir aceptando condiciones en la cabeza: vale, el pedido está confirmado, vale, hay stock, vale, no está cancelado. Y cuando por fin llego a la línea que hace algo, ya no me acuerdo bien de cómo he llegado. Luego, al final, una escalera de llaves que cierran sin decir cuál cierra qué. El método no es difícil por lo que hace. Es difícil por la forma que tiene.
Ese anidamiento es un vicio que se cuela solo. Cada regla nueva que aparece se envuelve en otro if, y la sangría crece un escalón más hacia la derecha. Nadie decide escribir una pirámide, se va formando sola, condición a condición, hasta que el cuerpo del método está tan adentro que hay que leerlo con el dedo puesto en la pantalla.
Dar la vuelta a la pregunta
Una cláusula de guarda es un cambio pequeño de punto de vista. En lugar de preguntar "si todo va bien, hago el trabajo", preguntas "si algo va mal, me largo". Le das la vuelta a la condición y sales cuanto antes. Lo que quedaba dentro de un if gigante pasa a ser el cuerpo normal del método, a la izquierda del todo, donde se lee sin esfuerzo.
public function withdraw(Order $order, int $units): void
{
if (!$order->isConfirmed()) {
return;
}
$this->available -= $units;
$this->registerMovement($order, $units);
}
Fíjate en lo que ha pasado. La condición ya no abraza a todo el método. Ahora es una frase corta al principio que dice "si el pedido no está confirmado, aquí no hay nada que hacer". Descartas el caso raro de un plumazo y el resto del método respira, porque a partir de esa línea sabes que estás trabajando con un pedido confirmado. La guarda no solo aplana la sangría, separa lo que no puede pasar de lo que pasa siempre.
El escalón que no hace falta
Cuando sales pronto, el else deja de tener sentido. Es una consecuencia bonita que mucha gente no ve a la primera. Si una rama termina en return, todo lo que viene después ya es la otra rama, sin necesidad de anunciarlo. Piensa en el precio de una línea de factura, que cambia según el cliente sea mayorista o de tienda. Con if y else encajados se escribiría de la siguiente forma.
public function priceFor(Order $order): Money
{
if ($order->isWholesale()) {
return $this->wholesalePrice($order);
} else {
return $this->retailPrice($order);
}
}
Las dos ramas cuelgan de la misma condición y arrastran su propio bloque con su cierre a juego. Dice lo que tiene que decir, pero te obliga a sostener las dos posibilidades a la vez para saber por dónde sale cada caso. En cuanto la primera rama termina en return, el else ya no aporta nada, así que lo puedes quitar tranquilamente.
public function priceFor(Order $order): Money
{
if ($order->isWholesale()) {
return $this->wholesalePrice($order);
}
return $this->retailPrice($order);
}
No hay else. No hace falta. La primera guarda se lleva el caso del mayorista y se va, así que la última línea es, sin más explicaciones, el precio de tienda. Es el mismo comportamiento con un escalón menos. Cada caso se resuelve y desaparece, y tú solo cargas con uno cada vez.
Varias guardas, una detrás de otra
Un método serio rara vez tiene una sola precondición. La salida de stock de verdad quiere que el pedido esté confirmado, que la cantidad tenga sentido y que haya existencias suficientes. En lugar de anidar esas tres comprobaciones, las pones en fila al principio, cada una con su salida. Si algo no cuadra, se corta ahí mismo, antes de tocar nada.
public function withdraw(Order $order, int $units): void
{
if (!$order->isConfirmed()) {
throw OrderNotReady::forWithdrawal($order->id());
}
if ($units <= 0) {
throw new InvalidArgumentException(
'Withdrawal units must be positive.'
);
}
if ($units > $this->available) {
throw InsufficientStock::forWithdrawal($units, $this->available);
}
$this->available -= $units;
$this->registerMovement($order, $units);
}
Léelo de arriba abajo y verás que el método se cuenta solo. Primero las reglas que hay que cumplir, cada una en su línea. Después, y solo después, el trabajo. Las guardas hacen de aduana en la frontera del método. Filtran lo que no debería entrar y dejan el cuerpo limpio. Cuando llegas a la resta ya no queda ningún "¿y si...?" pendiente, porque todos los "¿y si...?" se han quedado arriba, resueltos.
Guardas que hablan
Habrás notado que un par de esas guardas no devuelven, lanzan. Y lo hacen con excepciones que dicen su nombre: OrderNotReady::forWithdrawal, InsufficientStock::forWithdrawal. No es casualidad. Una guarda es el sitio natural donde nombrar aquello que no puede pasar, y una excepción con nombre convierte ese corte en una frase del negocio. Quien lee la línea entiende qué se ha violado sin bajar a leer las tripas de nada.
Compáralo con un throw new RuntimeException('no hay stock') montado ahí mismo. La guarda con nombre reúne en un solo punto qué error es y con qué datos se construye, y de paso deja la comprobación tan clara como el resto. La condición dice cuándo, y la excepción dice qué. Las dos ideas, la de las guardas y la de dar nombre a la creación de objetos, tiran para el mismo lado, que el código se lea como se habla.
Que la condición diga su nombre
A veces la guarda no es una comprobación sencilla, sino una mezcla de condiciones que solo tienen sentido juntas. Que un pedido pueda enviarse depende de que esté confirmado, de que esté pagado y de que no lo hayan cancelado. Meter todo eso en el paréntesis del if vuelve a ensuciar la lectura, aunque ahora sea a lo ancho y no a lo alto.
if (!($order->isConfirmed() && $order->isPaid() && !$order->isCancelled())) {
return;
}
Esa línea hay que descifrarla, y la negación de todo el grupo no ayuda. La salida es encapsular la condición en un método con nombre y dejar que la guarda pregunte en el idioma del negocio.
public function ship(Order $order): void
{
if (!$this->canBeShipped($order)) {
return;
}
// preparar el envío
}
private function canBeShipped(Order $order): bool
{
return $order->isConfirmed()
&& $order->isPaid()
&& !$order->isCancelled();
}
La guarda ahora se lee de corrido. Si el pedido no se puede enviar, salimos. El cómo se decide eso vive en un solo sitio, con su propio nombre, listo para que lo reutilices o lo afines sin tocar el método de envío. La condición ha dejado de ser un jeroglífico entre paréntesis para convertirse en una pregunta que cualquiera del almacén respondería. Y el día que cambie la regla de cuándo se puede enviar, sabes exactamente dónde ir.
La ventaja que de verdad importa
Las cláusulas de guarda no añaden ninguna funcionalidad. El método sigue haciendo lo mismo que hacía dentro de su pirámide de if. Lo que cambia es que ahora se entiende a la primera. Aplanan la sangría, se llevan los else que sobran, ponen las reglas por delante del trabajo y dejan que cada caso raro se resuelva y se aparte en cuanto aparece.
Eso es lo que persigue el clean code, y no por capricho estético. Un método que se lee de arriba abajo, sin retroceder ni sostener cinco condiciones en la cabeza, es un método que se mantiene sin miedo y que crece sin volverse un nudo. Cada regla nueva es una guarda más en la fila, no un escalón más hacia la derecha. La legibilidad de hoy es la facilidad de cambio de mañana, y ese es justo el terreno donde un proyecto se vuelve sostenible o se hunde en su propia complejidad.
Escribir la comprobación como una guarda que sale pronto, en lugar de como un if que lo envuelve todo, no cambia lo que el método hace. Cambia lo rápido que entiendes lo que hace.