NextRiot

Los elementos de la Ingeniería de Interfaces

December 30, 2018 • ☕️☕️ 9 min read

En mi artículo anterior hablé acerca de mis vacíos de conocimiento. Podrías concluir que sugiero conformarse con la mediocridad. Para nada, este es un campo amplio.

Creo firmemente que puedes «empezar por cualquier lugar» y no necesitas aprender tecnologías en un orden particular. Pero también valoro mucho la experiencia. Personalmente he estado interesado en la creación de interfaces de usuario.

He estado reflexionando sobre que sí sé y considero valioso Seguro, estoy familiarizado con algunas tecnologías (ejemplo: JavaScript y React). Es difícil reconocer las lecciones que vienen de la experiencia. Nunca he intentado ponerlo en palabras. Este es mi primer intento de catalogar y describir algunas.


Hay muchas «rutas de aprendizaje» sobre tecnologías y librerías. ¿Qué librería estará de moda en el 2019? ¿qué hay del 2020? ¿Debería aprender Vue o React? ¿Angular? ¿Qué hay de Redux o Rx? ¿Debo aprender Apollo? ¿o GraphQl? Es fácil perderse. ¿Qué si el autor está mal?

Mis grandes descubrimientos no fueron en una tecnología en particular Más bien, aprendía más cuando tuve que arreglar algún problema de IU (Interfaz de Usuario). Algunas veces, descubría librerías o patrones que me ayudaron. En otros casos, creaba mis propias soluciones (unas buenas y otras malas).

Es la combinación de entender los problemas, experimentar con las soluciones y aplicar diferentes estrategias lo que me condujo a las experiencias de aprendizaje más gratificantes de mi vida. Este artículo está enfocado solo en los problemas

Si trabajaste en una interfaz de usuario, es probable que hayas resuelto al menos algunos de estos desafíos, ya sea directamente o utilizando una librería. En cualquier caso, te animo a crear una pequeña aplicación sin librerías, y jugar con la reproducción y resolución de estos problemas. No hay una solución correcta para ninguno de ellos. El aprendizaje proviene de explorar el espacio problemático y probar diferentes posibles soluciones.


  • Consistencia. Al hacer clic en el botón «Me gusta» y el texto se actualiza: «A ti y a otros 3 amigos les gustó esta publicación». Al hacer clic nuevamente, regresa a su estado inicial sin el texto. Suena fácil. Pero tal vez exista este elemento en varios lugares de la pantalla. Tal vez haya alguna otra indicación visual (como el fondo del botón) que deba cambiar. La lista de «me gusta» que se extrajo previamente del servidor y que está visible al pasar el mouse ahora debe incluir tu nombre. Si navegas a otra pantalla y regresas, la publicación no debería «olvidarse» de que te ha gustado. Incluso la consistencia local por si sola crea un conjunto de desafíos. Pero otros usuarios también pueden modificar los datos que mostramos (por ejemplo, si le gusta una publicación que estamos viendo). ¿Cómo mantenemos los mismos datos sincronizados en diferentes partes de la pantalla? ¿Cómo y cuándo hacemos que los datos locales sean consistentes con el servidor y al revés?

  • Adaptación. Las personas solo pueden tolerar la falta de retroalimentación visual de sus acciones durante un tiempo limitado. Para acciones continuas como gestos y desplazamiento, este límite es bajo. (Incluso saltarse un solo fotograma de 16ms se siente poco responsivo). Para acciones discretas como clics, hay investigaciones que indican que los usuarios perciben cualquier retraso <100ms como igualmente rápido. Si una acción toma más tiempo, necesitamos mostrar un indicador visual. Pero hay algunos retos contraintuitivos. Los indicadores que hacen que el diseño de la página «salte» o que pasan por varias «etapas» de carga pueden hacer que la acción se sienta más larga de lo que era. De manera similar, manejar una interacción dentro de los 20 ms al costo de soltar un fotograma de animación puede sentirse más lento que manejarlo dentro de los 30 ms y sin fotogramas descartados. Los cerebros no son puntos de referencia. ¿Cómo mantenemos nuestras aplicaciones sensibles a diferentes tipos de cambios?

  • Latencia. Tanto los cálculos como el acceso a la red toman tiempo. A veces podemos ignorar el costo computacional si no afecta la capacidad de respuesta en nuestros dispositivos de destino (asegúrese de probar su aplicación en el espectro de dispositivos de gama baja). Pero el manejo de la latencia de la red es inevitable, ¡puede llevar segundos! Nuestra aplicación no puede simplemente detenerse a la espera de que se carguen los datos o el código. Esto significa que cualquier acción que dependa de nuevos datos, códigos o activos es potencialmente asíncrona y necesita manejar el estado de «carga». Pero eso puede pasar en casi todas las pantallas. ¿Cómo manejamos con gracia la latencia sin mostrar una «cascada» de spinners o «espacios» vacíos? ¿Cómo evitamos los layout saltarines? ¿Y cómo cambiamos las dependencias asíncronas sin «reescribir» nuestro código cada vez?

  • Navegación. Esperamos que la interfaz de usuario permanezca «estable» a medida que interactuamos con ella. Las cosas no deberían desaparecer justo delante de nuestras narices. La navegación, ya sea iniciada dentro de la aplicación (por ejemplo, al hacer clic en un enlace) o debido a un evento externo (por ejemplo, al hacer clic en el botón «atrás»), también debe respetar este principio. Por ejemplo, cambiar entre las pestañas /profile/likes y /profile/follow en una pantalla de perfil no debería borrar una entrada de búsqueda fuera de la pestaña. Incluso navegar a otra pantalla es como entrar en una habitación. Las personas esperan volver más tarde y encontrar cosas a medida que las dejaron (con, quizás, algunos artículos nuevos). Si estás en el medio de un feed, haces clic en un perfil y vuelves, es frustrante perder tu posición en el feed o esperar a que se cargue de nuevo. ¿Cómo diseñamos nuestra aplicación para manejar la navegación arbitraria sin perder un contexto importante?

  • Estancamiento. Podemos hacer que la navegación del botón «atrás» sea instantánea introduciendo una caché local. En esa caché, podemos «recordar» algunos datos para un acceso rápido, incluso si pudiéramos recuperarlos teóricamente. Pero el almacenamiento en caché trae sus propios problemas. Las cachés pueden quedar obsoletas. Si cambio un avatar, también debería actualizarse en la caché. Si hago una nueva publicación, debe aparecer en la caché inmediatamente, o la caché debe ser invalidada. Esto puede llegar a ser difícil y propenso a errores. ¿Qué pasa si la publicación falla? ¿Cuánto tiempo permanece la caché en la memoria? Cuando volvemos a pedir datos, ¿«parchamos» los datos recién obtenidos con la caché, o tiramos la caché? ¿Cómo se representa la paginación u orden en la caché?

  • Entropía. La segunda ley de la termodinámica dice algo así como «con el tiempo, las cosas se vuelven un desastre» (bueno, no exactamente). Esto se aplica a las interfaces de usuario también. No podemos predecir las interacciones exactas del usuario y su orden. En cualquier momento, nuestra aplicación puede estar en uno de los tantos estados posibles. Hacemos nuestro mejor esfuerzo para que el resultado sea predecible y limitado por nuestro diseño. No queremos ver una captura de pantalla de un error y preguntarnos «cómo sucedió eso». Para N estados posibles, hay N × (N – 1) posibles transiciones entre ellos. Por ejemplo, si un botón puede estar en uno de 5 estados diferentes (normal, activo, hover, peligro, deshabilitado), el código que actualiza el botón debe ser correcto para 5 × 4 = 20 transiciones posibles, o prohibir algunas de ellas. ¿Cómo domesticamos la explosión combinada de estados posibles y hacemos predecible el rendimiento visual?

  • Prioridad. Algunas cosas son más importantes que otras. Es posible que un diálogo tenga que aparecer físicamente «arriba» del botón que lo generó y «romper» los límites de su contenedor. Una tarea recién programada (por ejemplo, responder a un clic) puede ser más importante que una tarea de larga duración que ya haya comenzado (por ejemplo, mostrar las siguientes publicaciones debajo del pliegue de la pantalla). A medida que nuestra aplicación crece, partes de su código escritas por diferentes personas y equipos compiten por recursos limitados como el procesador, la red, el estado de la pantalla y el tamaño del paquete. A veces puede clasificar a los contendientes en una escala compartida de «importancia», como la propiedad CSS z-index. Pero rara vez termina bien. Todo desarrollador está predispuesto a pensar que su código es importante. Y si todo es importante, !entonces nada lo es! ¿Cómo conseguimos widgets independientes que cooperen en lugar de luchar por los recursos?
  • Accesibilidad. Los sitios web inaccesibles no son un problema de nicho. Por ejemplo, en el Reino Unido, la discapacidad afecta a 1 de cada 5 personas. (Aquí hay una buena infografía.) También lo he sentido personalmente. Aunque solo tengo 26 años, me cuesta leer sitios web con fuentes delgadas y de bajo contraste. Intento usar el trackpad con menos frecuencia y temo el día en que tendré que navegar por sitios web mal implementados con el teclado. Tenemos que hacer que nuestras aplicaciones no sean horribles para las personas con dificultades, y la buena noticia es que hay mucho a nuestro alcance. Comienza con la educación y las herramientas. Pero también tenemos que facilitar que los desarrolladores de productos hagan lo correcto. ¿Qué podemos hacer para que la accesibilidad sea la regla en lugar de una idea de último momento?

  • Internacionalización. Nuestra aplicación necesita funcionar en todo el mundo. La gente no solo habla diferentes idiomas, sino que también necesitamos soportar los diseños de derecha a izquierda con el menor esfuerzo de los ingenieros de producto. ¿Cómo admitimos diferentes idiomas sin sacrificar la latencia y la capacidad de respuesta?

  • Entrega. Necesitamos obtener el código de nuestra aplicación en la computadora del usuario. ¿Qué transporte y formato utilizamos? Esto puede sonar sencillo pero hay muchas implicaciones aquí. Por ejemplo, las aplicaciones nativas tienden a cargar todo el código por adelantado al costo de un tamaño de aplicación enorme. Las aplicaciones web tienden a tener una carga útil inicial más pequeña a costa de una mayor latencia durante el uso. ¿Cómo elegimos en qué punto introducir la latencia? ¿Cómo optimizamos nuestra entrega en función de los patrones de uso? ¿Qué tipo de datos necesitaríamos para una solución óptima?

  • Resiliencia. Puede que te gusten los bichos si eres entomólogo, pero es probable que no disfrutes viéndolos en tus programas. Sin embargo, algunos de tus errores inevitablemente llegarán a la producción. ¿Qué pasa entonces? Algunos errores causan un comportamiento incorrecto pero bien definido. Por ejemplo, tal vez su código muestre un resultado visual incorrecto bajo alguna condición. Pero, ¿y si el código de renderizado falla? Entonces no podemos continuar de manera significativa porque la salida visual sería inconsistente. Un fallo que genera un solo artículo no debería «derribar» un feed completo ni ponerlo en un estado semi-interrumpido que cause más bloqueos. ¿Cómo escribimos el código de manera que aísle las fallas de representación y recuperación y mantenga el resto de la aplicación en ejecución? ¿Qué significa la tolerancia a fallos para las interfaces de usuario?

(Nota del traductor: bicho es la traducción literal del inglés bug usado para referirse a un error de programación)

  • Abstracción. En una aplicación pequeña, podemos programar muchos casos especiales para explicar los problemas anteriores. Pero las aplicaciones tienden a crecer. Queremos poder reutilizar, bifurcar y unir partes de nuestro código, y trabajar en él colectivamente. Queremos definir límites claros entre las piezas que son familiares para diferentes personas y evitar que la lógica que cambia con frecuencia sea demasiado rígida. ¿Cómo creamos abstracciones que ocultan los detalles de implementación de una parte de la interfaz de usuario en particular? ¿Cómo evitamos volver a introducir los mismos problemas que acabamos de resolver a medida que nuestra aplicación crece?

Por supuesto, hay muchos problemas que no he mencionado. ¡Esta lista no está completa! Por ejemplo, no he hablado sobre el diseñador y la colaboración de ingeniería, ni sobre la depuración y las pruebas. Tal vez en otro momento.

Es tentador leer sobre estos problemas con una librería de vistas en particular o una librería de obtención de datos en mente como una solución. Pero te animo a que finjas que estas librerías no existen y que vuelvas a leer desde esa perspectiva. ¿Cómo abordarías la resolución de estos problemas? Pruébalos en una aplicación pequeña! (Me encantaría ver tus experimentos en GitHub; no dudes en enviarme un tweet como respuesta).

Lo interesante de estos problemas es que la mayoría de ellos se presentan a cualquier escala. Puedes verlos tanto en pequeños widgets como un typeahead o tooltip, y en aplicaciones enormes como Twitter y Facebook.

Piensa en un elemento de interfaz de usuario no trivial de una aplicación que disfrutes usar, y repasa esta lista de problemas. ¿Puedes notar algunas de las implicaciones elegidas por sus desarrolladores? ¡Intenta recrear un comportamiento similar desde cero!

Aprendí mucho sobre ingeniería de IU al experimentar con estos problemas en aplicaciones pequeñas sin usar librerías. Recomiendo lo mismo a cualquier persona que quiera obtener una apreciación más profunda de las implicaciones en la ingeniería de IU.