El conocimiento es el nuevo dinero.
Aprender es la nueva manera en la que inviertes
Acceso Cursos

9 patrones de código para ingenieros senior

Eliminaban la complejidad innecesaria. Hacían que los fallos fueran predecibles. Mantenían las funciones pequeñas.

· 6 min de lectura
9 patrones de código para ingenieros senior

La mayoría de los desarrolladores no mejoran por aprender más sintaxis. Mejoran cuando aprenden a pensar como ingenieros con experiencia.

Durante mucho tiempo pensé que convertirme en una mejor desarrolladora significaba aprender más.

Más Python. Más frameworks. Más librerías. Más patrones de diseño.

Pero después de pasar más tiempo rodeada de ingenieros con experiencia, noté algo que cambió mi forma de abordar la programación.

Los mejores desarrolladores no necesariamente escribían código más complicado.

De hecho, muchas veces hacían exactamente lo contrario.

Eliminaban la complejidad innecesaria. Hacían que los fallos fueran predecibles. Mantenían las funciones pequeñas. Cuestionaban las abstracciones. Escribían código que otra persona pudiera entender sin necesidad de organizar una reunión para explicarlo.

Con el tiempo, empecé a adoptar esos hábitos.

Estos son nueve patrones de programación que aprendí de ingenieros senior y que han tenido un impacto mucho mayor en mis habilidades como desarrolladora que aprender otro truco de sintaxis.

1. Haz que el camino principal sea evidente

Una de las primeras cosas que noté fue lo poco que les gusta a los ingenieros senior el anidamiento innecesario.

Quienes están empezando suelen escribir código como este:

if user:
    if user.is_active:
        if user.has_permission:
            process_request(user)

Funciona, pero la operación importante queda escondida entre varias capas de condiciones.

Un enfoque basado en cláusulas de guarda hace que el flujo normal sea mucho más fácil de seguir:

if not user:
    return

if not user.is_active:
    return

if not user.has_permission:
    return

process_request(user)

Ahora la función se lee casi como una lista de comprobación.

Primero verificas las condiciones que pueden detener la ejecución y después realizas el trabajo real.

No se trata de seguir una regla que diga que todas las funciones deben utilizar cláusulas de guarda. Se trata de reducir la carga mental innecesaria.

Cuando alguien abra tu código seis meses después, debería poder entender el flujo principal sin tener que desenredar mentalmente un laberinto de sentencias if.

El buen código hace que lo importante sea fácil de ver.

2. Valida los datos en los límites del sistema

Una cantidad sorprendentemente grande de la complejidad de una aplicación aparece porque permitimos que datos no válidos avancen demasiado dentro del sistema.

Imagina una API que recibe información de usuarios.

En lugar de aceptar un diccionario cualquiera y comprobar su contenido en diferentes partes de la aplicación:

def create_user(data):
    # validación distribuida por toda la función
    ...

define desde el principio cómo deben ser los datos válidos.

Por ejemplo, utilizando Pydantic:

from pydantic import BaseModel, EmailStr

class User(BaseModel):
    name: str
    email: EmailStr
    age: int

De esta forma, las entradas no válidas pueden rechazarse antes de que lleguen a la lógica de negocio.

El mismo principio se aplica a la configuración, los argumentos de línea de comandos, los registros de bases de datos, los archivos subidos y las respuestas de servicios externos.

Cuanto antes establezcas un contrato fiable, menos código defensivo necesitarás en el resto de la aplicación.

Valida una sola vez en el límite del sistema, en lugar de cuestionar constantemente los datos en toda la aplicación.

3. Mantén las funciones aburridas

Antes pensaba que una función capaz de hacer cinco cosas era eficiente.

Normalmente, solo era difícil de mantener.

Por ejemplo:

def process_order(order):
    # validar pedido
    # calcular precio
    # guardar en la base de datos
    # enviar correo electrónico
    # escribir registro
    ...

Técnicamente, la función funciona, pero tiene demasiadas responsabilidades.

Un diseño más limpio separa las operaciones:

def validate_order(order):
    ...

def calculate_total(order):
    ...

def save_order(order, total):
    ...

def send_confirmation(order):
    ...

def process_order(order):
    validate_order(order)

    total = calculate_total(order)
    save_order(order, total)
    send_confirmation(order)

Ninguna de estas funciones resulta especialmente impresionante.

Y precisamente por eso me gusta este patrón.

Cada función tiene un propósito claro. Cada una puede probarse de manera independiente. Y cuando cambian los requisitos, tienes menos elementos que desenredar.

Los ingenieros senior suelen escribir código que, cuando lo ves por primera vez, puede parecer incluso aburrido.

Normalmente, eso es una ventaja, no un defecto.

Si una función es fácil de explicar en una sola frase, probablemente también sea más fácil de mantener.

4. Dale a cada componente una responsabilidad clara

Una lección relacionada con la anterior consiste en saber dónde debe vivir cada funcionalidad.

Observa este ejemplo:

def get_users():
    response = requests.get(API_URL)
    users = response.json()

    with open("users.json", "w") as file:
        json.dump(users, file)

    return users

Es práctico, pero la función está realizando dos trabajos que no están directamente relacionados:

  1. Obtener los usuarios.
  2. Guardar los usuarios.

Separar esas responsabilidades crea límites más claros:

def fetch_users():
    response = requests.get(API_URL)
    response.raise_for_status()
    return response.json()

def save_users(users):
    with open("users.json", "w") as file:
        json.dump(users, file)

Ahora puedes cambiar el mecanismo de almacenamiento sin tocar la lógica relacionada con la red.

Quizás mañana los datos deban almacenarse en PostgreSQL en lugar de en un archivo JSON.

Ese cambio no debería obligarte a reescribir el código responsable de comunicarse con la API.

Esta es una de las ideas más sencillas de la ingeniería de software, pero se vuelve cada vez más importante a medida que las aplicaciones crecen.

Un componente debería tener un motivo para cambiar, no cinco motivos diferentes.

5. Trata los fallos como un resultado normal

El código de quienes están empezando suele asumir que todo va a funcionar.

El código en producción no puede permitirse esa suposición.

Piensa en una petición a una API:

response = client.get(url)
data = response.json()

¿Qué ocurre si el servidor no está disponible?

¿Qué pasa si la solicitud supera el tiempo de espera?

¿Qué sucede si el servidor devuelve un error 500?

¿Y si la respuesta no contiene un JSON válido?

Una implementación más cuidadosa podría verse así:

try:
    response = client.get(url, timeout=10)
    response.raise_for_status()
    data = response.json()

except TimeoutError:
    logger.error("La solicitud superó el tiempo de espera")

except ValueError:
    logger.error("Se recibió una respuesta no válida")

Las excepciones exactas y la estrategia de recuperación dependerán de cada aplicación.

Lo importante es la forma de pensar.

Las redes fallan.

Los archivos desaparecen.

Los usuarios introducen datos inesperados.

Las bases de datos dejan de estar disponibles.

Los servicios externos cambian.

Los ingenieros con experiencia no tratan estas situaciones como casos extremos imposibles. Deciden qué debe hacer la aplicación cuando ocurren.

Un software fiable no es un software que nunca falla. Es un software que falla de manera predecible.

6. Mantén la configuración separada de la lógica de negocio

Una configuración escrita directamente en el código rara vez parece peligrosa al principio.

API_URL = "https://api.example.com"
TIMEOUT = 10
MAX_RETRIES = 3

El problema empieza cuando la configuración se extiende por todo el proyecto.

Con el tiempo, terminas teniendo URLs de bases de datos en un módulo, endpoints de APIs en otro, indicadores de funcionalidades en otro lugar y valores específicos de cada entorno escondidos dentro de funciones.

Un enfoque más limpio consiste en establecer un límite claro para la configuración.

Por ejemplo:

import os

API_URL = os.getenv("API_URL")
TIMEOUT = int(os.getenv("TIMEOUT", "10"))
MAX_RETRIES = int(os.getenv("MAX_RETRIES", "3"))

Ahora la lógica de la aplicación no necesita saber en qué entorno se está ejecutando.

Desarrollo, pruebas y producción pueden proporcionar valores diferentes sin necesidad de modificar el código fuente.

El principio general es importante:

El código debería describir el comportamiento. La configuración debería describir el entorno.

7. Prefiere la composición antes que las abstracciones prematuras

Una de las trampas más fáciles en las que podemos caer mientras crecemos como desarrolladores es intentar que el código parezca demasiado “arquitectónico” demasiado pronto.

Descubres las clases, la herencia, las factorías, las interfaces y los patrones de diseño.

De repente, un problema de 30 líneas termina teniendo 12 clases.

Los ingenieros con experiencia suelen ser mucho más cuidadosos con esto.

Muchas veces empiezan con componentes pequeños y los combinan:

validator = Validator()
repository = Repository()
notifier = Notifier()

service = UserService(
    validator=validator,
    repository=repository,
    notifier=notifier,
)

Cada componente tiene una responsabilidad concreta, pero la aplicación los combina para realizar una operación más grande.

Esto también facilita las pruebas.

Puedes sustituir el repositorio por una implementación falsa sin modificar el resto del servicio.

La lección importante no es “evita las clases”.

Es esta:

No construyas una abstracción hasta que entiendas el problema que esa abstracción necesita resolver.

Una buena arquitectura debería surgir de requisitos reales, no del deseo de hacer que un proyecto pequeño parezca sofisticado.

8. Escribe código que sea fácil de reemplazar

Esta es una de las ideas más útiles que aprendí de desarrolladores con experiencia.

Normalmente pensamos en hacer que el código sea fácil de ampliar.

Pero conseguir que también sea fácil de reemplazar es igual de importante.

Supongamos que tu aplicación se comunica con un servicio externo mediante una única función claramente definida:

def get_exchange_rate(currency):
    ...

El resto de la aplicación no necesita saber cómo funciona ese servicio.

Si más adelante cambias de proveedor, el resto del sistema puede permanecer intacto.

Compáralo con tener llamadas a la API repartidas por decenas de archivos.

Cambiar de proveedor se convierte en un proyecto enorme.

Por eso son importantes las interfaces pequeñas y los límites bien definidos.

El objetivo no es predecir todos los requisitos futuros.

Eso es imposible.

El objetivo es conseguir que los cambios razonables sean baratos.

El buen código no elimina los cambios. Hace que cambiar sea menos doloroso.

9. Optimiza el código pensando en la persona que vendrá después

Este puede ser el patrón más importante de todos.

Cuando escribes código, sabes lo que estabas pensando.

La siguiente persona que trabaje con ese código no lo sabe.

Puede encontrarse con una variable llamada:

x = d[a]

y tendrá que averiguar qué significa.

Compáralo con:

user_email = users[user_id]

La segunda versión comunica inmediatamente cuál es su propósito.

El mismo principio se aplica a los nombres de las funciones, los mensajes de error, la estructura de los módulos, los comentarios y la configuración.

Antes de terminar una funcionalidad, ahora intento hacerme estas preguntas:

¿Esto tendría sentido para alguien que no conoce mi forma de trabajar?

¿Los nombres son descriptivos?

¿La lógica poco habitual está explicada?

¿Los errores realmente proporcionan información útil?

¿La estructura es predecible?

El mejor código no siempre es el código más corto.

Muchas veces es el código que necesita menos explicaciones.

Gracias por leer Código en Casa.
Si esto te a ayudado y te sumo algo Dale un 👏 , compártelo con tu red o dejame un comentario para saber tu opinión.