FyHooks

Cette page formalise un pattern de programmation utilisé depuis des dizaines d'années sous de nombreuses formes. Il ne me semble pas que des outils génériques aient été développés pour programmer de cette manière.

Programmation par fonctionnalité

Toutes les applications qui évoluent deviennent difficilement maintenables car leur code devient de plus en plus compliqué et surtout intriqué.

Pour éviter de problème, il suffit de ne pas modifier le code source de l'application. On ajoute une fonctionnalité à l'application, en ajoutant un fichier contenant toutes les informations nécessaires pour l'implémenter.

Ceci est possible en empruntant un concept utilisé par Emacs : les hooks (crochets), ceux-ci permettent de modifier une fonction du système en ajoutant un crochet qui sera exécuté avant ou après la fonction dont le comportement doit être modifié. Plusieurs crochets peuvent s'exécuter avant et après la fonction initiale.

Utiliser cette méthode de programmation, a de nombreux avantages :

  • des fichiers sources extrêmement courts et indépendants les uns des autres.
  • des possibilités d'introspection et de modifications dynamiques de l'application.
  • Désactivation facile de n'importe quelle fonctionnalité.
  • Ajouter une nouvelle fonctionnalité est très facile, il suffit de partir du code source d'une fonctionnalité existante.

Les exemples sont en python mais le principe est adaptable à tous les langages de programmation même bas niveau. De la même façon que l'on a pas besoin d'un langage objet pour faire de la programmation objet. Un version minimale en langage C existe.

Une fonctionnalité

Elle est définie par un fichier source. Le nom de la fonctionnalité est le nom du fichier source. Outre les définitions de fonctions, et variables, elle définit les fonctions à ajouter aux hooks et éventuellement de nouveaux hooks permettant de l'étendre.

Comment utiliser un hook

Le hook défini un endroit par lequel le code pourra être étendu par les fonctionnalités.

Création d'un hook :

h = hooks.new("nom_du_hook",
              "Description de ce *hook*.",
              # Arguments attendus
              args = ("chaine", "un entier"),
              )

Ajout d'une fonction à exécuter :

@hooks.add_to("nom_du_hook",
              # Optionnel
              after="nom_de_fonctionnalité",
              before=("fonctionnalité A",
                      "fonctionnalité B:fonction X"),
              always=False,
             )
def ma_fonction(chaine, entier):
    print chaine, entier
    # return True    pour ne pas appeler les suivantes

Si l'attribut 'always' est vrai, la fonction est appellée même si une précédente a retourné True.

Les attributs 'after' et 'before' peuvent contenir :

  • Un nom de fonctionnalité
  • Un nom de fonctionnalité ':' un nom de fonction
  • Un tuple/list des valeurs précédentes peut être indiqué.
  • '*' pour indiquer toutes les fonctionnalités, mais cela ne s'applique pas aux fonctionnalité pour lesquelles il a été indiqué directement ou indirectement qu'elle s'exécutait après.
  • '**' pour indiquer que l'on est après toutes celle qui sont après '*'. Ainsi de suite.

Utilisation d'un hook, est simple, on l'utilise comme une fonction :

h = hooks.get("nom_du_hook")
annulation = h("une chaine", 42)

La valeur de retour est la valeur de retour de la dernière fonctions à avoir retourné une valeur vrai afin afin d'arrêter l'évaluation du hook

L'ordre de création des hooks et de l'ajout des fonctions n'a pas d'importance. Tout est ordonné au moment de l'exécution du programme en fonction des contraintes after, before qui ont été définies.

Changer l'évaluateur du hook

Parfois il est nécessaire de changer la manière dont les fonctions contenues dans le hook sont appelées.

Evaluateur défini par défaut

On arrête l'exécution si une des fonctions retourne vrai, mais l'on exécute quand même les fonctions ajoutées avec l'attribut always.

def default_evaluator(hook, *args, **kargs):
    run = True
    value = None
    for function in hook.functions:
        if function.enabled and (run or function.always):
            v = function.function(*args, **kargs)
            if v:
                run = False
                value = v
    return value

h = hooks.new("nom_du_hook",
              "Documentation expliquant comment utiliser\n"
              "ce *hook*."),
              evaluator = default_evaluator
              )

Questions pour les futurs évolutions :

  • L'attribut always doit-il devenir un attribut de la fonction qui est ajoutée. Ce mot-clef pollue les arguments de la méthode add, pourquoi celui-ci et pas les autres. On pourrait l'ajouter comme un décorateur de la fonction.
  • L'évaluateur reçoit toutes les fonctions même celles des fonctionnalités qui ont été désactivées. Est-ce une bonne idée ? Existe-t-il un cas ou c'est utile ?

Conseils d'utilisation

Dans la pratique n'importe quelle fonctionnalité peut interagir avec les autres. Il est donc préférable de toujours passer le même argument à tous les hooks, il contiendra le contexte de l'application. Si l'application est multi-thread il faut ajouter un deuxième argument contenant les informations locales à la thread.

Chaque fonction du hook va lire et modifier ces contextes.

Les hooks minimaux recommandés sont :
  • "init" appelé au démarrage
  • "args" analyse les arguments de la ligne de commande
  • "action" exécute le travail

Les autres hooks sont créés par les fonctionnalités qui en ont besoin.

Il est possible de ne définir qu'un seul hook initial, mais cela complique l'ordonnancement des fonctionnalités. Et bien sûr cela ne facilite pas la compréhension du code.

Exemples

Vous pouvez essayer l'application exemple sur le web. Mais elle est bridée afin de ne pas être déconfigurée par les utilisateurs. Son adresse est http://demo710.univ-lyon1.fr/FYHOOKS/

Les sources complets sont dans http://perso.univ-lyon1.fr/thierry.excoffier/FYHOOKS/fyhooks.tar.gz En comptant tous les fichiers Python, même ceux qui ne sont pas des fonctionnalités, on a une taille moyenne de 40 lignes et la taille médiane est de 30 lignes.

Dynamisme

La fonctionnalité 'reload' permet de recharger le code source de l'application afin de pouvoir la mettre à jour sans avoir besoin de l'arrêter. Elle crée un hook 'reload' qui est appelé à chaque rechargement afin de vider les caches internes ou recalculer ce qui est nécessaire dans les différentes fonctionnalités.

Sortie de l'application

La fonctionnalitée 'exit' crée un hook 'exit' qui est appelé lorsque le processus se termine. Toutes les traitements qui nécessitent de faire le ménage avant de quitter s'ajoutent à ce hook.

Multi thread

La fonctionnalitée 'thread' crée un hook 'thread' qui crée autant de thread qu'il y a de fonctions dans le hook. L'application ne pourra se terminer que quand tous les threads se seront terminés.

Serveur web

La fonctionnalité serveur web va créer un hook 'url' dans lequel on ajoutera toutes les fonctions permettant de traiter les adresses afin de générer la page web.

Quand une fonction a fini de générer le contenu de la page, elle retourne True pour que les suivantes ne soient pas inutilement appelées.

  • La fonctionnalité 'profilage' ajoutera une fonction au début afin de mémoriser le moment de début de la requête et une à la fin pour afficher le temps de génération de la page. La fonction finale est toujours exécutée.
  • La fonctionnalité 'bad url' affiche la page d'erreur si aucune fonction n'a arrêté l'exécution du hook
  • La fonctionnalité 'media' s'occupe des url commençant par 'media' elle crée un hook 'media'. Les url de ce type envoient les fichiers avec un type mime différent de 'text/html'
  • La fonctionnalité 'css' s'ajoute dans le hook 'media' Elle crée un hook 'css' qui permet de générer le contenu du fichier CSS. Elle s'inscrit au reload afin de vider son cache en cas de modification du code. L'url du fichier CSS est versionnée afin que le rechargement du CSS par le navigateur web soit immédiate.
  • Des actions qui sont indiquées comme paramètre de l'url après le '?'. Les actions sont prioritaires dans le hook. Quand l'action est exécutée, elle s'enlève de l'url et laisse l'exécution continuer afin de générer la page web.
  • La vérification de session est très prioritaire, elle ouvre une nouvelle session s'il n'y en a pas. Si la session est déjà ouverte, elle met le contexte courant à jour.
  • Une fonctionnalité permet de changer le nom de la racine du site web qui par défaut est '/'. Contrairement aux autres fonctionnalités, 2 fichiers existant ont du être modifiés car il n'existe pas de hook de transformation globale du HTML

Calculatrice

Elle définit un hook dans lequel s'ajoutent toutes les opérations. Elle ajoute au contexte de l'application une fonction qui permet d'évaluer son paramètre.

Les opérations dans le hook peuvent être :

  • Détection des moins unaires
  • Addition et soustraction
  • Multiplications et divisions
  • Élimination des parenthèses inutiles

Le calcul sans interface utilisateur ne sert à rien. On ajoute donc des fonctionnalités pour interagir avec cette calculatrice :

  • Évaluer l'expression indiquée dans la ligne de commande
  • Lire le clavier et évaluer chaque expression saisie
  • Ajouter la calculatrice à l'interface web
  • Ouvrir un socket et évaluer les expressions reçues.

Traduction

Le hook traduction permet d'ajouter de nombreuses fonctionnalités :

  • Évidemment la traduction de la langue
  • L'ajout de liens web automatique sur des mots clef
  • L'ajout de style d'affichage
  • L'ajout de bulle d'aide contenant par exemple le nom de la fonctionnalité qui a affiché le texte.

Problèmes en suspends

Des questions sans réponses :

  • Conseiller de faire un sous contexte par hook pour éviter les conflits de nom ?