Diseño de software

El principio de la mínima sorpresa: por qué getUser no debe enviar correos

¿Alguna vez te has encontrado con una función así durante una revisión de código?

4 min de lectura
Imagen de portada de El principio de la mínima sorpresa: por qué getUser no debe enviar correos

¿Alguna vez te has encontrado con una función así durante una revisión de código?

Se llamaba getUser(), pero al abrirla descubrí que consultaba al usuario, actualizaba la hora del último acceso, renovaba la caché y hasta enviaba un correo de notificación si la cuenta estaba inactiva.

Si, basándote solo en el nombre, la hubieras llamado dentro de un bucle pensando «solo consulta datos, así que será segura», ¿no habrías enviado una avalancha de correos a los usuarios?

El principio que veremos hoy es el principio de la mínima sorpresa, que ayuda a detectar este tipo de código. Es parte de nuestra serie sobre principios de desarrollo, después de KISS, DRY, YAGNI y SOLID, y el de hoy tiene un nombre especialmente divertido.

El principio de la mínima sorpresa (Principle of Least Astonishment, POLA) afirma lo siguiente:

El código debe comportarse como esperan quienes lo leen. Si un diseño sorprende a sus usuarios, hay que replantearlo.

Es un principio antiguo surgido del diseño de sistemas de las décadas de 1960 y 1970, pero se aplica ampliamente al diseño de UI y API, así como al código cotidiano.


¿Por qué la sorpresa tiene un coste?

En programación, la «sorpresa» no es un problema emocional, sino de costes.

Al leer código, los desarrolladores formulan hipótesis basadas en los nombres y las convenciones. getUser seguramente solo consulta datos y isValid seguramente devuelve un booleano. Cuando el código se comporta según la hipótesis, se lee sin esfuerzo.

Pero en cuanto la hipótesis falla, hay que detener la lectura, entrar en la función y comprobar qué hace realmente. Aunque solo haya diez funciones así en un código base, aparece la desconfianza: «No puedo fiarme de los nombres de este código». Desde entonces, tendrás que abrir todas las funciones mientras lees. Tu velocidad de lectura se reduce a la mitad.

Lo peor ocurre cuando usas el código según tu hipótesis sin comprobarlo y provocas una incidencia, como la avalancha de correos anterior.


Cuándo aparece la sorpresa en el código

Voy a resumirlo a partir de patrones que he experimentado en la práctica.

1. Funciones cuyo nombre no coincide con su comportamiento

// El nombre indica una consulta, pero modifica el estado
func getUser(id: Int) -> User {
    let user = db.find(id)
    user.lastSeenAt = Date()  // Sorpresa 1: Efecto secundario
    db.save(user)
    if user.isDormant {
        mailer.send(to: user.email, message: "Aviso de reactivación de una cuenta inactiva")  // Sorpresa 2: Llamada externa
    }
    return user
}

Las escrituras ocultas en funciones de consulta son una fuente clásica de sorpresas. get debería limitarse a leer; si modifica el estado, usa un nombre como update o touch.

2. Valores de retorno que traicionan las convenciones

Si en el mismo código base una función devuelve nil cuando no encuentra algo, otra lanza una excepción y otra devuelve un objeto vacío, quienes llaman a esas funciones tienen que apostar cada vez. Es importante unificar el comportamiento.

3. Fallos silenciosos

func parseConfig(_ json: Data) -> Config {
    guard let config = try? JSONDecoder().decode(Config.self, from: json) else {
        return Config()  // Sorpresa: la configuración está dañada, pero se devuelve una configuración vacía como si no hubiera pasado nada
    }
    return config
}

Si el archivo de configuración está dañado y el sistema vuelve silenciosamente a los valores predeterminados, el usuario sufrirá más tarde preguntándose: «¿Por qué no se aplicó mi configuración?». Los fallos sorprenden menos cuando se notifican claramente.

Cuando el nombre y el comportamiento difieren, quien lee paga el precio
Cuando el nombre y el comportamiento difieren, quien lee paga el precio

Cómo escribir código menos sorprendente

Estas son las prácticas que intento seguir.

Práctica Contenido
El nombre es un contrato Haz solo lo que promete el nombre; si haces más, cambia el nombre
Sigue las convenciones No te apartes sin motivo del estilo existente del lenguaje, el framework o el equipo
Haz visibles los efectos secundarios El nombre debe dejar claro que la función modifica el estado
Documenta lo que vaya a sorprender Si un comportamiento inusual es inevitable, anúncialo claramente en los comentarios y la documentación

La segunda es la práctica más poderosa. Frente a los enfoques ingeniosos y personales, los métodos estándar y aburridos siempre ganan cuando se considera al equipo completo. Es exactamente la misma idea que en el episodio anterior sobre KISS: un bucle normal es mejor que una «línea de código espectacular».

Al final, el código aburrido y predecible gana
Al final, el código aburrido y predecible gana

Para terminar

Hay tres cosas que recordar sobre el principio de la mínima sorpresa.

  • El código debe comportarse según las expectativas creadas por sus nombres y convenciones. El código que rompe esas expectativas es un caldo de cultivo para los errores.
  • No ocultes efectos secundarios en las funciones de consulta ni te tragues los fallos en silencio.
  • En un equipo, el código aburrido y predecible gana al código ingenioso.

Si tuviera que elegir un único criterio para el buen código, elegiría este: código que no sorprende a quien lo lee.

Si recientemente leíste código y pensaste «¿Eh? ¿Por qué funciona así?», ese fue precisamente un momento en el que se rompió este principio.