Capsule de théorie Modules, bibliothèques, paquets et Cie

Introduction

Cette capsule a pour but de clarifier un nombre de termes et de notions auxquels, un développeur débutant se trouve confronté lors de la réalisation d’une première application. En effet, la complexité d’une application, même modeste, nécessite une bonne organisation du code et l’utilisation des bibliothèques et d’outils appropriés pour faciliter la réalisation de certaines tâches.

En premier lieu, nous revenons brièvement sur les notions de module et de bibliothèque en tant qu’unités d’organisation et de réutilisation du code. En second lieu, nous introduisons les notions de gestion de dépendances, de gestionnaires de dépendances et de paquet. Avant de conclure, nous terminons par quelques mots sur la confusion possible entre gestionnaire de dépendances et gestionnaire de paquets.

Module

La conception d’une application informatique selon une méthode modulaire — la conception orientée objet en est une — conduit à la définition de différentes parties. Chacune de ces parties correspond à un module. Lorsque la décomposition est achevée, il doit être possible de développer les modules de manière indépendante. Cet objectif peut être atteint en respectant deux principes. Le premier stipule qu’un module doit partager aussi peu d’informations que possible, et le second qu’un module doit avoir une interface aussi simple que possible.

Un module est une unité d’organisation du code. Il s’agit généralement d’un fichier source dont une ou plusieurs classes, fonctions ou constantes sont mises à la disposition de l’utilisateur du module. En jargon technique, on dit qu’un module exporte un ou plusieurs symboles. Ces symboles forment l’interface du module. Par exemple, en Java, un module est un fichier .java. Les symboles exportés sont la classe elle-même et les membres statiques de cette classe.

Le fait que les modules peuvent être développés de manière indépendante n’implique pas que les modules ne dépendent pas les uns des autres. Cela signifie que le développeur d’un module n’a pas à connaître les détails de l’implémentation des autres modules, mais seulement leurs interfaces. Lorsqu’un module dépend d’un autre module, le programmeur doit disposer d’un moyen d’indiquer cette dépendance dans le code source. C’est ce que l’on fait avec le mot clé import en Java et en Python, avec le mot clé using en C#, ou encore avec le mot clé use en PHP. Rendre les dépendances explicites est une condition nécessaire à l’automatisation de tâches telles que la détection de dépendances circulaires ou la création de bundle en JavaScript (voir section suivante).

Bibliothèque

La réalisation d’une application informatique ne part généralement pas de zéro (from scratch), mais se base sur un certain nombre de bibliothèques (library). Une bibliothèque est un moyen de regrouper un ou plusieurs modules dont la fonctionnalité est suffisamment générale pour être utilisée dans différents programmes. Ces modules implémentent souvent des fonctionnalités complexes telles que la bibliothèque standard d’un langage, une API pour un système logiciel, un framework.

La manière concrète de regrouper ces modules varie d’un langage à l’autre. Toutefois, si l’on part du principe qu’une bibliothèque est une unité de réutilisation de modules, il en ressort que :

  • Puisqu’on peut utiliser un module sans connaître son code source, une bibliothèque peut ne contenir que le code objet des modules. C’est également vrai pour les langages interprétés tels que PHP et JavaScript, si l’on considère la minification comme une forme de compilation.
  • Puisque pour utiliser un module, on doit connaître son interface, une bibliothèque doit contenir des métadonnées qui décrivent cette interface.

Voyons cela à travers quelques exemples :

  1. En Java, une bibliothèque est une archive ZIP dont l’extension est .jar. La structure de répertoires contient les fichiers .class et un fichier de métadonnées (META-INF/MANIFEST.MF). L’ensemble doit respecter la spécification des fichiers JAR.
  2. En PHP, une bibliothèque est une archive ZIP ou TAR dont l’extension est .phar. La structure de répertoires contient des fichiers .php, souvent minifiés et un fichier de métadonnées (Phar Manifest). L’ensemble doit respecter la spécification des fichiers PHAR.
  3. En JavaScript une bibliothèque est également une structure de répertoires. Elle contient des fichiers .js, souvent minifiés, et un fichier de métadonnées. Selon la spécification de Node.js (devenue un standard de fait), les métadonnées se trouvent dans le fichier package.json.

Une bibliothèque n’est pas un composant logiciel autonome. Elle ne trouve sont utilité qu’une fois liée à un programme.

Liaison des bibliothèques

La liaison de bibliothèque à un programme peut être réalisée de deux manières :

  • statiquement à la compilation (static linking)
  • dynamiquement à l’exécution (dynamic linking)

La liaison statique ne concerne en principe que la réalisation d’exécutables natifs avec des langages tels que C, C++ ou Go. La liaison dynamique est beaucoup plus commune et offre un certain nombre d’avantages, même pour les exécutables natifs. En effet, décomposer une application en plusieurs bibliothèques (composant) liées dynamiquement, permet de :

  • Réduire le temps de compilation en ne compilant que les composants qui ont changé.
  • Distribuer seulement les composants nécessaires lors d’une mise à jour.
  • Partager des composants entre plusieurs applications (shared libraries).

Les langages interprétés tels que Java, C#, PHP ou Python n’utilisent que la liaison dynamique. En effet, lier les bibliothèques à la compilation reviendrait simplement à copier les fichiers des bibliothèques dans l’archive ou la structure de répertoires du programme. Mais puisque le code doit être interprété, l’endroit où se trouvent les fichiers n’a pas d’importance pour peu qu’ils soient accessibles dans le système de fichier. Si le but est de faciliter la distribution, il vaut mieux se tourner vers une solution d’empaquetage (voir plus loin).

Il existe toutefois un cas où une forme de liaison statique s’avère utile pour un langage interprété. Il s’agit du bundling pour une application en JavaScript dans un navigateur. Le bundling consiste à combiner tous les modules (fichiers .js) des bibliothèques et du programme dans un seul fichier (ou dans un petit nombre de fichiers) à l’aide d’un outil appelé bundler. Le but est de réduire le nombre de balises script dans les documents HTML et donc le risque d’erreur. Webpack est un exemple notable de bundler. Comme d’autres, il permet d’utiliser la plateforme Node.js pour la gestion des dépendances.

Gestion des dépendances

Comme nous l’avons déjà dit, la réalisation d’une application nécessite l’utilisation de bibliothèques et d’outils de développement. Les outils et les bibliothèques qui sont nécessaires à l’exploitation du programme sont les dépendances de production (prod dependencies), les outils et les bibliothèques qui ne sont utilisés que lors du développement sont les dépendances de développement (dev dependencies).

Garder une trace de la version de chaque dépendance pour un projet est une bonne pratique. Cela permet de faciliter l’installation de l’application pour son exploitation et le travail des développeurs chargés de la maintenance du projet. Bien qu’il soit possible d’effectuer la gestion des dépendances manuellement, il est recommandé de s’aider pour cela, d’un outil approprié.

Gestionnaire de dépendance

Un gestionnaire de dépendances (dependency manager) est un outil qui nous aide à réaliser certaines tâches telles que :

  • La gestion des dépendances du programme et de leurs versions.
  • L’installation des bibliothèques et des outils en développement ou en maintenance.
  • La création d’un package pour le déploiement ou la distribution du programme.

Comme exemples de gestionnaires de dépendances, on peut mentionner :

  • Maven et Gradle pour la plateforme JVM (Java, Groovy, etc.)
  • npm et yarn pour la plateforme Node.js (JavaScript et TypeScript)
  • bundler pour le langage Ruby.
  • pipenv pour le langage Python.
  • NuGet pour la plateforme .NET (C#, F#, etc.).

Il s’agit généralement d’outils munis d’une interface en ligne de commande (CLI). Pour ces logiciels, un projet est généralement un répertoire dans lequel se trouve un fichier de configuration qui contient la liste des composants à installer. Par exemple, npm cherche un fichier package.json, maven cherche un fichier pom.xml, bundler cherche un fichier Gemfile au format YAML, et pip cherche un fichier Pipfile dont le format ressemble au format INI. Selon les cas, l’ajout d’une dépendance dans le fichier de projet est effectué à l’aide de l’outil ou manuellement.

Chaque dépendance fait référence à un paquet et l’installation automatique des dépendances repose sur l’existence d’un dépôt en ligne dans lequel sont stockés ces paquets. De ce point de vue, un gestionnaire de dépendance ressemble à un gestionnaire. Avant de revenir sur ce point, examinons brièvement ces notions de paquet et de dépôt.

Paquet (logiciel)

Le terme paquet (package) désigne indifféremment n’importe quel composant logiciel (bibliothèque, outil, etc.) empaqueté d’une façon ou d’une autre dans un fichier pour faciliter sa distribution. Parmi les formats les plus courants, on trouve : des archives (.zip, .tar.gz, etc.), des fichiers d’installation (.exe, .msi, etc.), des images de disque (.dmg) ou encore des conteneurs. Un paquet contient des métadonnées qui comprennent au minimum un nom et une version. Dans ce contexte, un paquet doit être vu comme une unité de distribution.

S’il existe différents moyens d’empaqueter un composant logiciel, il existe également différents moyens de distribuer un paquet. La distribution sur support physique tend à disparaître, mais il existe encore bien des situations où un paquet est téléchargé manuellement depuis un site web ou un serveur FTP.

Toutefois, la distribution via un dépôt (repository) en ligne, public ou privé, devient la norme. La réalisation de dépôts privés nécessite généralement l’usage d’un gestionnaire de dépôt d’objets binaires (binary object manager). Un exemple notable est le logiciel libre NexusOSS qui supporte une grande variété de formats et de protocoles (Maven, npm, NuGet, PyPI, Gems, Git LFS, Apt, Yum, etc.).

Gestionnaire de dépendance et gestionnaire de paquet

Un gestionnaire de paquet (package manager) est un outil du système d’exploitation qui permet l’installation, la mise à jour et la désinstallation de composants logiciels (applications, programmes utilitaires, logiciel système, etc.) à partir d’un dépôt de paquets (package repository ou repository).

Un gestionnaire de paquet effectue en principe l’installation en respectant les conventions du système d’exploitation. Par exemple, les programmes sont installés dans le répertoire prévu à cet effet (/usr/local/bin, /Application, Program Files, etc.). Son utilisation nécessite donc le plus souvent des privilèges d’administrateur. Des exemples de gestionnaires de paquets sont :

  • APT pour les systèmes Debian et Ubuntu
  • RPM ou YUM pour les systèmes Red Hat, CentOS, Fedora et bien d’autres
  • HomeBrew ou MacPort pour les systèmes macOS (OS X)
  • Chocolatey pour les systèmes Windows

Un gestionnaire de dépendance permet également l’installation de paquets — les dépendances — mais en principe la ressemblance s’arrête là. En effet, un gestionnaire de dépendances installe des composants logiciels pour un projet particulier. L’installation est confinée au répertoire du projet qui appartient au développeur et ne nécessite donc pas de privilège particulier. Ce confinement est particulièrement utile lors de la maintenance d’un programme, car il permet de travailler avec des versions de bibliothèques ou d’utilitaires spécifiques au projet lorsque cela s’avère nécessaire.

Toutefois, certains outils disposent à la fois des fonctionnalités d’un gestionnaire de dépendances et de celles d’un gestionnaire de paquets. L’outil npm de la plateforme Node.js, par exemple, permet l’installation de paquets au niveau du système et dispose d’une interface utilisateur très proche de celle d’un gestionnaire de paquets. C’est pourquoi on parle parfois (souvent) de npm comme d’un gestionnaire de paquets pour la plateforme Node.js.

Pour faire court, un gestionnaire de paquet fait partie de la boîte à outil d’un administrateur système qui dispose de privilèges élevés alors qu’un gestionnaire de dépendance fait partie de celle d’un développeur qui ne dispose d’aucun privilège particulier. L’un sert à l’installation, la mise à jour et la suppression de composant logiciel dans un système, l’autre sert à faciliter la gestion des dépendances dans un projet de développement. Ils ont en commun le fait d’utiliser un dépôt de paquets.

Il est tout à fait possible de concevoir un outil étant à la fois un gestionnaire de dépendances et un gestion de paquets. Le nom qu’on lui donne dépend de ce pourquoi on l’utilise et de la casquette que l’on porte, mais aussi de l’usage.

Conclusion

En guise de conclusion et pour finir avec une note d’humour, une double planche à propos du sens des mots qui met en scène quatre philosophes ayant contribué à la philosophie du langage : Rudolf Carnap, Ludwig Wittengestein, John Langshaw Austin et Gottlob Frege à qui l’on doit la citation suivante : « never to ask for the meaning of a word in isolation, but only in the context of a proposition ».

Fig. 1 – Is a Hotdog a Sandwich? A Definitive Study. Fig. 1 – Is a Hotdog a Sandwich? A Definitive Study.
Fig. 1 – Is a Hotdog a Sandwich? A Definitive Study.