Modularité du code d'un programme

Version imprimableVersion imprimable

Introduction

La modularité des programmes est une notion fondamentale et pourtant souvent méconnue du novice. Indispensable pour la structuration, la maintenance et l'interopérabilité, cet article constitue une tentative d'approfondir le concept et d'expliquer clairement le vocabulaire qui lui est associé.

Distinction entre l'interface et l'implémentation

Pour la suite de l'article, il est très important de bien comprendre la distinction entre une interface et son implémentation. Avant d'utiliser un mode de description technique, puisons des exemples dans la vie de tous les jours. Imaginons que vous soyez très fortuné, vous êtes tranquille, dans votre manoir en plein été. De plus, supposons qu'un majordome soit à votre disposition. Vous pouvez lui demander d'effectuer plusieurs tâches comme par exemple :

  • La préparation des repas.
  • La réception des invités.
  • La coordination du ménage.

Si vous voulez demander à votre majordome d'effectuer une et/ou plusieurs de ces tâches, considérez que votre but est qu'elles soient accomplies, peu importe la manière dont il s'y prend (ce n'est pas toujours exact, mais faisons cette supposition pour commencer).

En ayant bien cet exemple en tête, il est facile de faire la différence entre une interface et une implémentation. L'interface est une sorte d'énumération des services qu'il est possible de demander, d'utiliser, voir même d'intégrer au sein d'autres services alors que l'implémentation est la manière de réaliser le service demandé, la façon de faire.

Illustration de l'interface par le majordome

Dans notre exemple, l'interface serait le majordome et l'implémentation serait la manière dont il décide d'effectuer ce que vous lui avez demandé. Il faut bien sur que les tâches que vous lui déléguez soient incluses dans son domaine de compétences (donc toute tâche demandée doit être présente dans l'interface). De plus, il ne faut pas que votre majordome se vante de pouvoir faire certaines choses qui lui sont impossible d'effectuer (donc tout ce que l'on peut voir dans une interface doit être implémenté).

On peut donc comprendre qu'en plus d'énumérer les services, l'interface doit absolument garantir que ceux-ci seront réalisés !

Qu'est-ce qu'un module ?

Un module est tout simplement un ensemble composé d'une interface, et de son implémentation. Un module doit avoir un rôle très précis. Notre majordome peut être vu comme un module car il possède bien une interface et une implémentation bien spécifique.

Schéma d'un module

Introduisons deux nouveaux modules intitulés, par exemple : cuisinier et serveur. Il est important de comprendre que plusieurs modules peuvent se servir l'un de l'autre, communiquer entre eux. Un exemple d'implémentation de la tâche préparation du repas de notre majordome pourrait être constitué des actions suivantes :

  • Demander au cuisinier de préparer le repas
  • Informer le serveur que le cuisinier a commencé à préparer le repas

Bien entendu, ces actions doivent être présentes dans les interfaces appropriées. De cette manière, il est facilement observable que notre majordome utilise les interfaces de notre cuisinier et de notre serveur. Le majordome n'a cependant aucune idée, encore une fois, de la manière dont le cuisinier va préparer le repas et de ce que le serveur va faire après avoir été informé que le cuisinier a commencé à préparer le repas.

Cependant, l'intérêt de ces interfaces n'est pas seulement de permettre au majordome de demander certaines choses au cuisinier et au serveur, mais de permettre à n'importe qui du manoir de communiquer avec eux. De cette manière, vous pourriez très bien aller faire en personne des suggestions à votre cuisinier ou faire des demandes supplémentaires à votre serveur, à la seule condition que leur interface respective le permette.

Un exemple concret

Imaginons que votre programme nécessite une pile, il est de bon sens de créer un module remplissant ce rôle. Ainsi, en C par exemple, on pourra créer une interface ressemblant à celle-ci :

/*----------------------------------------------------------------------------*
 * Stack interface                                                            *
 *----------------------------------------------------------------------------*/
#ifndef _STACK_H_
#define _STACK_H_

#include <stdbool.h>
#include <stddef.h>

typedef struct Stack_t Stack;

Stack* newStack(void);

int push(Stack* stack, void* content);

void* pop(Stack* stack);

void* peek(Stack* stack, size_t numElem);

size_t sizeOfStack(Stack* stack);

bool isEmptyStack(Stack* stack);

#endif

Cette interface doit être sauvegardée dans un fichier .h et son implémentation sera présente dans un fichier .c. Peu importe les endroits de votre programme où vous devrez utiliser une pile, vous n'utiliserez seulement que les fonctions présentes dans cette interface en faisant abstraction de son implémentation. Pour bien illustrer ce concept, lorsque vous utilisez la fonction printf() de la librairie standard, vous utilisez une fonction d'un certain module (stdio) sans vous soucier de la manière dont cette fonction va procéder.

Importance de la documentation de l'implémentation

Vous l'aurez compris, l'interface d'un module impose un niveau d'abstraction sur l'implémentation permettant le confort de l'utilisateur. Cependant, ce niveau d'abstraction tel que présenté dans l'interface ci-dessus est bien trop élevé. C'est pour ça qu'il faut toujours suivre les règles suivantes :

  • Choisir des noms de fonctions, de structures, de constantes,... clairs. De cette manière, les programmeurs aguerris pourront connaitre le rôle d'un élément rien qu'en lisant son nom.
  • Documenter de manière approfondie chaque élément de l'interface en détaillant le rôle, les paramètres et la valeur de retour pour les fonctions.
  • Donner certains détails sur l'implémentation.

La dernière règle semble en contradiction avec tout ce que l'on a dit précédemment. Cependant, il y a plusieurs manières d'implémenter une pile. Les plus connues sont par l'intermédiaire d'un tableau ou par l'intermédiaire d'une liste liée. Il est très important de préciser ce "détail" d'implémentation car la complexité algorithmique et l'utilisation de l'espace mémoire sont totalement différents. En clair, ces deux implémentations répondent à des besoin très différents.

L'implémentation par tableau va réserver un espace mémoire fixe qui permettra de stocker un nombre limité d'éléments (contrainte facilement contournable) mais garantira la complexité de la fonction peek (permet d'aller chercher un élément dans la pile à une position donnée sans l'effacer) à O(1). Elle trouvera donc son utilité dans la nécessité d'accéder rapidement à des éléments. L'implémentation par liste liée permettra l'utilisation d'un espace mémoire dynamique mais la complexité de la fonction peek s'élèvera à O(n).

Voici un exemple de documentation possible pour l'interface de la pile avec une implémentation par liste liée en tenant compte des remarques précédentes :

/*----------------------------------------------------------------------------*
 * Stack interface                                                            *
 *----------------------------------------------------------------------------*
 * This stack is implemented with a linked list.                              *
 *----------------------------------------------------------------------------*/
#ifndef _STACK_H_
#define _STACK_H_

#include <stdbool.h>
#include <stddef.h>

/* Structure of the stack */
typedef struct Stack_t Stack;

/*----------------------------------------------------------------------------*
 * This function creates a new stack.                                         *
 *----------------------------------------------------------------------------*
 * PARAMETERS:                                                                *
 *   - void.                                                                  *
 *                                                                            *
 * RETURN:                                                                    *
 *   - Stack* : a pointer to the new stack.                                   *
 *----------------------------------------------------------------------------*/
Stack* newStack(void);

/*----------------------------------------------------------------------------*
 * This function pushes an element into a stack.                              *
 *----------------------------------------------------------------------------*
 * PARAMETERS:                                                                *
 *   - Stack* : a pointer to the stack to use.                                *
 *   - void*  : a pointer to the element to push.                             *
 *                                                                            *
 * RETURN:                                                                    *
 *   - int    : 0 for success or -1 for failure.                              *
 *----------------------------------------------------------------------------*/
int push(Stack* stack, void* content);

/*----------------------------------------------------------------------------*
 * This function pops an element into a stack.                                *
 *----------------------------------------------------------------------------*
 * PARAMETERS:                                                                *
 *   - Stack* : a pointer to the stack to use.                                *
 *                                                                            *
 * RETURN:                                                                    *
 *   - void*  : a pointer to the poped element.                               *
 *----------------------------------------------------------------------------*/
void* pop(Stack* stack);

/*----------------------------------------------------------------------------*
 * This function returns a pointer to an element into a stack at a given      *
 * position.                                                                  *
 *----------------------------------------------------------------------------*
 * PARAMETERS:                                                                *
 *   - Stack* : a pointer to the stack to use.                                *
 *   - size_t : the given position.                                           *
 *                                                                            *
 * RETURN:                                                                    *
 *   - void*  : a pointer to the element at the position numElem.             *
 *----------------------------------------------------------------------------*/
void* peek(Stack* stack, size_t numElem);

/*----------------------------------------------------------------------------*
 * This function returns the size of a stack.                                 *
 *----------------------------------------------------------------------------*
 * PARAMETERS:                                                                *
 *   - Stack* : a pointer to the stack to use.                                *
 *                                                                            *
 * RETURN:                                                                    *
 *   - size_t : the size of the stack.                                        *
 *----------------------------------------------------------------------------*/
size_t sizeOfStack(Stack* stack);

/*----------------------------------------------------------------------------*
 * This function returns a boolean which indicates whether a stack is empty   *
 * or not.                                                                    *
 *----------------------------------------------------------------------------*
 * PARAMETERS:                                                                *
 *   - Stack* : a pointer to the stack to use.                                *
 *                                                                            *
 * RETURN:                                                                    *
 *   - bool   : true if the stack is empty or false otherwise.                *
 *----------------------------------------------------------------------------*/
bool isEmptyStack(Stack* stack);

#endif

Qu'est-ce qu'une bibliothèque ?

Une bibliothèque est un ensemble de modules appartenant à la même catégorie. Par exemple, on peut créer une bibliothèque regroupant les structures de données, une autre regroupant la gestion des éléments graphiques, ou encore, les différentes procédures mathématiques. Il suffit d'imaginer une véritable bibliothèque où les livres (modules) sont classés par thèmes. L'utilisation d'une bibliothèque est donc caractérisée par l'exploitation de l'ensemble ou d'un sous-ensemble des interfaces des modules de cette même bibliothèque appelées API.

Schéma d'une bibliothèque

La connaissance des APIs (Application Programming Interface) est donc indispensable pour utiliser une bibliothèque. Il faut souligner le fait que la conception des APIs est très importante de par le fait qu'elles doivent respecter la propriété d'interopérabilité. Rajoutons qu'en Belgique, l'article 7 de la loi du 30 juin 1994 (consultable ici) autorise la décompilation des parties d'un programme relatives à l'interopérabilité sans autorisation préalable ! Cette autorisation n'est valable uniquement si les APIs ne sont pas appropriées à l'interopérabilité de votre programme.

On peut trouver plusieurs synonymes au terme bibliothèque, le plus important étant librairie. Dans la programmation orientée-objet, la notion de package est similaire à la notion de bibliothèque.

Qu'est-ce qu'un framework ?

Un framework est un ensemble de bibliothèques (et dans certains cas, d'outils) permettant le développement d'un programme. On en trouve pleins d'exemples : J2SE, J2EE et J2ME sont des frameworks, Eclipse est un framework, Microsoft .NET également,...

Schéma d'un framework

Importance de la modularité

Un code bien construit doit impérativement être modulaire. Nous avons exploré les diverses raisons au cours de cet article :

  • Un module à un rôle bien précis, il peut être inclus dans une bibliothèque qui elle même peut être inclue dans un framework. De cette manière, les rôles sont correctement identifiés et le code d'une action bien précise peut facilement être repéré.
  • La modularité est indispensable pour la maintenance du code. Si vous trouvez un bug dans votre implémentation d'une pile en guise d'exemple, vous n'aurez seulement qu'à modifier l'implémentation du module, sans vous soucier des endroits où vous utilisez votre pile.
  • Le point précédent implique que le code est réutilisable où bon vous semble dans votre programme. Vous pourriez même reprendre vos modules/bibliothèques dans d'autres programmes sans y toucher (à condition que vos implémentations soient génériques).

Pour conclure, il faut toujours prendre le temps de penser à la manière dont le code va être fragmenté avant de développer.

Share/Save