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 ?