Concevoir une application
La méthode de découpage : des écrans et des fonctionnalités vers les composants et les messages. Son cœur est une doctrine de granularité : ne pas multiplier les composants ni les messages.
Quatre règles
1. Un composant = une fonction cohérente, jamais une action ni un widget
Découpe par fonction et/ou déploiement. Un agrégat de une à trois tables liées, toujours lues et écrites ensemble, fait un composant serveur. Une zone fonctionnelle (une vue et ses contrôles) fait un composant client. À l'intérieur, programmez normalement.
Deux morceaux méritent d'être deux composants seulement si l'une de ces questions reçoit « oui » : pourraient-ils être déployés séparément ? l'un est-il réutilisable ou affichable indépendamment de l'autre ? ont-ils des cycles de vie ou des responsabilités franchement différents ? Sinon, un seul.
2. Un message = une capacité, regroupée par oneOf
gerer_clients: {
versions: { 1: { revisions: { 0: {
oneOf: ["read", "create", "update", "delete"],
fields: {
read: { type: "object", fields: { filtre: { type: "object", required: true, fields: { nom: { type: "string" } } } } },
create: { type: "object", fields: { client: { type: "object", required: true, fields: {
nom: { type: "string", required: true },
email: { type: "string", required: true, pattern: "^[^@]+@[^@]+$" },
} } } },
update: { type: "object", fields: { id: { type: "number", required: true }, patch: { type: "object", required: true, fields: {} } } },
delete: { type: "object", fields: { id: { type: "number", required: true } } },
},
} } } },
examples: { 1: { 0: [ { read: { filtre: {} } }, { create: { client: { nom: "Acme", email: "a@b.co" } } }, { delete: { id: 42 } } ] } },
counterexamples: { 1: { 0: [ { reason: "aucun membre oneOf", payload: {} }, { reason: "deux membres oneOf", payload: { read: { filtre: {} }, delete: { id: 42 } } } ] } },
},
À fuir : lire_clients, creer_client, maj_client, suppr_client. On note un couple gerer_clients.read. Séparer en messages distincts seulement si les opérations ont des abonnés différents, des cycles de vie disjoints ou une frontière de déploiement différente : la requête et sa réponse sont deux messages.
3. L'authentification se branche sur cette granularité
session.allowMessage = (name, payload) => {
const user = session.data.user;
if (!user) return name === "login";
if (name === "gerer_clients" && payload.delete) return user.role === "admin";
return true;
};
4. Les résultats aussi : un message de retour, ciblé
this.broker.publish(this, { clients_result: { ok: true, rows }, id: message.id });
Tableau des anti-patterns
| réflexe naïf | conception fwtozza |
|---|---|
| un composant par action sur chaque table | un composant par agrégat de 1 à 3 tables liées |
| micro-composants d'UI (liste, champ, bouton) reliés par le broker | un composant par zone fonctionnelle ; UI interne libre |
lire_x, creer_x, maj_x, suppr_x | un message gerer_x avec oneOf |
| un message de résultat par opération | un x_result, ciblé par id |
| autoriser en éparpillant la logique dans chaque composant | allowMessage central, au grain du membre oneOf |
| reconnaître un composant par un nom d'instance passé en payload | composants uniques ; corréler par ref, cibler par id |
La procédure, à partir d'une spécification
Inventorier
Les données (tables) et les fonctionnalités (actions, écrans, règles d'accès).
Regrouper les données en agrégats
De 1 à 3 tables liées. Un agrégat = un composant serveur.
Un message d'opérations par agrégat
gerer_<agrégat>avec unoneOf, et un<agrégat>_resultciblé.Tracer les frontières client/serveur
Ce qui doit être relocalisable devient un composant plus un message de transit, déclaré partagé et autorisé au canal.
Côté client, un composant par zone fonctionnelle
Pas par widget.
Poser l'authentification
Un composant serveur abonné à
loginécritsession.data.user; auconnected, il installeallowMessage.Écrire les catalogues, puis les composants
Avec
examplesetcounterexamples.Contrôle final
Chaque message a un émetteur et au moins un abonné ; aucun couple de composants ne se distingue par un nom d'instance ; le nombre de composants et de messages reste proportionné. Sinon, re-regroupez.
Exercice guidé : le chat
La spécification
Un salon unique. Connexion par pseudo et mot de passe ; un pseudo inconnu est créé. Fil de messages avec historique à l'arrivée. Liste des connectés avec mention « écrit… ». Lignes d'arrivée et de départ. /quit. Seul un participant identifié peut poster.
Le découpage attendu, et pourquoi ces frontières
- Données : utilisateurs, messages. Deux agrégats ? Les messages référencent les utilisateurs, mais leurs cycles de vie diffèrent franchement (un compte dure, un message s'accumule). Le chat en fait deux composants serveur :
gestion_connexion(utilisateurs, sessions, présence) etgestion_messages(messages, mention d'écriture, historique). L'état « connectés » est partagé par la session, pas par un troisième composant. - Client : deux zones fonctionnelles, l'écran de connexion et l'écran du fil. La liste des connectés et le fil ne sont pas séparés : ils lisent le même
presence_changeeet s'affichent ensemble. - Messages : sept. Les réponses à deux formes utilisent
oneOfplutôt que deux contrats. La présence est une liste complète, pas des messages « a rejoint » et « a quitté ». La mention d'écriture voyage dans la présence, pas dans un message à part. - Auth :
gestion_connexionpose la porte auconnected; la liste blanche est la demande d'accès et la déconnexion.
Comparez avec le graphe réel.
Exercices à faire seul
Liste de tâches partagée
Spécification. Plusieurs personnes voient la même liste. Chacune peut ajouter une tâche, la cocher, la supprimer. Les tâches cochées restent visibles. Connexion par pseudo seul.
Correction
Serveur : auth (pseudo → session.data.user, allowMessage), taches (un agrégat : la table des tâches). Client : login_panel, liste_taches (liste, champ d'ajout, cases : une zone). Messages : login / login_result ; gerer_taches avec oneOf: ["read", "create", "check", "delete"] ; taches_changees (serveur → tous, liste complète), ciblé par id pour la lecture initiale, diffusé pour les modifications. Quatre messages, quatre composants.
Piège : un message tache_cochee, tache_ajoutee, tache_supprimee en retour. Un seul taches_changees suffit : le client repeint la liste.
Quiz en salle
Spécification. Un animateur pose des questions à choix multiple ; les participants répondent sur leur téléphone ; l'animateur voit la répartition des réponses en direct et passe à la question suivante. Les participants ne voient pas les réponses des autres.
Correction
Rôles : animateur et participant, distingués par session.data.role après login. Serveur : auth, quiz (questions + réponses : un agrégat). Client : ecran_animateur, ecran_participant, login_panel. Messages : login/login_result ; piloter_quiz avec oneOf: ["suivante", "ouvrir", "fermer"], admis par allowMessage seulement si role === "animateur" ; repondre ; question_posee (diffusé à tous) ; repartition (ciblé par id sur la seule session de l'animateur). L'autorisation par rôle est au grain du message, posée une fois dans la grille.
Tableau de scores
Spécification. Un jeu quelconque envoie des scores ; un tableau affiché sur un grand écran montre les dix meilleurs, en direct ; une page d'administration permet d'effacer un score.
Correction
Deux populations de clients (le jeu, l'écran, l'admin) peuvent partager un canal ou en avoir deux ; avec deux canaux, deux fichiers de catalogue et un composant d'auth par canal. Serveur : scores (un agrégat), auth. Messages : gerer_scores avec oneOf: ["ajouter", "effacer"] (effacer réservé à l'admin par allowMessage), classement (diffusé). Le grand écran est un composant client qui n'émet rien : emits = [], et c'est normal.