Ir al contenido principal
Laravel, shipping fast.
Capítulo 21 · Más allá de una aplicación

Las lecturas y las escrituras pueden ir por caminos distintos

Julian Beaujardin

Hay un lugar donde la regla del hub cede, y vale la pena nombrarlo para que siga siendo el único.

Una escritura cambia un estado del que dependen otros servicios. Si dos escrituras compiten, o una se aplica a medias, alguien tiene que reconciliar el resultado. Así que las escrituras pasan por el hub, donde hay un solo lugar para ordenarlas, reintentarlas y verlas fallar.

Una lectura no cambia nada. Si la aplicación del cliente muestra un recuento de visitantes con un segundo de antigüedad porque se lo pidió directamente al servicio de analíticas, nada se rompió más abajo. Así que una lectura puede ir directa al servicio dueño de los datos.

La ventaja de decirlo en voz alta es que quien revisa no necesita un diagrama de arquitectura para decidir dónde va una llamada nueva. Necesita la respuesta a una pregunta: ¿cambia algo? Si cambia algo, pasa por el hub. Si no, puede ir directa.

Tendrás la tentación de hacer excepciones, normalmente para una herramienta interna. Cuando las hagas, escribe la excepción junto a la regla. Una excepción sin escribir es la manera en que «las escrituras pasan por el hub» deja de ser verdad sin que nadie lo haya decidido.

Compartir código: un paquete, a la manera de Laravel

Un segundo servicio necesita el mismo log de peticiones, el mismo ID de traza, los mismos códigos de error, el mismo limitador. Puedes escribir ese código otra vez, o escribirlo una vez y depender de él.

La respuesta de Laravel a «escríbelo una vez» es un paquete: una dependencia de Composer con un service provider. Es lo que son todos los paquetes propios de Laravel, incluidos los que ha usado este libro. (Documentación de Laravel: Package Development.)

// src/ApiFoundationServiceProvider.php
class ApiFoundationServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/api.php',
            'api',
        );
    }

    public function boot(): void
    {
        $this->loadMigrationsFrom(__DIR__.'/../migrations');

        $config = __DIR__.'/../config/api.php';

        $this->publishes([
            $config => config_path('api.php'),
        ], 'api-config');
    }
}

La aplicación no registra el service provider. El paquete lo nombra en su propio composer.json, bajo extra.laravel.providers, y Laravel lo descubre al instalarlo. Añadir la dependencia es toda la integración.

La configuración se fusiona, no se copia. mergeConfigFrom() le da a cada aplicación los valores por defecto del paquete. Una aplicación que necesita algo distinto publica el archivo y cambia un valor. Nadie tiene que mantener al día una copia completa.

Se prueba como un paquete. Orchestra Testbench arranca una aplicación mínima de Laravel alrededor de tu paquete dentro de su propia suite de tests, de modo que el middleware compartido queda demostrado antes de que ningún servicio lo instale.

Una vez que el middleware de trazas del Capítulo 7 se mudó al paquete, ningún otro servicio tuvo que escribirlo. Llega con la dependencia, como la paginación llega con Eloquent.

El principio de fondo es que lo que debe ser idéntico en todas partes debería heredarse, no recordarse. «Cada servicio debería registrar las peticiones de la misma manera, y cada desarrollador sabe que hay que copiar el patrón» se salta en algún sitio, tarde o temprano, por alguien nuevo, con un plazo encima. Alguien se olvidará. Un paquete decide si olvidarse es posible.

Qué va dentro

Emitir licencias le corresponde a la License API. El contrato, el driver, el objeto tipado: nada de eso va en el paquete compartido. Solo un servicio tiene esa preocupación. Moverlo «por consistencia» les daría a todos los demás servicios una dependencia de la idea que Statamic tiene de una licencia, y cada cambio pagaría un impuesto de coordinación por una preocupación que solo tiene un servicio.

La prueba cabe en una frase, y es lo que impide que un paquete compartido se convierta en un cajón de cosas sueltas: si borraras este código del paquete y lo pegaras en cada servicio, ¿seguirían siendo idénticas todas las copias?

El log de peticiones la pasa. Los ID de traza la pasan. Los códigos de error la pasan. La lógica de negocio nunca, porque en el momento en que la pegas en un segundo servicio empieza a desviarse para ajustarse al problema de ese servicio.

Una biblioteca sigue siendo una biblioteca

Un paquete es código que se ejecuta dentro del proceso de cada aplicación. No es algo que despliegas, y no se lo puede llamar por la red.

Que siga así. El log de peticiones del Capítulo 6 escribe en la base de datos que la aplicación anfitriona ya tenga, después de que la respuesta ha salido. El paquete no levantó infraestructura para ejecutarlo. Tomó prestado lo que había.

En el momento en que un «paquete compartido» ejecuta su propio worker, mantiene su propia conexión a una base de datos o expone un endpoint al que llaman todos los servicios, se ha convertido en un servicio en todos los sentidos que importan salvo en el de aparecer en tu diagrama de arquitectura y en tu turno de guardia. Ese es el peor tipo de servicio que se puede operar: el invisible.

El precio de una versión compartida

Un arreglo en el paquete no está en producción en ningún sitio hasta que cada servicio lo instala. Etiquetar una versión no hace eso. Cada servicio tiene que pasar a la versión nueva, y cada servicio tiene que verificarse sobre ella, porque «el paquete cambió» y «todos los servicios que dependen de él siguen funcionando» son dos hechos distintos, y solo el primero es automático.

Así que un arreglo de una línea cuesta una versión y después, por cada servicio, una actualización, una pasada de análisis estático y una pasada de tests. Ese es el precio honesto de garantizar que seis servicios nunca puedan discrepar, sin que nadie lo note, sobre cómo se registra una petición.

Automatiza la actualización. Un job programado que abre un pull request en cada servicio cuando el paquete publica una versión es el trabajo de una tarde. Que la automatización nunca se lleve por delante la verificación: el pull request se fusiona porque el pipeline del Capítulo 14 de ese servicio pasó contra la versión nueva, y por ninguna otra razón.

Por eso la superficie compartida tiene que seguir siendo pequeña. Cada clase que añades al paquete es una clase que cada servicio verifica en cada versión, para siempre. Un paquete compartido se gana su lugar conteniendo las cinco o seis cosas que de verdad deben ser idénticas.

Resumen del capítulo 21

  • Conserva una sola aplicación hasta que un límite real te fuerce a una segunda. No dividas para sentirte sofisticado.
  • Decide cómo se relacionan los servicios. Un hub con hojas mantiene todos los caminos de llamada con la misma forma.
  • Cada proveedor externo tiene un servicio dueño, y solo el dueño tiene sus credenciales.
  • Las escrituras pasan por el hub. Las lecturas pueden ir directas. Deja por escrito cada excepción.
  • Comparte código como un paquete de Laravel: un service provider descubierto, configuración fusionada, tests bajo Testbench.
  • Comparte solo lo que seguiría siendo idéntico si se pegara en cada servicio.
  • Un paquete se ejecuta sobre la infraestructura del anfitrión. Si necesita la suya, es un servicio, y debería tratarse como tal.
  • Una versión nueva no está en producción hasta que cada servicio la ha instalado y ha pasado sus propias comprobaciones.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.