El problema que casi nadie te cuenta
Cuando descubres los patrones de diseño hay una especie de euforia. De repente tienes nombres para cosas. Puedes ver Observers y Decorators en todas partes. Y como los ves en todas partes, empiezas a usarlos en todas partes.
Hace un tiempo revisé un PR de un compañero y me encontré con algo que me hizo detenerme. El código resolvía el problema, los tests pasaban...
pero tenía un Factory, un Strategy y un Observer para manejar básicamente tres condiciones de un if. Nadie en el equipo lo entendía a primera
lectura. Y lo único que hacía era mandar un email o un SMS según un config.
class NotificationService {
private strategy: NotificationStrategy
constructor(private readonly factory: NotificationStrategyFactory) {
this.strategy = factory.create(config.get("notificationType"))
}
notify(event: DomainEvent) {
this.strategy.handle(event)
}
}
El problema no era técnico. Era que le tenía miedo a ser simple.
Primero, escribe la solución más directa
Antes de buscar el patrón, escribe lo más directo posible. No lo más elegante. No lo más extensible. Lo más directo. Esto no es el código final, es un paso del proceso - lo que estás haciendo es entender el problema antes de abstraerlo.
function sendNotification(type: "email" | "sms", message: string) {
if (type === "email") return sendEmail(message)
return sendSms(message)
}
¿Cómo identificar cuándo un patrón sí aplica?
La respuesta honesta es: con tiempo y con haberla cagado varias veces. Pero hay un proceso.
Primero, describe el problema en palabras simples. Sin código, sin abstracciones. ¿Qué está pasando? ¿Qué necesita cambiar? ¿Qué no debe cambiar?
Segundo, observa qué duele. ¿El if tiene 15 ramas y sigue creciendo? ¿Estás tocando el mismo archivo cada vez que agregan un canal nuevo?
Cuando el if empieza a verse así, ahí es donde el patrón tiene sentido real.
function sendNotification(type: string, message: string) {
if (type === "email") return sendEmail(message)
if (type === "sms") return sendSms(message)
if (type === "push") return sendPush(message)
if (type === "slack") return sendSlack(message)
if (type === "webhook") return callWebhook(message)
}
Cuando el dolor existe de verdad, la abstracción paga su costo. Agregar un canal nuevo se convierte en crear una clase nueva sin tocar lo que ya funciona.
interface NotificationChannel {
send(to: string, message: string): Promise<void>
}
class EmailChannel implements NotificationChannel {
async send(to: string, message: string) {
await emailClient.send({ to, subject: "New notification", body: message })
}
}
class SlackChannel implements NotificationChannel {
async send(to: string, message: string) {
await slackClient.postMessage({ channel: to, text: message })
}
}
const channels: Record<string, NotificationChannel> = {
email: new EmailChannel(),
slack: new SlackChannel(),
}
function sendNotification(type: string, to: string, message: string) {
return channels[type].send(to, message)
}
La diferencia entre el primer ejemplo y este no es el patrón en sí. Es que el segundo resuelve un dolor real que ya existía.
La trampa de la sobre-ingeniería
YAGNI - You Aren't Gonna Need It - es brutalmente difícil de aplicar cuando estás diseñando. El Repository tiene sentido en un dominio complejo donde la lógica de negocio no debería saber nada de la base de datos. Tiene mucho menos sentido en un CRUD donde el 90% de las operaciones son "dame todos" y "guarda esto".
abstract class RepositoryBase<T, ID> {
abstract findById(id: ID): Promise<T | null>
abstract save(entity: T): Promise<T>
abstract delete(id: ID): Promise<void>
}
class UserRepositoryImpl extends RepositoryBase<User, string> {
async findById(id: string) {
return db.query("SELECT * FROM users WHERE id = $1", [id])
}
async save(user: User) { ... }
async delete(id: string) { ... }
}
async function getUserById(id: string) {
return db.user.findUnique({ where: { id } })
}
No es que uno sea mejor en abstracto. Es que uno resuelve el problema que tienes hoy, y el otro resuelve uno que quizás nunca tengas.
Lo que te va convirtiendo en senior
No es cuántos patrones conoces. Es la calidad de las preguntas que haces antes de escribir código.
¿Qué va a cambiar aquí? ¿Qué no debería cambiar? ¿A quién le duele si esto falla? ¿Cuántos casos de uso reales hay, no hipotéticos?
El senior no llega a una reunión de diseño con la solución. Llega con las preguntas correctas. Y eso se desarrolla despacio - a través de ver código que envejeció mal y entender por qué, de decidir agregar una abstracción y que seis meses después sea lo peor que hiciste.
La próxima vez que vayas a introducir un patrón, una pregunta simple: ¿qué problema concreto, que ya existe hoy, esto resuelve?
Si no tienes una respuesta clara, probablemente todavía no es el momento. Y eso no es señal de que eres junior. Es señal de que estás empezando a pensar bien.
Cómo se desarrolla criterio (y por qué nadie te lo puede enseñar directo)
Criterio no es saber más. Es saber qué preguntarte antes de actuar.
Un junior ve un problema y busca cómo resolverlo. Un mid ve el mismo problema y empieza a preguntarse si está resolviendo el correcto. Un senior a veces cuestiona si hay que resolverlo del todo.
Esa diferencia no viene de leer libros, aunque los libros ayudan. Viene de acumulación de consecuencias. De haber tomado decisiones, que esas decisiones tuvieron efectos, y haber prestado atención a esos efectos.
El problema es que mucha gente acumula años sin acumular criterio. Porque criterio requiere algo específico: que te importe lo que pasa después de que commiteas.
Ir a producción y ver cómo se comporta tu código bajo carga real. Mantener algo que escribiste hace ocho meses y sentir en carne propia lo que fue buena o mala decisión. Revisar código de alguien más y obligarte a entender por qué está así antes de criticarlo. Esas experiencias, si las procesas conscientemente, son las que construyen criterio.
Lo que en realidad separa un mid de un sr
No es velocidad. No es conocer más frameworks. No es escribir código más sofisticado.
Es que el senior tiene un modelo interno del sistema que va más allá del código. Entiende por qué existe esa tabla en la base de datos aunque nadie se lo haya explicado. Sabe cuándo una estimación de tres días es honesta y cuándo es optimismo disfrazado. Nota cuando un requerimiento nuevo va a romper un invariante del dominio, antes de que alguien escriba una sola línea.
Todo eso viene de haber estado presente cuando las cosas se rompieron, cuando el negocio cambió de dirección, cuando una decisión técnica que parecía razonable se convirtió en deuda que alguien tuvo que pagar meses después.
// Un junior ve esto y piensa: "funciona, siguiente"
async function processOrder(orderId: string) {
const order = await getOrder(orderId)
await chargeCard(order.cardId, order.total)
await markOrderPaid(orderId)
await sendConfirmationEmail(order.userId)
}
Un senior ve esto y pregunta: ¿qué pasa si chargeCard falla después de cobrar pero antes del markOrderPaid? ¿Qué pasa si se cae el proceso
entre el mark y el email? ¿Este código puede correr dos veces en paralelo para el mismo orderId?
No porque sea más inteligente. Sino porque ya vio qué pasa cuando esas preguntas no se hacen a tiempo.
El criterio también es saber comunicar incertidumbre
Una de las cosas más subestimadas del seniority es saber decir "no lo sé todavía" con precisión. No como excusa, sino como información útil.
"No sé cuánto va a tardar porque no entendí bien el alcance" es una respuesta de senior. "Tres días" cuando en realidad no sabes, es una respuesta de alguien que aprendió que toca dar un número pero no aprendió qué hacer con la incertidumbre real.
El criterio en la comunicación es entender que tu trabajo no termina cuando el código funciona. Termina cuando la persona que tomó la decisión de pedirte eso tiene la información que necesita para tomar la siguiente.
Criterio no se enseña, se cultiva
Se cultiva leyendo código ajeno con curiosidad, no con juicio. Se cultiva en los code reviews donde te obligas a justificar cada comentario en lugar de solo señalar lo que haría diferente. Se cultiva cuando preguntas "¿por qué está así?" antes de refactorizar, porque a veces la respuesta revela una restricción que no estaba en el código.
Y se cultiva siendo honesto contigo mismo sobre las veces que te equivocaste. No para flagelarte, sino para extraer el patrón de qué supusiste que resultó ser falso.
Con el tiempo eso se convierte en intuición. En esa sensación de "esto va a ser un problema" que no puedes justificar del todo racionalmente pero que casi siempre tiene razón. Esa intuición es criterio acumulado. Es la diferencia real.
Conclusión:
No tengas miedo de ser simple. La simplicidad no es una mala práctica si el proyecto lo requiere. A veces, elegir una solución simple demuestra que entiendes mejor que los demás las verdaderas necesidades del software.
