HeadlinesBriefing favicon HeadlinesBriefing.com

Architecture à Noyau Déterministe et Enveloppe

Hacker News •
×

Il y a quatorze ans, Gary Bernhardt a popularisé le terme Functional Core, Imperative Shell. Comme la plupart des bonnes idées en informatique, il n'était pas entièrement nouveau, mais sa conception avait une grande clarté et constitue une excellente base pour parler des tests et de la déterminisme dans les systèmes existants. Brèvement, l'architecture Functional Core Imperative Shell divise le code en deux parties.

Le Functional Core est purement fonctionnel - c'est-à-dire sans E/S et sans mises à jour d'état destructives. Il se préoccupe de la logique métier de l'application. L'Imperative Shell a moins de branches, mais maintient l'état, coordonne les dépendances externes et traite avec le monde extérieur - c'est-à-dire les E/S.

Son travail est de requêter le cœur avec des valeurs, de recevoir des valeurs en retour comme résultat d'une décision blackbox, et de les utiliser pour interagir avec le monde extérieur ; que ce soit en écrivant dans une base de données, en envoyant une requête, ou en mettant à jour une interface graphique. Le cœur et l'enveloppe dans ce modèle ont des caractéristiques distinctes : cœur enveloppe Prend des décisions Coordonne les dépendances Beaucoup de chemins d'exécution ramifiés Plus d'exécution linéaire Isolé du monde Intègre au monde Cela rend le cœur très adaptable aux tests. Étant donné qu'il est purement fonctionnel, les mêmes entrées donneront toujours les mêmes résultats. Étant donné qu'il est isolé, il n'y a rien à simuler ou à stubber. Et étant donné qu'il gère une logique métier complexe, les tests peuvent nous en dire beaucoup sur le comportement du système.

Pureté fonctionnelle et déterminisme Une façon plus courte de décrire les propriétés qui rendent les fonctions pures adaptables aux tests est qu'elles sont déterministes. C'est-à-dire : donné un flux d'entrées, une fonction pure retourne toujours le même flux de sorties ; leur comportement est répétable. Mais la programmation fonctionnelle pure n'est pas la seule façon d'y parvenir.

En inclinant légèrement la tête, nous pouvons voir qu'un flux de valeurs et une séquence d'affectations sont des moyens différents d'exprimer la même chose, et les machines à états peuvent nous apporter les mêmes avantages. Considérons le code suivant : 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 fonction add est facile à comprendre ; elle est pure et donc déterministe. Mais l'Add Machine l'est aussi - donnée la même séquence d'appels à la fonction transition, l'Add Machine retournera le même état. Être impératif ne change pas cela. 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 programmation fonctionnelle pure est un bon paradigme, mais en raison de considérations linguistiques ou de performances, elle n'est pas toujours pratique - je ne voudrais pas l'essayer en C ! Mais en affaiblissant les exigences de programmation purement fonctionnelle pour ne garder que le déterminisme, nous conservons les avantages de testabilité de Functional Core Imperative Shell, tout en élargissant son applicabilité.

Et voici le titre de ce post : Noyau Déterministe, Enveloppe Non Déterministe. Le déterminisme peut sembler être un concept plus abstrait que la pureté fonctionnelle. Comment le reconnais-tu ? Je trouve qu'il est plus facile de commencer par ce qui n'est pas déterministe et de travailler en arrière.

Voici quelques exemples courants d'un comportement non répétable : Appeler des générateurs de nombres aléatoires qui ne sont pas seedés Opérations asynchrones et multi-threadées Communication sur le réseau Communication avec d'autres processus Lecture Écriture dans le stockage local Interactions avec des bases de données Demander au système d'exploitation la date ou l'heure Tous cela appartient à l'enveloppe non déterministe. Lorsque vous les découvrez dans votre logique métier, vous avez une cible naturelle pour la fragmentation - soit diviser la fonction en deux autour d'eux, soit les soulever d'une couche et injecter leur résultat en tant que paramètre. Il est instructif de penser la métaphore de l'enveloppe littéralement ; elle devrait entourer la logique, interroger le cœur de l'application pour obtenir ce dont elle a besoin.

Travailler avec ce que l'on a Tout cela est bien, vous pourriez penser, mais à quoi ça sert pour moi, laboureur dans le code legacy...