Travail des fondations de l’application : Appliquer le pattern Model/View

Voilà un moment que je n’ai pas écrit d’article, plusieurs raisons à cela mais je vais entrer dans les détails. 

Le plus important est que j’arrive avec du neuf et de nouvelles idées pour l’application.

Je me suis remémoré la façon dont j’utilisais le Store VueX de VueJS en Javascript et je trouvais l’algorithme vraiment léger, rapide et complet. Du coup, je me suis dit que ce serait une excellente solution pour notre application et sa structure fondatrice.

L’algo se base sur le pattern Model/View qui est lui même une variante du pattern Model/View/Controller. L’idée est d’avoir une vue (View) constamment mise à jour à chaque modification des données en mémoire (Model).

Pour débuter cet article j’aimerais revenir sur une image de la structure globale de l’application telle que je la voyais :

L’important dans cette structure, c’est de pouvoir faire en sorte que tous les composants puissent communiquer entre eux et ce de plusieurs manières.

Prenons un exemple bien concret : Créer une instance d’un marché d’une plateforme d’échange et écouter toutes les mises à jours de ce marché.

  1. On va devoir faire tourner une boucle pour charger les données régulièrement.
  2. Il faut que l’interface graphique puisse être mise à jour à chaque changement.
  3. Il faut que les éventuels indicateurs puissent se mettre à jours à chaque mise à jour des données.
  4. Toutes les données doivent pouvoir être sauvegardée en base de données.

On se retrouve avec trois types d’exécutions :

  1. Des Actions concrètes telles que le chargement de données à l’extérieur, calcul d’indicateurs, sauvegarde de données en BDD, etc..
    Toutes ces actions ne modifies pas directement les données ou l’état de l’application. Par contre elles pourront ordonner des mutations sur l’état de l’application.
  2. Les Mutations sur l’état de l’application. L’état de l’application ce sont toutes les données qui définisse l’application. Par exemple, le titre d’une fenêtre, la taille de la fenêtre, l’instance d’un marché qu’on écoute, la timeframe du marché qu’on écoute. En gros, toutes ces données qui donneront une information de l’état de l’application.
    Lorsqu’on veut changer l’état global de l’application, on lui apporte une ou plusieurs mutations.
  3. Les Évènements liés à l’état de l’application et des actions entreprise dessus.
    Pour reprendre l’exemple d’une instance de marché : Lorsqu’on charge les dernières mise à jour de prix on exécute une action qui va lancer un événement “LOAD_MARKET_DATA” par exemple. On va donc pouvoir choisir d’écouter cet évenement ou non en fonction de nos besoin.

    Lorsque les données auront été correctement chargée et qu’on les aura à disposition, il faudra opérer une Mutation dans l’état de l’application pour les prendre en compte. Cette mutation va aussi envoyer un événement, par exemple “MARKET_DATA_UPDATED”, qu’on pourra écouter et exploiter à notre guise.

    Par exemple la mutation MARKET_DATA_UPDATED enverra un event sur lequel on enregistrera une action à entreprendre qui serait par exemple de mettre à jour la vue de l’interface graphique, on pourra aussi enregistrer le calcul des indicateurs qu’on aura défini sur ce marché.

Définition d’un prototype de base d’application

Maintenant qu’on a défini les types d’exécution, on va définir une première structure d’Application, un prototype qu’on pourra réutiliser un peu partout.

On défini notre Root Application en plusieurs parties :

  1. Les States : Ce sont les différents états de l’application, c’est là qu’on enregistrera les données en mémoire. La mémoire dont on parle ici est la mémoire vive, on ne parlera pas de mémoire morte telle qu’une base de données ou un fichier, ou tout autre chose stockée sur un disque.
  2. Les Actions : Ce sont les différentes Actions de l’application qu’on pourra exécuter, ces actions sont des algorithmes plus ou moins complexe qui exploite l’état de l’application et envoie des ordres de mutation pour modifier l’état de notre application.
  3. Les Mutations : Les Mutations ne servent qu’à une seule chose, changer l’état de l’application, c’est à dire changer une donnée stockée en mémoire. 
  4. Les Événements : Ils sont déclenché par les Actions et Mutations, on peut les écouter ou les déclencher directement (bien qu’il soit préférable de laisser ça aux Action et Mutation).
  5. Les Composants : Je n’en ai pas parlé, mais c’est avec eux qu’on va construire toute l’application et ce sont eux qui exploiteront tout ce petit système d’échange de manière intense. L’idée des composants c’est de pouvoir construire l’application en plusieurs morceaux distinct avec chacun leurs rôle respectif.

Pour exploiter correctement la structure de l’application il va nous falloir définir des méthodes d’exécution qui correspondent à chaque parties :

  1. dispatch() : On veut pouvoir exécuter une action juste en la nommant, donc sans avoir besoin d’aller chercher son instance en mémoire de manière manuelle. On doit pouvoir parler au système et dire “exécuter l’ordre 66” par exemple.
    Cette méthode est forcément asynchrone afin de pouvoir permettre d’exécuter toute une chaîne d’actions et d’attendre la fin de l’exécution.
  2. commit() : Le rôle du commit est de lancer l’exécution d’une mutation et d’envoyer un événement de changement du State.
  3. on() : On veut pouvoir définir l’exécution d’une méthode callback sur l’écoute d’un événement, quel qu’il soit.
  4. emit() : On veut pouvoir définir l’envoi d’un évènement et de données liée à cet événement. 

Passons donc au code maintenant !

Application.py

class ApplicationClass:
    def __init__(self):
        self.states = {}
        self.actions = {}
        self.mutations = {}
        self.events = {}
        self.components = {}

    async def dispatch(self, action_name, payload={}):
        if action_name in self.actions:
            self.emit(action_name, payload)
            return await self.actions[action_name](payload)
        else:
            raise AttributeError(f"Action [{action_name}] doesn't exist")

    def commit(self, mutation_name, payload={}):
        if mutation_name in self.mutations:
            self.mutations[mutation_name](self.states, payload)
            self.emit(mutation_name, payload)

    def on(self, event_name, callback):
        self.events[event_name] = callback

    def emit(self, event_name, payload):
        if event_name in self.events:
            self.events[event_name](payload)

Voilà, avec ce code d’une trentaine de lignes, on peut enregistrer des méthodes d’actions, des méthode de mutations, le tout relié par des événements permettant d’écouter et d’exécuter d’autres méthodes de manière “automatique”.

D’ailleurs, essayons un peu tout ça histoire de voir directement ce qu’on peut déjà en faire.

main.py

from Application import ApplicationClass
import random
import asyncio

"""
On défini une méthode de choix d'un nom parmis un tableau de nom.
"""
async def choose_name_action(payload):
    choice = random.choice(payload['names'])
    app.commit('UPDATE_NAME', {'name': choice})

"""
On défini une méthode qui permettra d'afficher le payload envoyé à
la méthode de choix de nom.
"""
def on_choose_name_action(payload):
    print('Action Choose Name Executed')
    print(f'Payload : {payload}')
    print('-----------------------------')

"""
On défini la méthode de mutation qui permettra de changer le state
"""
def update_name_mutation(states, payload):
    states['name'] = payload['name']

"""
On défini une méthode permettant de vérifier le payload de la mutation
et de vérifier que le state à bien été muté.
"""
def on_update_name_mutation(payload):
    print('Mutation UPDATE_NAME executed')
    print(f'Payload : {payload}')
    print('-----------------------------')
    print('Actual State')
    print(app.states['name'])
    print('-----------------------------')


if __name__ == '__main__':
    app = ApplicationClass()
    # Définition du state "name"
    app.states['name'] = "NoName"
    # Enregistrement de la méthode d'action
    app.actions['chooseNameAction'] = choose_name_action
    # Enregistrement de la méthode de mutation
    app.mutations['UPDATE_NAME'] = update_name_mutation
    # Définition des méthode d'écoute d'évenement
    app.on('chooseNameAction', on_choose_name_action)
    app.on('UPDATE_NAME', on_update_name_mutation)

    i = 0

    # On exécute l'action plusieurs fois pour clairement voir les effets
    while i < 5:
        print(f'LOOP : {i}')
        asyncio.run(app.dispatch('chooseNameAction', {'names': ['marc', 'morgan', 'elie', 'mia']}))
        i += 1

Rien de tel qu’un exercice simple pour se rendre compte des possibilités.

Avec ce petit bout de code on peut déjà créer une application assez dynamique.

Le prochain article traitera des Composants, comment les construire, comment les intégrer et comment les utiliser et les faire interagir.
Il faudra définir un espace de nom afin d’éviter les conflits de nommage entre les différents composants leurs action, mutation et autre événement.
On verra également comment intégrer directement un “contexte” de l’application directement via les méthodes dispatch et commit.

Enfin, dans un autre article, je veillerai à montrer comment je compte gérer le multithreading avec cette méthode, je n’ai pas encore fini mes investigation de ce côté, mais ça ne saurait tarder.

Avec cette méthode je pense que j’ai enfin trouvé mon saint graal, une méthode de travail structurée qui me permet de bien diviser les tâches et les dépendances et qui me permettra surtout de créer plusieurs mouture de l’application sans devoir tout recommencer.

En effet, j’ai parlé d’une application Desktop, mais j’ai très fort envie de pouvoir faire une déclinaison pour Serveur. Et tout le travail apporté à la version Desktop pourra être simplement réutilisé sur la version Serveur en enlevant le composant de l’interface graphique et en le remplaçant par une version serveur.

Comme d’habitude, j’espère que la lecture vous aura plus, n’hésitez pas à vous inscrire pour taper un commentaire 😉

A bientôt !