Systèmes FileMaker maintenables · Article 9 sur 18

Les contrats : la surface de communication

Une méthode sans contrat est une unité de comportement sans surface. L’architecture a défini la méthode. Elle n’a pas encore défini comment la méthode communique.

L’article 8 a défini la méthode. Une unité de comportement liée par contrat, avec une identité logique stable, une entrée déclarée, une sortie déclarée, et aucune dépendance au contexte d’exécution.

Il a établi l’unité de comportement. Il n’a pas encore établi la surface de communication.

Comment l’entrée arrive. Comment la sortie est retournée. À quoi ressemble un échec — et qui en est responsable. Une méthode qui ne peut pas recevoir d’entrée sans importer du contexte, et qui ne peut pas retourner un échec sans produire d’effet de bord, n’a pas échappé aux conditions que la série élimine. Elle les a reproduites à l’intérieur de l’unité qui était censée en être libre.


Nouveaux concepts introduits

  • Enveloppe
  • Enveloppe de requête
  • Enveloppe de réponse
  • Session (clé de l’enveloppe de requête)
  • Sortie anticipée
  • Erreur comme donnée

Ce que la communication exige

Une méthode est indépendante du contexte. Ce n’est pas une préférence. C’est une exigence structurelle. Une méthode dont la sortie dépend de l’ensemble trouvé, du modèle actif, de la fenêtre ouverte à l’écran, ou d’une variable globale ne peut pas être testée en isolation. Son comportement change sans que son contrat change. Cette architecture ne peut pas l’intégrer.

L’indépendance du contexte a une implication directe sur comment la communication doit fonctionner. Si une méthode ne peut pas dépendre d’un état ambiant, elle ne peut pas recevoir d’entrée à travers un état ambiant. L’entrée ne peut pas être lue depuis une variable globale qu’un script précédent a définie. L’entrée ne peut pas être inférée depuis l’enregistrement actif. L’entrée ne peut pas être empruntée à l’environnement. Tout ce dont la méthode a besoin doit arriver explicitement, sous une forme déclarée, avant que l’exécution commence.

La même règle s’applique à la sortie. Une méthode qui signale un échec en affichant une boîte de dialogue ne retourne pas un échec — elle produit un effet de bord. L’appelant ne peut pas voir une boîte de dialogue comme de la donnée. L’architecture ne peut pas raisonner sur une boîte de dialogue comme résultat contractuel. Si un échec doit être traité programmatiquement, il doit être retourné programmatiquement. Par le même canal que le succès. Dans une forme que l’appelant peut inspecter.

Un contrat de méthode n’est pas satisfait par le simple fait d’avoir une identité logique. Le contrat exige une structure de communication qui correspond à la pureté de la méthode elle-même : explicite, déclarée, et libre de dépendances implicites dans les deux directions.


L’enveloppe

L’enveloppe est l’unité de communication.

Une méthode possède le comportement. L’enveloppe possède la communication de ce comportement. Dans cette architecture, toute requête vers une méthode arrive sous forme d’enveloppe. Dans cette architecture, toute réponse d’une méthode repart sous forme d’enveloppe. Rien n’est transmis en dehors d’elle. Rien n’est supposé arriver par un autre canal.

L’enveloppe n’est pas une commodité. C’est la forme concrète du contrat. Le contrat d’une méthode déclare l’entrée et la sortie. L’enveloppe est la façon dont cette déclaration est honorée à l’exécution. Elle rend le contrat testable : si les entrées d’une méthode peuvent être décrites en examinant l’enveloppe, la méthode est conforme au contrat. Si une méthode exige quelque chose qui n’est pas dans l’enveloppe, elle dépend du contexte — peu importe si ce contexte est habituellement présent.


L’enveloppe de requête

Une méthode doit savoir ce qui lui est demandé. Elle doit recevoir son entrée déclarée. L’appel peut transporter des métadonnées décrivant les conditions dans lesquelles il a été effectué. Un appelant peut déclarer une intention pour ce qui doit se produire après l’exécution de la méthode. Certaines contraintes de plateforme exigent un contexte explicite qui ne peut pas être supposé. Et la requête peut déjà transporter un historique d’exécution accumulé avant que la méthode soit atteinte.

Ces responsabilités n’émergent pas d’une préférence d’implémentation. Elles découlent de ce qu’une méthode liée par contrat et indépendante du contexte exige pour fonctionner sans dépendances non déclarées. L’enveloppe de requête est la structure qui les contient.

{
  "app":          "…",
  "module":       "…",
  "method":       "…",
  "obj":          {},
  "meta":         {},
  "session":      {},
  "callbackData": {},
  "windowData":   {},
  "callstack":    "…"
}

La forme n’est pas arbitraire. L’ordre des clés reflète la façon dont la requête est construite et progressivement enrichie à mesure qu’elle traverse le système. Les trois premières clés établissent l’identité de l’appel. Tout ce qui suit est la substance de l’appel.

Les noms de clés présentés ici sont des conventions utilisées tout au long de cette série. Le contrat exige que ces responsabilités existent et demeurent explicites. Il n’exige pas ces noms précis. Les implémentations peuvent choisir des noms différents pourvu que la propriété, le rôle et la sémantique contractuelle demeurent inchangés.

app porte l’identité applicative — le nom de l’application appelée. Il délimite la requête à un contexte de déploiement spécifique et est requis quand l’appel traverse des frontières de fichier.

module porte l’identité de module — la frontière de propriété logique dans laquelle la méthode vit. Il nomme le groupe de responsabilités cohérent, pas un fichier ni un dossier.

method porte l’identité de méthode — l’unité de comportement spécifique demandée. Avec app et module, il forme l’identité adressable complète de l’appel : une opération spécifique, dans une frontière de propriété spécifique, dans une application spécifique.

obj porte l’entrée déclarée — la donnée sur laquelle la méthode a été invitée à opérer. Elle est fournie par l’appelant. C’est la seule donnée fournie par l’appelant sur laquelle la méthode peut agir sans effectuer une requête explicite paramétrée. Elle n’est pas optionnelle en concept, même quand elle est vide en pratique — un obj vide est une déclaration qu’aucune entrée n’est requise, ce qui est lui-même un énoncé contractuel.

meta porte les métadonnées d’appel — l’identité de l’appelant et les conditions d’exécution déclarées par l’appelant et applicables à l’invocation. Elle peut aussi porter des directives d’exécution que la frontière destinataire utilise pour déterminer si une information contextuelle déclarée, telle que session ou windowData, doit être complétée. Ce ne sont pas des données métier. Elles ne participent pas à la logique de la méthode. Elles existent pour transporter le contexte d’invocation et les exigences d’exécution sans polluer obj.

session porte le contexte de session déclaré — l’utilisateur courant, la locale, et d’autres conditions propres à la session que le contrat d’une méthode déclare explicitement consommer. Elle est optionnelle, et elle est ajoutée par la frontière qui reçoit l’appel selon le contrat déclaré de la méthode cible — non assemblée directement par l’appelant. Une méthode ne peut consommer que les valeurs de session que son propre contrat nomme ; tout le reste dans session n’est pas une dépendance que la méthode est autorisée à avoir. C’est ce qui empêche le contexte de session de devenir un état ambiant : il arrive par un canal déclaré, et le comportement d’une méthode ne peut dépendre que des valeurs précises qu’elle s’est engagée à utiliser.

callbackData porte l’intention de rappel — l’identité de ce qui doit se produire après l’exécution de cette méthode. Elle est fournie par l’appelant. La méthode ne la lit pas et n’agit pas sur elle. Sa présence ou son absence ne modifie pas le contrat de la méthode. Elle appartient à l’enveloppe comme intention appartenant à l’appelant, pas comme entrée de méthode.

windowData porte le contexte de fenêtre — des informations sur la fenêtre depuis laquelle l’appel provient, lorsque ces informations sont exigées par la méthode cible. Cette clé existe pour que le contexte lié à la fenêtre puisse franchir la frontière de la méthode explicitement plutôt qu’être inféré de la fenêtre FileMaker active. La méthode qui l’exige déclare cette exigence. La méthode qui ne l’exige pas ne la voit jamais. windowData décrit la fenêtre, non les données métier visibles à travers cette fenêtre.

callstack porte l’historique d’exécution — le registre accumulé de chaque appelant et méthode ayant participé au traitement de cette requête. La clé entre dans la méthode en portant déjà le chemin qui a mené à cet appel. La méthode y ajoute sa propre entrée avant de traiter. Elle fait partie du contrat. Elle voyage dans l’enveloppe parce que l’historique d’exécution est une donnée contractuelle, pas un état d’infrastructure.

Propriété

La propriété fait partie du contrat de l’enveloppe. app, module, method, obj, meta et callbackData proviennent de l’appelant. windowData et session sont ajoutés par la frontière qui valide la requête, selon le contrat déclaré de la méthode cible. callstack est enrichi à chaque saut d’exécution par les appelants et les méthodes. Rien dans l’enveloppe ne transite par un canal non déclaré. Rien n’entre dans l’enveloppe sans propriétaire.


L’enveloppe de réponse

Toute méthode retourne la même forme.

{
  "version": "1.0",
  "success": true,
  "result":  {},
  "error": {
    "type": "none",
    "code": 0,
    "data": {}
  }
}

version identifie la forme de l’enveloppe elle-même, afin que cette forme puisse évoluer sans briser les consommateurs existants.

success est un booléen. C’est la première chose qu’un appelant vérifie. Il ne porte pas de détail. Il répond à une seule question : la méthode s’est-elle complétée comme prévu ?

result porte la sortie de la méthode en cas de succès. Sa forme est déclarée. Pas par convention — par contrat. La structure de sortie d’une méthode est aussi stable que son identité logique. Un appelant qui dépend de result dépend d’une forme déclarée spécifique. Une méthode qui change la structure de ce qu’elle retourne dans result a changé son contrat, exactement de la même façon que changer les clés attendues dans obj change le contrat. Une sortie non déclarée n’est pas une valeur de retour. C’est un état ambiant qui voyage dans un canal structuré.

error porte les informations d’échec structurées sous forme d’un seul objet imbriqué, présent quel que soit le résultat. error.type classifie le résultat — none, exit, fail, ou contract — et c’est ce qui permet à un appelant de distinguer ces cas sans se fier à success seul. error.code et error.data portent les précisions : un code numérique et le contexte dont l’appelant a besoin pour comprendre ou récupérer de ce qui s’est passé. Aucun de ces éléments n’est une alternative à result — ce sont des structures supplémentaires dans la même enveloppe. Une méthode en échec retourne success: false, un result vide ou non pertinent, et un objet error renseigné. Un appelant qui reçoit un échec peut l’inspecter sans analyser du texte libre, faire correspondre des chaînes de dialogue, ou examiner une variable qui peut ou non avoir été définie.

La forme de l’enveloppe est invariante. Un succès et un échec retournent les mêmes clés. C’est cette invariance qui rend le contrat applicable : les appelants n’ont pas besoin de savoir à l’avance si une méthode va réussir pour traiter sa réponse. La structure est toujours la même. Seules les valeurs diffèrent.

Diagramme de la surface contractuelle montrant l'appelant construisant l'enveloppe de requête avec les neuf champs — app, module, method, obj, meta, session, callbackData, windowData, callstack — traversant la méthode, et recevant une enveloppe de réponse avec version, success, result, et un objet error imbriqué (type, code, data). Les annotations entrée déclarée et sortie déclarée encadrent la frontière. L'énoncé de frontière contractuelle indique : le contrat définit ce qui entre, ce qui sort, et ce qui n'est jamais supposé.
La surface contractuelle complète. L'appelant possède la requête. La méthode possède la réponse. Rien ne franchit la frontière sans être déclaré.

Trois résultats

Toute invocation de méthode produit exactement l’un de trois résultats.

Succès. La méthode a complété son contrat. success est vrai. result porte la sortie déclarée. error.type est none et error.code est zéro. L’appelant continue.

Sortie anticipée. La méthode s’est arrêtée avant de compléter son chemin d’exécution complet, pour une raison qui n’est pas un échec. Une condition de garde n’a pas été satisfaite. Une précondition a été évaluée à faux. Le système demeure dans un état valide : rien n’a échoué et rien n’a été laissé incomplet. success est faux, error.type est exit, et error.code est zéro. Cette combinaison est le signal mécanique. Un appelant qui reçoit success: false avec error.type: exit sait qu’une terminaison délibérée s’est produite. Aucune erreur n’a besoin d’être remontée. Aucune récupération n’est requise. La méthode n’a simplement pas continué, et l’a dit.

Échec. La méthode a rencontré une condition qu’elle ne pouvait pas satisfaire. Une validation a échoué. Un enregistrement requis n’existait pas. Une opération ne pouvait pas être effectuée. success est faux, error.type est fail, et error.code est non nul. Le code identifie l’échec. error.data porte ce que l’appelant doit comprendre et, si approprié, remonter.

La distinction entre sortie anticipée et échec est donc lisible depuis l’enveloppe sans interprétation. error.type: exit est une sortie. error.type: fail est un échec. Un appelant n’a pas besoin de savoir ce que la méthode fait en interne pour savoir quel type de non-succès il a reçu — et n’a pas besoin d’inférer cette distinction depuis error.code seul.

Ces trois résultats sont les seuls qui existent dans le modèle. Une méthode n’a pas de quatrième résultat. Elle ne se termine pas silencieusement. Elle ne laisse pas le système dans un état intermédiaire sans retourner. Elle ne produit pas de sortie par un canal autre que l’enveloppe de réponse. L’une de trois choses se produit, et l’enveloppe de réponse nomme toujours laquelle.

Diagramme de flux illustrant les trois résultats d'une invocation de méthode : succès quand le champ success est vrai ; sortie anticipée quand success est faux et error.type est exit ; échec quand success est faux et error.type est fail.
Toute invocation de méthode produit exactement l'un de trois résultats. L'enveloppe de réponse nomme toujours lequel.

L’erreur comme donnée

Lorsqu’elles ne sont pas explicitement supprimées ou capturées, les erreurs FileMaker sont souvent communiquées par des boîtes de dialogue. Une erreur de script déclenche un message de plateforme, ou un développeur ajoute un appel Afficher une boîte de dialogue personnalisée pour expliquer ce qui s’est mal passé. Dans les deux cas, l’échec communique à travers la surface d’interface — de façon visible, interactive, et au moment de l’échec.

Ce modèle est incompatible avec l’architecture décrite ici.

Une boîte de dialogue est un effet de bord. Ce n’est pas une donnée. L’appelant qui a déclenché la méthode ne peut pas inspecter une boîte de dialogue. Il ne peut pas la passer à la méthode suivante. Il ne peut pas la journaliser, la propager, ou l’acheminer. La boîte de dialogue disparaît quand l’utilisateur la ferme, emportant avec elle les informations d’échec. Le système n’a aucune trace que l’échec s’est produit, excepté à travers l’état qui a pu être modifié avant que la boîte de dialogue apparaisse.

L’architecture exige que l’échec soit une donnée. Non pas parce que les boîtes de dialogue sont inconvénientes, mais parce qu’un échec qui sort par tout autre canal que le contrat ne peut pas être gouverné. Une méthode qui affiche une boîte de dialogue a produit un effet de bord. Cela viole la pureté de méthode. Une méthode qui produit un effet de bord ne peut pas être raisonnée comme une unité. Le contrat ne décrit plus ce que la méthode fait.

L’enveloppe de réponse résout ceci en faisant retourner l’échec de la même façon que le succès. À travers l’enveloppe. Comme donnée structurée. L’appelant reçoit une réponse success: false avec un objet error — type, code, data — qui décrit ce qui s’est passé. La méthode a terminé. L’appelant décide quoi faire avec cette information.

C’est le seul arrangement compatible avec le reste du modèle. Les méthodes ne doivent pas afficher de boîtes de dialogue. Les méthodes ne doivent pas définir de variables globales avec des chaînes d’erreur. Les méthodes ne doivent pas signaler un échec par tout mécanisme extérieur à l’enveloppe de réponse. L’échec est retourné. Il n’est pas exécuté.

Ce qui se passe après qu’un échec est retourné est la responsabilité de l’appelant. Seul un appelant qui opère au niveau de la couche interface est en position de décider si un échec doit être remonté à un utilisateur — et comment. Cette décision n’appartient pas à la méthode. Le contrat de la méthode se termine à l’enveloppe de réponse. Tout ce qui vient après est hors de la portée de la méthode.


Validation du contrat

Un contrat est valide quand trois conditions sont vraies.

L’entrée est déclarée. L’entrée requise de la méthode est décrite par la forme de obj dans l’enveloppe de requête. Une méthode qui exige des données absentes de obj a une dépendance non déclarée. Le contrat ne couvre pas cette dépendance. La méthode n’est pas conforme au contrat.

La sortie est déclarée. Les données retournées par la méthode sont décrites par la forme de result dans l’enveloppe de réponse. Une méthode qui retourne des clés qu’un appelant n’attendait pas a changé sa sortie sans changer son contrat. Le contrat n’est plus précis. Un changement de contrat non suivi est indiscernable d’une rupture silencieuse.

L’échec est déclaré. Les modes d’échec qu’une méthode peut produire sont identifiés par les codes d’erreur qu’elle peut retourner. Une méthode qui échoue avec un code non déclaré a produit une condition d’échec que le contrat ne décrit pas. L’appelant ne peut pas traiter ce que le contrat ne nomme pas.

Ces trois exigences ne sont pas indépendantes. Une méthode peut en satisfaire deux et violer la troisième. Une méthode qui déclare l’entrée et la sortie mais échoue silencieusement a un contrat de requête valide et un contrat d’échec invalide. L’architecture raisonne sur les trois parce que les trois sont des chemins par lesquels un état implicite peut réentrer dans une unité qui devait être explicite.

Un contrat qui satisfait les trois peut être audité. Une enveloppe de requête donnée correspond ou ne correspond pas à l’entrée déclarée. Une enveloppe de réponse donnée porte ou ne porte pas la structure de sortie déclarée. Un code d’erreur donné appartient ou n’appartient pas à l’ensemble d’échecs déclarés. Ce sont des conditions testables, pas des intentions.


Le contrat est la contrainte

L’article 8 a établi que les méthodes sont liées par contrat. Cette affirmation a maintenant un sens spécifique. Une méthode est liée à son enveloppe de requête : la forme de ce qu’elle peut recevoir, et les canaux par lesquels elle peut le recevoir. Elle est liée à son enveloppe de réponse : la forme de ce qu’elle retourne, quel que soit le résultat. Elle est liée aux trois résultats : succès, sortie anticipée, échec — et aucun autre. Et elle est liée au modèle d’erreur : l’échec est une donnée, retournée à travers le contrat, jamais produite comme effet de bord.

Ces contraintes ne sont pas imposées aux méthodes depuis l’extérieur du modèle. Elles découlent de ce qu’une méthode est tenue d’être. Une unité indépendante du contexte ne peut pas accepter une entrée non déclarée. Un contrat stable ne peut pas permettre une sortie arbitraire. Un système qui raisonne sur le comportement ne peut pas tolérer des échecs qui sortent par des effets de bord.

L’enveloppe n’est pas un choix d’implémentation. C’est ce que les exigences propres de la méthode demandent. Une fois que la méthode est définie comme la série l’a définie, dans ce modèle l’enveloppe est la seule structure de communication permise par cette définition.


Ce que le modèle contient maintenant

L’architecture a une unité de comportement. Elle a une structure de communication. Le comportement n’existe que sous forme de méthodes. La communication n’existe que par des enveloppes. Aucune n’a une dépendance que l’autre ne déclare pas.

Un système construit sur ces deux fondations peut décrire son propre comportement en lisant les contrats de méthode. Il peut inspecter toute interaction en examinant les enveloppes. Il peut retracer les échecs à travers les données qu’ils retournent. Il peut raisonner sur tout changement — à l’entrée, à la sortie, aux modes d’échec — parce que ces choses sont déclarées, pas inférées.

Le comportement exige des méthodes. Les méthodes exigent des contrats. Une méthode sans contrat ne peut pas satisfaire les contraintes établies par la série. Elle ne peut pas être indépendante du contexte sans canal d’entrée déclaré. Elle ne peut pas être stable sans forme de sortie déclarée. Elle ne peut pas participer à un système qui raisonne sur l’échec sans rendre l’échec disponible comme donnée.

L’enveloppe est la forme concrète de ce contrat.

Ressources complémentaires

Cet article reflète ma propre expérience et ma perspective. De nombreuses idées ici ont été façonnées par des contributions venues de forums, de communautés, de blogues et de livres blancs. Des outils d'IA ont été utilisés comme assistant à la rédaction pour structurer et affiner le contenu.

Discussion

La discussion est ouverte pour cet article. Les questions, objections, corrections et contre-exemples sont bienvenus.

Envoyer un commentaire privé