Volver al blog

La pregunta que nos hizo un desarrollador de Java

¿Qué patrones de diseño usan? Nuestro frontend casi no tiene clases. La respuesta terminó siendo una buena descripción de cómo decidimos qué construir y cómo hablamos entre nosotros al respecto.

engineeringhow-we-workdesign-patternsproduct

La pregunta

Un desarrollador que postulaba para trabajar con nosotros venía de Java. Estábamos hablando de estilo de código, y preguntó qué patrones de diseño orientados a objetos usamos.

Usamos la mayoría todo el tiempo, le dijimos. Strategy para una familia de comportamientos intercambiables. Observer para un cambio que necesita llegar a todo lo que esté interesado en él. Composición sobre herencia, más como costumbre que como regla.

Su pregunta a continuación fue más incisiva. ¿Cómo es posible, si nuestra parte frontal prácticamente no declara clases ni interfaces? Java expresa esos patrones a través de un tipo base y los elementos que lo implementan. Nuestro código realiza la misma función mediante otro método.

Así es. Un componente de Vue ya contiene lo que contiene un objeto: sus propios datos, sus propios métodos y aquella parte que un objeto de Java suele delegar en otro lugar, es decir, la forma en que se presenta. Un «composable» es la forma en que un componente obtiene un comportamiento de otro, la misma relación «tiene-un» que una clase obtiene de la composición, pero sin la clase base. Vocabulario diferente, mismo argumento.

El problema de negocio detrás de todo esto

Vendemos software que nuestros propios clientes configuran. Un departamento de obras públicas construye sus propios formularios de solicitud de servicio. Una empresa de servicios públicos define sus propias categorías de activos. Un municipio escribe sus propias reglas de automatización para lo que ocurre cuando llega un informe.

Eso significa que las solicitudes llegan todo el tiempo, y son pequeñas. ¿Podemos agregar un campo de firma a este formulario? ¿Podemos traer datos de este otro sistema? ¿Podemos enviar esa alerta también por mensaje de texto además de correo?

Cada una de esas es fácil de aceptar una vez. La pregunta interesante es cuánto cuesta la décima. Si cada tipo de campo nuevo implica editar los mismos tres archivos, la décima solicitud es más lenta que la primera, y la quincuagésima es un proyecto entero. Si cada una es una simple adición, la quincuagésima cuesta más o menos lo que costó la primera.

Nada de eso se aprecia en una demostración. Se decide años antes, en función de cómo se estructura el código. Prestamos especial atención a esa estructura.

Ponerle nombre a la forma

La mayoría de los consejos sobre calidad de código terminan en dos ideas. No te repitas. Prefiere la composición sobre la herencia. Ambas son ciertas, y ninguna resuelve mucho a las cuatro de la tarde de un jueves cuando alguien está decidiendo dónde va una función nueva.

Lo que lo resuelve es un nombre compartido.

Cuando alguien del equipo dice "hagamos eso como un registro," todos saben lo que va a llegar en la revisión: una tabla con una fila por tipo, una función por fila, y una sola línea que busca la fila correcta. Nadie lo dibuja en una pizarra. Nadie discute al respecto. La revisión toma un minuto, porque la forma ya se acordó antes de escribir el código.

Ese vocabulario es lo que realmente sacamos de estudiar patrones. Vive entre las personas, y se gana su lugar dos veces. Hace que una buena decisión sea rápida, y hace que una mala decisión sea visible mientras todavía es una frase, antes de convertirse en tres archivos.

Mantenemos una lista escrita de las formas que usamos y dónde va cada una. Todo trabajo nuevo empieza nombrando cuál de ellas es. Esa única pregunta resuelve la mayor parte del diseño, y el resto es escribir código.

Lo que eso compra, en concreto

Tomemos las notificaciones. Cuando ocurre algo que una persona necesita saber, se lo enviamos. Correo, mensaje de texto, una notificación push al teléfono.

Un cliente nos pidió agregar mensajes de texto para una alerta en particular. Es una solicitud pequeña, y es exactamente el tipo que recibimos cada semana.

Mantenemos una sola tabla con lo que enviamos, con una entrada por notificación. Cada entrada responde una sola pregunta en código sencillo: ¿quién debería enterarse de esto?

'report.submitted': {
  recipients: (args) => [args.report.assignee, ...args.report.watchers],
},

Añadir una notificación supone una entrada. Las plantillas de mensaje existentes para ella determinan los canales, y se envía un mensaje de texto en cuanto alguien redacta la versión textual del mensaje. El motor que se encarga del envío permanece tal y como está. Tiene una extensión de unas cuarenta líneas, y ha mantenido esa extensión de unas cuarenta líneas a lo largo de todas las notificaciones que hemos añadido.

No empezó viéndose así. La primera notificación que construimos fue un toast, ese mensaje pequeño que se desliza en la esquina de la pantalla para decir que lo que acabas de hacer funcionó. Un toast es de lo más simple que existe, y había una versión del trabajo donde lo escribíamos como un toast y seguíamos adelante.

En vez de eso escribimos la tabla, porque un toast ya está respondiendo las dos preguntas que responderá todo lo demás. Algo ocurrió. ¿Quién necesita saberlo, y qué debería ver?

A partir de entonces, las respuestas aumentaron considerablemente. Algunas notificaciones tenían que llegar a una persona que había cerrado la pestaña. Esas se convirtieron en correos electrónicos. Otras tenían que llegar a un equipo que no disponía de ordenador portátil. Esas se convirtieron en mensajes de texto. Otras debían llegar a un teléfono a las dos de la madrugada. Esas se convirtieron en notificaciones push. Cada una de ellas conllevó un trabajo real, incluida una nueva interfaz para que las personas pudieran elegir sobre qué querían recibir información, y nada de ello modificó la respuesta a las dos preguntas originales.

La regla que mantenemos es que cualquier cosa inusual sobre una notificación vive dentro de la entrada de esa notificación. En el momento en que un caso especial se muda al motor, el motor empieza a acumular una marca por cada caso, y la siguiente persona tiene que leerlas todas para agregar algo.

Una segunda estructura consiste en que el código de envío pregunte «¿se trata de un correo electrónico, un mensaje de texto o una notificación push?» y cree una rama cada vez. Nuestro código fuente incluye ambas estructuras. Reservamos tiempo para convertir la versión con ramas. Una fila requiere un martes. Una rama requiere una conversación.

El desarrollador de Java llamaría a la primera versión Strategy. No hay una interfaz ni una clase en ninguna parte, y hace exactamente lo que promete el patrón.

El objeto ya estaba ahí

Empecemos por la parte de la respuesta que menos nos sorprendió al analizarla directamente. En Java, un objeto se expresa como una clase: un conjunto de datos y los métodos que actúan sobre ellos. La forma en que se muestra en pantalla es una cuestión independiente que se gestiona en otro lugar.

Un componente de Vue empaqueta esas mismas tres cosas, más la tercera pieza que Java suele entregarle a otro:

<script setup lang="ts">
const props = defineProps<{ userId: string }>()
const user = ref<User | null>(null)
const isAdmin = computed(() => user.value?.role === 'admin')

async function load() { user.value = await getFetch(`/api/users/${props.userId}`) }
</script>

<template>
  <div>{{ user?.name }} <span v-if="isAdmin">Admin</span></div>
</template>

props es un argumento de constructor. user e isAdmin son variables de instancia. load es un método. La plantilla es la parte que Java suele dejarle a otra cosa: la que decide cómo se presenta el objeto. Pon dos de estos en la misma pantalla para dos usuarios distintos, y cada uno guarda su propio user, privado para sí mismo, sin ver nunca al otro. Eso son dos objetos.

Nada de eso requería el término «clase». En los patrones en los que nos basamos, el objeto nunca fue realmente el elemento en cuestión. Lo que varía es cómo se relacionan los objetos entre sí: si uno hereda de otro, si se puede sustituir una familia de comportamientos, o si un cambio en uno debería repercutir en los demás. Para el tipo de aplicación que desarrollamos, en el marco de trabajo en el que la creamos, esa relación funciona mejor mediante componentes y tablas. Nuestras clases se ubican en el único lugar que el lenguaje les exige, y la siguiente sección aborda este tema.

A dónde se fue la herencia

Por curiosidad, buscamos cada lugar donde nuestro código usa herencia. Hay un puñado, y todos son tipos de error, donde el lenguaje la exige.

Lo que usamos en su lugar es composición. En nuestro código toma la forma de pequeñas funciones que devuelven las piezas que pediste.

const { record, canEdit, save } = useRecordEditor(recordId)

Esa línea produce su propio objeto de trabajo con su propio estado. Dos editores abiertos al mismo tiempo sostienen dos registros y nunca se ven entre sí. Un desarrollador de Java lee eso y reconoce un objeto con métodos públicos, con el resto guardado en privado.

El libro que nos enseñó a la mayoría de nosotros estos patrones establece lo siguiente como principio: dar prioridad a la composición frente a la herencia. La herencia le vincula a un elemento padre y le transfiere todo lo que este tiene. La composición toma las tres cosas que necesita. El estilo «composable» de Vue se ajusta estrechamente a ese principio, y nuestro código se decantó por él desde el principio.

Cuando son tus clientes quienes lo extienden

El patrón en el que más nos apoyamos es aquel donde un cambio se anuncia solo y cualquier cosa interesada reacciona.

Nuestras reglas de automatización funcionan así, y lo interesante es quién las escribe. Un cliente abre la interfaz y construye una regla: cuando llega un informe para este tipo de activo, notifica a esta cuadrilla y abre una orden de trabajo. Esa regla es una fila en su propia base de datos. Cuando el evento se dispara, el motor encuentra cada regla que lo está observando y ejecuta cada una, manteniendo los fallos separados para que una regla rota deje a las demás funcionando.

El Observer de los libros de texto tiene a programadores registrando a los observadores. Aquí lo hace el cliente, a través de un formulario, un martes por la tarde, sin ningún despliegue de por medio.

Ese cambio plantea problemas que el libro de texto nunca tuvo que resolver. Una regla puede activar un evento que, a su vez, desencadena otra regla. Cada ejecución cuenta con un presupuesto que determina hasta dónde puede llegar una cadena. Cada regla tiene su propio límite de frecuencia, y si un sensor está ocupado, la cola queda disponible para todos los demás. Los clientes crean cosas que no habíamos previsto, y esas dos medidas de seguridad garantizan que la cola siga funcionando cuando ello ocurre.

Por qué hablamos de esto

La pregunta con la que empezamos era sobre patrones, y la respuesta real es sobre cómo trabajamos.

Utilizamos estas ideas constantemente, y en nuestro lenguaje se expresan de forma diferente a las palabras clave. Compartimos un vocabulario para referirse a ellas, y una decisión de diseño suele ser una breve conversación.

El toast es el ejemplo más claro. Se escribió en un lenguaje y un framework que apenas se parecen a lo que hoy ejecuta ese código, por personas que no habían leído los mismos libros. La forma debajo de él se sostuvo de todas formas, y sigue sosteniéndose ahora que la misma notificación puede llegar como correo, mensaje de texto o push a las dos de la mañana.

Eso es lo que vale la pena llevarse. Los patrones sobreviven al lenguaje que se usa para describirlos. Java llama a una cosa una interfaz con clases que la implementan, nosotros la llamamos una tabla de funciones, y el argumento que hace cada uno es idéntico. Aprende el argumento y la sintaxis deja de importar.

Si estás evaluando si trabajar con nosotros, de cualquiera de los dos lados de la mesa, eso es lo que quisiéramos que sepas. Pensamos en la forma de algo antes de construirlo, escribimos lo que aprendemos, y volvemos a arreglar los lugares donde nos equivocamos. Lo que ven nuestros clientes es que las solicitudes pequeñas siguen siendo pequeñas.

Resumen

  • Nuestros clientes configuran el producto ellos mismos, y las solicitudes pequeñas llegan constantemente. El costo de la quincuagésima lo decide la forma del código, años antes de que se pida.
  • El valor duradero de los patrones de diseño es un vocabulario compartido. Acordar la forma antes de que exista el código convierte un debate de diseño en una revisión corta.
  • Un componente de Vue ya empaqueta lo que empaqueta un objeto: sus propios datos, sus propios métodos, y cómo se presenta. Nuestras clases están donde el lenguaje las exige.
  • Guardamos el comportamiento en tablas. Una capacidad nueva es una fila, y el motor que lee la tabla se queda tal como está escrito. Nuestro motor de notificaciones se ha mantenido en unas cuarenta líneas a través de cada notificación que hemos agregado.
  • Ese motor empezó como un toast en la esquina de una pantalla. El correo, el mensaje de texto y el push trajeron cada uno trabajo real, incluida una interfaz nueva para elegir qué quieres escuchar, y nada de eso cambió la forma de fondo.
  • La herencia está prácticamente ausente de nuestro frontend. Pequeñas funciones componibles devuelven las piezas que necesita una pantalla, cada una con su propio estado.
  • Nuestras reglas de automatización son los propios observadores del cliente, guardados como datos. Eso necesita protecciones que una versión de libro de texto no tiene: un límite a cuánto puede viajar una cadena de reglas, y un límite de frecuencia por regla.
  • Mantenemos una lista escrita de las formas que usamos, y volvemos a los lugares que son anteriores a ella.
  • Los patrones sobreviven al lenguaje que se usa para describirlos. La sintaxis difiere entre Java y JavaScript. El argumento que hace cada patrón no.