Los datos en la programación de UI: modelo, estado y configuración
Publicado el 12 de octubre de 2021
Las interfaces gráficas son uno de los campos de la programación en el que el código tiende más al desorden y la chapuza. Es un área cercana al diseño, y por lo tanto propensa a una definición difusa de los requerimientos y a cambios y añadidos de funcionalidad espontáneos. Tampoco ayuda que la mayoría de bibliotecas para programar interfaces gráficas tengan un diseño en el que priorizan claramente unos aspectos del desarrollo sobre otros, lo que hace que el programador tenga que compensar al lidiar con ellos, y esto resulta tedioso. Este texto explora la estructura y el significado detrás de los datos manipulados por el código de UI, y la forma en la que estos datos son manipulados por código de UI escrito para ser componible. Se estudian como ejemplo práctico las bibliotecas ImGui y Slate, que son dos buenas bibliotecas de UI que permiten escribir código de manera composicional, pero tienen prioridades totalmente opuestas.
Definimos como UI composicional una arquitectura donde un elemento interactuable, al que llamaremos widget, es reusable en distintos lugares de la misma interfaz. Por ejemplo, la misma caja de texto puede servir para pedir al usuario su nombre, su contraseña, su dirección o su número de teléfono. Cambia el dato que está siendo modificado en cada caso, pero la funcionalidad es la misma. También existen algunos widgets cuyo propósito no es exponer datos al usuario, sino definir la posición y relación de otros widgets, formando así una jerarquía. Esto pueden ser listas horizontales o verticales, tablas o lienzos en los que cada widget tiene una posición arbitraria. De esta forma, se puede crear widgets más complejos a partir de widgets simples y disposiciones estructuradas de éstos en la pantalla.
Los datos con los que trabaja un widget pueden clasificarse en tres categorías fundamentalmente distintas: el modelo, el estado y la configuración. Un código de UI ordenado tiene que entender la distinción entre estas tres categorías para poder ser componible sin sorpresas ni casos extraños.
El modelo es la información siendo manipulada. En esencia, podemos entender una UI como una función sobre el modelo, que coge el modelo y el input del usuario y modifica el modelo o crea un modelo nuevo. En una aplicación de edición de un documento, el modelo es el documento siendo editado. En un programa para rellenar un formulario, el modelo es el formulario. Para cada widget, el modelo es la información externa que ese widget visualiza y modifica. Por ejemplo, para una caja de texto, el modelo es el texto siendo editado. Se puede componer un widget de formulario a partir de múltiples cajas de texto, donde cada caja de texto modifica una de las entradas del formulario.
El estado de un widget es la información que el widget necesita guardar y persistir de un fotograma al siguiente que no es parte del modelo, sino que es necesaria para la funcionalidad del widget. El estado de un widget es irrelevante para el exterior, es decir para cualquier widget por encima en la jerarquía y para las partes del programa que no tienen que ver con la interfaz gráfica. Supongamos por ejemplo un encabezado colapsable que puede estar abierto o cerrado. Cuando está abierto, su contenido se vuelve visible. El dato de si el widget está abierto o no, no es parte del modelo. No es parte de la información siendo editada. Es un detalle de implementación para hacer del editor más cómodo de usar. Sin embargo, es información que tiene que persistir en el tiempo. Este tipo de información es el estado de un widget.
Encabezado colapsable
Por último, la configuración es la información constante que describe la apariencia de la interfaz. Elementos como colores, tamaños, texturas, sonidos o márgenes suelen componer la configuración. La configuración es por lo general constante durante la ejecución del editor.
Es importante entender también que a veces el estado de un widget es el modelo de otro. Esto suele suceder cuando el estado de un widget compuesto por otros es modificable por los widgets por los que está compuesto. Por ejemplo, supongamos un widget compuesto por una barra de búsqueda y una lista de elementos seleccionables. La barra de búsqueda se usa para filtrar la lista. El modelo de este widget es cuál de los elementos está seleccionado. Puede ser un pointer o un índice. El texto de la barra de búsqueda es parte del estado del widget, y es irrelevante hacia afuera. Sin embargo, la barra de búsqueda es una caja de texto cuyo modelo es el texto de filtro.
Barra de búsqueda
Es importante entender la distinción entre estas categorías distintas de datos porque cuando una biblioteca ignora una de ellas surgen problemas. Cuando es difícil interactuar con el modelo, la API es tediosa de usar. Escribir nueva funcionalidad es costoso, y termina habiendo más código que mantener, que además suele ser más complejo. El flujo de control se vuelve menos intuitivo, muchas veces dependiendo de callbacks.
Cuando es difícil abstraer el estado, es difícil componer widgets y crear widgets con comportamientos complejos. Es común que la frontera entre modelo y estado se difumine y que el modelo termine conteniendo datos añadidos ad hoc que no son valiosos, sino que son simplemente estado de la interfaz que no había otro sitio donde poner.
Cuando es difícil abstraer la configuración es difícil hacer que la UI sea configurable. Esto suele resultar en interfaces con aspecto poco profesional. No ser suficientemente configurable puede ser razón suficiente para descartar una biblioteca para uso en producción a la hora de hacer un producto de cara al público, y quedar relegada únicamente al desarrollo de herramientas internas.
En Slate, una UI es un árbol heterogéneo de widgets. Cada widget está compuesto por una clase con una serie de funciones virtuales. A Slate se le da bien abstraer el estado y la configuración de la interfaz. Al tener cada widget su propia clase, es trivial implementar el estado en las variables privadas de la clase. También, al tener una UI mayormente compuesta por un árbol estático, es fácil hacer un editor visual que permite a artistas modificar la configuración en un editor interactivo. Sin embargo, interactuar con el modelo es complicado. Es frecuente que cada widget guarde dentro de sí una copia del modelo sobre el que opera, y propague los cambios a través de callbacks. Escribir nueva funcionalidad en Slate se siente tedioso por la cantidad de pasos e indirecciones necesarias para conectar el código de interfaz con el modelo.
En ImGui, una UI es una función que coge el modelo por referencia, e internamente llama a otras funciones. Al hacer que cada widget sea una función que coge el modelo y lo muta, la conexión entre UI y modelo es muy fácil y evidente y esto resulta en que escribir nueva funcionalidad sea muy fácil. Además, la cantidad de tecleo necesario para escribir una función es menor que para una clase, lo que lo hace todavía más fácil. Es también más sencillo hacer interfaces que cambien dependiendo del modelo o el estado, como una lista de botones cuyo número de elementos dependa del número de elementos en un array del modelo, ya que los widgets de la UI están definidos por el flujo de control de la función y no por un árbol estático. Sin embargo, esto hace que abstraer el estado y la configuración sea difícil. ImGui tiene soluciones ad hoc para algunos de sus widgets, como un sistema para recordar si los colapsables están abiertos o cerrados, pero no es fácilmente extensible a estado arbitrario que puedan necesitar widgets creados por terceros. Lo mismo sucede con la configuración. Existe una estructura monolítica con la configuración para los distintos widgets que trae la biblioteca, lo que funciona muy bien ya que le da a toda la UI un aspecto coherente y unificado, pero no permite extenderla para widgets del usuario. Modificar la configuración de un widget en concreto para elementos especiales que requieran salirse de lo por defecto es tedioso y a veces imposible.
Programar UI de una forma estructurada y composicional puede ser muy beneficial para reducir los tiempos de iteración y crear código más fácil de entender y mantener de cara a futuro. Esto requiere, sobre todo, entender muy bien los datos manipulados por el código, para poder organizarlos en una forma que tenga significado, y no sea una solución ad hoc para cada problema. Es especialmente importante entender la diferencia entre modelo, estado y configuración, y cómo hay que tratar a cada uno de una forma distinta, de forma que cada parte del código tenga accesible la información que le es relevante y nada más. Desde el punto de vista del diseño de bibliotecas, es importante entender lo que se sacrifica al priorizar unos casos de uso sobre otros, y la forma en la que una arquitectura concreta o el explotar unos elementos del lenguaje sobre otros puede ayudar o hacer más difícil el trabajar con una de las categorías de dato.
Logo of RSS.