Opinión & Recortes

Miscelánea de mi Web Anterior

Componentes & Modelos

Orientación a Componentes

Javier Vélez Apr 2017 17 mins

El primer paso para acercarnos a la programación orientada a componentes es entender la anatomía interna de los mismos. A lo largo de este artículo analizaremos las partes constituyentes de estos artefactos y describiremos la responsabilidad de dichas partes en relación al modelo de comportamiento que deben presentar los componentes.

Todo paradigma define los estereotipos de artefactos en torno a los cuales giran sus prescripciones. En la programación estructurada se habla de subrutinas (procedimientos y funciones). En la orientación a objetos, de clases y objetos. En la programación funcional de funciones. Y en la orientación a componentes de componentes (Figura 1).

Hasta aquí todo parece sencillo, pero lo cierto es que hay una diferencia fundamental entre nuestro paradigma de programación - la orientación a componentes - y todos los anteriores. En los primeros existen lenguajes de programación que dan soporte preciso a las definiciones establecidas. Pascal o C para la programación estructurada, Java o C# para la orientación a objetos y Lisp o Haskell para la programación funcional.

Sin embargo, la orientación a componentes es un paradigma sin un lenguaje de soporte propio y específico. Las ideas de esta corriente de desarrollo deben desplegarse sobre los mimbres de una sintaxis ajena y tomando prestadas conceptualizaciones foráneas. Típicamente, se escogen para este fin lenguajes orientados a objetos, donde los componentes se materializan en forma de objetos. Pero, nada impediría hacer este mismo desarrollo conceptual, por ejemplo, dentro del mundo de la programación funcional.

Esta elección tiene, sin embargo, una repercusión importante. Las capacidades operativas que pueden articularse con componentes se verán, en general, fuertemente condicionadas por la pila tecnológica elegida y, en particular, por los mecanismos proporcionados por el lenguaje en relación a su sintaxis y semántica asociada. Al ser los componentes objetos, las técnicas básicas al servicio de la composición son la encapsulación, la delegación, la herencia, la genericidad y el polimorfismo. Si los componentes hubiesen sido funciones, dichas técnicas se restringirían a la aplicación del orden superior, la evaluación parcial y la composición funcional.

El Modelo de Componente
El Modelo de Componente

En línea con lo anterior, deberíamos elegir un lenguaje y un stack tecnológico sobre el que desarrollar todo este discurso. Sin embargo, dado que está en mi intención centrarme, a partir de ahora y en los siguientes artículos de esta serie, en los componentes de front, la elección ya está en buena parte tomada. Cuando hablemos de componentes, nuestro lenguaje de soporte será JavaScript y por extensión todos aquellos super-lenguajes que transpilan a JavaScript. Los mecanismos para la composición serán los propios de este lenguaje y el concepto de componente se articulará en base al modelo de objetos por prototipado de JavaScript.

Dicho esto, el resto de elementos de la ecuación se centra en determinar cuál es el mejor framework para desarrollar nuestras soluciones. En este sentido, actualmente compiten varias alternativas entre las que destacan. Los propósitos son similares y las aproximaciones bastante miméticas. Pero si en algo se diferencian todas ellas es en el modelo de componente. Es decir, en la conceptualización que hacen del término componente y en el uso pragmático que le dan.

Modelo de componente. El modelo de componente de un framework orientado a componentes describe las partes constituyentes de los componentes y cómo éstas dan soporte al conjunto de requisitos a los que se pretende dar cobertura.

Nuestro foco de atención a lo largo de este artículo será, en este mismo sentido, precisar qué es un componente en términos de su anatomía interna y analizar las responsabilidades de cada una de sus partes constituyentes. Pero todo ello hecho con agnosticismo de una solución particular.

Perfiles de Contrato

Todos tenemos una idea intuitiva de lo que es un componente: una etiqueta personalizada fuera del léxico estándar HTML que presenta un rendering y comportamiento reactivo específico y que atiende a unos objetivos claramente definidos en el marco de algún problema. No obstante, precisar algo más esta idea en busca de las partes constituyentes de los componentes nos resultará conveniente.

 Perfiles de Contrato
Perfiles de Contrato

Independientemente del framework seleccionado, todo modelo de componente puede caracterizarse a partir de 4 perfiles de contrato (figura 2). Aquí el término contrato puede asociarse mentalmente con la idea de API que todos tenemos en nuestro imaginario colectivo: una colección de capacidades que permiten al usuario configurar el componente y acceder a sus servicios. No obstante, deberemos mantener una concepción ampliamente entendida de la idea de contrato, ya que con esta palabra, frecuentemente, no sólo nos referiremos a un conjunto de métodos y propiedades en JavaScript sino en ocasiones también a elementos de HTML o CSS como discutiremos más adelante.

Asimismo, hablamos de perfiles de contrato y no simplemente de contratos para hacer hincapié en que cada una de estas 4 partes constituyentes son en realidad un espacio definido libremente por los desarrolladores y diseñadores que construyen cada componente y no algo prescrito en sentido alguno.

Perfil de Contrato. Un perfil de contrato de un componente define una interfaz del mismo que ofrece una colección de capacidades cohesionadas entre sí y relacionadas con cierto propósito. Cada perfil de contrato opera sobre un espacio sintáctico diferente (HTML, CSS o JS).

El término contrato es una metáfora que viene a recordar que, en alineamiento con lo anterior, las capacidades y elementos expuestos en un contrato determinado son, en forma y fondo, un convenio establecido documentalmente entre las partes implicadas en el proceso. De un lado el desarrollador del componente y de otro los desarrolladores usuarios de dicho componente que, en el marco de un escenario de colaboración, juegan un papel de clientes. En fin, hechas todas estas aclaraciones preliminares podemos describir en detalle cada uno de estos perfiles de contrato:

  • El contrato declarativo. El contrato declarativo se opera desde el lenguaje HTML y se utiliza exclusivamente para dotar al componente de toda la información de configuración inicial necesaria. De acuerdo a esto, esta configuración debería tener un marcado carácter semántico y no debería reflejar los cambios de estado del componente a lo largo de su tiempo de vida. Sin pérdida de generalidad, podemos pensar que en realidad se trata de un espacio reservado para que el desarrollador o diseñador inyecten las dependencias que el componente necesita para funcionar adecuadamente. Un diseño correcto de un componente parte de la definición cuidadosa del ecosistema de dependencias que éste requiere para operar. Un buen modelo de componente - en el marco de un framework de orientación a componentes - debería proporcionar mecanismos flexibles para inyectar estas dependencias de diferentes maneras. Esto es relevante para articular inversión de control pero dejaremos estos aspectos para otro articulo de esta serie.

  • El contrato funcional. El contrato funcional corresponde a una API en JavaScript caracterizada por métodos y propiedades que permite dar soporte a todos los aspectos operativos y funcionales del componente. Este contrato puede dividirse conceptualmente en 3 tipos de elementos. De un lado, están aquéllos que permiten alterar o reflejar el estado interno del componente. De otro, están los elementos que se encargan de dar acceso a todos los servicios proporcionados por el componente. Y finalmente, se distinguen aquellos que, teniendo un carácter utilitario, no deberían ser accesibles desde fuera.

  • El contrato reactivo. El contrato reactivo, es también otra API JavaScript encargada de exponer todas las capacidades reactivas del componente. En sentido estricto este contrato puede, en algunos casos, no tener contenido alguno. Sin embargo, dado que la Web conforma una arquitectura de ejecución asíncrona basada en el uso de eventos, lo más habitual es que todos los componentes presenten algún tipo de comportamiento reactivo. En primer lugar, destacan las reacciones que tienen que ver con lógica que se ejecuta cuando se produce un cambio dentro del ciclo de vida del componente. Por ejemplo, frecuentemente es interesante ejecutar algún código de inicialización cuando el componente ha sido creado con éxito y vinculado a un árbol DOM anfitrión. En segundo lugar, está la lógica reactiva que responde a cambios en el estado del componente. La gestión de este tipo de reacciones ha dado pie a implementar arquitecturas de data-binding bidireccional de las que hablaremos someramente más adelante. Y Finalmente, están las capacidades reactivas operadas a través de la escucha y emisión activa de eventos. En el primer caso, el componente puede ejecutar cierto código cuando cambian las condiciones ambientales del entorno mientras que en el segundo caso puede emitir notificaciones para advertir al entorno de la ocurrencia de cambios internos que se consideren de potencial relevancia como para que otros componentes puedan responder reactivamente.

  • El contrato visual. El contrato visual, descrito sobre la especificación CSS, permite gestionar el proceso de rendering asociado a la parte visual de los componentes. Las capacidades nucleares de este contrato se centran esencialmente en ofrecer un mecanismo que permita codificar la vista de un componente de una manera cómoda y flexible. A este respecto se distinguen dos aproximaciones esenciales: aquéllas - más centradas en los estándares Web - basadas en el uso de plantillas y aquellas otras - más propias de las soluciones basadas en programación funcional - que proponen la creación de funciones puras para renderizar las vistas en función de una colección de parámetros explícitos. Además de esto, muchos modelos ofrecen capacidades de configuración y adaptación visual que permiten estilizar la vista de acuerdo a determinados parámetros. La mayor parte de las soluciones en este sentido se basan en el uso de propiedades y mixins CSS incluidos recientemente dentro del estándar.

En este contexto, cada contrato puede interpretarse como un punto de entrada para operar con el componente desde un espacio sintáctico diferente (HTML, CSS, JS) al que el contrato pertenece. En efecto, como puede apreciarse en la figura 2, cada uno de los 4 perfiles de contrato anteriores se operan desde un espacio sintáctico distinto. El contrato declarativo corresponde a la declaración sintáctica en HTML que se hace del componente cuando éste se declara dentro del documento anfitrión donde se aloja. El contrato funcional se opera desde el cuerpo programático del objeto JavaScript donde se describe el comportamiento del componente. El contrato reactivo también corresponde al mismo espacio que el contrato funcional - el objeto JavaScript del componente - pero contiene la colección de métodos dedicados a la lógica reactiva. Y finalmente, el contrato visual se desarrolla dentro del plano de la especificación CSS.

Dicho esto, si centramos nuestro interés en la división de responsabilidades conferidas a cada tipo de contrato descubriremos que, con frecuencia, estas responsabilidades interesa operarlas desde otro espacio sintáctico diferente. Por ejemplo, aunque el contrato declarativo se desarrolla dentro del espacio HTML, nada impediría que en el espacio de JavaScript del componente se pudieran incluir métodos para llevar a cabo la misma lógica de gestión de dependencias pero articulada de forma dinámica. Por su parte, el contrato funcional se desarrolla esencialmente en el espacio sintáctico de JavaScript pero con frecuencia alcanza manifestaciones en el HTML, por ejemplo, cuando se proyectan las propiedades del componente hacia los atributos del contrato declarativo. Esto es especialmente recurrente en soluciones de orientación a componentes que utilizan arquitecturas de data-binding. Por su parte, el contrato reactivo también opera, en términos generales, dentro del espacio sintáctico de JavaScript. Pero es frecuente, por ejemplo, encontrar registros a escuchadores de eventos desplegados en el espacio HTML. Finalmente el contrato visual se desarrolla especialmente con capacidades que se operan, típicamente, desde el espacio sintáctico del estándar CSS. Pero siempre es interesante, por ejemplo, disponer de métodos en JavaScript que te permitan cambiar en caliente la especificación visual para alcanzar requisitos adaptativos de personalización y temificación de forma dinámica.

Con todo lo anterior queremos estresar la idea de que la descripción de los contratos de un componente debería responder a sus capacidades y alcance operativo con agnosticismo del espacio sintáctico donde dichas capacidades operen.

Planos de Actividad

Lo que desde luego pone de manifiesto lo anterior, es que con frecuencia las capacidades de los 4 perfiles de contrato de un componente desbordan el espacio sintáctico donde se ubican y alcanzan otros espacios sintácticos. Todo ello invita a pensar que deberían formularse otras formas de describir la operativa de los componentes para liberarnos de las ataduras de lo sintáctico.

Planos de Actividad
Planos de Actividad

Si ahora representamos los perfiles de contrato de todo modelo de componente como 4 puntos en el espacio orientados en forma de tetraedro regular podemos obtener una imagen similar a la de la figura 3 donde se pueden reconocer 3 planos de actividad. Describamos el alcance de cada uno de estos planos:

  • Plano de configuración. El plano de configuración lo conforman los vértices del contrato declarativo, funcional y visual. Desde este plano se articula cualquier configuración que el componente necesite para operar. La configuración de carácter visual se hace con el contrato visual pero frecuentemente requiere de capacidades que se exponen desde el contrato declarativo y funcional. Asimismo, la configuración de información semántica se realiza sobre el contrato declarativo pero también hace uso en ocasiones del contrato funcional. Por su parte, tal y como explicábamos con anterioridad, el contrato funcional opera frecuentemente en sintonía con el contrato declarativo para exponer hacia fuera su información de estado en forma de atributos HTML definidos por el contrato declarativo.

  • Plano de comportamiento. El plano de comportamiento se centra en describir el comportamiento que debe presentar el componente a lo largo de su tiempo de vida. Este plano está formado por el contrato declarativo, funcional y reactivo. El funcional se encarga de ofrecer acceso a todos los servicios del componente y a la gestión de su estado interno. Por su parte el contrato reactivo describe el comportamiento del componente en términos de su lógica reactiva. Es decir, se encarga de describir cómo se comporta el componente ante cambios de carácter ambiental. La participación del contrato declarativo en este plano se justifica desde el hecho de que es el contrato encargado de definir la configuración del componente y por ende describe su comportamiento en el arranque.

  • Plano de presentación. El plano de presentación lo conforman el contrato declarativo, visual y reactivo. Su responsabilidad es dar soporte al comportamiento que tendrá el componente desde una perspectiva visual. La presencia del contrato visual dentro de este plano se justifica por si sola. Por su parte, y al igual que ocurría en el plano anterior, el contrato declarativo tiene responsabilidades en la gestión de toda la configuración inicial que competa a los aspectos visuales del componente. Finalmente, el contrato reactivo es relevante en este plano ya que con frecuencia la respuesta reactiva de un componente a los cambios ambientales tiene un carácter visual.

Por resumir lo dicho hasta el momento podemos afirmar que construir un componente haciendo uso de un framework específico consiste, en términos generales, en implementar capacidades concretas para cada uno de los 4 perfiles de contrato que hemos descrito según los grados de libertad que al respecto conceda dicho framework. Sin embargo, ¿qué papel juegan en todo este proceso los planos de actividad? La idea es esforzarse por garantizar que las capacidades que se den soporte en cada uno de los 4 contratos de un componente queden alineadas sobre los espacios de responsabilidad que delimitan cada uno de los 3 planos de actividad anteriores. Expliquemos con un par de ejemplos que queremos decir con este alineamiento de contratos a planos.

Ejemplo 1. Sobre un componente podemos replicar la colección de atributos HTML definidos en el contrato declarativo para que coincida homónimamente con la colección de propiedades JS establecidas en el contrato funcional. Para ello, podemos montar una arquitecturas de escuchadores y observadores de cambio de manera que cuando desde el contrato declarativo se cambie el valor de un atributo HTML, dicho cambio se propague internamente hacia la propiedad JS homónima residente en el contrato funcional. Y recíprocamente, los cambios producidos sobre las propiedades del contrato funcional interesará que se propaguen externamente hacia los atributos del contrato declarativo. A la primera estrategia se le conoce como property-binding mientras que la segunda recibe el nombre de event-binding y junto con la interpolación dinámica son las 3 características esenciales de las arquitecturas de data-binding bidireccional que implementan la mayor parte de frameworks actuales centrados en la orientación a componentes.

Ejemplo 2. Cualquier cambio operado desde el contrato funcional debería ser atendido por el contrato reactivo y esos cambios a su vez deberían propagarse en cambios presentacionales visual operados desde el contrato visual. Cuando en el componente wc-todos, por ejemplo, invocamos al método done para marcar una tarea como realizada, debe, en primer lugar, emitirse una notificación al exterior en forma de evento y, a su vez, preparar el estilizado CSS para que reaccione y cambie la representación visual de la tarea de alguna forma.

Es decir, lo importante cuando diseñamos un componente no es dar cohesión a los elementos internos de cada contrato - que también - sino mantener una cohesión de cada uno de estos 4 contratos con los objetivos de servicio que se enmarcan en cada uno de los 3 planos de actividad del componente. Éstos últimos son los realmente importantes. Todo esto conduce a la idea de la coordinación contractual.

Coordinación Contractual. El funcionamiento de los diferentes contratos de un componente debe coordinarse para que éste muestre un comportamiento cohesionado y homogéneo.

Esto en esencia quiere decir que, a lo largo del uso de un componente, no se tienen que detectar inconsistencias entre el estado, los servicios o el comportamiento que se recoge de cada uno de los contratos del mismo. Nótese no obstante, que no todos los contratos tienen a este respecto los mismos objetivos. Por ejemplo, el contrato declarativo en HTML, con frecuencia, tiene como misión mantener el estado de un componente solamente durante el momento de la carga e inicialización del mismo y no debe reflejar en sus atributos los cambios de estado a lo largo del tiempo. Esta gestión suele delegarse al contrato funcional que soporta el estado típicamente a través de propiedades reales y computadas. Este es otro buen ejemplo de coordinación contractual.

Todo este ejercicio de coordinación esconde, no obstante, una prescripción relevante de carácter arquitectónico. Si bien es cierto que se debe proporcionar cohesión contractual para mantener el alineamiento en los planos de actividad no es menos cierto que esta cohesión no puede garantizarse a través de técnicas que conduzcan al acoplamiento entre contratos. Por ejemplo, es un error reutilizar un método del contrato funcional en el contrato reactivo simplemente porque, de manera fortuita, contengan la misma lógica interna. Por ejemplo, en un componente reproductor, el método play y el método onPlaydeberían mantenerse siempre diferentes aun cuando contengan el mismo código.

En fin, que todas las soluciones de front actuales se apoyan en arquitecturas y frameworks que operan dentro de la orientación a componentes. Entender las diferencias entre estas propuestas consiste, en buena medida, en descubrir el alcance que tiene el modelo de componente que utilizan así como el nivel de respeto a los estándares establecidos, hecho este último que cada vez terminará siendo de mayor importancia. Todo este segundo bloque de la serie dedicado al modelo de componentes nos conducirá a presentar un modelo de madurez que nos ayude a comparar alternativas de manera objetiva. Pero antes veremos como se pueden - y deben - adaptar y extender componentes. Estoy plenamente convencido de que, en muy corto espacio de tiempo, esta batalla no se librará en términos de qué framework utilizo (lo cual equivale a pensar a con qué solución locked-vendor me caso) sino, más bien, en términos de cómo extiendo mis componentes en la dirección que interesa a mi proyecto. De eso, ya os hablo en la siguiente entrega.