Data-Context-Interaction, introduction avec exemple:

Aujourd’hui, un sujet un peu technique mais qui me tiens à cœur, et qui est très proche des tenants de l’agilité : le paradigme Data-Context-Interaction (DCI).

Je mettrais les passages les plus techniques en citation pour les épargner à ceux·elles qui ne souhaitent pas les lire.

Je dis « très proche de l’agilité » pour deux raisons:

  • Cette façon d’architecturer le code fait directement référence à la vision qu’avait originalement Alan Kay pour l’Orienté Objet, à savoir un réseau d’objets communiquant les uns avec les autres pour aider l’utilisateur·rice à comprendre le monde qui l’entoure, en modelisant directement dans le code son model mental. Il est donc difficile et inefficace de l’utiliser sans une collaboration étroite avec l’utilisateur et les experts du domaine modélisé puisqu’il se base en premier lieu sur l’implémentation de use-cases.

  • L’un des deux inventeurs du paradigme, James Coplien (l’autre étant Trygve Reenskaug (inventeur du concept Model-View-Controller, rien que ça)), a pu collaboré avec Jeff Sutherland, Ikujiro Nonaka, Christopher Alexander et d’autres noms connus, mais est également un grand contributeur de l’éco-système Scrum (voir le Scrum Pattern Language).

Le but est donc ici d’arriver au plus proche du modèle mental de l’utilisateur, ce qui rend la compréhension de l’architecture du logiciel plus simple.

Pour ça, prenons un exemple simple:

Je veux modéliser (grossièrement) un transfert bancaire.

A l’initialisation du transfert, un compte source va vérifier, selon ses propres fonds, si un transfert est possible. Si oui, il soustraira à son fond le montant du transfert, puis signalera à un compte destinataire d’augmenter son propre fond du montant désiré. Une fois que le compte destinataire aura fait de même, lui-même signalera au dépôt de transferts d’enregistrer la transaction avec les informations nécessaires.

C’est le Context.

Pour « jouer » le scénario de ce Context, j’aurais sûrement besoin de plusieurs acteurs·rices (comme au cinéma), à savoir le compte source, le compte destinataire et le dépôt d’enregistrement du transfert.

En schématisant, le code correspondant ressemblerait à ça

Context TransfertDeCompteACompte(
  montant,
  CompteSource :{augmenter() => void},
  CompteDestinataire: {baisser() => void},
  DepotDeTransferts: {logTransfert() => void}
) {

Role
CompteSource
  baisserLeMontantDuFond = montant => {
    CompteSource.baisser(montant)
    CompteDestinataire.augmenterLeMontantDuFond(montant)
  }

Role
CompteDestinataire
  augmenterLeMontantDuFond = montant => {
    CompteDestinataire.augmenter(montant)
    DepotDeTransfert.enregistrerLeTransfert(montant)
  }

Role
DepotDeTransfert
  enregistrerLeTransfert = montant => {
    DepotDeTransfert.logTransfer(CompteSource, CompteDestinataire, montant)
  }

// On lance la scène : 
CompteSource.baisserLeMontantDuFond(montant)
}

// Et on appelle le Context pour lancer le transfert
TransfertDeCompteACompte(400, CompteA, CompteB, SuperLoggerDeBank3000)

Vous remarquerez que dans ce scénario (qui ressemble fortement à un use-case simplifié), je n’ai mentionné nul part à quoi ressemblaient ces comptes et ce dépôt. C’est normal: ils peuvent être de n’importe quel forme, du moment qu’ils possèdent les attributs et possibilités d’avoir un fond, de pouvoir l’augmenter ou le diminuer (pour les comptes), et de pouvoir enregistrer une transaction (pour le dépôt).

Toutes les interactions décrites plus haut sont des interactions entre des Rôles, qui ont seulement besoin de « passer le casting » pour pouvoir jouer dans la scène qui nous intéresse. Le compte source pourrait être un « Compte Jeunes » et le destinataire un « Livret A », ça n’a aucune importance ici. De même, le dépôt pour enregistrer la transaction sur un fichier .txt ou une base de donnée, ça ne change rien à notre scénario ici.

C’est la partie Interaction entre les Rôles

C’est là où DCI commence à diverger de l’orienté Class qu’on prend aujourd’hui comme le standard de l’OOP. Ces rôles et interactions sont attachés au runtime à des objets, et n’ont d’existence et d’intérêt que dans le Context du use-case. Pour qu’un compte puisse participer au scénario, il suffit simplement qu’il satisfasse l’interface

{
  fondDisponible: number
  augmenter: (number) => void
  baisser: (number) => void
}

L’architecture n’est donc pas formé de façon hiérarchique avec une class « compte » de laquelle on dériverait tous les comportements imaginables rendant le comportement de l’objet peu lisible. Le comportement des objets est ici lié au contexte, aux use-cases, dynamiquement.

La partie Data est par contre celle où le domaine métier entre en jeu, avec les Class comme on les utilisent aujourd’hui. Il faut bien évidemment que les comptes répondent à des exigences et des caractéristiques précises (le compte jeune peut avoir un découvert maximal, et aura surement un client associé avec un ID, nom, prénom, etc…), mais ces informations ne nous intéressent pas dans le Context en question. Tout ce qu’on veut, en tant qu’utilisateur, c’est que le compte destinataire reçoivent l’argent au final.

La View étant elle même un objet, elle peut également être une actrice dans ces scénarios. Pour un exemple concret, jetez un œil sur cette implémentation du Morpion : src · master · Samuel Abiassi / TicTacToe-with-DCI · GitLab

Avez vous déjà entendu parlé de ce paradigme ? Que pensez vous de cette approche ? Résout-elle selon vous des problèmes que vous avez pu observer dans vos bases de code ?

1 « J'aime »

Bien sûr, cette présentation est seulement une introduction et plutôt partielle. N’hésitez pas à poser des questions, je serais ravi d’y répondre en preparation d’un talk sur le sujet plus tard.

J’ai amélioré le use-case en ajoutant un bouton permettant de relancer le jeu.

Vous pouvez voir la différence ici : Add a reset button to enhance the use case (7c404ed0) · Commits · Samuel Abiassi / TicTacToe-with-DCI · GitLab

Personne d’intéressé par le sujet ? :astonished: @Alexandre_Quercia @nicobiot ?

Ce sujet demande du temps pour y participer de manière constructive. Et j’ai d’autres sujets d’apprentissage sur le feu en ce moment.

À propos du kata du morpion que tu partages.
Comme il n’y a pas d’exemple du comportement, cela ajoute une couche supplémentaire de compréhension.

Exemple du comportement ? T’entends quoi par ça ?

Exemple naïf :

Quand le premier joueur place sa croix
Alors personne n’a encore gagné, le jeu continu
Et c’est au tour du second joueur

Ah bah si, au contraire ! Absolument tout le comportement est dans le use-case.

C’est exactement pour ça que le paradigme est intéressant !

Le seul problème, c’est que le langage est pas strictement designé pour implémenter DCI, mais si tu regardes ligne 167, t’as ta porte d’entrée.

Comprends-tu maintenant pourquoi …

Oui, sur ça, pas de soucis. :sweat_smile: Je m’emballe un peu sur le sujet parce que je le trouve extrêmement plus intéressant que la DoR par exemple :stuck_out_tongue: .

2 « J'aime »

Par contre, je peux te poser des questions. :slight_smile:

@Samuel_Abiassi

  1. Quels problèmes as-tu eus dans tes bases de code ?
  2. Et lesquels ont été résolus grâce à ce concept ?
  3. As-tu pu appliquer ce concept dans le contexte professionnel, et lequel ?

J’avais créé une implémentation d’un morpion il y a longtemps. J’apprenais React à l’époque, et j’avais encore du mal avec les notions de séparations de responsabilités. Le code qui en a résulté était totalement fonctionnel, mais en revenant dessus cette semaine, je me suis rendu compte que j’étais incapable de voir qui déclenchait quoi, où, comment, et surtout pourquoi.

Avec DCI, j’ai une séparation clair entre ce que le système est et ce que le système fait. Le use case existe dans son contexte, est auto-suffisant et j’ai beaucoup plus de facilité à rentrer dedans.

L’autre avantage, c’est que ma solution en React de l’époque était étroitement liée au framework. En DCI, le détail de ce qui implémente le use-case m’importe peu, et toute l’information qui se trouve directement dans le contexte est exhaustive et significative dans le contexte en question. Mais surtout, le use-case pourrait être utilisé très rapidement dans n’importe quel autre contexte parce qu’il n’a pas d’adhérence avec l’implémentation autre que les signatures requises dans les paramètres de la fonction (le duck typing de JS aidant beaucoup).

En plus de ça, je me suis amusé à dériver 3 implémentations différentes partant du même use case, et je n’ai jamais eu à modifier une seule ligne de code dudit use-case, justement parce qu’il ne changeait pas.

Pour l’instant non, pas en dehors du domaine du hobby. Mais je t’avouerais que se battre contre l’idée très installée de ce qu’est l’orienté objet aujourd’hui (avec ses factory, impl, heritage, dep injection…) est assez compliqué… Quasi autant que de faire comprendre l’importance d’avoir de solides bases en UX et Psychologie aux dev

@Samuel_Abiassi ici, principalement j’essaie de reformuler, pour m’assurer que j’ai bien compris, reprend moi si je me trompes.

Le problème de ne pas comprendre son propre code écrit il y a des mois.

Le but est aussi qu’il compréhensible par les autres développeurs ? Personnellement, je n’ai pas trouvé facilement le point d’entrée.

src/use-cases/play-tictactoe-game.ts

Étant dans le dossier use-cases, il est peut être noyé parmi les cas d’usages.

Le point d’entrée de l’application, est-ce aussi un cas d’usage ou bien c’est le contexte ?

En effet, la dépendance des règles métiers au framework, n’est jamais une bonne idée.

Donc, les règles métiers ignorent entièrement le framework, c’est cool ça.

En quoi, ces trois implémentations était différentes ?


As-tu essayé d’en parler sans utiliser le terme d’orienter-object ?

Cela me fait penser au même problème qu’avec l’agilité.

Le but c’est d’en parler sans utiliser le champ lexical de l’agilité. Car chacun peut avoir une définition distinctes de ces termes.

Par exemple, moi je fais le rapprochement avec l’architecture hurlante.

C’est un point d’entrée et un cas d’usage parmi tout ceux disponible. Mais tous les cas d’usage sont disponible dans use-cases.

C’est les trois UI qui se trouvent dans le dossier components. Le premier est simplement une grille clickable par deux joueurs, le deuxième simule un API envoyant des coordonnées d’emplacement dans la grille, et le troisième permet de jouer contre une IA (très mauvaise, mais c’était pas le but de la démo). Tous les trois utilise le context play-tictactoe-game de manière transparente. Que l’UI soit une grille ou un générateur de coordonnées et que les joueurs soit humains ou IA ne change rien au use-case.

Non, parce que c’est bien le problème. La compréhension de ce qu’est le paradigme et ce qu’il évoque est importante. Redéfinir le contexte est vital.