Separación de responsabilidades: UI, lógica de negocio y acceso a datos

¿Qué significa separar responsabilidades?

Cuando un programa crece, cada archivo tiende a acumular más de una tarea. Un componente de interfaz puede terminar mezclando la forma en que se ven los datos, las reglas para validarlos y la manera de obtenerlos. Separar responsabilidades consiste en dividir esa mezcla en piezas con un propósito único.

La idea central es mantener claramente separados tres niveles: la interfaz (UI), la lógica de negocio y el acceso a datos. Mantenerlos separados permite que cada parte evolucione sin romper a las demás.

Las tres responsabilidades

En una aplicación típica conviene identificar tres responsabilidades claras:

  • UI (interfaz): cómo se presenta la información y cómo reacciona el usuario.
  • Lógica de negocio: las reglas que deciden qué puede hacerse y qué no.
  • Acceso a datos: cómo se guarda, consulta y modifica la información.

Cada una responde a una pregunta distinta: la interfaz responde a cómo se ve, la lógica a qué se puede hacer y los datos a dónde viven y cómo se obtienen.

El problema de mezclarlas

Veamos una función que hace todo: escucha al formulario, valida una regla y consulta la API.

function guardarUsuario(event) {
  const datos = new FormData(event.target);
  const edad = Number(datos.get('edad'));

  if (edad < 18) {
    return alert('Debes ser mayor de edad');
  }

  fetch('/api/usuarios', {
    method: 'POST',
    body: JSON.stringify({ nombre: datos.get('nombre'), edad })
  }).then((respuesta) => respuesta.json());
}

Esta función mezcla los tres niveles: lee la interfaz, decide según una regla de negocio y toca la red. Si mañana cambia el diseño del formulario, la validación o el endpoint, hay que revisar la misma función.

Una separación simple

Primero se mueve el acceso a datos a su propia función.

export async function crearUsuario(datos) {
  const respuesta = await fetch('/api/usuarios', {
    method: 'POST',
    body: JSON.stringify(datos)
  });

  if (!respuesta.ok) {
    throw new Error('No se pudo crear el usuario');
  }

  return respuesta.json();
}

Después se separa la regla de negocio en una función pura, sin DOM ni red.

export function esMayorDeEdad(edad) {
  return edad >= 18;
}

Finalmente el componente solo junta las piezas.

function FormularioUsuario({ onCreado }) {
  async function onSubmit(event) {
    event.preventDefault();
    const datos = Object.fromEntries(new FormData(event.target));

    if (!esMayorDeEdad(Number(datos.edad))) {
      return;
    }

    const usuario = await crearUsuario(datos);
    onCreado(usuario);
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="nombre" placeholder="Nombre" />
      <input name="edad" type="number" placeholder="Edad" />
      <button type="submit">Guardar</button>
    </form>
  );
}

Ahora cada pieza puede probarse por separado. La validación no depende de un formulario y la llamada a la API no sabe cómo se ve la interfaz.

Beneficios

Separar no agrega archivos por lujo; agrega límites que facilitan el trabajo:

  • Pruebas: una función de lógica se prueba sin abrir el navegador ni tocar la red.
  • Cambios: reemplazar la API o rediseñar la interfaz afecta solo a la capa correspondiente.
  • Lectura: cada archivo responde a una sola pregunta y tiene una sola razón para cambiar.

Hasta dónde dividir

No hace falta crear una capa por cada línea de código. La separación debe acompañar el tamaño del problema. Un botón que abre un modal no necesita tres carpetas; un módulo de pagos sí.

La regla práctica puede resumirse así: una parte del programa debe tener una sola razón para cambiar, y esa razón debe poder explicarse sin contar cómo funciona el resto.

Consideraciones

Separar la interfaz, la lógica de negocio y el acceso a datos es una de las decisiones de estructura que más mejora la mantenibilidad de un proyecto. No se trata de un patrón mágico, sino de mantener cada responsabilidad en un lugar donde pueda leerse, probarse y reemplazarse de forma independiente.

© Arturo Juárez Monroy