Bien programmer en langage C
Date de publication : 28 mai 2008
    
    
	
	
	
	
	
	
	
	
	
	
	V. Concevoir et réaliser un composant logiciel en C
			
			V-A. Introduction
				
				
				
			
			V-B. Stratégie de développement d'un composant logiciel
				
				
				
				V-B-1. Modélisation d'un composant logiciel
					
					
					
					
				
				V-B-2. Implémentation du modèle.
					
					
					
					
					
				
			
			V-C. Etude de cas : Un gestionnaire d'alarme à niveaux
				
				V-C-1. Spécifications
					
					
					
				
				V-C-2. Conception
					
					
					
					
					
					
					
					
				
				V-C-3. Réalisation en C
					
					V-C-3-a. Nommage du composant logiciel
						
						
					
					V-C-3-b. Interface
						
						
						
						
						
						
					
					V-C-3-c. Implémentation
						
						
						
						
						
						
						
					
				
			
		
	
    
    
	
	
	
	
	
	
	
	
	
	
	V. Concevoir et réaliser un composant logiciel en C
			
			V-A. Introduction
				
				
					Lors du développement d'un projet logiciel, il est courant d'écrire des portions de codes qui pourraient 
					être utiles dans d'autres projets. Pour ce faire, il faut définir une stratégie de développement 
					de modules de logiciels dont le comportement est clairement spécifié, les performances et les limites 
					connues, et qui soient facilement intégrables. 
				
				
					Ce type de module logiciel est appelé 'composant logiciel', par analogie avec les composants utilisés 
					dans l'industrie de l'électronique, par exemple. 
				
			
			V-B. Stratégie de développement d'un composant logiciel
				
				
					Les qualités requises pour un composant logiciel sont : 
				
				
					- 
						Une spécification claire
					
 
					- 
						Comportement
					
 
					- 
						Performances
					
 
					- 
						Interface (entrées, sorties)
					
 
					- 
						Gestion des erreurs
					
 
					- 
						Une grande fiabilité
					
 
					- 
						Respect des normes du langage
					
 
					- 
						Respect des normes de codage
					
 
					- 
						Vérification du domaine des paramètres
					
 
					- 
						Elimination des pratiques douteuses
					
 
					- 
						Testabilité (indépendance vis à vis des sorties)
					
 
					- 
						Jeu de tests de non regression
					
 
					- 
						Une facilité d'integration
					
 
					- 
						Portabilité
					
 
					- 
						Respect des normes de nommage
					
 
					- 
						Elimination des pratiques empêchant la réentrance
					
 
					- 
						Elimination des pratiques empêchant l'instanciation
					
 
					- 
						Simplicité des points de sorties
					
 
				
				V-B-1. Modélisation d'un composant logiciel
					
					
						L'expérience montre que la plupart des composants logiciels servent à effectuer un traitement sur des 
						données. 
					
 ---------------
  Entrées    |               | Résultat
  ---------> |  Traitements  | ----------->
             |               |
              ---------------
  | 
 
					
						Certains évènements extérieurs peuvent agir sur les données (entrées). Il est bien sûr possible de fournir 
						un résultat immédiat par valeur de retour ou par un paramètre de sortie, mais certains traitements 
						peuvent aussi donner lieu à des réactions en fonction des résultats et/ou de l'état courant (seuil, 
						erreur etc.). Se sont les sorties. 
					
					
						On peut donc modéliser un composant logiciel de la façon suivante : 
					
----------------------------------------
   |          Composant logiciel            |
   |----------------------------------------|
   |                                        |
   |             ---------------            |
   |    Entrées |               | Sorties   |
  ------------> |  Traitements  | ----------->
   |   Résultat |               |           |
  <------------ |               |           |
   |             ---------------            |
   |                                        |
    ----------------------------------------
  | 
 
				
				V-B-2. Implémentation du modèle.
					
					
						Les entrées sont implémentées par de simples appels de fonctions appelées 'points d'entrées'. Vu de l'extérieur, 
						il s'agit d'une interface (API). Chaque interface de fonction est décrite par son nom, ses paramètres 
						(nom et type) et son type de retour. 
					
					
						Les traitements sont le corps des fonctions. Certains traitements sont des actions directes sur les données, 
						et sont retournées immédiatement, d'autres nécessitent de la mémoire permanente pour gérer des 
						configurations, états, compteurs, files etc. Il peut aussi y avoir des fonctions de lecture qui 
						permettent d'accéder à certaines données internes. Enfin, selon des critères bien définis, le traitement 
						peut générer des appels à des fonctions de sorties (évènements). 
					
					
						Les fonctions de sorties sont extérieures au composant. Leur appel se fait donc via une variable contenant 
						l'adresse de la fonction. Par contre, la spécification de l'interface des fonctions de sorties 
						appartient à la définition du composant. Il est probable que dans le cadre de l'intégration du 
						composant dans un projet, les fonctions de sorties doivent être développées projet par projet. 
						C'est une opération simple qui nécessite de fournir l'interface spécifiée, et l'appel à la fonction 
						extérieure (qui peut très bien être un point d'entrée d'un autre composant logiciel, voire du même), 
						parfois au prix d'un transcodage de valeur. 
					
					
						La modélisation détaillée d'un composant logiciel et de son environnement est donc la suivante : 
					
--------------------------------------------------------------------
   |                      Application                                   |
   |--------------------------------------------------------------------|
   |         ----------                   ------------------            |
   |        | Appelant |                 |                  |           |
   |         ----------                  |                  v           |
   |            |^                       |               --------       |
   |            ||                       |              | Sortie |      |
   |            ||                       |               --------       |
    ------------||-----------------------|------------------------------
                v|                       |
           --------------------------    |
    ------|         Entrées          |---|------
   |C L |  --------------------------    |      |
   |o o |         |^                     |      |
   |m g |         ||                     |      |
   |p i |         v|                     |      |
   |o c |  ----------------              |      |
   |s i | |                |         ---------  |
   |a e | |  Traitements   | -----> | Sorties | |
   |n l | |                |         ---------  |
   |t   | |                |                    |
   |    |  ----------------                     |
   |    |         |^                            |
    --------------||----------------------------
                  ||
                  v|
              --------------
             | Bibliothèque |
             |   standard   |
              --------------
  | 
 
				
			
			V-C. Etude de cas : Un gestionnaire d'alarme à niveaux
				
				V-C-1. Spécifications
					
					
						On dispose d'un système qui fait des mesures de grandeurs physiques. On souhaite ajouter un mécanisme 
						qui déclenche une alarme lorsque un certain seuil est dépassé, et qui supprime l'alarme lorsqu'on 
						repasse en dessous d'un autre seuil. 
					
					
						La gamme de mesure acceptable couvre les valeurs de -32767 à 32767. Les seuils sont programmables sur 
						toute la gamme de mesure. Le seuil de retour doit être inférieur au seuil de déclenchement. 
					
				
				V-C-2. Conception
					
					
						A partir du modèle de composant logiciel standard, on ajoute les spécificités du projet : 
					
----------------------------------------------
      |         Composant logiciel 'Alarme'          |
      |----------------------------------------------|
      |                                              |
      |             ---------------                  |
      |    Entrées |  Traitements  | Sorties         |
      |            |               |                 |
      |            |               | Alarme (ON/OFF) |
      |  Réglage   |               | ------------------>
      |  seuils    |               |                 |
  ---------------> |               |                 |
  <--------------- |               |                 |
      |            |               |                 |
      |  Mesure    |               |                 |
  ---------------> |               |                 |
  <--------------- |               |                 |
      |             ---------------                  |
      |                                              |
       ----------------------------------------------
  | 
 
					
						Le traitement peut être décrit par un algorithme textuel : 
					
Si la mesure est supérieure ou égale au seuil de déclenchement,
   et que l'on est pas en alarme, declencher l'alarme.
   Si l'alarme est déclenchée et que la mesure est inférieure au seuil
   de retour, arréter l'alarme.
  | 
 
					
						Ce que l'on peut traduire en langage algorithmique de la façon suivante : 
					
IF NOT in_alarme
      IF mesure >= seuil_declenchement
         in_alarme := TRUE
         sortie_alarme (ON)
      ENDIF
   ELSE
      IF mesure < seuil_retour
         in_alarme := FALSE
         sortie_alarme (OFF)
      ENDIF
   ENDIF
  | 
 
					
						Les données permanentes sont donc 
					
					
						- 
							Les données de configuration
						
 
						- 
							Le seuil de déclenchement
						
 
						- 
							Le seuil de retour
						
 
						- 
							L'adresse de la fonction de sortie
						
 
						- 
							Les données d'état
						
 
						- 
							L'état courant de l'alarme
						
 
					
					
						Les fonctions nécessaires à l'exploitation sont : 
					
					
						- 
							Les fonctions de configuration
						
 
						- 
							Enregistrement de la fonction de sortie 'alarme'
						
 
						- 
							Réglage du seuil de déclenchement
						
 
						- 
							Réglage du seuil de retour
						
 
						- 
							Les fonctions d'évaluation
						
 
						- 
							Evaluation de la mesure courante
						
 
					
				
				V-C-3. Réalisation en C
					
					V-C-3-a. Nommage du composant logiciel
						
						
							Le composant logiciel est nommé 'alarme'. Le prefixe est donc 'alarme_' 
						
					
					V-C-3-b. Interface
						
						
							Les données sont rassemblées dans la structure suivante. 
						
typedef struct
{
   int seuil_declenchement;
   int seuil_retour;
   unsigned active:1;
}
alarme_s;
  | 
 
						
							Les seuils sont de type int, qui couvre la plage requise par la spécification. Le champ 'active', qui 
							contient l'état courant de l'alarme, peut prendre les valeurs 0 et 1, d'où l'utilisation d'un 
							champ de bits de 1 bit. 
						
						
							Il manque le champ qui contient l'adresse de la fonction de sortie. Il est très pratique d'utiliser un 
							typedef pour spécifier le type de cette fonction, surtout qu'à ce stade de la réalisation, on 
							n'a pas une vision très claire de cette fonction. Le type risque donc d'évoluer, d'où l'intérêt 
							du typedef qui concentre la définition du type en un seul endroit. Dans un premier temps, je propose 
							: 
						
typedef void alarme_out_f (int on);
  | 
 
						
							La structure peut donc s'écrire 
						
typedef struct
{
   
   int seuil_declenchement;
   int seuil_retour;
   alarme_out_f *pf_out;
   
   unsigned active:1;
}
alarme_s;
  | 
 
						
							Il reste à définir le prototype des fonctions d'exploitations. Il est d'usage d'implémenter un système 
							de gestion d'erreur minimum, qui consiste à retourner 0 quand tout va bien, et 1 en cas de problèmes. 
							
						
int alarme_install_out (alarme_s *this, alarme_f *pf);
int alarme_trigger_level (alarme_s *this, int level);
int alarme_return_level (alarme_s *this, int level);
int alarme_eval (alarme_s *this, int value);
  | 
 
					
					V-C-3-c. Implémentation
						
						
							Maintenant que l'interface des fonctions est spécifiée, on peut écrire l'implémentation des fonctions 
							publiques (points d'entrée). Il suffit de recopier l'interface et d'écrire le corps des fonctions. 
							Il est d'usage de sécuriser les fonctions en effectuant des tests de validité (valeur, domaine) 
							sur les paramètres. D'autre part, afin d'éviter des erreurs à venir, il est fortement recommandé 
							de définir les paramètres 'non-modifiables' à l'aide du qualificateur 'const'. Ca évite de perdre 
							une valeur qui aurait pu être précieuse. 
						
int alarme_install_out (alarme_s *const this, alarme_f *const pf)
{
   
   int err = 0;
   
   if (this != NULL)
   {
      
      this->pf = pf;
   }
   else
   {
      err = 1;
   }
   return err;
}
int alarme_trigger_level (alarme_s *const this, int const level)
{
   int err = 0;
   if (this != NULL)
   {
      if (level >= ALARME_VALUE_MIN
       && level <= ALARME_VALUE_MAX)
      {
         this->seuil_declenchement = level;
      }
      else
      {
         err = 1;
      }
   }
   else
   {
      err = 1;
   }
   return err;
}
int alarme_return_level (alarme_s *const this, int const level)
{
   int err = 0;
   if (this != NULL)
   {
      if (level >= ALARME_VALUE_MIN
       && level <= ALARME_VALUE_MAX)
      {
         this->seuil_retour = level;
      }
      else
      {
         err = 1;
      }
   }
   else
   {
      err = 1;
   }
   return err;
}
int alarme_eval (alarme_s *const this, int const value)
{
   int err = 0;
   if (this != NULL)
   {
      if (valuel >= ALARME_VALUE_MIN
       && value <= ALARME_VALUE_MAX)
      {
         if (!this->active)
         {
            if (value > this->seuil_declenchement)
            {
               this->active = 1;
               out (this, 1);
            }
         }
         else
         {
            if (value < this->seuil_retour)
            {
               this->active = 0;
               out (this, 0);
            }
         }
      }
      else
      {
         err = 1;
      }
   }
   else
   {
      err = 1;
   }
   return err;
}
  | 
 
						
							Pour terminer l'implémentation, il reste à définir les constantes ALARME_VALUE_MIN et ALARME_VALUE_MAX 
							... 
						
#define ALARME_VALUE_MIN -32767
#define ALARME_VALUE_MAX 32767
  | 
 
						
							... et la fonction interne (privée) out(). 
						
static void out (alarme_s *this, int on)
{
   if (this->pf_out != NULL)
   {
       this->pf_out (on);
   }
   
}
  | 
 
						
							L'ensemble du composant logiciel 'alarme' est stocké ici (Ver 1.0) : 
alarme.halarme.c avec le fichier de 
							test rudimentaire (rien à voir avec un véritable test unitaire) 
test.c
						 
						
							Nous disposons d'une sorte de 'maquette' qui permet de faire quelques tests, de vérifier l'algorithme 
							etc. 
						
						
							Tant qu'il n'a pas passé une séance de validation unitaire intense, ce n'est certainement pas un produit 
							fini. 
						
					
				
			
		
	
    
    
	
	
	
	
	
	
	
	
	
	
	
	
		

 
Copyright © 2008 Emmanuel Delahaye.
Aucune reproduction, même partielle, ne peut être faite
de ce site ni de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.