6 décembre 2006

Un exemple de gestion de projet

Ce style de caractères est utilisé pour les commentaires.

Afin d'illustrer l'article sur la programmation, je vais montrer pas à pas les étapes de la réalisation d'un projet. Bien sûr il s'agit d'un projet simple, pour être court.

Dans le but de rester lisible et éducatif, l'application est réalisée en C standard simple. Les spécifications sont donc basées sur des concepts un peu 'rigides' (tailles fixes etc.).

En situation réelle, on chercherait probablement beaucoup plus de souplesse, à condition que le surcout induit soit raisonnable. Un léger surdimensionnement bien protégé et évolutif (par une nouvelle version) est souvent plus efficace qu'un système complètement souple. Le choix doit être analysé avec le client, étudié et motivé.

1 - Spécifications



Il s'agit de réaliser un logiciel permettant d'afficher une série de Questionnaires à Choix Multiples (QCM), de saisir les réponses et d'afficher une note sur 20 à la fin de la séquence.

1.1 - Limites



Les QCM sont regroupés dans un fichier 'thème'. Le nombre de questions par thème varie de 1 à 10. Le nombre de choix possibles pour une question varie de 2 à 6. Il n'y a qu'un seul choix possible.

1.2 - Comportement, erreurs



Toutes les saisies sont terminées par [enter].

Lors du lancement de l'application, le nom du fichier thème est demandé. Sil il n'existe pas, un message d'erreur est émis. Si on entre rien, le programme se termine.

Si le format du fichier n'est pas conforme, un message d'erreur est émis.

La question est présentée, ainsi que la liste des réponses possibles numérotées de 1 à 10 (maximum). La saisie de 0 permet d'interrompre le QCM.

Exemple de présentation :

Quelle est la date de la bataille de Marignan ?

1 - 1415
2 - 1515
3 - 1155

Choix ou 0 pour quitter :

Si une saisie est erronée (valeur hors limites), un message d'erreur est émis.

Lorsque la série est terminée, le programme affiche les résultats. La note est affichée avec 2 décimales significatives.

Exemple :

Nombre de questions : xx
Nombre de bonnes réponses : yy
Note : zz.tt/20


Ensuite, le programme demande un nouveau nom de fichier thème. Si on entre rien, on quitte le programme.

1.3 - Interface



1.3.1 - IHM



L'Interface Homme Machine est de type conversationnel.

Les entrées s'effectuent sur l'entrée standard avec saisie obligatoire de [enter] pour valider.

La correction est possible. Aucune entrée ne doit provoquer de dysfonctionnement de l'application.

Les sorties se font sur la sortie standard au fil de l'eau (séquenciellement). Il n'y a ni mise en forme, ni effacement.

1.3.2 - Fichier de thème



Le format des données dans le fichier de thème est le suivant :

Fichier texte avec des lignes de 256 caractères utiles au maximum (hors-champ et fin de ligne).

Les caractères supplémentaires sont ignorés. Ils ne provoquent pas de dysfonctionnement de l'application.

Le fichier est composé d'une séquence de champs. Les lignes vides sont ignorées.

Chaque champ est composé d'un mot clé obligatoirement suivit d'une ligne de texte. En cas d'absence de texte, une erreur est notifiée.

Les champs sont :
  • [theme]Nom du theme du questionnaire
  • [question]La question posée sans ? (mis automatiquement par l'application).
  • [ok]La réponse correcte
  • [ko]Une réponse fausse
Le séquencement des champs est le suivant :
  • Le premier champ est [theme]. Il est obligatoire. Si il est absent, une erreur est notifiée.
  • Il est obligatoirement suivit par le champ [question]. Si il est absent, une erreur est notifiée.
  • Il est obligatoirement suivit par le champ [ok] ou [ko]. Si il est absent, une erreur est notifiée.
  • Il est obligatoirement suivit par le champ [ok] ou [ko]. Si il est absent, une erreur est notifiée.
  • Il ne peut y avoir qu'un [ok]. Si il y en a plus d'un, une erreur est notifiée.
  • Il peut y avoir jusqu'à cinq [ko]. Si il y en a plus de cinq, une erreur est notifiée.
  • Il peut y avoir jusqu'à dix questions. Si il y en a plus de dix, une erreur est notifiée.


Exemple de fichier de theme :

[theme]Histoire
[question]Quelle est la date de la bataille de Marignan
[ko]1415
[ok]1515
[ko]1155
[question]Quelle est la date de la prise de la Bastille
[ko]4 Aout 1789
[ko]14 Juillet 1790
[ok]14 Juillet 1789

Il est facile de réaliser un fichier thème avec un simple éditeur de texte.

C'est la fin (provisoire) de la spécification. Comme dans la réalité, il se peut qu'il manque des éléments, ou qu'il y ait des incohérences fonctionnelles, mais il faut bien commencer un jour. Si des modifications sont apportées, elles sont listées dans un journal des modifications et reportées dans le texte directement.

On peut maintenant passer à la phase de conception

2 - Conception



Il s'agit d'un programme assez simple qui peut se résumer à un algorithme :

2.1 - Comportement global de l'application



DEBUT
BOUCLE
Demander le nom du fichier theme
SI le nom est vide
quitter
SINON
charger le fichier theme
BOUCLE
afficher le theme, la question et les reponses
demander une reponse
SI la reponse est 0
quitter
SINON
evaluer le resultat
incrementer le compteur de question et de bonnes reponses
lire la suite
FIN
FIN
calculer la note
afficher le resultat
FIN
FIN
FIN

Une analyse rapide de cet algorithme fait apparaitre les blocs fonctionnels suivants :
  • SD : Saisie de données (nom du theme, numero de reponse)
  • GF : Gestion du fichier (verification, lecture des QCM)
  • AQ : Affichage d'un QCM
  • GC : Gestion des compteurs
  • ER : Evaluation de la reponse
  • CN : Calcul de la note
  • PN : Présentation de la note

A chaque BF est attribué un groupe de deux lettres utilisé comme prefixe aux identificateurs d'objets.

Le traitement étant synchrone (question/réponse), il n'y a qu'un processus.

2.2 - Gestion des données




On peut maintenant se demander si on charge les données en mémoire ou si on lit le QCM au fur et à mesure, à la volée. Les possibilités doivent être analysées, les performances comparées et un décision doit être prise.


Le fichier de données est lu séquenciellement. Les informations sont affichées au fur et à mesure de l'avancement du QCM. Les seules données mémorisées sont les états de la réponse (ok/ko). Cela permet au programme de savoir si la réponse est bonne.

Un tableau de 10 booléens suffit donc.

Un compteur de questions et un compteur de bonnes réponses sont aussi gérés au fur et à mesure de l'avancement du QCM. Ces compteurs sont de type entier.

La calcul de la note utilise une regle de 3 :

nombre de réponses justes
note = 20 x -------------------------
nombre de questions

Le calcul s'effectuant sur des nombres réels, des précautions de codage et d'affichage doivent être prises.

3 - Réalisation



Le langage utilisé est le C. Le développement est organisé de la façon suivante :

  • Un fichier main.c qui contient l'algorithme de l'application
  • Un fichier par BF avec son header

Pour une application simple comme celle-ci, ça peut sembler démesuré, et ça l'est. mais il s'agit de montrer les bonnes pratiques permettant de réaliser un véritable projet industriel pouvant impliquer des dizaines de BF comprenant chacun des dizaines de fonctions...

3.1 Codage de l'algorithme principal




/* main.c */
#include
#include "sd.h"
#include "aq.h"
#include "er.h"
#include "gc.h"
#include "cn.h"
#include "an.h"
#include "data.h"

int main (void)
{
do
{
char theme[32] ;

SD_get_string("Fichier theme", theme, sizeof theme);
if (*theme == 0)
{
exit (0);
}
else
{
struct data data;
GF_open (&data, theme);
int end;
do
{
end = GF_read_qcm (&data, theme);
if (!end)
{
AQ_display_qcm (&data);
int response = SD_get_integer("choix");
if (response == 0)
{
exit (0);
}
else
{
ER_result(&data, response);
GC_update(&data);
}
}
}
while (!end);
GF_close (&data);
CN_compute (&data);
AN_display (&data);
}
}
while (1);
return 0;
}

A ce stade d'avancement du projet, le code n'est pas testable, ni même compilable. C'est en quelque sorte un pseudo-code. De toutes façons, il représente la phase 4, c'est à dire l'intégration. Son rôle consiste à fixer (provisoirement) le développement et à montrer clairement les blocs fonctionnels.

Il faut maintenant écrire et tester unitairement chaque BF.

On commence par définir un embryon de
struct data que l'on complètera au fur et à mesure des besoins :


#ifndef H_ED_DATA_20060913232412
#define H_ED_DATA_20060913232412
/* data.h

Log
15-09-2006 Ajout de min et max
14-09-2006 creation

*/

struct data
{
char theme[256];

int min;
int max;
/* ... */
};

#endif /* guard */


3.2 Le bloc fonctionnel "Saisie de Données (SD)"

3.2 Le bloc fonctionnel "Saisie de Données (SD)"



Un bloc fonctionnel se traite comme un mini projet, avec specification, conception codage et test. Le test correspond au Test unitaire

3.2.1 Specification


Le but de ce bloc fonctionnel est de fournir à l'application les services de saisies de données sécurisées. Les besoins concernent :

  • La saisie de chaine : SD_get_string()
  • La saisie d'entiers : SD_get_integer()

3.2.1.1 SD_get_string()


Affichage d'un prompt et attente de la saisie d'une chaine sur l'entrée standard validée par la touche <enter>. La correction est possible par la touche <backspace>. La taille maximale est déterminée. Les caractères hors limites sont ignorés.

Le premier paramètre est l'adresse de la chaine 'prompt' à afficher. La fonction ajoute les caractères " : ".

Le 2ème paramètre est l'adresse du tableau de char dans lequel la chaine saisie sera stockée. La taille utile du tableau ne doit pas être inférieure à la valeur passée en 3ème paramètre.

Le 3ème paramètre indique la taille maximale de la chaine à saisir (0 final inclu).

Aucune valeur n'est retournée. Le prototype est :

void SD_get_string (char const *prompt, char *s, size_t size);

3.2.1.2 SD_get_integer()


Affichage d'un prompt et attente de la saisie d'un entier sur l'entrée standard validée par la touche <enter>. La correction est possible par la touche <backspace>. Les valeurs minimales et maximales sont déterminées. Les caractères hors limites sont ignorés et un message est affiché, ainsi qu'un invitation à re-saisir la valeur. Idem pour les valeurs hors limites. Attention, tant que la saisie est incorrecte, la fonction redemande la valeur.

Le premier paramètre est l'adresse de la chaine 'prompt' à afficher. La fonction ajoute les caractères " : ".

Le 2ème paramètre est la valeur minimale autorisée.

Le 3ème paramètre est la valeur maximale autorisée.

La fonction retourne la valeur valide saisie. Le prototype est :

int SD_get_integer (char const *prompt, int min, int max);

3.2.2 Conception


La saisie est basée sur la fonction standard fgets(), suivie d'une fonction qui supprime le '\n' si il est présent et purge les éventuels caractères non lus. Une fois la chaine nettoyée, celle-ci est prête pour la fonction SD_get_string(). Pour SD_get_integer(), une conversion en entier signé est appliquée avec strtol(). Les erreurs de format et de limites sont détectées et signalées sur la sortie standard. Tant que la saisie est incorrecte, celle-ci est redemandée.

3.2.3 Codage


Voici l'interface et l'implémentation du BF SD.

#ifndef H_ED_SD_20060913232333
#define H_ED_SD_20060913232333
/* sd.h */

#include <stddef.h>

void SD_get_string(char const *prompt, char *s, size_t size);
int SD_get_integer(char const *prompt, int min, int max);

#endif /* guard */


/* sd.c

log

05-03-2007 mise au point. test des limites
07-10-2006 mise au point. efface errno.
15-09-2006 Passage au test unitaire. Mise au point.
15-09-2006 creation

*/

#include "sd.h"

#include <string.h>
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <errno.h>

static void clean (char *s, FILE * fp)
{
/* chercher le '\n' */
char *p = strchr (s, '\n');
if (p != NULL)
{
/* si on l'a trouve, on l'elimine. */
*p = 0;
}
else
{
/* sinon, la saisie est incomplete.
Les caracteres restants sont lus (le flux est 'purge')
*/
int c;
while ((c = fgetc (fp)) != '\n' && c != EOF)
{
}
}
}

void SD_get_string (char const *prompt, char *s, size_t size)
{
printf ("%s : ", prompt);
fflush (stdout);

fgets (s, size, stdin);
clean (s, stdin);
}

int SD_get_integer (char const *prompt, int min, int max)
{
int n = 0;
int err;

do
{
err = 1;
errno = 0;
printf ("%s : ", prompt);
fflush (stdout);
{
char s[16];
fgets (s, sizeof s, stdin);
clean (s, stdin);

if (*s != 0)
{
char *p_end;
long x = strtoul (s, &p_end, 10);
if (*p_end == 0 && INT_MIN <= n && n <= INT_MAX
&& errno != ERANGE)
{
if (min <= x && x <= max)
{
n = (int) x;
err = 0;
}
else
{
printf ("saisie hors limites (%d %d)\n", min, max);
}
}
else
{
printf ("saisie erronee\n");
}
}
else
{
printf ("saisie erronee\n");
}
}
}
while (err);
return n;
}

Ce code semble correct en ce qui concerne l'écriture, il compile sans erreurs ni avertissements en mode sevère (gcc : -Wall -Wextra -O2), mais son comportement n'a pas été vérifié. Il faut maintenant écrire une petite application de test qui permet de vérifier le fonctionnement et tester le code en situation normale, limite et erronée. C'est le rôle du test unitaire

3.2.4 Test unitaire


Afin de ne pas tomber dans le cercle vicieux du "mais comment est testé le programme de test ?", celui-ci doit être réalisé de façon la plus simple et la plus directe possible, sans 'astuces' dangereuses et, si nécessaire, en réutilisant du code validé.

Voici un test basique permettant de vérifier le fonctionnement de la fonction SD_get_string().

/* tu_sd.c

Test unitaire pour le BF QCM.SD

*/

#include "sd.h"
#include <stdio.h>

int main (void)
{
char s[16];

do
{
SD_get_string ("test", s, sizeof s);

printf ("'%s'\n", s);
}
while (*s != 0);

return 0;
}

Il permet de réaliser le "test du singe" et de s'assurer que le comportement reste stable, même dans les pires conditions.

Cependant, on préfère qualifier la fonction avec un test plus, disons, orthodoxe, pour ne pas dire scientifique. Une des méthodes consiste à ecrire un fichier texte qui servira d'entrée au programme via une indirection (stdin), et un fichier de sortie de référence comprenant les données attendues sur stdout. Les sorties du programme sont alors enregistrées dans un fichier texte de sortie via une indirection (stdout). Le fichier de sortie est alors comparé au fichier de référence. Si ils sont identiques, le test est satisfaisant.

L'ensemble peut être écrit dans un batch, un script, ou un programme C (ou autre, Python, etc.).

Exemple de fichier de test (entrées)

C:\dev\qcm>type sd_in.txt
a
abcd
abcd efg
012345678901234567890123456789012345678901234567890123456789


Exemple de fichier de test (sorties attendues)

C:\dev\qcm>type sd_out.txt
test : 'a'
test : 'abcd'
test : 'abcd efg'
test : '012345678901234'
test : ''

Exemple de batch (Mode commande de Windows)

C:\dev\qcm>type tusd.bat
tu_sd.exe < sd_in.txt > out.txt
fc sd_out.txt out.txt

Exemple d'execution

C:\dev\qcm>tusd

C:\dev\qcm>tu_sd.exe 0<sd_in.txt 1>out.txt

C:\dev\qcm>fc sd_out.txt out.txt
Comparaison des fichiers sd_out.txt et OUT.TXT
FC : aucune différence trouvée

C:\dev\qcm>

Le test unitaire est étendu à la vérification de la fonction SD_get_integer()

/* tu_sd.c

Test unitaire pour le BF QCM.SD

Log

15-06-2006 ajoute TU pour SD_get_integer()
15-06-2006 creation

*/

#include "sd.h"
#include <stdio.h>

void tu_string(void)
{
char s[16];
do
{
SD_get_string ("test s", s, sizeof s);

printf ("'%s'\n", s);
}
while (*s != 0);
}

void tu_integer(void)
{
int n;
do
{
n = SD_get_integer ("test i", 0, 4);
printf ("'%d'\n", n);
}
while (n != 0);
}

int main (void)
{
tu_string();
tu_integer();

return 0;
}

Le fichier d'entrée est

a
abcd
abcd efg
012345678901234567890123456

1
4
1234
9999999
99999999
-1
-9999999
azer
12az

0

Le fichier de sortie attendu est

test s : 'a'
test s : 'abcd'
test s : 'abcd efg'
test s : '012345678901234'
test s : ''
test i : '1'
test i : '4'
test i : saisie hors limites (0 4)
test i : saisie hors limites (0 4)
test i : saisie hors limites (0 4)
test i : saisie hors limites (0 4)
test i : saisie hors limites (0 4)
test i : saisie erronee
test i : saisie erronee
test i : saisie erronee
test i : '0'

Le resultat est conforme.

Le bloc fonctionnel SD est donc déclaré vérifié. Il peut être intégré à l'application.

A suivre ...

9 commentaires:

Anonyme a dit…

Le coup de faire afficher des choses à un programme de test et de comparer les sorties standard, c'etait bien trouvé.
On l'utilise maintenant pour le programme de test de nos modules utilitaires.

Unknown a dit…

Je suis bien content que mes petits conseils servent à quelque chose.

A+
Emmanuel

Anonyme a dit…

Salut,

Merci bcp pour cet article ! J'espère que les autres arriveront bientot ;)

Anonyme a dit…

Salut ed,

Tres interessant cet article, ça permet de savoir ce qu'on fait et de plus se plonger tete baissé dans des codes qui vont finir à la poubelle ...

Une question : dans la fonction get_integer du BF SD, donc SD_get_integer, cette ligne :
if (x <= min && x >= max)
ne devrait elle pas etre plutot :
if (x >= min && x <= max)
Etant donné que le nombre doit etre compris dans les limites, donc min <= x <= max ?

A quand le prochain article ? ^^

Unknown a dit…

Il est possible qu'il y ait une erreur. Je vais vérifier. Merci.

Unknown a dit…

Correcton faite, Merci.

Anonyme a dit…

Bonjour,
Je trouve ce blog, pour les novices comme moi.
Et je compte bien y revenir pour les autres rubriques.
J'aimerai aussi une explication si possible sur une instructions de la fonction 'clean':
...
while ((c = fgetc (fp)) != '\n' && c != EOF)
{
}
...
Je vois pas son utilité,quelle est sont but?!

Unknown a dit…

Il s'agit de 'vider le flux' des caractères qui n'auraient pas été lus par la fonction de saisie appelé précédemment.

Je conseille la lecture de ces articles pour les détails et les raisons.

http://mapage.noos.fr/emdel/notes.htm#fgetc
http://mapage.noos.fr/emdel/inputs.htm

http://mapage.noos.fr/emdel/notes.htm#saisie
http://mapage.noos.fr/emdel/notes.htm#fichiers

Unknown a dit…

bonjour

merci pour l'effort, c'est le premier truc que je v'ai lire dans se dommaine.