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 ...

10 septembre 2006

Initiation à la programmation

Qu'est-ce que la programmation ?

"La programmation est l'art d'écrire un programme !"

Malheureusement, force est de constater, que ce soit chez les étudiants ou même chez les professionnels, que parfois, les programmes sont mal conçus, mal écrits, peu robustes, ou qu'ils ne correspondent pas aux désirs du client.

Il est des domaines d'application où ce genre de comportement est inacceptable (avionique, médical, nucléaire). D'une façon générale, un programme instable ou peu performant est extrêmement mal perçu par le client, et l'image de l'entreprise ou la réputation de l'auteur s'en trouve largement ternie. Il faut donc prendre des mesures pour éviter ça.

"La bonne programmation est l'art d'écrire correctement un programme !"

Le secret de la réussite d'un projet informatique tient en trois points essentiels :
  • Une conception solide
  • Une réalisation impeccable
  • Une vérification profonde
Il s'agit donc de mettre en oeuvre, de la meilleure façon possible, la succession d'étapes qui permet de traduire une application en un langage compréhensible par une machine. Les étapes principales sont :
  • La définition du projet (que veux-t-on faire ?)
  • La conception du projet (comment allons-nous le faire ?)
  • La réalisation (transcription du comportement en algorithmes, automates, fonctions)
  • Le codage dans un langage de programmation (C, Ada, Pascal, PhP etc.)
  • La traduction en langage machine (assembleur, compilateur, interpréteur)
L'ensemble de ses opérations s'inscrit dans le schéma plus général de la gestion d'un projet qui se décompose en 5 phases principales :
  1. La définition
  2. La conception
  3. La réalisation
  4. L'intégration
  5. La validation
La programmation concerne principalement les phases 1 à 4 de la gestion d'un projet, avec une insistance particulière sur le point 3. Elle consiste donc à maitriser l'ensemble de ces phases.

1 - La définition
C'est l'étape initiale qui permet de définir le projet. Par définition, on entend :

  • La spécification des interfaces (IHM, autres machines, fichiers)
  • La spécification des comportements (Normal, Extrême, Hors limite)
  • La specification des performances minimales attendues
2 - La conception

Cette étape découle obligatoirement de la précédente. L'ensemble des caractéristiques spécifiées est regroupé en grandes entités appelées blocs fonctionnels (BF), puis, chaque BF est découpé en entités plus petites selon une hiérarchie de type arborescente.

Les comportements sont décrits en détail par des algorithmes ou des machines à états. Les interfaces sont pris en compte. Des choix technologiques peuvent être effectués (qui fait quoi, matériel, logiciel). Il peut être choisi de recourir à des technologies plus ou moins existantes ou innovantes. Ces choix sont importants et déterminent en grande partie la réussite du projet.

La phase de conception est difficile et nécessite une bonne expérience et une certaine intuition. A ma connaissance, il n'existe pas de recette miracle ni de méthode toute faite pour réaliser une bonne conception

3 - La réalisation

C'est ici qu'intervient tout l'art du programmeur. Les phases précédentes sont essentielles, mais n'ont fait que préparer le terrain. Il s'agit maintenant de passer au choses sérieuses, au concret, au réel.

La conception fixe le cadre du développement. Elle défini les sous ensembles, les blocs fonctionnels et les comportements détaillés.

Une bonne méthode de réalisation d'un composant logiciel quelconque consiste à écrire les tests unitaires qui vérifient la conformité du composant en terme d'interface, de comportement et de performances, puis d'écrire le composant lui-même, et de vérifier le comportement au fur et à mesure.

Une bonne réalisation est aussi basée sur une connaissance approfondie du langage que l'on utilise. En effet, il est absolument primordial de s'assurer qu'en aucun cas, il ne puisse se produire de comportement indéfini (Undefined Behaviour ou UB). Le UB est la plaie du programmeur. Il en suffit d'un pour semer le doute sur la fiabilité du programme en entier.

Il faut donc être absolument sûr que chaque ligne de code est écrite dans le respect absolu de la définition du langage. C'est particulièrement vrai avec des langages 'souples' et au typage faible comme le C.

Il est important de souligner que les conséquences d'un UB étant imprévisibles par définition, il est illusoire de compter sur les tests pour le révéler. Encore une fois, seule une excellente maitrise du langage, éventuellement confirmée par une lecture croisée (travail en binôme) est à même de garantir la qualité d'un code en terme d'absence de comportements indéfinis.

Ensuite, le test sert à vérifier la conformité du code aux spécifications (nominales, extrêmes, hors limites).

4 - Intégration

C'est l'assemblage des composants et vue de réaliser le produit final. Les composants étant testés et réputés fiables, seuls les comportements globaux sont testés.

5 - Validation

C'est tout simplement la vérification de la conformité du produit à ses spécifications.

1 septembre 2006

Introduction

Bonjour,

En prolongement de mon site http://www.bien-programmer.fr/ consacré à la programmation, je crée ce blog destiné à améliorer l'interactivité avec le site.

Voici quelles seront les têtes de chapitres

  • Initiation à la programmation
  • Notions d'algorithmique
  • Initiation au langage C
  • Organisation du développement
  • Algorithmique en C
  • Pièges à éviter en C
  • C avancé
  • Programmation système
A bientôt

Emmanuel Delahaye