JavaScript - Apprendre les avantages du modèle RORO : fonction recevant et retournant un objet,
Par Bill Sourour et traduit par danielhagnoul

Le , par danielhagnoul, Rédacteur
Auteur : Bill Sourour
Source : Elegant patterns in modern JavaScript: RORO

Ce billet résume, en français, les avantages du modèle RORO (recevoir un objet, retourner un objet) de Bill Sourour.

  • Paramètres nommés
  • Paramètres par défaut et requis plus propres
  • Valeurs de retour plus riche et composition de fonction plus facile


Paramètres nommés

Supposons que nous ayons une fonction qui renvoie une liste d'utilisateurs dans un rôle donné et supposons que nous devons fournir une option pour inclure les informations de contact de chaque utilisateur et une autre option pour inclure les utilisateurs inactifs, traditionnellement nous pourrions écrire : function findUsersByRole ( role, withContactInfo, includeInactive ) {...}.

Un appel à cette fonction pourrait ressembler à : findUsersByRole( 'admin', true, true ).

En passant un objet notre fonction semble presque identique sauf que nous mettons des accolades autour de nos paramètres : function findUsersByRole ( { role, withContactInfo, includeInactive } = {} ) {...}.

Cela fonctionne en raison d'une fonctionnalité JavaScript introduite dans ES2015 appelée la déstructuration (destructuring).

Déstructuration : const {x, y} = {x: 11, y: 8}; ce qui est pareil que : const {x: x, y: y} = {x: 11, y: 8};.
Vous pouvez également combiner des raccourcis de valeur de propriété avec des valeurs par défaut : const {x, y = 1} = {}; // x = undefined; y = 1.

Maintenant, nous devons appeler notre fonction comme ceci : findUsersByRole( { role: 'admin', withContactInfo: true, includeInactive: true } ).

Nous utiliserons cette méthode (RORO) lorsqu'elle ajoute de la valeur en rendant une liste de paramètres plus claire et flexible et en rendant une valeur de retour plus expressive. Bien entendu, si vous écrivez une fonction qui n'a besoin de recevoir qu'un seul paramètre, la réception d'un objet est trop lourde. De même, si vous écrivez une fonction qui peut communiquer une réponse claire et intuitive à l'appelant en retournant une valeur simple, il n'est pas nécessaire de retourner un objet.

Paramètres par défaut et requis plus propres

Code JavaScript : Sélectionner tout
1
2
3
4
5
function findUsersByRole ({ 
  role = kRequiredParam('role'), // fonction incluse dans dvjhUtilities-1.7.0.js, voir le billet précédent. 
  withContactInfo = true,  
  includeInactive = true 
} = {} ) {...}

Maintenant, nous pouvons appeler notre fonction comme ceci : findUsersByRole( { role: 'admin' } ) ou comme ceci : findUsersByRole( { role: 'admin', includeInactive: false } ) ou comme ceci : findUsersByRole( { includeInactive: true, role: 'admin' } ).

N'oubliez pas que l'on passe un objet, nous pouvons donc donner les propriétés dans un ordre aléatoire et omettre une propriété lorsqu'elle a une valeur par défaut.

La propriété "role" ne pouvant recevoir une valeur par défaut valide, on lui donne la valeur de retour (Error) de kRequiredParam('role'). On peut également utiliser la fonction kRequiredParamVerbose() (fonction incluse dans dvjhUtilities-1.7.0.js, voir le billet précédent). Exemple :

Code JavaScript : Sélectionner tout
1
2
3
4
5
function findUsersByRole ({ 
  role = kRequiredParamVerbose( { param : "role", type : "String", info : "rôle de l'utilisateur" } ), 
  withContactInfo = true,  
  includeInactive = true 
} = {} ) {...}

Si quelqu'un appelle findUsersByRole sans fournir de rôle, il obtiendra l'erreur suivante :
Required parameter is missing : param = role, type = String, info = role jouer par le personne
at findUsersByRole
.

Valeurs de retour plus riche et composition de fonction plus facile

Imaginez une fonction qui enregistre un utilisateur dans une base de données. Lorsque cette fonction renvoie un objet, elle peut fournir beaucoup d'informations à l'appelant. Lorsque nous insérons des lignes dans une table de base de données (si elles n'existent pas déjà) ou les mettons à jour (si elles existent), il serait utile de savoir si l'opération effectuée par notre fonction "Save" était un "INSERT" ou un "UPDATE". Il serait également bon d'obtenir une représentation exacte de ce qui était stocké dans la base de données, et il serait bon de connaître l'état de l'opération ; a-t-elle réussi, est-ce qu'elle est en attente dans le cadre d'une transaction plus importante, a-t-elle expiré ?

Lorsque vous renvoyez un objet, il est facile de communiquer toutes ces informations à la fois.

La composition de fonction est le processus consistant à combiner deux ou plusieurs fonctions pour produire une nouvelle fonction. Composer des fonctions ensemble, c'est comme assembler une série de tuyaux pour que nos données circulent ("Function composition is the process of combining two or more functions to produce a new function. Composing functions together is like snapping together a series of pipes for our data to flow through.” — Eric Elliott).

Utiliser RORO lève la limitation de la composition de fonctions qui est que chaque fonction de la liste ne peut recevoir qu'un seul paramètre.

Nous utiliserons la fonction kFunctionPipe( ...funcs ) qui est incluse dans dvjhUtilities-1.7.0.js, voir le billet précédent. Cette fonction prend une liste de fonctions et renvoie une fonction qui peut appliquer la liste de gauche à droite, en commençant par un paramètre donné, puis en transmettant le résultat de chaque fonction de la liste à la fonction suivante de la liste.

Voici un exemple où nous avons une fonction saveUser qui canalise un objet userInfo à travers 3 fonctions distinctes qui traitent les informations de l'utilisateur en séquence.

Code JavaScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function validate ( { 
  id = kRequiredParam( id), 
  firstName = kRequiredParam( "firstName" ), 
  lastName = kRequiredParam( "lastName" ), 
  email = kRequiredParam( "email" ), 
  username = kRequiredParam( "userName" ), 
  pass = kRequiredParam( "pass" ), 
  address = kRequiredParam( "addres" ), 
  ...rest 
} = {} ) { 
  // valider les données 
  
  return { id, firstName, lastName, email, username, pass, address, ...rest } 
} 
  
function normalize( { email, username, ...rest } = {} ){ 
  // "normaliser" les données 
  
  return { email, username, ...rest }; 
} 
  
function persist( { upsert = true, ...info } = {} ){ 
  // sauvegarder userInfo dans la base de données 
  
  return { operation : "INSERT", status : "Success", saved : info }; 
} 
  
function saveUser( userInfo = kRequiredParam( "userInfo" ) ) { 
  return kFunctionPipe( validate, normalize, persist )( userInfo ); 
} 
  
let userInfo = { 
  id : "id42", 
  firstName : "Daniel", 
  lastName : "Hagnoul", 
  email : "moi@ici.be", 
  username : "danielhagnoul", 
  pass : "motdepasse", 
  address : "adresse", 
  rue : "ma rue", 
  codePostal : 4242, 
  ville : "Les Mimosas en fleurs" 
}; 
  
let result = saveUser( userInfo ); 
  
for ( const key in result ){ 
  if ( key === "saved" ){ 
    for ( const key2 in result[ key ] ){ 
      console.log( key2, " = ", result[ key ][ key2 ] ); 
    } 
  } else { 
    console.log( key, " = ", result[ key ] ); 
  } 
} 
  
/* 
  * operation = INSERT 
  * status = Success 
  * email = moi@ici.be 
  * username = danielhagnoul 
  * id = id42 
  * firstName = Daniel 
  * lastName = Hagnoul 
  * pass = motdepasse 
  * address = adresse 
  * rue = ma rue 
  * codePostal = 4242 
  * ville = Les Mimosas en fleurs 
  */


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster un commentaire

Avatar de blbird blbird - Membre éprouvé https://www.developpez.com
le 31/03/2018 à 22:03
Article très intéressant, merci.

Concernant la composition de fonction, les Promise fonctionneraient aussi non?
Avatar de danielhagnoul danielhagnoul - Rédacteur https://www.developpez.com
le 01/04/2018 à 11:16


Bien entendu, voir https://developer.mozilla.org/fr/doc..._les_promesses au chapitre Composition.

Promise.resolve() et Promise.reject() sont des méthodes qui permettent de créer des promesses déjà tenues ou rompues.
Promise.all() et Promise.race() sont deux outils de composition qui permettent de mener des opérations asynchrones en parallèle.

Il est possible de construire une composition séquentielle de la façon suivante : [func1, func2].reduce((p, f) => p.then(f), Promise.resolve());. Dans ce fragment de code, on réduit un tableau de fonctions asynchrones en une chaîne de promesse équivalente à : Promise.resolve().then(func1).then(func2);.

Avatar de blbird blbird - Membre éprouvé https://www.developpez.com
le 01/04/2018 à 18:04
Merci! Bon ca va alors, ca veut dire que j'ai compris.
Avatar de sekaijin sekaijin - Expert éminent https://www.developpez.com
le 08/04/2018 à 18:25
Largement utilisé dans webix ou sencha cela rend le code lisible.

J'ai utilisé un langage ou le nom des arguments était obligatoire à l'appel de plus il n'y avait pas de parenthèses pour délimiter les arguments.
dans le genre ça donnait des truc comme
Code javascript : Sélectionner tout
1
2
3
function getUser(withFirstName, andName) {...} 
//l'appel donnant 
getUser withFirstName "paul" andName "Dupond";

c'est très déroutant au début. on a tendance à créer des fonctions comme on a l'habitude
Code javascript : Sélectionner tout
findUsersByRole( role, withContactInfo, includeInactive )

Mais rapidement on se met à choisir les nom de paramètre pour que l'appel fasse une phrase

Code javascript : Sélectionner tout
1
2
3
findUsers( withRole, withContactInfo, includeInactive) 
//ce qui donne un appel comme 
findUsers withRole 'admin' withContactInfo true includeInactive true

du coup lorsque j'utilise le passage d'argument avec ce pattern je fais le même genre de choses.

Code javascript : Sélectionner tout
1
2
findUsers( withRole, withContactInfo, includeInactive); 
findUsers( { withRole: 'admin', withContactInfo: true, includeInactive: true } )
Code javascript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
webix.ui({ 
    view:"table", 
    columns:[ 
        {  
            id:"id",     
            header:"",  
            width:50 
        }, 
        {  
            id:"value", 
            header:"Film title",  
            template:"{common.treetable()} #value#" 
        }, 
        {  
            id:"chapter",    
            header:"Mode",   
            width:200 
        } 
    ], 
    data: "..." //dataset, variable or path 
}) 
//un usage classique serrait 
  
webix.table( 
   [ 
     webix.column("id", "", "", 50), 
     webix.column("value","Film title","{common.treetable()} #value#"), 
     webix.column("chapter","Mode",""200) 
   ], 
   "..." //dataset, variable or path 
)

comme on le voit l'écriture est concise mais moins lisible. de plus dans une approche traditionnelle il faut connaitre et appeler les méthodes pour passer le résultat dans l'appel (webix.column)
alors qu'avec la notation objet on passe une description et la méthode peut s'occuper de tout. c'est le créateur de table qui appelle le créateur de colonnes.

l'inconvénient c'est que l'objet à passer peut être très grandement diversifié et les éditeur ne peuvent pas assister sur le contenu. là où f(int age, string name) l'éditeur présentera age et name avec f(cfgObject params) ils demandera un objet params sans aider à le remplir.

A+JYT
Contacter le responsable de la rubrique Accueil