Saltar al contenido principal
Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Software Engineering Craft

Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Diseño de APIs e interfaces en lo pequeño

Una API aquí es la local: la firma de una función, una interfaz, la superficie exportada de una librería. Las mismas reglas rigen recursivamente para APIs HTTP y servicios RPC, pero el oficio de la API pequeña es el cimiento — la mayor parte del dolor de “los sistemas distribuidos son difíciles” es, al inspeccionarlo, que la API local estaba mal diseñada primero.

Este tema trata de los movimientos de diseño que hacen que las APIs sobrevivan a sus llamadores a lo largo de años de rotación de llamadores.

Diseñar la firma de una función

Una firma es un contrato escrito una vez y leído miles de veces. Tres reglas componen:

  1. Pocos parámetros posicionales. Más allá de tres argumentos posicionales, cada callsite falla al primer intento. Pasa a un objeto de opciones cuando n > 3 o cuando cualquier parámetro es opcional.
  2. Obligatorios primero, opcionales al final. El orden sorpresivo es el bug más común al llamar. Obligatorio → opcional → defaults.
  3. Los booleanos suelen ser un olor. setMode(true) se lee como código de formulario fiscal; enableFeature() y disableFeature() se leen como intención. Prefiere enums o dos funciones cuando el booleano significa un cambio de modo.
Mal:     redirectTo(user, targetUrl, true, false)   // qué son 1, 0?
OK:      redirectTo(user, { target: url, preserveHistory: true, force: false })
Mejor:   redirectTo(user, { target: url })           // cuando los defaults sirven al 95% de las llamadas

Formas de retorno

Una función devuelve o un valor o una disposición (éxito o fallo). La forma del retorno le señala al llamador cómo manejarlo.

Forma de retornoCuándo es correctaModo de fallo
Valor o nullUn caso verdaderamente ausentenull se propaga; los llamadores olvidan el check
Valor o throwFallo verdaderamente excepcionalEl catch-all se traga en el código del llamador
Result<T, E>Dos resultados esperadosAlgunos lenguajes no tienen soporte nativo
Optional / monad nullableLa semántica de pipelineObliga al downstream a reconocer la ausencia

La regla que envejece bien: prefiere Result/error sobre null sobre throw. El llamador no puede ignorar un Result — el compilador lo obliga a manejar ambos casos. null es olvidable. Las excepciones a veces se olvidan y a veces son demasiado ruidosas (la función aborta un test no relacionado del llamador).

Naming que escala

Un nombre es la documentación que viaja con la función. Tres pruebas:

  • La prueba de una llamada: desde cualquier callsite, ¿se entiende solo el propósito de la función? fetchUser(id) no debería necesitar un comentario.
  • La prueba de la señal: ¿el nombre se desambigua de sus hermanos? getUser y findUser deberían diferir — uno lanza y el otro devuelve null? Nómbralos requireUser y findUser.
  • La prueba de la honestidad: validateX no debería también normalizar X; si lo hace, renómbralo a normaliseX. Los verbos honestos permiten nombres honestos.

El costo de un mal nombre es cada lector futuro. Invierte el minuto extra.

El Pit of Success

Una API es un pit of success si la manera más fácil de usarla es la manera correcta. Lo opuesto — un pit of failure — hace que el camino incorrecto sea más corto que el correcto.

Pit of failure:
  Map<String, Object> config = new HashMap<>();
  config.put("timeout", 5000);          // ¿segundos o milisegundos?
  config.put("secure", "yes");          // cualquier string sirve, solo "yes" significa sí

Pit of success:
  client.withTimeout(Duration.ofSeconds(5))
        .withSecureFlag(true)            // tipos fuertes, métodos nombrados

Tres propiedades crean el pit:

  1. Tipos fuertes — una clave de config mal escrita es un error de compilación, no un fallo silencioso en runtime.
  2. Estilo builder — cada método devuelve el tipo que se está configurando, así que el autocompletado guía al llamador.
  3. Defaults sensatos — el caso del 95% es una línea; el del 5% queda más claro que antes.

Versionado de internos

Las APIs internas sí necesitan versionado — cualquier función llamada desde más de un módulo es una API interna. El mínimo:

  • Versionado semántico a nivel de paquete: MAJOR.MINOR.PATCH. Un cambio breaking es siempre un bump de major; el número de major es el contrato de compatibilidad.
  • Ciclo de deprecación para cualquier cambio breaking:
    1. Marca el símbolo antiguo como @deprecated con una nota de migración apuntando al reemplazo.
    2. Añade el reemplazo en paralelo, habilitado por defecto para los nuevos llamadores.
    3. Tras un ciclo de release, rutea los llamadores restantes al nuevo símbolo.
    4. Tras otro ciclo de release, elimina el símbolo antiguo.

Saltarse el ciclo es la causa de cada historia de “actualizamos y se rompió todo”.

Deprecación como proceso

Matar una API es más difícil que escribirla. El proceso:

EtapaEstado de la audienciaCosto para el desarrollador
AnunciarTodos los llamadores actualesEscribir la doc de migración; desplegar el reemplazo
CoexistirCódigo nuevo usa reemplazo; el viejo aún funcionaMantener ambos durante la transición
AdvertirTodos los llamadores ven un log de deprecationAlgunos migrarán; otros no
Prohibir (si hace falta)El símbolo viejo lanza o está gateadoAhora eres dueño del costo del rollout
EliminarEl símbolo viejo desapareceLa mayoría migró; el breakage audita al resto

El periodo de gracia depende de tu audiencia. Una librería interna puede moverse en un trimestre. Una librería open-source pública puede necesitar años. La disciplina es: nunca rompas en silencio, nunca rompas sin un camino, nunca rompas sin una fecha.

Trayectoria de práctica

  1. Elige una función con n > 3 parámetros posicionales; refactoriza a un objeto de opciones. Compara los callsites antes y después.
  2. Encuentra una función que devuelve null o nullable que los llamadores olvidan chequear; reescribe su retorno como Result<T, E> (o el equivalente de tu lenguaje) y observa cómo el compilador obliga a manejarlo.
  3. Audita un parámetro booleano en una función exportada: reemplázalo por dos métodos nombrados o un enum, y documenta cuál era el correcto para cada callsite.
  4. Añade un ciclo de deprecación a un símbolo en tu propio código: anuncia en una tag, coexiste en la siguiente, advierte en la siguiente.
  5. Lee una API de una librería desconocida que uses; identifica un pit of failure en ella. Propón — no en un PR, en papel — el cambio de forma más pequeño que la convertiría en un pit of success.

Cuándo es la herramienta correcta

SituaciónConclusión
Firmas que acumulan más parámetros opcionalesRefactoriza a objeto de opciones antes de que la sorpresa del orden posicional muerda
Funciones que devuelven nullable y los llamadores lo ignoranPasa a Result para que el compilador enforce el manejo
Hermanos indistinguibles en el naming (get vs find)Renombra para expresar la disposición, no solo la acción
Cambio breaking propuestoAgenda el ciclo de deprecación; nunca rompas en silencio
API nueva en diseñoHaz que el camino correcto sea el camino fácil: pit of success, no pit of failure