Gestion des composants dans le prototype

Dans l’article précédent, j’abordais avec simplicité le pattern Model/View et j’expliquais comment on pouvait avoir une structure robuste en quelques lignes de code.

Aujourd’hui on va améliorer ça en ajoutant une gestion de composants, la gestion d’un tout petit espace de nom, une injection d’un contexte d’application dans les actions, les mutations et les évènements.

L’idée d’un composant c’est d’avoir une partie d’application qui puisse avoir les mêmes méthodes de base que l’application racine et les autres composant.
On va donc commencer par créer une classe abstraite qui nous permettra d’imposer cette structure.

class AbstractComponent:
    def __init__(self, name, root):
        self.name = name
        self.root = root

        self.state = {}
        self.actions = {}
        self.mutations = {}

    async def dispatch(self, namespace, payload={}):
        return await self.root.dispatch(namespace, payload)
    
    def dispatch_sync(self, namespace, payload=Payload()):
        return self.root.dispatch_sync(namespace, payload)

    def commit(self, namespace, payload={}):
        self.root.commit(namespace, payload)

    def on(self, event_name, callback):
        self.root.on(event_name, callback)

    def emit(self, event_name, payload):
        self.root.emit(event_name, payload)

Chaque composant détient donc un state qui lui appartient, et de même pour les actions et les mutations.
De plus le composant pourra appeler les méthodes du Root Application.
Donc peu importe dans quel Composant on se trouvera, les fonctionnements de base sont les mêmes.

Maintenant on doit pouvoir injecter le composant dans le Root App, on va donc ajouter une méthode qui ressemble à ça :

    def use(self, component_class):
        if issubclass(component_class, AbstractComponent):
            new_component = component_class(self)
            self.states[new_component.name] = new_component.state
            self.actions[new_component.name] = new_component.actions
            self.mutations[new_component.name] = new_component.mutations
            self.components[new_component.name] = new_component

L’important, c’est la manière dont on va construire nos composants. En étendant la classe AbstractComponent un composant pourra définir ses actions, ses mutations et ses évènements directement à la construction de l’instance du composant.

Créons un composant de Test en reprenant une partie du code de l’article précédant :

class TestComponent(AbstractComponent):
    def __init__(self, root):
        super().__init__("Test", root)

        self.state['name'] = "NoName"
        self.actions['chooseNameAction'] = self.chooseNameAction
        self.mutations['updateNameMutation'] = self.updateNameMutation
        self.on('Test.chooseNameAction', self.onChooseNameAction)
        self.on('Test.updateNameMutation', self.onUpdateNameMutation)

    """
    On défini une méthode de choix d'un nom parmis un tableau de nom.
    """
    async def chooseNameAction(self, context, payload):
        choice = random.choice(payload.names)
        context.commit('Test.updateNameMutation', {'name': choice})

    """
    On défini une méthode qui permettra d'afficher le payload envoyé à
    la méthode de choix de nom.
    """
    def onChooseNameAction(self, context, 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 updateNameMutation(self, states, payload):
        self.state['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 onUpdateNameMutation(self, context, payload):
        print('Mutation UPDATE_NAME executed')
        print(f'Payload : {payload}')
        print('-----------------------------')
        print('Actual State')
        print(context.states[self.name]['name'])
        print('-----------------------------')

Les évolutions sont les suivantes

  1. On n’enregistre plus les actions, mutations, et évènement directement dans le root app mais plutôt directement pendant la construction de l’instance du composant.
  2. On peut constater l’apparition d’un paramètre « context » dans les méthodes. Ce contexte est la représentation d’une partie du contexte du Root App, on va pouvoir accéder à des méthodes ou des données qu’on choisira au préalable.
    Par exemple, dans les actions on peut trouver les méthodes Dispatch et Commit dans ce contexte. Cela pourra paraître comme un double emploi, mais après certaine modification, cela deviendra plus clair.
  3. On peut également constater l’apparition de la méthode dispatch_sync() qui est un alias de la méthode du même nom dans le RootApplication.
    Dispatch_Sync() permet d’exécuter la méthode Dispatch() de manière synchrone avec le module Asyncio directement.

Maintenant on va devoir faire évoluer le code du Root App :

Application.py

import asyncio


class Context:
    def __init__(self):
        pass


class Payload:
    def __init__(self):
        pass


class AbstractComponent:
    def __init__(self, name, root):
        self.name = name
        self.root = root

        self.state = {}
        self.actions = {}
        self.mutations = {}

    async def dispatch(self, namespace, payload=Payload()):
        return await self.root.dispatch(namespace, payload)

    def dispatch_sync(self, namespace, payload=Payload()):
        return self.root.dispatch_sync(namespace, payload)

    def commit(self, namespace, payload=Payload()):
        self.root.commit(namespace, payload)

    def on(self, event_name, callback):
        self.root.on(event_name, callback)

    def emit(self, event_name, payload=Payload()):
        self.root.emit(event_name, payload)


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

    def get_namespace(self, namespace, _space="actions"):
        parts = namespace.split('.')
        root = getattr(self, _space)

        for i in parts:
            if root[i]:
                root = root[i]

        return root

    @staticmethod
    def build_payload_object(data):
        payload = Payload()
        for i in data:
            payload.__setattr__(i, data[i])
        return payload

    async def dispatch(self, action_name, payload=Payload()):
        action = self.get_namespace(action_name)

        context = Context()
        context.__setattr__("commit", self.commit)
        context.__setattr__("dispatch", self.dispatch)
        context.__setattr__("dispatch_sync", self.dispatch_sync)

        self.emit(action_name, payload)

        return await action(context, self.build_payload_object(payload))

    def dispatch_sync(self, action_name, payload=Payload()):
        return asyncio.run(self.dispatch(action_name, payload))

    def commit(self, mutation_name, payload=Payload()):
        mutation = self.get_namespace(mutation_name, 'mutations')
        mutation(self.states, self.build_payload_object(payload))
        self.emit(mutation_name, payload)

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

    def emit(self, event_name, payload=Payload()):
        if event_name in self.events:
            context = Context()
            context.__setattr__("commit", self.commit)
            context.__setattr__("dispatch", self.dispatch)
            context.__setattr__("states", self.states)

            self.events[event_name](context, payload)

    def use(self, component_class):
        if issubclass(component_class, AbstractComponent):
            new_component = component_class(self)
            self.states[new_component.name] = new_component.state
            self.actions[new_component.name] = new_component.actions
            self.mutations[new_component.name] = new_component.mutations
            self.components[new_component.name] = new_component

Pas mal de chose ont évolués dans le code de base du RootApplication.

  1. On a défini deux objet vide, chacun ayant leur nom, Context et Payload. Ils serviront respectivement à construire des instances de ces objets, ainsi et permettront d’avoir une syntaxe plus accessible par après.
  2. On peut désormais récupérer une méthode (action, mutation) via une chaine de caractères espacée de point.
  3. On peut construire un Context d’application, qui permet de donner accès aux méthode commit, dispatch, aux states à l’intérieur d’une fonction.
    A l’intérieur même d’un composant, ce n’est pas si utile on va me dire. Mais en fait le contexte est donné afin de pouvoir l’utiliser dans une méthode qui n’est pas forcément une méthode du composant.
  4. Lorsqu’on passe un dictionnaire comme payload dans la méthode dispatch, il est converti en objet Payload

Et comme on l’a déjà bien retaper, on peut finir le code qui nous permet de tester l’application :

main.py

from Application import AbstractComponent, RootApplication
import random


class TestComponent(AbstractComponent):
    def __init__(self, root):
        super().__init__("Test", root)

        self.state['name'] = "NoName"
        self.actions['chooseNameAction'] = self.chooseNameAction
        self.mutations['updateNameMutation'] = self.updateNameMutation
        self.on('Test.chooseNameAction', self.onChooseNameAction)
        self.on('Test.updateNameMutation', self.onUpdateNameMutation)

    """
    On défini une méthode de choix d'un nom parmis un tableau de nom.
    """
    async def chooseNameAction(self, context, payload):
        choice = random.choice(payload.names)
        context.commit('Test.updateNameMutation', {'name': choice})

    """
    On défini une méthode qui permettra d'afficher le payload envoyé à
    la méthode de choix de nom.
    """
    def onChooseNameAction(self, context, 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 updateNameMutation(self, states, payload):
        self.state['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 onUpdateNameMutation(self, context, payload):
        print('Mutation updateNameMutation executed')
        print(f'Payload : {payload}')
        print('-----------------------------')
        print('Actual State')
        print(self.state['name'])
        print('-----------------------------')


if __name__ == '__main__':
    app = RootApplication()
    app.use(TestComponent)

    i = 0

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

Là on a utilisé qu’un seul composant, mais il n’y a absolument rien qui nous empêche d’en faire un deuxième et constater qu’on peut tout à fait les faire fonctionner et interagir entre eux.

Et maintenant la suite c’est quoi ?

La suite, c’est problème épineux qui me trotte en tête depuis un moment, d’ailleurs qui me trottait en tête déjà pendant que je développais sur NodeJS.

Le Multithreading.

Mon besoin, c’est d’avoir une application capable d’être scalable, capable de se renforcer si le besoin s’en fait ressentir.
Si je ne charge qu’un seul marché par exemple et qu’il est mit à jours chaque seconde, rien de problématique, tout sera calculé rapidement que ce soit les indicateurs voir des calcul d’IA.

Mais si j’ai besoin d’écouter plusieurs marchés, la demande en puissance va augmenter assez rapidement et je vais me retrouvé assez vite avec une app qui rame.

Le prochain article va donc porter sur ce sujet, comment exploiter le multithreading via la méthode Dispatch.
Il faudra que je puisse faire en sorte que chaque fois que cette fonction est appelée, cet appel construise une nouvelle « tâche de travail » à faire pour les cœurs du processeur et que toute la charge de travail soit répartie entre eux.

J’espère que cet article vous aura plu ! Encore une fois n’hésitez pas à vous inscrire sur le site pour taper des commentaires 🙂

A tout bientôt !

Ecrire un commentaire