January 12, 2019

Branch by Abstraction en Componentes React

En ocasiones, nos vemos en la situación de tener que actualizar varios componentes de nuestra interfaz de usuario para ajustarlos a nuevas directrices de diseño. Otras veces, necesitamos reemplazar su implementación para hacer frente a nuevos requisitos de negocio. Si el número de cambios es lo suficientemente alto, es posible que no se puedan realizar de manera atómica (o tengan que ser sincronizados con eventos fuera del control del equipo de desarrollo como, por ejemplo, la publicación de una nueva imagen de marca) a la vez que necesitamos continuar entregando valor en forma de nuevas funcionalidades, corrigiendo defectos o aplicando alguna que otra mejora.

En este artículo vamos a hablar sobre una estrategia que nos permita obtener un flujo de integración continua evitando, por tanto, mantener los cambios en nuestros componentes en una rama independiente en el sistema de control de versiones durante el transcurso de su desarrollo.

¿Cómo lo conseguiremos? Haciendo uso de dos técnicas muy habituales en equipos que trabajan con prácticas como continuous integration o continuous delivery: Feature Flags (o Feature Toggles si leéis a Martin Fowler) y Branch By Abstraction.

Ambas estrategias nos permitirán fusionar código de manera regular sobre la rama principal, aún con funcionalidades parcialmente terminadas, evitando así mantener ramas en paralelo que podrían estar días (o semanas) sin integrarse con el trabajo realizado por el resto del equipo.

Las feature flags son, conceptualmente, bloques condicionales que permiten habilitar o deshabilitar una funcionalidad (o un camino de ejecución) en base a un conjunto de condiciones (como pueden ser: el entorno de ejecución, el perfil del usuario o un porcentaje de tráfico)

Pongamos como ejemplo que, en nuestra aplicación, llevamos ya un tiempo utilizando <AwesomeCalendar /> (un componente de terceros) como widget de calendario. En los puntos donde es necesario mostrar un calendario, importamos el componente y lo insertamos en el layout de la página.

import React from "react";

import { AwesomeCalendar } from "awesome-calendar";

function Page() {
  return (
    <Page>
      <Header />

      <Body>
        <AwesomeCalendar date={new Date(2018, 6, 21)} />
      </Body>
    </Page>
  );
}

Debido a una serie de requisitos, tenemos que crear un nuevo componente para mostrar un calendario que sustituya a AwesomeCalendar y le añada un extra de funcionalidad para dar cabida a nuevas historias de usuario. El desarrollo del componente se extenderá durante varias semanas.

El primer paso para poder integrar nuestro componente de manera continua es asegurar que somos capaces de controlar cuándo y cómo se utiliza para poder aislar a los consumidores de AwesomeCalendar de los cambios en el mismo. Para ello, vamos a empezar creando una nueva abstracción Calendar que pasará a ser utilizada por el resto de componentes y mantendrá la misma interfaz que ofrecía AwesomeCalendar:

import React from "react";

import { AwesomeCalendar } from "awesome-calendar";

export function Calendar({ date }: { date: Date }) {
  return <AwesomeCalendar date={date} />;
}

Como vemos, la manera de consumir este nuevo componente sería muy parecida a la versión anterior:

import React from "react";

import { Calendar } from "../components";

function Page() {
  return (
    <Page>
      <Header />

      <Body>
        <Calendar date={new Date(2018, 6, 21)} />
      </Body>
    </Page>
  );
}

En este punto, ya podríamos fusionar nuestros cambios con la rama de código principal para que el resto de trabajo en curso pueda ir haciendo uso de nuestro (no tan) nuevo componente.

A continuación, vamos a crear una implementación muy sencilla de nuestro propio calendario, que empezará apoyándose en una implementación nativa de datetime pero respetando la interfaz original del componente AwesomeCalendar:

import React from "react";

export function NewCalendar({ date }: { date: Date }) {
  const value = /* implementation details */;

  return <input type="datetime-local" value={value} />;
}

Una vez hemos eliminado la responsabilidad de elegir qué calendario utilizar de los consumidores del componente, podemos utilizar feature flags para determinar qué calendario mostrar en cada momento.

En este caso, vamos a permitir que cualquier entorno que no sea producción utilice siempre nuestro propio calendario, con el objetivo de obtener feedback sobre su experiencia de uso a medida que avanzamos en su desarrollo:

export function isNewCalendarEnabled() {
  return process.env.NODE_ENV !== "production";
}

Este caso, representamos la feature flag con una simple función que comprueba el entorno de ejecución actual, pero existen sistemas más sofisticados para gestionar el estado de los toggles en nuestra aplicación.

Ahora, nuestra nueva abstracción Calendar (que hace las veces de factoría) puede hacer uso de esta función para determinar qué componente de calendario utilizar, manteniendo al resto de consumidores aislados de toda complejidad:

import React from "react";

import { AwesomeCalendar } from "awesome-calendar";

import { NewCalendar } from "./components";

import { isNewCalendarEnabled } from "../features";

export function Calendar(props: { date: Date }) {
  return isNewCalendarEnabled() ? (
    <NewCalendar {...props} />
  ) : (
    <AwesomeCalendar {...props} />
  );
}

A partir de este momento, todas las pantallas de nuestra interfaz de usuario que necesiten de un calendario, utilizarán la abstracción Calendar y será esta misma abstracción la que se encargará de utilizar la implementación concreta del calendario que corresponda en función del entorno de ejecución.

Como vemos, aplicar técnicas como branch by abstraction y feature flags nos permite alcanzar un flujo de integración (y entrega) continua en nuestros componentes aportándonos beneficios cómo:

  • Minimizar la superficie de cambios a fusionar cuando integramos trabajo en curso
  • Tomar el control sobre cuándo se ejercita un camino de ejecución y poder habilitarlo y deshabilitarlo en base a una serie de condiciones
  • Dotar a los equipos de negocio de visibilidad sobre el trabajo en curso en tareas de larga duración (sin sacrificar ninguna de las premisas anteriores)