Académique Documents
Professionnel Documents
Culture Documents
SD2023
SD2023
Ludewig François
31 août 2022
2
Table des matières
1 Introduction 7
2 Les exceptions 9
2.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 Pourquoi ? Et quand ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3 Exceptions en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3.1 Lancer une exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3.2 Attrapper une exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.3.3 Créer une exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.4 Exceptions en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.4.1 Lancer une exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.4.2 Attraper une exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.4.3 Créer une exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4 Fichiers 29
4.1 Qu’est ce qu’un fichier ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.2 Type de fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.3 Fichier binaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.4 Fichier texte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.4.1 ASCII et ISO-Latin1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.4.2 Universal character set Transformation Format . . . . . . . . . . . . . . . . . 31
4.4.3 Conversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3
4 TABLE DES MATIÈRES
5 Serialisation 33
5.1 Organisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.1.1 Relative . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.1.2 Séquentielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
7 Les fichiers en C# 55
7.1 Architecture des classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
7.2 Créer un stream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
7.3 Fichiers binaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
7.4 Fichiers textes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8 Architecture 63
8.1 Repository . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.2 Librairies orientées objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
8.2.1 Inter-programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
8.2.2 Intra-programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
11 ADO.NET en C# 89
11.1 Configurer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
11.2 Faire une requête : IDbCommand . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
11.3 Ajouter et Modifier des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
11.4 Lire des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
TABLE DES MATIÈRES 5
12 Tests et ressource 99
12.1 Une ressource pour le test, pourquoi ? . . . . . . . . . . . . . . . . . . . . . . . . . . 99
12.2 Une ressource pour le test, concrètement ! . . . . . . . . . . . . . . . . . . . . . . . . 100
12.3 Tester la lecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
12.4 Tester l’écriture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
B Décorateur 107
C Adaptateur 111
6 TABLE DES MATIÈRES
Chapitre 1
Introduction
Afin de rendre la lecture plus facile, le syllabus suppose que le lecteur possède des connaissances
en Programmation Orienté Objets, en Java et en C#.
Les applications de bureau sauvegardent leurs données dans des fichiers ou dans des bases de
données. Nous les appellerons ”ressources”. Leur utilisation dans des programmes nécessite de
bonne notion dans la gestion des exceptions et des ressources elles-mêmes. La mise en œuvre de
ces ressources sera aussi présentée.
Les données contenues dans les objets d’un programme sont structurées au sein d’un diagramme
de classes. Cette structure ne correspond pas, la plupart de temps, aux structures de données uti-
lisées par les différentes ressources. La découverte de ces différentes structures et des techniques
de passage d’une à l’autre est au menu de ce syllabus.
La réussite du développement d’une application passe par un bon testing. Ce point sera abordé
afin de baliser la mise en place de test d’une application gérant une ressource.
L’ensemble du cours est éclairé par des exemples de code Java et C# afin d’aider le lecteur
dans sa compréhension et la mise en œuvre des ressources dans ses propres projets.
7
8 CHAPITRE 1. INTRODUCTION
Chapitre 2
Les exceptions
2.1 Définition
Une exception représente ce qui n’est pas dans la norme. C’est-à-dire quelque chose qui est hors
du commun.
Une exception, en programmation orientée objet, est une classe qui permet de signaler un
problème vis-à-vis du fonctionnement attendu.
La plupart des langages de programmation propose un Système de Gestion des Exceptions
(SGE).
Une méthode peut recevoir des paramètres et peut retourner une valeur. Il est possible de faire
des hypothèses, appelée aussi pre-requis, sur les valeurs des paramètres. Que faire si celles-ci ne
sont pas rencontrées ?
Il est possible d’implémenter des ”assert” afin de garantir nos pré-requis. Néanmoins, si ceux-ci
ne sont pas corrects, le programme sera alors arrêté ! Ce qui est peu souhaitable dans le cadre d’un
software déployé chez un client.
Une autre possibilité est de retourner des valeurs particulières qui signalent un problème au
client (celui qui appelle notre méthode). Cette solution n’est pas possible dans tous les cas (voir
Listing 1). Si une méthode retourne un entier pouvant prendre toutes les valeurs possibles, il n’est
pas possible de choisir une ou plusieurs valeurs pour signaler un problème.
9
10 CHAPITRE 2. LES EXCEPTIONS
Listing 1 – Exemple de méthode qui ne permet pas de valeur retour pour signaler un problème.
Il est possible d’étendre la méthode de la valeur de retour avec une version orientée objet A.
Les exceptions (et le SGE) offrent une alternative à ce problème. Nous pouvons créer et lancer une
exception afin d’alerter le client du problème. Tout lancement d’exception doit être documenté afin
de prévenir tous les utilisateurs de cette méthode. Le client devient alors responsable de traiter
l’exception. Car il est le seul à pouvoir donner un sens en relation à cette exception en fonction de sa
tâche. L’exception permet de stopper l’exécution d’une portion de code, lors d’un événement hors
du commun, et de rendre la main à une autre partie de code. Les paragraphes suivants présentent
les exceptions en Java et C#.
Figure 2.1 – Illustration de la structure des classes Error et Exception en Java. Les classes bleues
sont dites ”Unchecked” et les oranges ”Checked”.
2.3. EXCEPTIONS EN JAVA 11
Les erreurs sont rares et correspondent à des problèmes de fonctionnement de la JVM. Il est
donc recommandé de ne pas les traitées. Dans la plupart des cas, de toute façon, il ne sera pas
possible d’y remédier. Les exceptions devront quant à elles être traitées. Les exceptions qui peuvent
ne pas être traitées sont les RunTimeException.
Comme son nom l’indique, toutes les classes qui héritent de ”Throwable” sont dites ”lançables”.
On peut donc lancer toutes les exceptions et les erreurs. Pour de plus amples informations, je renvoie
le lecteur vers la documentation officielle de Java [1].
Dans ce cas précis, l’exception lancée est une RunTimeException, il n’est donc pas nécessaire
de le préciser dans la signature de la méthode, comme pour les erreurs. Pour toutes les autres
exceptions, la signature de la méthode devra indiquer que celle-ci peut lancer une exception.
La signature de la méthode doit alors se terminer par le mot-clé ”throws” suivi du type d’ex-
ception qui sera lancée en cas de problème (voir listing 3).
12 CHAPITRE 2. LES EXCEPTIONS
Lorsque vous êtes amenés à utiliser des méthodes qui sont susceptibles de lancer des exceptions,
il faut les traiter. Les exceptions sont traitées à l’aide d’un bloc ”try-catch” comme illustré dans
le listing 4.
7 try {
8 return divide(sum, data.size());
9 } catch(IllegalArgumentException ex) {
10 System.out.println("Cannot compute average over an empty list !");
11 throws new ExecutionException("Cannot compute average over an
12 empty list !", ex);
13 }
14 }
Le traitement d’une exception se déroule dans le contexte du catch. Le plus souvent, l’infor-
mation sera ”loggée” dans la console ou une autre sortie. Il est parfois pertinent de relancer une
exception différente qui aura du sens pour le client de notre méthode (ici compute average). Vous
pouvez noter que l’exception lancée par ”divide” devient l’attribut ”cause” de l’exception lancée.
Il est formellement déconseillé d’utiliser un try −catch qui traite le cas général Exception. Dans
ce cas précis, vous allez capturer toutes les exceptions. Ce qui peut paraı̂tre comme un avantage
est en réalité un inconvénient. En effet, dans un tel contexte, il vous est impossible d’identifier le
type d’exception et donc de la traiter en conséquence.
Si plusieurs types d’exception peuvent être lancés, il vous faut cumuler les ”catch” : un pour
chaque exception comme illustré dans le listing 5.
2.3. EXCEPTIONS EN JAVA 13
7 try {
8 return divide(sum, data.size());
9 } catch(IllegalArgumentException ex) {
10 System.out.println("Cannot compute average over an empty list !");
11 throws new ExecutionException("Cannot compute average over an
12 empty list !", ex);
13 } catch(IndexOutOfBoundsException ex) {
14 System.out.println("Try to access a non-existent data item.");
15 }
16 }
Lancer une exception, c’est stopper le code courant qui ne peut plus s’exécuter. L’exécution
du programme saute alors des lignes de codes pour démarrer celles du catch correspondant à
l’exception lancée. Un problème peut survenir si nous avons du code que nous souhaitons exécuter
à coup sûr. Dans le try, il ne sera exécuté que s’il n’y pas de lancement d’exception avant. Dans
le catch, il sera exécuté uniquement dans le cas où l’exception adéquate est lancée. Pour éviter le
code dupliqué (principe DRY), le langage propose un nouveau contexte avec le mot clé ”finally”.
Le code présent dans le finally sera exécuté après le bloc try-catch.
Le listing 6 montre un exemple d’utilisation du bloc ”f inally”. Je laisse en exercice, au lecteur,
de déterminer si le f inally est exécuté lorsqu’une exception est relancée dans un catch.
Il possible de créer sa propre exception. Celle-ci pourra alors être lancée et être traitée comme
n’importe quelle exception.
14 CHAPITRE 2. LES EXCEPTIONS
7 try {
8 return divide(sum, data.size());
9 } catch(IllegalArgumentException ex) {
10 System.out.println("Cannot compute average over an empty list !");
11 throws new ExecutionException("Cannot compute average over an
12 empty list !", ex);
13 } catch(IndexOutOfBoundsException ex) {
14 System.out.println("Try to access a non-existent data item.");
15 } finally {
16 System.out.println("finally block executed");
17 }
18 }
2.4 Exceptions en C#
Le traitement des exceptions en C# est similaire à celui de Java. Elles seront donc passées en
revue de façon un peu plus rapide.
La différence entre erreurs et exceptions en Java, n’existe pas en C#.
Notez que contrairement à Java, le lancement d’une exception n’est pas accompagné par une
modification de la signature de la méthode.
La gestion des exceptions se fait au travers des mots-clés ”try”, ”catch” et ”finally” comme en
Java (voir listing 9).
Le fonctionnement est identique à celui décrit pour Java. Il existe néanmoins, une subtilité
supplémentaire en C# : les filtres. Il est possible de traiter une même exception en fonction de son
statut (voir listing 10).
16 CHAPITRE 2. LES EXCEPTIONS
1 WebClient wc = null;
2 try {
3 wc = new WebClient(); //downloading a web page
4 var resultData = wc.DownloadString("http://google.com");
5 }
6 catch (ArgumentNullException ex) {
7 //code specifically for a ArgumentNullException
8 }
9 catch (WebException ex) {
10 //code specifically for a WebException
11 }
12 catch (Exception ex) {
13 //code for any other type of exception
14 }
15 finally {
16 //call this if exception occurs or not
17 }
1 WebClient wc = null;
2 try
3 {
4 wc = new WebClient(); //downloading a web page
5 var resultData = wc.DownloadString("http://google.com");
6 }
7 catch (WebException ex) when (ex.Status == WebExceptionStatus.ProtocolError)
8 {
9 //code specifically for a WebException ProtocolError
10 }
11 catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.NotFound)
12 {
13 //code specifically for a WebException NotFound
14 }
15 catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.InternalServerEr
16 {
17 //code specifically for a WebException InternalServerError
18 }
19 finally
20 {
21 //call this if exception occurs or not
22 }
L’exemple du listing 10 propose de gérer une exception W ebException en trois sous cas plus
précis : P rotocolError, N otF ound et InternalServerError.
19
20 CHAPITRE 3. GESTION DES RESSOURCES
Le listing 12 présente une classe qui respecte le principe RAII. Elle acquiert une ressource de
type mémoire dans son constructeur et la libère dans son destructeur.
1 class DataCollection {
2 public:
3 // Interdiction du construteur par defaut.
4 DataCollection() = delete;
5
16 private:
17 int *collection;
18 };
L’utilisation d’une telle classe devient extrêmement simple. Comme l’illustre le listing 13, il
suffit de construire la classe et puis de l’utiliser.
3.3. LE PRINCIPE RAII 21
1 void WorkWithDataCollection() {
2 // Construction de la DataCollection
3 DataCollection myDataCollection{25};
4
5 // Utilisation de la DataCollection
6 ...
7 }
Analysons plus en détails ce qu’il se passe. La mémoire est divisée en deux parties : la stack et
le heap (de manière similaire à Java). En C++, un objet construit sans l’opérateur ”new” est dans
la stack et dans le heap avec l’opérateur ”new”. L’accès à la mémoire de la stack est évidemment
plus rapide. Notre objet ”myDataCollection” est donc dans la stack alors que le tableau d’entier
créé via l’opérateur ”new” est dans le heap comme illustré sur la figure 3.1.
Figure 3.1 – Schéma de la mémoire Stack et Heap après le constructeur de la classe DataCollec-
tion.
En cas de non respect, si l’objet est détruit sans libérer la ressource (voir figure 3.3), celle-ci
reste alors bloquée. Dans notre cas, le tableau d’entier ne sera jamais libéré. Il faudra faire un reset
de la RAM de la machine ou la redémarrer.
Figure 3.3 – Si le schéma standard n’est pas respecté, une fuite de mémoire apparaı̂t.
Figure 3.4 – Schéma de la mémoire Stack et Heap, en Java, après le constructeur de la classe
DataCollection.
Il y a un revers à la médaille. Vous ne maitrisez pas quand cette tâche sera accomplie. L’appel
au destructeur se fera à un moment durant la vie de l’application ou lors de la fin de l’exécution
de celle-ci. Cette fonctionnalité met à mal le principe RAII tel qu’il a été énoncé en C++.
Le Java a donc développé une autre stratégie pour adapter le principe RAII. L’appel au destruc-
teur n’étant pas garanti, le Java introduit une nouvelle méthode : ”close()”. Celle-ci est proposée
via l’interface Closeable et AutoCloseable (Java 7).
24 CHAPITRE 3. GESTION DES RESSOURCES
L’interface ”AutoCloseable” définit une méthode ”close()” qui peut lancer tout type d’excep-
tion. Celle-ci est spécialisée par l’interface ”Closeable” qui hérite de ”AutoCloseable” et propose
une méthode ”close()” qui ne peut lancer que des exceptions de type ”IOException”.
Le listing 14 présente une classe qui implémente l’interface ”Closeable”. La ressource est donc
acquise dans le constructeur et libérée dans la méthode ”close()”. L’utilisation de celle-ci est
illustrée dans le listing 13. Java propose une syntaxe qui assure l’appel à la méthode ”close()” dans
tous les cas. C’est-à-dire si la méthode (ou le programme) se termine bien ou mal via un lancement
d’une exception. Il faut utiliser le ”Try-With-Resources”. Lorsqu’un objet, qui implémente une
de ces deux interfaces, est construit dans les parenthèses du ”Try-With-Resources”, ma méthode
”close()” sera appelée à la fin du ’Try”.
3.3.3 RAII en C#
La problématique rencontrée en Java existe aussi en C#. Seule la nomenclature est différente.
La méthode qui prendra le rôle de ”close()” est ”Dispose()”. Elle est définie dans l’interface ”IDis-
posable”. Le listing 16 présente la classe MyResource qui implémente l’interface ”IDisposable”.
L’utilisation est aussi simple qu’en Java. Le mot clé (statement) à utiliser est ”using”. Le listing
17 illustre un exemple d’utilisation d’une classe ”disposable”.
Si un objet, implémentant l’interface ”IDisposable”, est créé dans les parenthèses d’un state-
ment using, alors la méthode ”Dispose()” est appelée à la fin du contexte associé au statement
using. Cet appel aura lieu quelque soit la condition de sortie du contexte : une sortie normale ou
une fin provoquée par le lancement d’une exception.
26 CHAPITRE 3. GESTION DES RESSOURCES
Les dernières versions de C# introduisent une nouvelle syntaxe pour l’utilisation du mot clé
using. En effet cette nouvelle version est plus simple et plus légère au sens du code, présentée dans
le listing 18. Le mot clé using doit être placé en début de ligne de déclaration-construction de
l’objet dont la classe implémente l’interface IDisposable. La méthode ”Dispose()” sera appelée à
la fin du contexte courant, généralement définit par une paire d’accolade.
L’application du principe RAII, en Java, se réalise par l’utilisation d’un try spécifique. Celui-ci
peut aussi être accompagné de catch, comme illustré dans le listing 19. Ceux-ci permettent de
gérer les différentes exceptions.
En C#, la solution est légèrement différente. Le contexte ”using” correspond en réalité à une
”try-finally” sans catch. Il est donc nécessaire d’ajouter une try-catch pour gérer les exceptions.
3.4. RESSOURCES ET EXCEPTIONS 27
Il est possible de s’interroger sur le choix de mettre le try-catch dans le using ou autour de
celui-ci. Une try-catch interne au using permettra de traiter les exceptions liées à l’utilisation de
la ressource. Les exceptions lancées lors de l’acquisition de la ressource ne seront capturées qui si
le try-catch est extérieur au contexte lié au ”using”. Il est donc primordial de placer le try-catch
autour du bloc ”using”.
28 CHAPITRE 3. GESTION DES RESSOURCES
Chapitre 4
Fichiers
29
30 CHAPITRE 4. FICHIERS
Java et C# possède chacun une liste de types primitifs. Le tableau ci-dessous 4.2 reprend une
partie avec la taille (nombre d’octet) de leur représentation binaire.
Table 4.2 – Liste des types primitifs du Java et C# avec leur taille en nombre d’octets.
4.4. FICHIER TEXTE 31
Cette table code les caractères sur un seul octet. Malgré cela, comme la figure 4.1 l’illustre,
seul un petit nombre de caractères imprimables est codé. Cet ensemble de caractères convient pour
l’anglais mais pas pour des textes en français !
La table ISO-Latin1 (norme ISO-8859-1) permet d’ajouter à la table ASCII la prise en compte
des accents.
Que faire des écritures cyrillique, arabe et asiatique ?
Figure 4.2 – Évolution au cours du temps de l’utilisation des différentes tables de codage des
caractères dans le milieu du web [6].
Comme pour l’Endianness des fichiers binaires, la table d’encodage utilisée n’est pas notée dans
un fichier texte. Il n’existe pas de standard pour faire cela. De fait, le lecteur d’un fichier texte
doit connaitre la table utilisée lors de la création du fichier.
4.4.3 Conversion
Les fichiers textes permettent d’écrire et lire des caractères selon une table de conversion stan-
dardisée. Toutes les données contenues dans un programme informatique ne sont pas un assemblage
de caractères. Comment sauvegarder les entiers, les floats ou encore les booleans ? Il faut ajou-
ter une étape de conversion. Lors de la sauvegarde des données, toutes les données doivent être
converties en caractères. Les langages Java et C# proposent des solutions comme les méthodes
”toString()”. Le chemin inverse est lui plus délicat. Au moment de convertir des caractères en un
autre type (int, float, double, boolean, ...), il faut contrôler que les caractères correspondent bien
au type que nous voulons créer. Dans le cas contraire, une exception sera lancée.
Chapitre 5
Serialisation
5.1 Organisation
Dans le chapitre précédent, nous avons découvert que nous pouvons écrire soit des octets, soit
des caractères (qui sont des octets associés à une table de conversion standardisée) dans un fichier.
Dans un programme, nous avons une quantité importante d’informations disséminées dans un
réseau d’objets.
La sérialisation, c’est la conversion de données (nos objets) en données élémentaires plus petites
(caractères ou octets).
L’objectif de ce chapitre est d’aborder la problématique de sauvegarder et de lire un réseau
d’objets dans un fichier.
Il y a trois niveaux d’analyse : (i) savoir écrire et lire un objet dans un fichier, (ii) savoir écrire
et lire une collection d’objet dans un fichier et (iii) savoir écrire et lire un réseau d’objets.
Pour ce faire, il faut organiser les données dans le fichier. Deux familles d’organisation existent :
Relative et Séquentielle.
5.1.1 Relative
Une organisation relative est composée d’une suite d’enregistrement qui sont un agglomérat
de champs. Les champs et les enregistrements sont de tailles constantes. Concrètement, un objet,
instance d’une classe, sera un enregistrement quand chacun de ses attributs deviendra un champs.
33
34 CHAPITRE 5. SERIALISATION
Listing 21 – Classe Sphere : position selon x, y, z (en 3D) et son rayon. Classe Student ; prénom,
nom et âge.
Le listing 21 montre deux classes. La première représente une sphère qui contient les coor-
données de son centre et son rayon. La seconde représente un étudiant et contient son nom, prénom
et son âge. Ces données peuvent être organisées de façon relative comme illustré sur la figure 5.1.
La taille des rectangles représente la mémoire utilisée pour sauvegarder la donnée indiquée.
Figure 5.1 – Organisation relative des données provenant des classes Sphere et Student. Chaque
rectangle correspond à un champs, c’est-à-dire un attribut de la classe.
Dans notre cas, quelle que soit la valeur contenue dans les attributs, la taille des rectangles
(taille en mémoire) doit être constante. Comment mettre cela en œuvre selon le type de fichier ?
Pour les fichiers binaires, enregistrer une variable consiste à écrire les octets de sa représentation
mémoire (celle du programme) dans le fichier. Les ”double” dans notre cas sont codés sur 8 octets.
Tous les champs d’une sphère ont donc une taille constante. Notre sphère peut s’écrire très faci-
lement en relatif binaire. Chaque enregistrement occupe alors 32 octets. Dans le cas de la classe
Student, l’âge est codé sur 4 octets car c’est un ”int”. Le nom et le prénom sont codés sous un
”String”. Ne pouvant connaı̂tre la valeur, nous pouvons fixer une taille maximum. Le nombre de
”char” écrit sera toujours le même. On ajoutera alors, par exemple, des ”espaces” pour assurer
une taille fixe de la chaine de caractères. Dans les cas dégradés, la fin de la string sera supprimée.
Pour éviter cela, il faut définir une taille suffisante mais pas trop importante pour ne pas gaspiller
de la mémoire.
Dans le cadre de fichier texte, les nombres posent une grande difficulté. En effet, ceux-ci ne
sont pas toujours composés du même nombre de chiffres et leur représentation textuelle peut avoir
des tailles variables. Une solution consiste à formater les nombres lors de leur conversion en string.
5.1. ORGANISATION 35
Cette approche permet de garantir une taille fixe. Néanmoins, elle sera accompagnée d’une perte
d’informations si tous les chiffres ne sont plus écrits. Pour les ”string” (nom et prénom de Student),
la même astuce qu’en binaire est mise en place afin d’assurer une taille fixe des champs et donc
des enregistrements.
Figure 5.2 – Organisation relative des données d’un tableau de Sphere et Student. Chaque ligne
correspond à un enregistrement, c’est-à-dire l’ensemble des champs formant une instance de la
classe correspondante.
Sachant que la taille d’un enregistrement est fixe, nous pouvons imaginer déplacer la ”tête
de lecture” sur l’enregistrement qui nous intéresse (exemple : en orange sur la figure 5.2). L’or-
ganisation relative permet d’accéder et/ou modifier des données sans devoir lire l’intégralité du
fichier.
5.1.2 Séquentielle
L’organisation séquentielle propose l’enregistrement de toutes les données les unes derrière les
autres. Aucune contrainte de taille n’est ici imposée. Dans un tel cadre, il est facile d’imaginer que
l’enregistrement de deux strings consécutives vont poser un problème à la lecture. Comment savoir
à quel caractère finit le premier ou commence le second. La solution est assez simple : ajouter
des symboles séparateurs dans le cas de fichier texte. Afin de discerner les enregistrements et les
champs, deux symboles distincts sont utilisés. Par exemple, un point-virgule entre les champs et
un retour à la ligne pour séparer les enregistrements. 1
1. Remarque : l’organisation séquentielle ne permet pas de résoudre les problèmes de précision pour les types
float et double. C’est leur conversion en texte qui génère des pertes d’informations et non l’organisation du fichier
sur lequel ils sont enregistrés.
36 CHAPITRE 5. SERIALISATION
x ;y ;z ;radius
x ;y ;z ;radius
x ;y ;z ;radius
...
Lastname ;Firstname ;Age
Lastname ;Firstname ;Age
Lastname ;Firstname ;Age
...
Table 5.1 – Exemple d’organisation séquentielle pour les classes Sphere et Student.
Dans le cas de fichiers binaires, nous pouvons ajouter des zones pour indiquer la taille de la
donnée suivante. Par exemple, pour enregistrer un string, nous allons enregistrer un entier qui
contient le nombre de char puis le string lui-même. Lors de la lecture, le programme charge l’entier
en premier. Celui-ci lui permet de savoir combien d’octets doivent être lus pour le string.
Il n’est donc pas possible, pour une organisation séquentielle, d’éditer une donnée précise sans
devoir lire l’intégralité du fichier, ou du moins, jusqu’à la dite donnée.
Globalement, les fichiers binaires se prêtent mieux à une organisation relative quand les fichiers
textes s’avèrent pratiques pour l’approche séquentielle. Les fichiers textes étant rendus universels
par leur table standardisée. Ils servent de base pour la création de format standard d’organisation
séquentielle. Les sections suivantes présentent les formats standards les plus répandus : CSV, XML
et JSON.
CSV
Les listings 22 -23 présentent des données exemples issues des classes Sphere et Student au
format CSV.
L’existence de variantes, induite par différents symboles séparateurs, rend ce format faiblement
standardisé. Son utilisation reste limitée à des échanges ponctuels.
XML
Le nom est l’acronyme de Extensible Markup Language. Le format XML impose l’utilisa-
tion de la table de conversion Unicode. C’est une première étape pour se rendre standard. L’objectif
du format XML est d’être lisible par l’homme comme les machines 2 . Le XML est un sous langage
2. On utilise souvent l’expression ”Human readable.”
38 CHAPITRE 5. SERIALISATION
de SGML 3 et est devenu une recommandation du W3C 4 en 1998. Il a été développé spécifiquement
pour l’échange de données pour les applications web.
Il existe divers exemples d’application du format XML : FXML pour la description des inter-
faces graphiques en Java avec la bibliothèque JavaFx, XAML pour la description des interfaces
graphiques en C# WPF, le standard SOAP 5 impose l’utilisation du format XML pour les échanges
de données entre le client et le provider du service.
Figure 5.3 – Exemple d’organisation relative respectant le format XML pour des données pro-
venant d’instances de la classe Sphere (à gauche) et de la classe Student (à droite).
La figure 5.3 présente deux exemples de fichiers respectant le format XML. La première ligne
est appelée le prologue. Celle-ci permet de préciser la version de XML 6 , le type d’encodage (UTF-
8). Il est possible aussi de préciser si le document est cohérent seul ou est accompagné d’autres
documents.
Le corps du document est décrit par une balise racine (<Spheres> ... </Spheres>). Cette ba-
lise est impérative. Il ne peut y avoir qu’une seule balise racine. La balise racine contient d’autres
balises. Chacune des balises peut contenir des données de type : chaine de caractères (Nom et
Prenom de classe Students voir figure 5.3), entier (Age de classe Students voir figure 5.3), double
(X, Y, Z et Radius de classe Spheres voir figure 5.3) ou une autre balise (<Spheres> contient la
balise <Sphere> voir figure 5.3). On parle alors d’arborescence ou de composition.
Les balises peuvent être vides et avoir des attributs. Le langage XML est bien plus riche que
la brève description donnée ici. Il est laissé au lecteur de se renseigner si l’envie ou le besoin se
manifeste.
3. Standard Generalized Markup Language
4. World Wide Web Consortium
5. Simple Object Access Protocol
6. La version 1.0 est la plus courante, mais il existe la version 1.1
5.1. ORGANISATION 39
JSON
Le nom est l’acronyme de JavaScript Object Notation. Le format JSON poursuit les mêmes
objectifs que le format XML. Il est par ailleurs utilisé dans la définition des API REST pour
l’échange de données pour le WEB. Ce type API tend à remplacer les services SOAP.
Le JSON structure les données de manière similaire aux langages de programmation modernes
[7]. Le format propose la notion d’objet qui est un ensemble de couple ”nom, valeur”. Le nom et la
valeur seront toujours séparés par deux points ( :). Ils seront toujours délimités par des accolades
(...). Cette notion rappelle celle de clé, valeur que nous trouvons dans les map en Java et les
dictionnaires en C#.
Le nom sera toujours un string entre double guillemets. Les valeurs peuvent être une chaine de
caractères entre double guillemet, un nombre, true/false et nul. Ces types de valeur permettent de
représenter la plupart des types natifs des langages de programmation.
JSON introduit aussi la notion de tableaux. Ceux-ci sont une collection de valeurs. Ils seront
délimités par des crochets ([...]).
Figure 5.4 – Exemple d’organisation relative respectant le format JSON pour des données pro-
venant d’instances de la classe Sphere (à gauche) et de la classe Student (à droite).
La figure 5.4 présente les données de la figure 5.3 au format JSON. Les deux exemples se
composent d’un tableau. Chaque valeur du tableau est un objet. Chaque objet représente un
instance de la classe correspondante (Sphere et Student). Chaque objet est composé des couples
clé-valeur. Les clés font référence au nom de l’attribut de la classe.
40 CHAPITRE 5. SERIALISATION
Chapitre 6
Comme introduit précédemment, les fichiers sont identifiés par un nom (name) et un chemin
(path). Java propose une classe, nommée Path [8], qui aide à la mise en œuvre des chemins et
noms des fichiers. Celle-ci permet d’identifier un fichier dans un ”file system”. La classe Paths [9]
propose des méthodes statiques pour construire des instances de la classe Path.
Listing 24 – Exemple de code utilisant les classes Path, Paths et Files. Remarque : \% doivent être
remplacés par des %.
41
42 CHAPITRE 6. LES FICHIERS EN JAVA
En parallèle de la classe Paths, il existe la classe Files [10]. Celle-ci permet de réaliser un liste
importante d’opérations pratiques sur les fichiers et répertoires. Le listing 24 présente quelques
exemples d’applications de ces classes. Le lecteur est fortement encouragé à consulter la documen-
tation Java, proposée en bibliographie, afin de prendre connaissance des fonctionnalités existantes
et proposées par ces classes.
Exercice
Exécuter ce code sur votre machine. Tester-le avec la présence et l’absence des fichiers.
Qu’observez-vous ?
La possibilité de préciser chacun des dossiers composants le path séparément est un atout
de la classe Paths. En effet, cette approche nous évite les variations d’écriture de chemins entre
les différents OS. L’écriture des chemins demande le doublement du symbole ”\” car celui-ci est
réservé dans la gestion des chaı̂nes de caractères. De fait, les symboles séparateurs sont gérés pour
nous automatiquement.
Il est souvent utile de pouvoir utiliser des fichiers internes à nos projets. De plus, si le projet est
partagé, le chemin absolu ne peut plus être utilisé. En effet, celui-ci sera différent d’une machine
à l’autre.
Listing 25 – Exemple de code utilisant les classes Path, Paths dans l’objectif d’utiliser des fichiers
du projet. Attention, ce path ne sera peut-être plus valide en cas JAR.
Le listing 25 illustre une solution. En faisant appel à la méthode get() de classe Paths avec
une string vide, vous récupérez le chemin courant de votre projet. Il vous suffit alors de préciser le
chemin relatif.
6.1. LA NOMENCLATURE DES CLASSES 43
Exercice
Exécuter ce code sur votre machine. Préciser le chemin d’un fichier existant dans votre projet.
Tester-le sans faire appel à la méthode getCurrentPath(). Que se passe-t-il ?
Note
Dans la pratique, un dossier sera désigné pour contenir les fichiers du programme. Dans
certains cas, les chemins des fichiers seront introduits par l’utilisateur du programme. La
solution, proposée ici, permet l’utilisation facile de fichiers au sein du projet, et ce quelque
soit l’endroit où est placé le projet. Cette solution ne convient pas pour un déploiement en
production.
Lecture Écriture
Binaire OutputStream InputStream
Texte Reader Writer
Les préfixes sont dépendants de la nature de la classe. En effet, Java propose des classes de type
flux et filtre. En général, les classes de type filtre ajoutent des fonctionnalités sur le traitement
ou le stockage des données. Elles contiennent, par composition, une instance d’une classe de type
flux. Ces dernières possèdent la responsabilité des flux entrant et sortant.
Les préfixes des classes de type flux indiquent la destination des flux sortants ou la source des
flux entrants. Voici une liste des valeurs :
— ByteArray
— CharArray
— File
— Object
— Pipe
— String
1. stream en anglais
44 CHAPITRE 6. LES FICHIERS EN JAVA
Quant aux classes de type filtre, leur préfixe indique la fonctionnalité ajoutée par cette classe :
— Buffered
— Sequence
— Data
— LineNumber
— PushBack
— Print
— Object
Toutes les fonctionnalités n’existent pas pour tous les types de flux.
Un échantillon des classes proposées par le package Java.io sera présenté dans les sections
suivantes. Le lecteur est toujours invité à se référencer à la documentation officielle de Java [12]
pour avoir des informations complémentaires.
Figure 6.1 – Hiérarchie des classes de flux de lecture de texte en Java.[Réalisé en PlantUML [13]]
L’outil de base pour accéder à un fichier texte en lecture est un FileReader. Le listing 26
montre un exemple d’acquisition de la ressource de type fichier. La classe FileReader gère une
ressource de type fichier. Il est primordial de gérer celle-ci correctement par l’utilisation d’un
try-with-resources (voir Chapitre chapitre 3).
Lors de la création d’une instance de la classe FileReader, il n’est pas possible de fournir une
table de codage des caractères. Le choix sera fait par défaut. Le codage par défaut peut dépendre
de la JVM et de votre système d’exploitation. Cette approche n’est pas la plus optimale à l’heure
de faire un programme transportable sur différentes plateformes.
Conseil
Pour connaitre l’encodage d’un fichier texte, vous pouvez l’ouvrir dans NotePad++ et consul-
ter le menu Encodage.
6.2. FICHIERS TEXTES 45
Listing 26 – Exemple de code de lecture dans un fichier texte : gestion de la ressources FileReader.
Listing 27 – Exemple de code de lecture dans un fichier texte : seule l’interface proposée par la
classe abstraite Reader est utilisée. La méthode displayData lit le contenu du fichier pour l’afficher
dans la console.
46 CHAPITRE 6. LES FICHIERS EN JAVA
Exercice
Compiler et exécuter ce code sur votre machine. Que se passe-t-il si vous remplacez le fichier
lu par un fichier codé en UTF-8 ?
Note
NotePad++ permet aussi de créer des fichiers textes en choisissant la table d’encodage via
le menu Encodage.
La méthode ”int read(char[])” propose de lire des caractères dans le fichier et de les écrire dans
le tableau reçu en argument. La valeur de retour correspond au nombre de caractères lus. Si aucun
caractère n’a été lu, lorsque la fin du fichier est atteinte par exemple, cette valeur sera égale à zéro.
Nous pouvons donc utiliser une boucle basée sur ce critère pour lire un fichier.
La plupart du temps, les fichiers textes sont structurés en ligne. Chaque ligne correspond
alors à une chaine de caractères (String). Le listing 28 illustre la gestion de la ressource de type
BufferedReader. Le BufferedReader est ici créé à l’aide de la classe statique Files proposée par la
package Java.nio.
Listing 28 – Exemple de code de lecture dans un fichier texte : gestion de la ressources Buffere-
dReader.
Lors de la création d’un BufferedReader, la table d’encodage des caractères est précisée. Cette
solution est plus générique que celle proposée plus haut par la classe FileReader. Cette classe
permet de lire le fichier ligne par ligne comme illustré dans le listing 29.
6.2. FICHIERS TEXTES 47
Listing 29 – Exemple de code de lecture dans un fichier texte : l’interface proposée par la classe
BufferedReader est utilisée. Elle permet de lire le fichier ligne par ligne. La méthode displayData
lit le contenu du fichier pour l’afficher dans la console.
Exercice
Compiler et exécuter ce code sur votre machine. Que se passe-t-il si vous remplacez le fichier
lu par un fichier codé en ANSI ?
La méthode ”String readLine()” retourne la chaine de caractères lue dans le fichier. Si la fin du
fichier est atteinte, cette méthode retourne alors nulle. Il important de noter la présence d’un try-
catch. Celui-ci intercepte une exception du type : MalformedInputException. Celle-ci sera lancée
si le contenu du fichier ne trouve pas de correspondance dans la table d’encodage qui a été précisée
lors de la création du BufferedReader (voir listing 28).
6.2.2 Écriture
La figure 6.2 présente la hiérarchie des classes pour l’écriture de textes proposée par Java.
Figure 6.2 – Hiérarchie des classes de flux d’écriture de texte en Java.[Réalisé en PlantUML [13]]
48 CHAPITRE 6. LES FICHIERS EN JAVA
L’outil de base pour accéder à un fichier texte en lecture est un FileWriter. A l’instar du File-
Reader, la classe FileWriter encapsule la gestion d’une ressource de type fichier. Par conséquence,
son acquisition doit se faire à l’aide d’une ”try-with-resources” afin de garantir sa fermeture dans
tous les cas même les plus dégradés.
Listing 30 – Exemple de code d’écritures dans un fichier texte par l’utilisation de classe FileWriter
et de l’interface Writer.
Exercice
Compiler et exécuter ce code sur votre machine. Que se passe-t-il si vous remplacez le fichier lu
par un fichier codé en UTF-8 ? Quel est le comportement du programme si la table d’encodage
de lecture et écriture n’est pas la même ?
Le listing 30 présente un code faisant la copie d’un texte d’un fichier vers l’autre. Le texte
6.2. FICHIERS TEXTES 49
est converti en majuscule en même temps. La classe FileWriter propose la méthode ”void
write(char[])”. Cette dernière effectue l’écriture dans le fichier du tableau de caractères donné
en argument.
Parallèlement à la lecture, Java propose une classe qui permet l’écriture ligne par ligne dans un
fichier texte. La classe BufferedWriter propose cette fonctionnalité. Le listing 31 illustre le même
code que le listing 30 mais réalisé avec les classes Buffered*. La méthode ”void write(String)”
écrit la String donné en argument dans le fichier et ajoute un retour à la ligne.
Listing 31 – Exemple de code d’écritures dans un fichier texte par l’utilisation d’un BufferedWriter.
50 CHAPITRE 6. LES FICHIERS EN JAVA
Exercice
Compiler et exécuter ce code sur votre machine. Que se passe-t-il si vous remplacez le fichier lu
par un fichier codé en ANSI ? Quel est le comportement du programme si la table d’encodage
de lecture et écriture n’est pas la même ?
Figure 6.3 – Hiérarchie des classes de flux de lecture binaire proposées par Java.io.[Réalisé en
PlantUML [13]]
Figure 6.4 – Hiérarchie des classes de flux d’écriture binaire proposées par Java.io.[Réalisé en
PlantUML [13]]
Le nom de ces classes respecte la nomenclature présentée plus tôt dans de ce chapitre.
Exercice
Réaliser un code qui utilise les classes présentées dans les figures 6.3 et 6.4 pour lire et écrire
dans un fichier binaire.
La découverte de l’utilisation de ces classes est laissée au lecteur. Cette section va se concentrer
sur la présentation de la solution proposée par Java dans le package Java.nio.
6.3. FICHIERS BINAIRES 51
La figure 6.5 présente la hiérarchie des classes du package Java.nio pour la lecture et l’écriture
dans les fichiers binaires.
Figure 6.5 – Hiérarchie des classes de flux de lecture et d’écriture binaire proposées par
Java.nio.[Réalisé en PlantUML [13]]
Que faut-il retenir de ce schéma d’héritage ? Que les classes concrètes DatagramChannel, So-
cketChannel et FileChannel implémentent toutes les trois une même interface ByteChannel. Elles
représentent chacune un canal de byte dont la source et la destination change de nature : datagram,
socket et file. Les deux premières source/destination sont d’application dans les communications
réseaux. Dans un second temps, il est important de noter que l’interface ByteChannel étend trois
interface Channel, ReadableByteChannel et WritableByteChannel. Nous pouvons en conclure que
les classes concrètes implémentant l’interface ByteChannel seront utilisées à la fois pour la lecture
et l’écriture de données binaires.
Le listing 32 montre un exemple d’utilisation de la classe FileChannel. Le programme pro-
posé lit des nombres premiers dans un fichier texte (data.txt 2 et les écrit dans un fichier binaire
(out.bin). L’instance de la classe FileChannel est obtenue par un appel à la méthode statique ”new-
ByteChannel” de la classe Files. Le premier argument est le chemin du fichier (Path), construit à
l’aide de la classe Paths. Le second est une option qui correspond au mode d’ouverture en écriture
dans notre cas. Finalement, le troisième est autre option, celle-ci précise que si le fichier n’existe
pas, il faut le créer. Le lecteur est invité à consulter la documentation sur les options standards
d’ouverture de fichier en Java [14].
La lecture du fichier est réalisée à l’aide de la méthode readAllLines de classe Files. Cette
méthode pratique présente tout de même des dangers. Vous ne pouvez pas maı̂triser la taille
du fichier. Ce dernier peut être modifié par d’autres programmes ou par un utilisateur dans un
éditeur de texte. Si la taille du fichier est trop importante, vous risquez une erreur grave de la JVM
2. ce fichier contient les 45 premiers nombres premiers (un par ligne) au format UTF-8 : 2, 3, 5, 7, 11, 13, 17,
19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151,
157, 163, 167, 173, 179, 181, 191, 193, 197, 199
52 CHAPITRE 6. LES FICHIERS EN JAVA
5 try {
6 writeTo(Files.readAllLines(cheminFichierLu), cheminFichierEcrit);
7 } catch (MalformedInputException ex) {
8 System.out.println("The file is not enconded in the expected charset.");
9 } catch (IOException ex) {
10 System.out.println("The specific path is inaccessible");
11 ex.printStackTrace(System.err);
12 }
13 System.out.println("Goodbye!");
14 }
15
La lecture des octets est réalisée à l’aide d’un ByteBuffer [15]. Cette classe propose la gestion
d’un buffer d’octets en lecture et en écriture. Lors de sa création via la méthode allocate, la taille
6.3. FICHIERS BINAIRES 53
du buffer doit être précisée. Pour chaque ligne, la méthode parseInt de la classe Integer permet
de convertir une String en int. Attention cette opération peut lancer une exception, NumberFor-
matException, dans le cas où la String ne représente pas un entier. La méthode putInt permet
d’écrire un entier (int) dans le buffer et d’avancer la tête d’écriture. La méthode position permet
de déplacer la tête de lecture/écriture. Après l’écriture, la tête est replacée au début pour la lecture
du buffer afin de l’écrire dans le fichier binaire.
Exercice
Compiler et exécuter le code du listing 32 sur votre machine. Le fichier binaire et le fichier
texte contiennent les mêmes données, ont-ils la même taille ? Pourquoi ? Changer les options
d’ouverture (supprimer le CREATE quand le fichier n’existe pas, remplacer le CREATE par
APPEND quand le fichier existe ou pas) et observer ce qui se passe.
Le listing 33 propose un programme qui va lire et afficher à l’écran le contenu du fichier créé par
le programme du listing reflst :Java :WriteBinaryFile. Lors de la création de l’instance de la classe
FileChannel, l’option d’ouverture est READ pour ouvrir le fichier en mode lecture. La lecture se
fait à l’aide de la méthode read via l’utilisation d’une ByteBuffer. La méthode read remplit le
buffer tant qu’il y a des octets à lire dans le fichier et tant qu’il y a de la place libre dans le buffer.
Elle retourne le nombre d’octets lus et placés dans le buffer.
Exercice
Compiler et exécuter le code du listing 33 sur votre machine. Que se passe-t-il si vous modifiez
le fichier out.bin à la main dans NotePad++ ? Que se passe-t-il si le fichier n’existe pas ?
Les fichiers binaires conviennent particulièrement bien à une organisation relative des données.
Le fichier créé et lu dans les exemples ci-dessus est un exemple d’organisation relative. En effet, le
fichier contient une liste d’entiers. Or les entiers, quelle que soit leur valeur, sont représentés sur
4 octets. Nous pouvons dès lors consulter les octets correspondants à un entier particulier sans
devoir lire l’entièreté du fichier. Le listing 34 présente une méthode qui permet de lire un n-ième
nombre premier dans le fichier out.bin et de retourner sa valeur. L’appel à la méthode position
sur le FileChannel canal permet de position la ”tête” de lecture du fichier à l’instar de la méthode
position de la classe ByteBuffer.
Exercice
A l’aide de cette méthode, faites un programme qui demande à l’utilisateur le quantième
nombre premier il vaut connaitre. Gérer tous les cas d’erreur possible.
Chapitre 7
Les fichiers en C#
Le language C# a beaucoup de similitudes avec Java. Néanmoins, comme pour la gestion des
ressources, la solution proposée pour la gestion des flux de données est différente.
Figure 7.1 – Liens d’héritage des classes concrètes de Streams.[Réalisé en PlantUML [13]]
Cette solution offre uniquement les bases pour la manipulation d’octets. L’ensemble de ces
classes gère une ressource externe. Elles implémentent l’interface IDisposable pour faciliter la ges-
tion de la fermeture de leur ressource respective.
Afin d’ajouter des fonctionnalités à ces classes, C# utilise le design pattern décorateur (voir
appendix B). Parmi ceux-ci nous pouvons trouver :
55
56 CHAPITRE 7. LES FICHIERS EN C#
Figure 7.2 – Schéma de classe des décorateurs de la classe Stream.[Réalisé en PlantUML [13]]
L’interface proposée par la classe Stream reste sommaire. Le langage C# propose donc une
série d’interfaces plus faciles à utiliser (”user-friendly”). Ces nouvelles classes sont créées à l’aide
du design pattern Adaptateur (voir appendix C). Par ce biais, nous pouvons accéder à des
interfaces pour lire et écrire des caractères (au lieu des bytes) : TextReader et TextWriter. La
classe StreamReader adapte la classe FileStream pour répondre à l’interface de la classe abstraite
TextReader. La figure 7.3 illustre le schéma de l’adaptateur pour la classe StreamReader.
Les adaptateurs présentés ci-dessus peuvent aussi adapter une instance de la classe FileStream
décorée par un des décorateurs décrit plus tôt dans ce chapitre. La figure 7.4 présente le schéma
d’architecture qui englobe les trois catégories présentées ici : streams de bases, décorateur et
adaptateurs.
Figure 7.4 – Schéma d’architecture des classes de la famille Stream [25, 26].
Afin de faciliter la création d’instances de classe Stream, le langage propose la classe File [27].
Celle-ci propose des méthodes statiques qui aident à la création de streams.
Listing 35 – Exemple de code pour la création d’une instance de la classe /it FileStream.
58 CHAPITRE 7. LES FICHIERS EN C#
Conseil
Lors de l’écriture de vos chemins en C#, si vous placez le caractère ’@’ avant la chaine de
caractères, les symboles ’\’ ne seront plus interprétés. Ils ne devront dès lors plus être doublés.
Le constructeur utilisé dans cette classe reçoit trois arguments : le chemin du fichier (ou path
en anglais) et le mode d’ouverture du fichier et le type d’accès. Il existe plusieurs valeurs possibles
pour le mode d’ouverture :
— Append : en écriture, pour ajouter des données à la fin du fichier. Si celui-ci n’existe pas,
il sera créé.
— Create : demande la création du fichier. S’il existe, il sera écrasé.
— CreateNew : demande la création du fichier. S’il existe, une exception (IOException) sera
lancée.
— Open : ouvre un fichier existant. Si le fichier n’existe pas, une exception (FileNotFoundEx-
ception) sera lancée.
— Truncate : ouverture d’un fichier en écriture. Si le fichier existe et contient des données,
celles-ci seront effacées.
Les valeurs pour les types d’accès sont : Read (pour la lecture seule), Write (pour l’écriture seule)
et ReadWrite (pour un accès en écriture et lecture simultanée).
Listing 36 – Exemple de code pour la création d’une instance de la classe FileStream décorée et
adaptée.
Le fileStream créé est décoré d’un cryptoStream qui ajoute la fonctionnalité de cryptage avant
l’écriture dans le fichier. Notre fileStream décoré est ensuite adapté à l’aide d’un streamWriter. Ce
dernier nous permet d’écrire facilement des données sous forme de String ou char. Lorsque le client
demande d’écrire une chaine de caractère à l’aide de notre streamWriter, ce dernier convertit la
string en tableau d’octets pour pouvoir la transmettre à un stream. Dans note cas, ce stream est
le crypto. Celui-ci va encrypter les octets pour les transférer au stream suivant : notre fileStream.
Ce dernier effectuera l’écriture dans le fichier des octets reçus.
7.2. CRÉER UN STREAM 59
Exercice
Compléter pour écrire une phrase encodée par l’utilisateur, compiler et exécuter ce code du
listing 36 sur votre machine. Analyser le fichier de sortie. Faire le programme opposé qui sera
capable de lire le fichier et d’afficher la phrase.
Conseil
Consulter la documentation de la classe File [27]. Cette dernière propose des méthodes sta-
tiques pour faciliter la création des streams.
60 CHAPITRE 7. LES FICHIERS EN C#
Listing 37 – Exemple de code pour l’écriture et la lecture de fichiers binaires. (Veiller à supprimer
le symbole ’\’ devant ’$’)
La deuxième partie du programme propose de lire le fichier créé précédemment. Cette étape
est réalisée à l’aide de la méthode Read. A l’instar de la méthode Write, cette dernière prend
trois arguments : tableau, offset et count. Il peut y avoir des cas où le fichier ne contient plus le
nombre d’octets demandés. Pour répondre à ce problème, la méthode retourne le nombre d’octets
en provenance du fichier et placés dans le tableau reçu en argument.
Exercice
Compiler et exécuter ce code du listing 37 sur votre machine. Analyser le fichier créer. Modifier
les modes d’ouverture et observer les différents comportements.
7.4. FICHIERS TEXTES 61
Listing 38 – Exemple de code pour l’écriture et la lecture de fichiers textes. (Veiller à supprimer
le symbole ’\’ devant ’$’)
Les méthodes Write et ReadLine proposent une interface utilisant directement des chaines de
caractères. Les adaptateurs s’occupent de la conversion de ces dernières en tableau d’octets pour
le fileStream.
Exercice
Compiler et exécuter ce code du listing 38 sur votre machine. Analyser le fichier créer. Modifier
les modes d’ouverture et observer les différents comportements.
Exercices récapitulatifs
Réaliser les programmes proposés dans le chapitre Java aux listings 32, 33 et 34 en C# en
utilisant les streams adéquats.
62 CHAPITRE 7. LES FICHIERS EN C#
Chapitre 8
Architecture
En programmation, il est très important de diminuer les dépendances. Au sein d’un projet,
une dépendance sera ajoutée si elle est strictement nécessaire. Au-delà, de la présence de module
supplémentaire, quand une dépendance est inévitable, tout sera fait pour l’affaiblir. Comment ? La
plupart du temps, l’utilisation d’interface et/ou d’un design pattern adéquat permettra de limiter
l’importance de la dépendance. Cette approche rend le code plus modulaire.
Les chapitres précédents sont consacrés à la sauvegarde et la lecture de données dans des fichiers
de diverses natures. L’utilisation de cette technique dans un projet ajoute un nouveau module.
Il faut rendre le reste du projet le moins dépendant possible de l’implémentation concrète de ce
nouveau module.
Mise en situation
Vous êtes développeur sur un programme qui gère ses sauvegardes dans des fichiers XML. Le
product manager vient avec une requête de gestion de fichiers JSON, sur demande de clients.
Néanmoins, il faut conserver la compatibilité avec les fichiers XML pour les autres clients.
Que faire ?
8.1 Repository
Le design pattern Repository est une réponse à la problématique décrite dans la ”Mise en
situation”. Ce dernier isole, à l’aide d’une interface, le reste du programme de l’implémentation
concrète de la gestion de la ressource utile à la sauvegarde des données.
63
64 CHAPITRE 8. ARCHITECTURE
Figure 8.1 – Exemple d’un schéma d’architecture des classes d’un Repository.
Dans un premier temps, l’utilisation d’outils de sérialisation ne remet pas en cause la mise en
place du Repository. Des adaptations complémentaires seront néanmoins nécessaire. Les précautions
à prendre vont dépendre du contexte dans lequel la sérialisation et la sauvegarde sont effectuées.
Soit la sauvegarde est destinée strictement au programme que nous développons, dans ce cas nous
parlerons d’intra-programme ; soit les sauvegardes peuvent être lue et modifiées par d’autres ap-
plications, il s’agit alors de sauvegarde inter-programme.
8.2. LIBRAIRIES ORIENTÉES OBJETS 65
8.2.1 Inter-programme
Si plusieurs applications peuvent lire et écrire les données utilisées par notre programme, la
structure des données dans le fichier sera alors définie indépendamment de notre méthode de sau-
vegarde. Cette approche permet de garantir la communication entre les applications.
Le choix d’architecture doit prendre en compte les futures évolutions du code de notre pro-
gramme ainsi que les potentielles changements de la structure de données échangée. Dans un tel
contexte, il est impensable de sérialiser directement les objets qui sont les instances de nos classes
”domain”. La solution consiste à définir des classes les plus simples possibles qui seront destinées à
être (de)sérialisées par notre librairies. Ces classes seront alors construites en fonction de la struc-
ture de données fixées. Elles sont nommées Data Transfer Object (DTO). Un outil supplémentaire
doit alors être développé pour effectuer la correspondance entre les classes ”domain” et les DTOs.
8.2.2 Intra-programme
Notre application est la seule à écrire et lire nos structures de données sauvegardée. Cette ap-
proche lève un certain nombre de contraintes. De fait, nous sommes libres de la définition de la
structure de données. Néanmoins, il subsiste des défis.
Un grand nombre d’application ont une longue vie couvrant plus d’une décennie. Durant ces
années, le code subit un grand nombre de modification. Afin de rendre le code indépendant de
la sauvegarde et réciproquement, les classes ”domain” ne peuvent pas être directement sérialisées
par un outil. Il est aussi possible que votre application exige la rétro-compatibilité avec les anciens
fichiers de sauvegarde.
Autant d’événements qui exige d’affaiblir au maximum la dépendance. Il est alors inévitable de
mettre en place une solution similaire à celle liée à la problématique inter-programme. Néanmoins,
les classes DTOs seront ici développée en accord avec les classes ”domain”.
Figure 8.2 – Illustration de la mise en place d’un mapper au sain d’un repository.
La figure 8.2 illustre un schéma de sauvegarde (orange) et restauration (vert) des données.
Au niveau de l’application, les données sont stockés dans les objets du ”domain”. Le ”Mapper”
effectue une conversion de ces objets en DTOs. Les DTOs sont ensuite sérialisé par une librairie en
octets (binary) ou caractères (composant un format standard JSON, XML, ...). Ces données sont
66 CHAPITRE 8. ARCHITECTURE
alors enregistrées dans un fichier à l’aide des outils du langages qui effectue des demandes auprès
du système de fichier rendu disponible par le système d’exploitation.
Le chargement des données suit le chemin opposé. Les données ”brutes” sont lues depuis un
fichier. Elles sont désérialisées en DTOs. Le ”Mapper” effectue alors la conversion en objets du
”domain”.
Cette solution permet de limiter les modifications au ”Mapper”. De fait, si vous modifier les
classes ”domain”, une adaptation du ”Mapper” permettra de continuer de fonctionner avec les
mêmes DTOs et donc les mêmes fichiers. Dans le cas d’une modification externe des fichiers, les
DTOs devront être ajuster ainsi que le Mapper. Nous obtenons alors une solution qui protège
les les classes ”domain” de toutes modifications non initier par le développement de l’application
elle-même.
Chapitre 9
L’utilisation d’une base de données dans une application est utile à plusieurs points de vue.
Lorsqu’une application manipule une grande quantité de données, il devient complexe de gérer
les fichiers sous-jacents et leurs liens. Déléguer la gestion d’une structure complexe de données à
une base de données, c’est l’utiliser pour ce pour quoi elles ont été inventées. Une fonctionnalité,
de plus en plus fréquente, est de mutualiser les données entre plusieurs applications. L’utilisation
d’une base de données distante permet son accès multiple simultané. Une telle fonctionnalité est
très ardue à mettre en œuvre avec des fichiers.
Développons notre raisonnement au travers d’un exemple.
Figure 9.1 – Exemple d’un schéma d’Architecture Orienté Service avec une base de données [29].
Supposons que nous développons une application de comptabilité selon une architecture orientée
service, comme illustré sur la figure 9.1. Par le principe d’architecture, nous allons déléguer la sau-
67
68 CHAPITRE 9. INTÉGRATION D’UNE BD DANS UNE APPLICATION OO
vegarde de nos données à un service : une base de données. D’autres applications exécutées sur
d’autres machines ou serveurs peuvent alors utiliser ces mêmes données. Néanmoins, la structure
de la base de données a été élaborée pour répondre au besoin de l’application de comptabilité.
Les autres applications sont alors dépendantes de cette structure. De plus, toutes modifications
de cette base de données peuvent alors engendrer des modifications dans toutes les applications
clientes. Pour affaiblir cette dépendance, une application (backend) spécifique est créée pour inter-
facer l’accès à la base de données des applications dites secondaires. Ces dernières dépendent alors
de l’API de l’émissaire et plus de la structure de données du logiciel de comptabilité.
Certes nous déléguons la gestion de la sauvegarde des données à un service de base de données.
Néanmoins, il faut construire un schéma de base de données. De plus, un lien entre les données
contenues dans les tables de la base de données et celles contenues dans les instances de classes de
notre application doit être établi.
Une structure de données ne se résume pas à une liste de classes ou tables indépendantes
entre elles. Il existe des associations entre les tables (BD) et les classes (OO). La nature de ses
associations diffère entre BD et OO. En OO, il existe deux types d’associations : l’héritage et la
composition, illustrées sur la figure 9.2. Une relation d’héritage est une notion d’”être”, la classe
fille est considérée comme étant aussi la classe mère (celle dont elle l’hérite). Une relation de
composition établit une association de type ”contenir”. La composition a lieu lorsqu’une classe
contient un attribut qui est une instance d’une autre classe.
Figure 9.2 – Exemples d’associations ; de gauche à droite : l’héritage (OO), la composition (OO),
la clé étrangère (BD).
9.1. ORIENTÉ OBJET VS BASE DE DONNÉES 69
En BD, il n’existe qu’un seul type d’association : la clé étrangère, illustrée sur la figure 9.2.
Cette dernière contient, la plupart du temps, la valeur de la clé primaire.
9.1.1 Héritage
La construction de la conversion des associations va être établie en partant du code (OO). Cette
approche est appelée ”Code First”. La première association à convertir est l’héritage. Il existe trois
stratégies pour transposer une hiérarchie de classes en tables et leurs associations :
— Créer une seule table pour toute la hiérarchie ;
— Prévoir une table par classe concrète ;
— Prévoir une table par classe (stratégie des jointures).
Analysons ces trois approches au travers d’un exemple. Une hiérarchie de classes est présentée
sur la figure 9.3. Nous avons la classe de base Player, et des classes qui héritent directement ou
pas de celle-ci. Chaque classe fille possède un attribut propre.
La première approche nous amène à concevoir une seule table pour représenter toutes les classes.
La solution est présentée dans la figure 9.4. Cette dernière contient alors tous les attributs de toutes
les classes réunies. Lors de la conversion d’un objet (OO) en tuple (BD), nous avons un certain
nombre de champs inutiles, ce qui n’est jamais une bonne chose. Dans le sens inverse, lors de la
conversion d’un tuple en objet, il faut répondre à la question suivante : le tuple correspond à un
objet de quelle classe ? Il impossible de répondre à cette question sauf en ajoutant un attribut
supplémentaire sans la table (type). Ce dernier permet d’identifier la nature exacte de l’objet
enregistré. Imaginons que nous souhaitions ajouter une classe GoalKeeper qui hérite de la classe
Footballer. Cette dernière possèderait un attribut cleanSheetCount. Afin de pouvoir enregistrer
ces nouveaux objets en base de données, nous allons être obligés de modifier une table existante.
70 CHAPITRE 9. INTÉGRATION D’UNE BD DANS UNE APPLICATION OO
Cet exemple est une violation typique du principe OCP (Open Close Principle). Néanmoins, cette
solution offre de bonne performance à l’exécution.
Figure 9.4 – Illustration de la table obtenue si une seule classe est utilisée une structure d’héritage
de classes (figure 9.3).
La seconde solution préconise la conception d’une table pour chaque classe concrète. La figure
9.5 présente les tables obtenues. Nous avons supposé que la classe P layer est abstraite. Nous pou-
vons immédiatement observer que l’attribut name est présent dans chaque table. Cette répétition
est en désaccord avec le principe DRY (Don’t Repeat Yourself). Cette approche permet de respec-
ter le principe OCP. En effet, dans le cas de l’ajout de notre nouvelle classe GoalKeeper, il suffira
d’ajouter une nouvelle table. La présence de plusieurs tables n’influencera pas de façon significative
les performances par rapport à la première solution.
Figure 9.5 – Illustration des tables obtenues si une classe est utilisée pour chaque classe concrète
d’une structure d’héritage de classes (figure 9.3).
La dernière approche propose une table par classe. Les tables sont décrites dans la figure 9.6.
Il apparaı̂t une table supplémentaire P layer. Cette dernière contient l’attribut name et permet
donc d’éviter sa répétition dans chaque table. L’attribut battingAverage n’est plus répété entre
les tables Cricketer et Bowler. Le principe DRY est alors respecté. Cette séparation des données
entre les tables augmente leur nombre et impose l’utilisation de jointures. En conséquence, les
performances de cette solution sont moindres que les précédentes.
9.1. ORIENTÉ OBJET VS BASE DE DONNÉES 71
Figure 9.6 – Illustration des tables obtenues si une classe est utilisée pour chaque classe d’une
structure d’héritage de classes (figure 9.3).
La table 9.1 résume l’analyse des trois solutions. Il est évident qu’il n’existe de pas de solu-
tion parfaite. Des concessions devront être faites quelle que soit l’approche sélectionnée lors du
développement d’une application.
Table 9.1 – Résumé des solutions de conversion d’une hiérarchie de classes en tables.
72 CHAPITRE 9. INTÉGRATION D’UNE BD DANS UNE APPLICATION OO
9.1.2 Composition
Les classes peuvent s’associer par composition. Cette solution est d’ailleurs souvent prônée par
les patrons de conceptions (Design Patterns). La manière de convertir ces dernières en associa-
tions entre tables dépendra de leurs caractéristiques. En effet, les compostions en orientées objets
présentent plusieurs propriétés listées avec leur valeur dans la table 9.2.
Propriété Valeur
Direction unique
double
Cardinalité un pour un
un pour plusieurs
plusieurs pour plusieurs
Obligation Optionnel
Requise
Intensité Faible
Forte
Toutes les compositions vont se transformer en clé étrangère. Ces dernières peuvent se trouver
dans une des tables, ou dans une tierce table. Elles peuvent être optionnelles ou obligatoires. Ces
différentes propriétés vont être mises en correspondance avec celles des associations. Les différentes
propriétés des compositions doivent être traitées dans un certain ordre de priorité. La prise de
décision se présente alors sous la forme d’un arbre illustré sur la figure 9.7. L’objectif est de
convertir dans un schéma de base de données qui respecte les formes normales I, II et III.
La première propriété qui est analysée est la cardinalité. En fonction de sa valeur, la localisation
des clés étrangères sera décidée. Nous avons alors trois branches :
— M anyT oM any implique la création d’une table tierce pour y associer les clés étrangères
des deux tables de base. De fait, les clés étrangères sont des attributs et ceux-ci doivent être
mono-valués. Ces dernières ne peuvent donc être définies dans les tables initiales.
— OneT oM any génère une clé dans la table correspondante au One.
— OnetoOne place les clés dans l’une, l’autre ou les deux tables.
La direction sera utile dans le cas de cardinalité OneT oOne. Elle permettra de préciser la
localisation des clés étrangères parmi les possibilités restantes.
— Unidrectionnelle signifie qu’un objet connaı̂t l’autre mais pas l’inverse. Une clé étrangère
sera alors placée dans la table correspondante à la classe qui connaı̂t.
— Bidirectionnelle induit une connaissance bilatérale. Une clé étrangère sera alors placée dans
chaque table.
Le caractère d’obligation de la composition va imposer des propriétés à la clé étrangère. Si
celle-ci est contenue dans une des tables de base. En effet, dans le cas de la présence d’une table
9.1. ORIENTÉ OBJET VS BASE DE DONNÉES 73
Faire la conversion des associations présentées dans la figure 9.8 à l’aide de l’arbre décisionnel
présenté dans la figure 9.7
Conseil
Il est possible que certains cas ne soit pas traités ici. Vous avez néanmoins suffisamment
d’informations pour déterminer comment cette conversion doit être réalisée.
74 CHAPITRE 9. INTÉGRATION D’UNE BD DANS UNE APPLICATION OO
Figure 9.7 – Arbre de décision pour la conversion d’une composition (OO) en association de clé
étrangère (BD).
9.2. OUTILS 75
9.2 Outils
L’intégration d’une base de données dans une application de type bureau sera effectuée à l’aide
d’outils. Il est évident que ceux-ci vont dépendre du langage. Néanmoins, il existe deux familles
d’outils : les solutions orientées relationnelles et celles orientées objets.
Les solutions orientées relationnelles dialoguent avec des requêtes SQL. Le programmeur est
alors amené à concevoir les requêtes SQL et doit traiter les résultats en retour. Cette solution est
facile à mettre en place et propose de bonne performance.
La figure 9.9 présente le schéma de fonctionnement qui est commun à tous les outils. Avant
toute chose, l’application doit se connecter au SGBD 1 . La connexion acquise, l’application peut
envoyer des requêtes SQL au SGBD. Ce dernier après les avoir traitées via la base de données
retournera le résultat à l’application. Dernière étape et pas des moindres, l’application doit se
déconnecter du serveur SGBD quand elle n’en plus besoin ou lors de sa fermeture.
1. Service de Gestion de Base de Données
76 CHAPITRE 9. INTÉGRATION D’UNE BD DANS UNE APPLICATION OO
Figure 9.9 – Schéma de fonctionnement d’un outil d’intégration de base de données dans une
application.
Différentes libraires utiles à l’intégration des bases de données sont présentées dans la figure
9.10. Cette liste ne se veut pas exhaustive. Dans la suite de ce syllabus, nous nous concentrerons
sur les solutions orientées relationnelles. Les deux prochains chapitres décriront la mise en œuvre
et l’utilisation de ces outils en Java (JDBC) et en C# (ADO.NET).
Figure 9.10 – Présentation des librairies pour chaque type d’outil d’intégration de base de
données.
Remarque
Quelques soit la nature de l’outil utilisé, il vous faudra être attentif à : (i) accéder à la base
de données en temps utile, (ii) mettre en place des caches et (iii) temporiser les écritures.
Chapitre 10
En Java, la libraire la plus commune pour une solution orientée objets est : Java DataBase
Connectivity (JDBC). Cette librairie propose une API standard qui unifie la manière de dialoguer
entre une application Java et un SGBD relationnelle. Ce chapitre est dédié à la configuration et
l’utilisation de cette libraire. La librairie JDBC n’est pas disponible par défaut dans le langage
Java. Il faut donc configurer le projet pour ajouter une dépendance vers une libraire externe.
10.1 Configurer
Pour gérer les dépendances des projets en Java, il existe différents outils. Les deux plus popu-
laires sont Maven [30] et Gradle [31]. La grande majorité des IDE Java intègrent ses deux outils.
La suite de ce chapitre sera centré sur l’utilisation de Gradle.
Votre IDE préféré présente un menu permettant la création d’un projet Gradle. Votre projet
contient alors un fichier nommé Build.gradle. Ce fichier contient toutes les informations de configu-
ration de votre projet Java. Le listing 39 présente un extrait de ce que nous pouvons trouver dans
le fichier Build.gradle. Plus précisément, il illustre comment fixer la version de Java (à Java 10 dans
ce cas). De cette manière, lors de l’importation du projet, celui-ci sera toujours pour fonctionner
avec cette version de Java.
1 Java {
2 sourceCompatibility = JavaVersion.VERSION_1_10
3 targetCompatibility = JavaVersion.VERSION_1_10
4 }
La gestion des dépendances est évidemment plus complexe. Dans un premier temps, il faut
préciser à quel endroit gradle va trouver les librairies. Cet endroit est appelé Repository. Le listing
77
78 CHAPITRE 10. JAVA DATABASE CONNECTIVITY
40 montre comment le Repository est défini. Il possible d’utiliser des Repository Maven dans un
projet Gradle.
1 repositories {
2 // Use jcenter for resolving dependencies.
3 // You can declare any Maven/Ivy/file Repository here.
4 jcenter()
5 }
Maintenant que le Repository est défini, il est possible de préciser les libraires dont notre projet
aura besoin pour fonctionner. Le listing 41 est un exemple de dépendance. Les deux premières
dépendances concernent JUnit. Les mots-clés devant précise pour quel type de tâche la dépencance
sera utile. Les trois dernières sont le cœur de notre chapitre. Pour communiquer avec une base
de données distante, le protocole http sera nécessaire. Au-delà du protocole de communication,
la contenu des requêtes sera dépendant du SGBD : dans notre cas MySQL. En dernier lieu, la
dépendance Derby permet de se connecter à une base données local pour les tests 1 .
1 dependencies {
2 // Use JUnit Jupiter API for testing.
3 testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
4
Exercices
Il est laissé en exercice au lecteur de créer et configurer un projet Maven avec les mêmes
dépendances.
1. Attention de synchroniser les versions des drivers entre votre projet Java et DataGrip
10.1. CONFIGURER 79
La création d’une connexion à la base de données sera la responsabilité d’une classe. Cette
dernière doit charger le driver correspondant au SGBD. Cette opération est généralement effectuée
dans le constructeur. Cette opération est réalisée via l’appel à la méthode ”forName” comme
illustré dans le listing 42, pour un SGBD MySQL 2 .
1 try {
2 Class.forName("com.mysql.cj.jdbc.Driver");
3 }catch(ClassNotFoundException ex) {
4 System.out.println("Driver not found");
5 }
Listing 42 – Code à exécuter pour charger le driver. L’exemple correspond au driver MySQL.
Conseil
Soyez attentif à la gestion des exceptions. Que faire si le driver n’est pas trouvé ? La réponse
dépend de l’importance de celui-ci dans le fonctionnement global de votre application.
La classe qui a chargé le driver est maintenant capable de créer des connexions à l’aide de ce
dernier.
Remarque
La connexion à une base de données est une ressource externe. Elle doit donc être gérée avec
les précautions d’usage (voir chapitre 3.)
10.2 Storage
Le Storage est, pour les bases de données, le pendant du Repository pour les fichiers. Littéralement
en anglais, le Repository représente le lieu de stockage quand le Storage est l’action, la méthode
pour enregistrer les données.
De la même manière que pour les fichiers, la gestion des accès sera isolée dans une classe
du type SQLDataStorage. Comme pour les fichiers, la connexion ne devra pas rester ouverte en
permanence. Afin de limiter les responsabilités de la classe SQLDataStorage, la construction de la
connexion sera déléguée à une classe SQLDataStorageF actory. La figure 10.1 présente les classes
ainsi définies.
Un Statement permet d’exécuter des requêtes SQL simples et non paramétrables. Pour rendre
les requêtes SQL plus souples, le P reparedStatement (qui dérive de Statement) permet de pa-
ramétrer la requête SQL. Le CallableStatement dérive du P reparedStatement et permet en plus
l’appel à des procédures stockées.
Remarque
La facilité tend à paramétrer les requêtes par concaténation de String. Cela est strictement
interdit ! ! Cette solution permet l’injection SQL et ouvre donc des failles dans votre solution.
Le P reparedStatement doit être utilisé pour paramétrer une requête.
Pour exécuter une requête SQL avec un statement, on transmet la requête sous la forme d’un
String à la méthode executeU pdate(String). La valeur en retour dépendra de la nature de la
requête. Ce sera le sujet des sections suivantes. La requête, sous forme de string, est donnée dès la
création d’un P reparedStatement. L’appel aux méthodes setString(int, String), setInt(int, int),
setDouble(intdouble),... permet de remplacer les points d’interrogation de la String qui représente
la requête par des valeurs. Le premier argument correspond à l’index du paramètre (point d’in-
terrogation), en commençant à 1 pour le premier dans l’ordre d’apparition dans la chaine de
caractère. L’exécution se fera aussi par l’appel à la méthode executeU pdate() sans argument.
La figure figure :JDBC :StatementVSPrepared présente un tableau comparatif de Statement et
P reparedStatement.
82 CHAPITRE 10. JAVA DATABASE CONNECTIVITY
Figure 10.2 – Tableau comparatif entre les classes Statement et P reparedStatement [35].
10.4 Modifications
En base de données, il existe deux types de modifications : (i) celle qui concerne la structure
de la base de données et qui regroupe les requêtes SQL qui appartiennent au DDL 4 . En d’autres
mots, toutes les requêtes qui modifient la structure de la base de données : CREAT E, DROP ,
ALT ER, T RU N CAT E. (ii) celle qui concerne la modification des données contenues dans la base
de données telle que IN SERT , DELET E et U P DAT E. Ces dernières font partie du DML 5 .
Lors de l’utilisation de ces requêtes, la base de données est modifiée. Afin de s’assurer de la
cohérence de la base de données, les requêtes doivent être réalisées à l’aide d’une transaction.
10.4.1 Transaction
Une transaction est une séquence d’opérations faisant passer une base de données d’un état A,
consistant, à un autre état B, également consistant, ou qui laisse la base de données dans l’état
initial A.
Habituellement, les transactions sont gérées automatiquement par la connexion fournie par la
libraire JDBC. Un mode manuel de transaction doit être activé pour effectuer une transaction.
Attention de bien désactiver celui-ci après.
Le listing 45 présente une implémentation de transaction avec JDBC en Java. Une transaction a
pour objectif de temporiser un ensemble de requêtes afin de les appliquer simultanément à la base de
données. Après l’activation de la gestion manuelle via la commande ”connection.setAutoCommit(f alse)”,
toutes les requêtes sont réalisées. L’appel à ”connection.commit()” permet de les envoyer toutes
en même temps. Si une requête échoue, une exception du type SQLException est lancée. A la
réception de celle-ci, nous devons annuler la totalité des requêtes pour garantir la cohérence de
la base de données. Cette action est réalisée par l’appel à la méthode ”connection.rollback()”.
Une exception est lancée après le rollback afin d’avertir l’utilisateur de l’échec de la procédure.
La réactivation du mode automatique des transactions est faite dans le bloc f inally afin d’être
exécuté à coup sûr.
La transaction assure la cohérence de la base de données. Mais elle se montre utile pour le
programme lui-même. Lors de l’insertion d’un grand nombre de tuples dans différentes tables, si
une requête échoue, comment savoir laquelle ? Si les mêmes requêtes sont relancées, elle échouera
si elles ont été réalisées lors de la première tentative. L’utilisation de la transaction permet donc
de toutes les annuler, d’avertir l’utilisateur du programme pour qu’il corrige le problème et qu’il
puisse essayer à nouveau d’importer les données.
84 CHAPITRE 10. JAVA DATABASE CONNECTIVITY
Remarque
Les transactions doivent être utilisées dès que plusieurs requêtes de modifications sont
réalisées. Il n’est pas efficace et utile de séparer les requêtes pour les exécuter une à une.
Cela ne résout en rien les problèmes de cohérence de la base de données ou du programme.
Le listing 46 illustre un exemple d’une requête CREAT E à l’aide d’un Statement. La méthode
”executeU pdate” retourne un zéro si la requête faisait partie du groupe DDL.
Il est possible de faire des requêtes paramétrées très pratiques dans le cas d’IN SERT . Le
listing est un exemple de l’utilisation d’un P reparedStatement pour un IN SERT . Au-delà de
la paramétrisation de la requête, celle-ci permet de récupérer les clés. Ces dernières sont les
identifiants des tuples dans la base de données. Ils peuvent s’avérer utile pour faire la cor-
respondance entre les objets en mémoire du programme et les tuples correspondants dans la
base de données. Pour ce faire, un paramètre supplémentaire est ajouté lors de la création du
P reparedStatement : Statement.RET U RN GEN ERAT ED KEY S. Si la requête se déroule
correctement, la clé générée est récupérée et placée en correspondance avec l’objet (p) concerné.
10.5. LIRE DES DONNÉES 85
Listing 47 – Exemple d’implémentation d’une requête de modification avec récupération des clés
à l’aide d’une P reparedStatement.
Listing 48 – Exemple de récupération d’un ResultSet lors de l’exécution d’une requête SELECT.
Exercices
Adapter le code du listing 48 pour qu’il gère correctement le ResultSet. Rechercher les
différents types d’exceptions qu’il peut lancer et pourquoi.
Lorsque vous réalisez une requête SELECT dans un programme client d’un SGBD. Ce dernier
vous affiche un tableau en retour (Figure 10.3). Chaque colonne correspond à une donnée demandée
dans le SELECT et chaque ligne aux valeurs associées correspondantes dans les tuples de la base
de données.
Le ResultSet propose la méthode next() pour se déplacer de ligne en ligne dans ce tableau.
Au départ, le ResultSet n’est pas placé sur la première ligne, car celle-ci peut ne pas exister. Il
faut donc appeler next() avant l’accès aux colonnes. La méthode retourne false s’il n’y a plus ou
pas de ligne. Une boucle while se prête donc parfaitement à ce cas. L’accès aux colonnes se fera
par une méthode get qui précisera le type = Int, String, Double, Date, ... Chaque méthode prend
en argument un identifiant de colonne. Les méthodes sont surchargées pour proposer deux types
d’identifiant. Il est possible d’identifier les colonnes à l’aide de leur nom comme illustré sur le listing
49. Il est obligatoire de les avoir renommées dans la requête à l’aide du mot-clé AS. L’identification
de la colonne peut être faite avec un numéro qui correspond à son ordre d’implémentation dans la
requête en commençant par 1 (voir listing 50).
10.5. LIRE DES DONNÉES 87
1 while(rs.next()) {
2 //Process the current row in rs
3 int personID = res.getInt("person_id");
4 String firstName = res.getString("first_name");
5 String lastName = res.getString("last_name");
6 Java.sql.Date dob = rs.getDate("dob");
7 double income = rs.getDouble("income");
8 // Do Something with column values
9 }
Listing 49 – Exemple de récupération d’un ResultSet lors de l’exécution d’une requête SELECT .
1 while(rs.next()) {
2 //Process the current row in rs
3 int personID = res.getInt(1);
4 String firstName = res.getString(2);
5 String lastName = res.getString(3);
6 Java.sql.Date dob = rs.getDate(4);
7 double income = rs.getDouble(5);
8 // Do Something with column values
9 }
Listing 50 – Exemple de récupération d’un ResultSet lors de l’exécution d’une requête SELECT .
Exercice
Réaliser une application personnelle en Java utilisant JDBC qui sauvegarde ses données
dans une base de données.
88 CHAPITRE 10. JAVA DATABASE CONNECTIVITY
Chapitre 11
ADO.NET en C#
En C#, la libraire la plus commune pour une solution orientée objets d’accès aux données
est : ADO.N ET 1 . Cette librairie est un ensemble de classes qui exposent les services d’accès aux
données pour les programmeurs .N ET F ramework [36]. Ce chapitre est dédié à la configuration et
l’utilisation de cette libraire pour l’intégration spécifique de bases de données. Il existe une solution
orientée relationnelle EntityF ramework qui est basée sur la libraire ADO.N ET .
11.1 Configurer
La libraire ADO.NET propose deux modes pour utiliser les données d’une base de données :
le mode connecté et le mode déconnecté. Dans le mode déconnecté, les programmeurs manipulent
des DataSets qui agglomèrent des DataT ables exposants des lignes et des colonnes. Le DataSet
est une représentation partielle et déconnecté d’une base de données. Les mises à jour, ou syn-
chronisation, seront faites à l’aide d’un DataAdapter. Le mode connecté gère automatiquement les
mises à jour et les connexions à la base de données. Ce mode propose un comportement similaire
à JDBC en Java.
Le dialogue avec la base de données se concrétise au travers d’un DataP rovider. Ce dernier
dépendra du type de SGBD qui héberge votre base de données. De façon analogue aux pilotes de
JDBC, un DataP rovider expose des classes qui implémentent des interfaces standards (voir figure
11.1).
89
90 CHAPITRE 11. ADO.NET EN C#
Figure 11.1 – Tableau des classes et interfaces exposées par le DataP rovider.
La configuration du projet est simplifié en C# par l’utilisation des packages N uget. Vous
avez besoin du package System.Data.dll qui est le cœur d’ADO.N ET . Ce dernier est dispo-
nible par défaut. Vous devez aussi préciser l’assembly du DataP rovider correspondant au SGBD
utilisé. Pour un SQLServer, vous aurez besoin de System.data.SQLClient qui est inclus dans
System.Data.dll. Dans le cas d’un SGBD M ySQL, vous devez ajouter la package M ySql.Data
dans un projet utilisant .N ET F RAM EW ORK4.7.2 2 .
Les connexions à la base de données sont créées à l’aide d’une DbP roviderF actory. La création
d’une instance de cette classe peut être associée au chargement du driver de JDBC en Java. Cette
opération est réalisé dans le listing 51 pour un SGBD M ySQL 3 .
2. Pour le projet de test, il est primordial de conserver le même f ramework. De plus, les packages N unit et
N unit3T estAdapter sont nécessaires pour réaliser les tests unitaires.
3. Pour une base de données SQLServerLocale : ”System.Data.SqlClient”
11.1. CONFIGURER 91
Lors de la création de la f actory, le ”RemoteP rovider” doit être précisé. Celui-ci dépend du
SGBD utilisé. Une exception du type ArgumentException sera lancée si le namespace corres-
pondant au driver n’existe pas. La classe qui possède la f actory est alors capable de créer des
connexions à la base de données. Selon la configuration de votre ordinateur, il est possible que le
provider ne soit pas trouvé. Auquel cas, une modification du fichier App.conf ig de votre projet
corrigera le problème. Le listing 52 montre le code à ajouter au fichier déjà existant. La balise
”remove” est utile dans le cas où l’invariant est déjà défini. De cette façon, le projet pourra être
généré quelques soit la configuration du pc hôte.
1 <system.data>
2 <DbProviderFactories>
3 <remove invariant="MySql.Data.MySqlClient" />
4 <add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient"
5 description=".Net Framework Data Provider for MySQL"
6 type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data" />
7 </DbProviderFactories>
8 </system.data>
Listing 52 – Adaptation du fichier App.conf ig du projet, nécessaire dans certains cas pour un
SGBD MySQL.
Lors d’une connexion, la ”ConnectionString” doit être précisé. Elle contient l’adresse IP du
serveur qui héberge le SGBD, ainsi que l’identifiant et le mot de passe pour accéder à la base de
92 CHAPITRE 11. ADO.NET EN C#
données 4 . Une exception du type ”ArgumentException” sera lancée si la chaine de connexion est
syntaxiquement incorrecte. Si la connexion à la base de données est impossible, un exception du
type SqlException sera lancée. La gestion des exceptions lors de l’acquisition des ressources est
primordiale (voir chapitre 3) !
1 try
2 {
3 using IDbConnection con = _factory.CreateConnection();
4 con.ConnectionString = "Data Source = ip; DataBase = dataBase; User id = id; Password = pass";
5 con.Open();
6 // Use con
7 }
8 catch (ArgumentException ex)
9 {
10 Console.WriteLine("Invalid Connection String");
11 }
12 catch (SqlException ex)
13 {
14 Console.WriteLine("Unable To Connect");
15 }
Remarque
La connxtion à une base de données est une ressource externe. Elle doit donc être gérée avec
les précautions d’usage (voir chapitre 3.)
Pour exécuter une requête SQL, ADO.N ET propose un seul objet une IDbCommand. Cette
dernière est créée par la connexion. Elle donc en lien direct avec la base de données. une IDbCommand
implémente donc IDisposable et doit être créée à l’aide d’un statement ”using” comme illustré
dans le listing 54.
1 try {
2 using (IDbConnection con = NewConnection())
3 using (IDbCommand dropCommand = con.CreateCommand() {
4 dropCommand.CommandText = "DROP TABLE PROJECT_ELEMENT";
5 dropCommand.ExecuteNonQuery();
6 }
7 }
8 catch (SqlException ex) {
9 //Should be Handled
10 Console.WriteLine(ex.Message);
11 }
La requête SQL est alors représentée par une chaine de caractère qu’il faut placé dans la pro-
priété CommandT ext de l’instance de IDbCommand. L’appel à la méthode ExecuteN onQuery()
provoque l’exécution de la requête pour les requêtes faisant partie du DDL 5 . Il est possible de
paramétrer les commandes afin de rendre le code plus souple. Le listing 55 est exemple d’une
requête d’IN SERT paramétrée. Contrairement à JDBC qui représente les paramètres avec point
d’interrogation, ADO.N ET propose un système de nommage.
5. pour une requête de lecture (SELECT), l’appel à la méthode ExectureReader() exécutera la requête
94 CHAPITRE 11. ADO.NET EN C#
13 insertCmd.Parameters.Add(nameParam);
14 //...
15 }
16 }
Dans le texte de la requête, chaque paramètre sera identifié par son nom précédé d’un symbol
@. Fixer la valeur d’un paramètre est un plus fastidieux qu’avec JDBC. En effet, il faut créer un
objet qui représente un paramètre via la commande. Fixer son nom, sa valeur et son type, et en
dernière étape, ajouter le paramètre à ladite IDbCommand.
Dans un programme, il est parfois intéressant de récupérer les identifiants des tuples pour faire
la correspondance avec les objets en mémoire du programme. Malheureusement, ADO.N ET ne
propose pas comme JDBC une méthode générique pour effectuer cette tâche. La technique de
récupération des identifiants de la base de données est donc dépendante du SGBD.
1 using(IDbCommand cmd=con.CreateCommand(
2 "INSERT INTO Mem_Basic(Mem_Na,Mem_Occ) output INSERTED.ID VALUES(@na,@occ)"))
3 {
4 cmd.Parameters.AddWithValue("@na", Mem_NA);
5 cmd.Parameters.AddWithValue("@occ", Mem_Occ);
6 int newId =(int)cmd.ExecuteScalar();
7 }
Le listing 57 propose une solution de récupération des identifiants valable pour SQLServer uni-
quement. Dans le cadre de M ySQL, il faudra travailler avec une commande spécifique : M ySQLcOM M AN D
96 CHAPITRE 11. ADO.NET EN C#
Exercices
Adapter le code du listing 57 dans le case d’utilisation de M ySQL.
Listing 58 – Exemple d’utilisation d’un DataReader : utilisation des noms pour identifier les co-
lonnes.
De façon similaire à JDBC, les colonnes peuvent être identifiées à l’aide de leur nom ou de
leur numéro. L’utilisation des noms comme identifiants des colonnes est présenté dans le listing
58. L’accès se fait de façon similaire à une map. Un cast doit être effectué après la récupération de
la valeur. Les colonnes peuvent être aussi identifiée par un numéro correspondant à leur position
dans la requête en commençant par zéro (illustré sur le listing 59). C’est la méthode utilisée qui
fixe le type de valeur : GetString(int), GetInt32(int), GetF loat(int), ...
11.4. LIRE DES DONNÉES 97
Listing 59 – Exemple d’usage d’un DataReader : utilisation de nombres pour identifier les colonnes.
Exercice
Réaliser une application personnelle en C# basé sur ADO.N ET qui utilise une base de
données pour la sauvegarde de ses données.
98 CHAPITRE 11. ADO.NET EN C#
Chapitre 12
Tests et ressource
La ressource de test n’est pas une simple copie de la ressource de production. Cette dernière
pouvant être de volume non négligeable, il sera préférable de constituer une ressource qui contien-
dra un sous-ensemble représentatif des données de production.
99
100 CHAPITRE 12. TESTS ET RESSOURCE
Pour JDBC, une base de données locale Derby sera créée. Il est facile de la mettre en œuvre
avec l’outil DataGrip de JetBrain. Une base de données SQLServerLocal sera mise en place
dans un projet C#. Tous les outils nécessaires à cette fin sont intégrés dans V isualStudio.
Cette base de données locale doit avoir les mêmes tables que la base de données de production.
L’insertion de tuples représentatifs devra être effectuée afin de garantir aux tests une couverture
complète.
Après l’écriture de données dans la ressource, il faut tester leur présence et cohérence. Pour ce
faire, vous avez deux possibilités : (i) implémenter une méthode de lecture dans votre classe de
test ou (ii) utiliser le code de lecture implémenter dans le projet testé. La seconde option présente
des dangers :
— vos tests d’écriture peuvent échouer à cause d’un problème dans le code de lecture. Pour
éviter ce problème, il faut garantir une bonne couverture de la lecture et toujours corriger
les tests de lecture avant ceux d’écriture.
— si pour une raison x ou y, vous faites appel au code d’écriture dans vos tests de lecture,
une dépendance croisée apparaı̂t. Lors de l’échec des tests, il sera plus difficile d’identifier
12.4. TESTER L’ÉCRITURE 101
l’origine de l’erreur : écriture/lecture. L’ordre dans lequel les tests doivent être corrigés est
impossible à déterminer.
La solution de totale indépendance est la plus idéale mais demande de refaire l’implémentation
de lecture et parfois écriture dans les tests. Une dépendance entre les tests de lecture et d’écriture
peut exister mais avec beaucoup de prudence et de documentation 2 .
2. n’oubliez jamais que votre code sera utilisé et/ou développé par quelqu’un d’autre.
102 CHAPITRE 12. TESTS ET RESSOURCE
103
104 ANNEXE A. GESTION D’ERREUR SANS EXCEPTION
Annexe A
10 }
11 public ArithmeticResult(String message) {
12 this.message = message;
13 }
14 public boolean isValid() {
15 return message.isEmpty();
16 }
17 public String getMessage() {
18 return message;
19 }
20 public int getValue() {
21 return result;
22 }
23 public String toString() {
24 if(this.isValid())
25 return String.format("Result value is %d",this.getValue());
26 else
27 return String.format("Result invalid : %s", this.getMessage());
28 }
29 }
30
Le listing 60 est un exemple d’implémentation d’une classe imbriquée 1 pour gérer les éventuelles
erreurs. Cette alternative présente l’avantage d’être plus économe que le lancement et la capture
d’une exception. Le listing 61 montre l’utilisation d’une telle classe.
Listing 61 – Exemple de l’utilisation d’une classe imbriquée pour la gestion des erreurs.
Exercice
Compiler et exécuter ce code des listing 60 et 61 sur votre machine.
Exercice
Réaliser le code des listings 60 et 61 en C#.
Décorateur
Le décorateur est un design pattern structurel. Son objectif est d’attacher dynamiquement,
c’est-à-dire à l’exécution, des responsabilités supplémentaires. C’est une alternative à l’héritage
pour étendre les fonctionnalités d’une classe [37]. Une contrainte de pattern est le fait qu’un
décorateur ne modifie pas l’interface de l’objet décoré. De cette façon, la présence ou non du
décorateur est invisible pour le client.
La figure B.1 présente le schéma de classe, standard, du design pattern décorateur. L’interface
Composant représente l’ensemble des méthodes de la classe de base qui sera décorée, Composant-
Concret dans notre cas. La classe abstraite Decorateur hérite de l’interface Composant pour garantir
le même comportement. Comme son objectif est d’ajouter une ou plusieurs fonctionnalités, elle
contient (par composition) une instance de classe qui implémente cette même interface. De fait,
la classe décorateur pourra déléguer les fonctionnalités de base à cet objet. Enfin, les classes De-
corateurConcretA et DecorateurConcretB implémentent la classe abstraite Decorateur. Chacune
d’entre elle correspond à l’ajout d’une fonctionnalité particulière. Il existe une version plus courte
pour laquelle la classe abstraite Decorateur n’existe pas. Les décorateurs héritent directement de
l’interface Composant et réalisent elle-même la composition de celle-ci comme illustré sur la Figure
B.2.
107
108 ANNEXE B. DÉCORATEUR
Figure B.2 – Schéma simplifié de classe servant de structure au design pattern décorateur.[Réalisé
en PlantUML [13]]
1 //______________________________________________________________________
2 // Déclarations
3 abstract class Voiture {
4 public abstract double Prix { get; }
5 }
6 class AstonMartin : Voiture {
7 public override double Prix { get { return 999.99; } }
8 }
9 //______________________________________________________________________
10 // Décorateurs
11 class Option : Voiture {
12 protected Voiture _originale;
13 protected double _tarifOption;
14 public Option(Voiture originale, double tarif) {
15 _originale = originale;
16 _tarifOption = tarif;
17 }
18 public override double Prix {
19 get { return _originale.Prix + _tarifOption; }
20 }
21 }
22 class VoitureAvecClimatisation : Option {
23 public VoitureAvecClimatisation (Voiture originale) : base(originale, 1.0) { }
24 }
25 class VoitureAvecParachute : Option {
26 public VoitureAvecParachute (Voiture originale) : base(originale, 10.0) { }
27 }
28 class VoitureAmphibie : Option {
29 public VoitureAmphibie (Voiture originale) : base(originale, 100.0) { }
30 }
31 //______________________________________________________________________
32 // Implémentation
33 class Program {
34 static void Main() {
35 Voiture astonMartin= new AstonMartin();
36 astonMartin = new VoitureAvecClimatisation(astonMartin);
37 astonMartin = new VoitureAvecParachute(astonMartin);
38 astonMartin = new VoitureAmphibie(astonMartin);
39
Adaptateur
L’adaptateur est un design pattern structurel. Il permet comme son nom l’indique d’adapter
un objet à une autre interface [37]. En général, la classe adaptateur hérite d’une interface qu’elle
doit donc implémenter (But) et contient (par composition) l’adapté.
L’adaptateur utilise alors l’adapté pour répondre à l’interface But. Les listings 63, 64, 65
présentent un exemple simple d’implémentation d’un adaptateur. La classe Adaptater utilise la
classe Adaptee pour répondre à l’interface But. Ce faisant, la classe Adaptater peut réaliser un
travail supplémentaire si nécessaire. Pour être plus complet, la classe Adaptater devrait recevoir
l’instance de la classe Adaptee en paramètre de son constructeur. En conséquence, pour que le client
ne connaisse que l’interface But, il possible d’ajouter une factory qui aurait la responsabilité de
construire les instances de la classe Adaptater.
Exercice
Modifier le code des listings 63, 64, 65 pour y ajouter le constructeur adéquat de la classe
Adaptater et la factory pour isoler le client de l’implémentation concrète de Adaptee.
111
112 ANNEXE C. ADAPTATEUR
1 /// <summary>
2 /// MainApp startup class for Structural
3 /// Adapter Design Pattern.
4 /// </summary>
5
6 class MainApp
7 {
8 /// <summary>
9 /// Entry point into console application.
10 /// </summary>
11 static void Main()
12 {
13 // Create adapter and place a request
14 But but = new Adapter();
15 but.Request();
16 // Wait for user
17 Console.ReadKey();
18 }
19 }
1 /// <summary>
2 /// The 'But' class
3 /// </summary>
4 class But
5 {
6 public virtual void Request()
7 {
8 Console.WriteLine("Called But Request()");
9 }
10 }
1 /// <summary>
2 /// The 'Adapter' class
3 /// </summary>
4 class Adapter : But
5 {
6 private Adaptee _adaptee = new Adaptee();
7 public override void Request()
8 {
9 // Possibly do some other work
10 // and then call SpecificRequest
11 _adaptee.SpecificRequest();
12 }
13 }
14
15 /// <summary>
16 /// The 'Adaptee' class
17 /// </summary>
18 class Adaptee
19 {
20 public void SpecificRequest()
21 {
22 Console.WriteLine("Called SpecificRequest()");
23 }
24 }
115
116 BIBLIOGRAPHIE