HeadlinesBriefing favicon HeadlinesBriefing.com

Arquitectura de Núcleo Determinístico y Capa Externa

Hacker News •
×

Hace catorce años, Gary Bernhardt acuñó el término Functional Core, Imperative Shell. Como la mayoría de las buenas ideas en computación, no era completamente nueva, pero su concepción tenía gran claridad y constituye una excelente base para hablar sobre pruebas y determinismo en sistemas existentes. Brevemente, la arquitectura Functional Core Imperative Shell divide el código en dos partes.

El Functional Core es puramente funcional: es decir, sin IO y sin actualizaciones de estado destructivas. Se encarga de la lógica de negocio de la aplicación. El Imperative Shell tiene menos ramificaciones, pero mantiene el estado, coordina dependencias externas y trata con el mundo exterior: es decir, IO.

Su trabajo es consultar el núcleo con valores, recibir valores como resultado de alguna decisión blackbox, y usarlo para interactuar con el mundo exterior: ya sea escribir en una base de datos, enviar una solicitud o actualizar una interfaz gráfica. El núcleo y la capa en este modelo tienen características distintas: Núcleo Capa Toma decisiones Coordena dependencias Muchas rutas de ejecución ramificadas Más ejecución lineal Aislado del mundo Integra con el mundo Esto hace que el núcleo sea muy apto para las pruebas. Dado que es puramente funcional, las mismas entradas siempre dan los mismos resultados.

Dado que está aislado, no hay nada que mockee o stub. Y dado que maneja lógica de negocio compleja, las pruebas pueden decirnos mucho sobre cómo se comporta el sistema. Pureza funcional y determinismo Una forma más corta de describir las propiedades que hacen que las funciones puras sean aptas para las pruebas es que son determinísticas.

Eso es: dado un flujo de entradas, una función pura siempre devuelve el mismo flujo de salidas; su comportamiento es repetible. Pero la programación funcional pura no es el único camino para llegar allí. Si inclinamos la cabeza un poco, podemos ver que un flujo de valores y una secuencia de asignaciones son formas diferentes de expresar lo mismo, y las Máquinas de Estado pueden traernos los mismos beneficios.

Considera el siguiente código: function add(ns) {return ns.reduce((a, b) => a + b, 0)} class Add Machine {#state = 0 transition(input) {this.#state += input} get state() {return this.#state}} La función add es fácil de razonar; es pura y por lo tanto determinista. Pero la Add Machine también es determinista: dada la misma secuencia de llamadas a la función transition, la Add Machine devolverá el mismo estado. Ser imperativo no cambia eso. const output = add([1, 2, 3]) const a = new Add Machine() a.transition(1) a.transition(2) a.transition(3) const output = a.state La programación funcional pura es un buen paradigma, pero debido a consideraciones de lenguaje o rendimiento, no siempre es práctico: ¡no querría intentarlo en C! Pero al debilitar los requisitos de funcionalidad pura a solo determinista, mantenemos los beneficios de testabilidad de Functional Core Imperative Shell, mientras ampliamos su aplicabilidad.

Y así el título de esta publicación: Núcleo Determinístico, Capa Externa No Determinística. El determinismo puede sentirse como un concepto más abstracto que la pureza funcional. ¿Cómo lo reconoces? Encuentro que es más fácil comenzar con lo que no es determinista y trabajar hacia atrás. Aquí tienes algunos ejemplos comunes de comportamiento no repetible: Llamar a generadores de números aleatorios que no están sembrados Asincrónico y operaciones multihilo Comunicación sobre la red Comunicación con otros procesos Lectura Escritura en almacenamiento local Interacciones con bases de datos Pedir al sistema operativo la fecha o la hora Todos estos pertenecen a la capa no determinista.

Cuando los encuentres en tu lógica de negocio, tienes un objetivo natural para la fragmentación: ya sea dividir la función en dos alrededor de ellos, o elevarte una capa e inyectar su resultado como parámetro. Es ilustrativo pensar en la metáfora de la capa de manera literal; debe rodear la lógica, consultando el corazón de la aplicación para obtener lo que necesita. Trabajando con lo que tienes Esta es toda una buena historia, podrías pensar, pero ¿qué sentido tiene para mí, trabajando en el código legado?.