Overblog
Editer l'article Suivre ce blog Administration + Créer mon blog
Laurent COCAULT

Rust, je persiste

Pour ce troisième article consacré au langage Rust, créons un nouveau paquetage qui viendra ajouter, après le paquetage de gestion des unités, les concepts du domaine de gestion des paramètres de configuration. Autrement dit, le cœur métier de notre exemple. Pour ce faire, reprenons la commande de création d'un paquetage :

PS> cargo new parameter --lib
Created library `parameter` package

Tout comme pour notre premier module, le nouveau paquetage propose un fichier "cargo.toml" qui va nous être utile pour introduire une dépendance avec notre paquetage précédent. Mais avant d'en arriver là, commençons par les tests. 

Rust, je persiste

La fonction de test get_existing_global_parameter récupère un paramètre auprès d'un "repository" à partir de son nom et s'attend à trouver une valeur existante. La fonction get_test_repository retourne ce "repository" en instanciant une doublure de test initialisée avec le paramètre attendu. Sur ces deux fonctions, rien de nouveau comparé aux mécanismes mis en œuvre avec notre premier module, si ce n'est le retour d'une valeur optionnelle qu'on voit apparaître dans le type de retour de la fonction get_parameter du "repository". En effet il est possible que le paramètre attendu n'existe pas (cf. exemple ci-dessous). 

Rust, je persiste

Ces fonctions de test, font référence à un trait définissant le contrat proposé par un "repository" de paramètres. Avant de se projeter sur la définition de ce trait, complétons notre premier test pour exploiter les informations attendues d'un paramètre.

Rust, je persiste

On notera qu'il est nécessaire de désenvelopper la valeur contenue dans le retour optionnel de la fonction "get_parameter", avec un appel à "unwrap", avant d'accéder à son contenu.

La définition de la structure Parameter et du trait ParameterRepository peuvent alors être définis comme suit.

Rust, je persiste

La définition de la structure Parameter fait appel à la notion de Quantity définie dans le module "unit". Pour que cette dépendance soit résolue, il est nécessaire de la déclarer dans le fichier Cargo.toml.

Rust, je persiste

La déclaration de la dépendance mentionne un "path"; il s'agit ici d'une dépendance locale, non versionnée. Une telle approche est pertinente dans le cas d'un développement en cours ; on s'attachera à considérer une dépendance versionnée dans un article ultérieur.

Jusqu'à présent, le "repository" utilisé pour les tests est une doublure qui s'appuie sur une simple "map". Cette implémentation, même basique, n'est pas sans intérêt et pourrait tout à fait répondre aux besoins de certains utilisateurs. On peut donc renommer le FakeParameterRepository en un InMemoryParameterRepository qu'on pourra déclarer dans un fichier source dédié.

Rust, je persiste

Cette implémentation du trait ParameterRepository s'appuie sur la collection HashMap dont on voit ici deux méthodes : "get" permettant de récupérer une valeur à partir de la clé (on notera que la valeur retournée est directement une "Option") et "insert" permettant d'ajouter un paramètre indexé par sa clé.

Pour aller au-delà de cette implémentation naïve, considérons la mise en place d'une persistance des paramètres dans un fichier. Créons pour cela un nouveau module "file" dans lequel sera définie une implémentation de ParameterRepository encapsulant une instance de InMemoryParameterRepository. Il ne reste plus alors qu'à lire un fichier de propriétés pour alimenter l'instance encapsulée. Pourtant, cette implémentation se heurte vite à une difficulté : il est nécessaire de construire la valeur du paramètre à partir de la fonction parse_quantity définie dans notre premier module. Or, la fonction d'ajout d'un paramètre au "repository" suppose de connaître sa valeur et son unité. On serait alors tenté de déclarer une nouvelle fonction upsert_parameter prenant en paramètre directement une quantité plutôt qu'un couple valeur/unité. Mais nous aurions alors deux implémentations de la fonction upsert_parameter qui ne se distinguent que par les paramètres d'appel. Et Rust ne supporte pas la surcharge des fonctions. Il va donc nous falloir reconsidérer notre API en donnant à la fonction upsert_parameter une donnée mieux typée : directement le paramètre.

Rust, je persiste

Une fois l'interface du "repository" redéfinie et la première implémentation "In Memory" adaptée, l'implémentation basée sur un fichier de propriété est la suivante.

Rust, je persiste
Rust, je persiste

La manipulation du système de fichiers expose à des cas dégradés qui ont été traitées dans l'exemple ci-dessus par une impression sur la sortie standard. On préfèrera s'appuyer sur un mécanisme de journalisation permettant de mieux caractériser le niveau du message affiché et son contexte. Le paquetage "log" peut ainsi être utilisé. Le traitement des cas dégradés devient alors :

Rust, je persiste

Avec le module "env_logger", la sortie correspondant à ces deux cas de figure est la suivante :

Rust, je persiste

Pour clore une première implémentation complète du gestionnaire de paramètres persistés, il reste à sauvegarder les mises à jour apportées aux paramètres existants ou l'ajout de nouveaux paramètres. Le comportement attendu est précisé dans le test suivant :

Rust, je persiste

En dehors des fonctions du paquetage "fs" qui permettent de manipuler le système de fichiers, ce test présente un élément qui n'a pas encore été mis en lumière dans cette série d'articles : l'utilisation du mot clé "mut" pour déclarer le repository à mettre à jour. Par défaut, les variables déclarées en Rust sont immutables (cf. premier article). Or, la déclaration du contrat d'interface ParameterRepository fait apparaître une différence dans la déclaration de la référence "self" entre la fonction get_parameter et upsert_parameter. Dans la première, on spécifie simplement le pointeur "&self" alors que dans la seconde on le qualifie "&mut self". On indique ici que la get_parameter ne modifie pas le repository alors qu'upsert_aparameter le modifie. Pour tester notre fonction upsert_parameter il devient donc nécessaire de déclarer le repository comme "mutable" (modifiable) en utilisant le mot clé "mut".

En termes d'implémentation, persister l'ajout ou la mise à jour d'un paramètre consiste simplement à réécrire le fichier après chaque appel à la fonction "upsert".

Rust, je persiste

L'implémentation de cette écriture étant proposée ci-dessous :

Rust, je persiste

On notera au passage que cette implémentation fait appel à une fonction "to_string" pour une quantité qui n'était pas implémentée dans la version initiale du paquetage "unit". Ces détails d'implémentation, ainsi que tous les codes sources proposés ci-dessus, peuvent être retrouvés ici.

Dans cet article, nous avons découvert comment modulariser son code avec plusieurs paquetages (ou "crates"), gérer des valeurs optionnelles, manipuler des "maps", accéder au système de fichiers, journaliser l'exécution du code. Nous aurons aussi commencer à voir quelques limites de Rust, dont l'impossibilité de surcharger les fonctions, mais également des caractéristiques favorisant la robustesse du code telle que l'immutabilité par défaut.

Le diagramme ci-dessous résume l'état d'avancement de nos implémentations.

Rust, je persiste

Crédit couverture: Tabble sur Wikimedia

Partager cet article
Repost0
Pour être informé des derniers articles, inscrivez vous :
Commenter cet article