Voir le flux RSS

danielhagnoul

[Actualité] Fonction recevant et retournant un objet

Noter ce billet
par , 30/03/2018 à 21h46 (1161 Affichages)
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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
  */

Envoyer le billet « Fonction recevant et retournant un objet » dans le blog Viadeo Envoyer le billet « Fonction recevant et retournant un objet » dans le blog Twitter Envoyer le billet « Fonction recevant et retournant un objet » dans le blog Google Envoyer le billet « Fonction recevant et retournant un objet » dans le blog Facebook Envoyer le billet « Fonction recevant et retournant un objet » dans le blog Digg Envoyer le billet « Fonction recevant et retournant un objet » dans le blog Delicious Envoyer le billet « Fonction recevant et retournant un objet » dans le blog MySpace Envoyer le billet « Fonction recevant et retournant un objet » dans le blog Yahoo

Mis à jour 08/04/2018 à 22h21 par danielhagnoul (manque un caractère ' dans 'admin')

Catégories
Javascript , Développement Web , ES2015

Commentaires

  1. Avatar de blbird
    • |
    • permalink
    Article très intéressant, merci.

    Concernant la composition de fonction, les Promise fonctionneraient aussi non?
  2. Avatar de danielhagnoul
    • |
    • permalink


    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);.
  3. Avatar de blbird
    • |
    • permalink
    Merci! Bon ca va alors, ca veut dire que j'ai compris.
  4. Avatar de sekaijin
    • |
    • permalink
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    1
    2
    findUsers( withRole, withContactInfo, includeInactive);
    findUsers( { withRole: 'admin', withContactInfo: true, includeInactive: true } )

    Code javascript : Sélectionner tout - Visualiser dans une fenêtre à part
    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