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ïfconception fwtozza
un composant par action sur chaque tableun composant par agrégat de 1 à 3 tables liées
micro-composants d'UI (liste, champ, bouton) reliés par le brokerun composant par zone fonctionnelle ; UI interne libre
lire_x, creer_x, maj_x, suppr_xun message gerer_x avec oneOf
un message de résultat par opérationun x_result, ciblé par id
autoriser en éparpillant la logique dans chaque composantallowMessage central, au grain du membre oneOf
reconnaître un composant par un nom d'instance passé en payloadcomposants uniques ; corréler par ref, cibler par id

La procédure, à partir d'une spécification

  1. Inventorier

    Les données (tables) et les fonctionnalités (actions, écrans, règles d'accès).

  2. Regrouper les données en agrégats

    De 1 à 3 tables liées. Un agrégat = un composant serveur.

  3. Un message d'opérations par agrégat

    gerer_<agrégat> avec un oneOf, et un <agrégat>_result ciblé.

  4. 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.

  5. Côté client, un composant par zone fonctionnelle

    Pas par widget.

  6. Poser l'authentification

    Un composant serveur abonné à login écrit session.data.user ; au connected, il installe allowMessage.

  7. Écrire les catalogues, puis les composants

    Avec examples et counterexamples.

  8. 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) et gestion_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_changee et s'affichent ensemble.
  • Messages : sept. Les réponses à deux formes utilisent oneOf plutô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_connexion pose la porte au connected ; 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.