Vous êtes sur la page 1sur 117

Structure de données

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

3 Gestion des ressources 19


3.1 Qu’est ce qu’une ressources ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.2 Pourquoi gérer une ressource ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.3 Le Principe RAII . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.3.1 RAII en c++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.3.2 RAII en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3.3 RAII en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.4 Ressources et exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

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

6 Les fichiers en Java 41


6.1 La nomenclature des classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6.2 Fichiers textes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
6.2.1 Lecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
6.2.2 Écriture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
6.3 Fichiers binaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

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

9 Intégration d’une BD dans une application OO 67


9.1 Orienté objet vs Base de Données . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
9.1.1 Héritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
9.1.2 Composition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
9.2 Outils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75

10 Java DataBase Connectivity 77


10.1 Configurer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
10.2 Storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
10.3 Faire une requête : Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
10.4 Modifications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
10.4.1 Transaction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
10.5 Lire des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85

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

A Gestion d’erreur sans exception 103

B Décorateur 107

C Adaptateur 111
6 TABLE DES MATIÈRES
Chapitre 1

Introduction

Lors du développement d’applications de bureau (desktop en anglais), il est fréquent que le


programme nécessite une sauvegarde de ces données. Ces dernières peuvent, dans certains cas, être
partagées entre plusieurs applications. Ce syllabus a pour objectifs d’introduire les concepts et les
notions nécessaires à la pérennisation des données d’un programme au-delà de son exécution.

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.

En vous souhaitant une bonne lecture.

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

2.2 Pourquoi ? Et quand ?

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

1 public int divide(int numerator, int denominator) {


2 return numerator / denominator;
3 }

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

2.3 Exceptions en Java


Java propose une structure de classe pour la gestion des problèmes : erreurs et exceptions.
Toutes ces classes contiennent un message et une cause. Le message est un objet String et la cause
est du type Throwable.

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

2.3.1 Lancer une exception


Le listing 2 propose un exemple de code où une exception est lancée. Le lancement d’une
exception se fait via le mot-clé ”throws” suivi de la création de l’exception (via l’opérateur ”new”).

1 public int divide(int numerator, int denominator) {


2 if(denominator == 0)
3 throws new IllegalArgumentException("Cannot divide by zero");
4 return numerator / denominator;
5 }

Listing 2 – Exemple de lancement d’une RuntimeException.

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.

1 public void DoSomething() throws IOException {


2 if(....)
3 throw new IOException("External problem cause this exception");
4 }

Listing 3 – Exemple de lancement d’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

2.3.2 Attrapper une exception

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.

1 public void ComupteAverage(List<int> data) {


2 double sum = 0;
3

4 for(int i = 0 ; i < data.size() ; ++i)


5 sum += data[i];
6

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 }

Listing 4 – Exemple du traitement d’une exception.

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

1 public void ComupteAverage(int[] data, int count) {


2 double sum = 0;
3

4 for(int i = 0 ; i < count ; ++i)


5 sum += data[i];
6

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 }

Listing 5 – Exemple du traitement de plusieurs exceptions.

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.

2.3.3 Créer une exception

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

1 public void ComupteAverage(int[] data, int count) {


2 double sum = 0;
3

4 for(int i = 0 ; i < count ; ++i)


5 sum += data[i];
6

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 }

Listing 6 – Exemple du traitement d’exceptions avec un bloc finally.

1 public class MyException extends Exception {


2 // unique id
3 private static final long serialVersionUID = 7526472295622776147L;
4 public MyException() {
5 super("LUDFR Exception");
6 }
7 }

Listing 7 – Exemple du traitement d’une exception.

Le listing 7 présente un exemple d’implémentation d’une exception. La classe MyException doit


hériter de classe Exception. Ne pas oublier d’appeler le constructeur de la classe Exception via le
mot-clé ”super”. Implémenter sa propre exception doit se faire uniquement si vous ne trouvez pas
d’exception correspondante à votre problème parmi celles proposées par Java [1].
2.4. EXCEPTIONS EN C# 15

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

2.4.1 Lancer une exception

Le lancement d’une exception en C# se fait à l’aide du mot-clé ”throw”. Le listing 8 illustre


un exemple.

1 public void DoSomething() {


2 if(....)
3 throw new System.IO.IOException("External problem cause this exception");
4 }

Listing 8 – Exemple de lancement d’une exception.

Notez que contrairement à Java, le lancement d’une exception n’est pas accompagné par une
modification de la signature de la méthode.

2.4.2 Attraper une exception

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 }

Listing 9 – Exemple de gestion d’exceptions [2].

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 }

Listing 10 – Exemple de gestion d’exceptions avec filtres [2].


2.4. EXCEPTIONS EN C# 17

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.

2.4.3 Créer une exception


Les frameworks de base du langage proposent déjà une série d’exceptions courantes :
— System.NullReferenceException : lancée quand une méthode reçoit un argument nul qu’elle
ne peut pas traiter.
— System.IndexOutOfRangeException : lancée quand on accède à un élément d’un tableau
qui n’existe pas.
— System.IO.IOException : lancée si des problèmes surviennent lors de la manipulation des
fichiers en entrée et sortie.
— System.Net.WebException : lancée en cas de problèmes lors de commandes HTTP.
— System.Data.SqlClient.SqlException : liée au serveur SQL.
— System.StackOverflowException : en cas d’appel récursif trop important.
— System.OutOfMemoryException : lancée si votre application manque de mémoire.
— System.InvalidCastException : lancée lors d’un cast en un mauvais type.
— System.InvalidOperationException : très courante dans un grand nombre de librairies.
— System.ObjectDisposedException : lancée si utilisation d’un objet qui ne l’est plus par un
appel à Dispose (voir chapitre sur les ressources).
Vous pouvez aussi consulter la documentation officielle pour une liste plus détaillée [3]. Dans
le cas où, vous ne trouvez pas une exception qui corresponde à votre problème, vous pouvez créer
la vôtre.

1 public class CustomException : Exception


2 {
3 public CustomException(string message)
4 : base(message)
5 {
6 }
7 }

Listing 11 – Exemple d’implémentation d’une exception [2].

Le listing 11 propose un exemple d’exception personnalisée. L’appel au constructeur de la classe


de base se fait via le mot clé ”base”.
18 CHAPITRE 2. LES EXCEPTIONS
Chapitre 3

Gestion des ressources

3.1 Qu’est ce qu’une ressources ?


Les ressources sont des composants, matériels ou logiciels. Elles peuvent être utilisées dans des
programmes en fonction de leur besoin.
Les ressources les plus courantes sont la mémoire. Celle-ci est gérée automatiquement dans
les machines virtuelles (Java et C#) via un garbage-collector. Dans d’autres langages comme le
C/C++, il est possible voire obligatoire de gérer la mémoire manuellement.
Il existe d’autres types de ressources comme les tubes, les sémaphores et les mémoires partagées.
Celles-ci seront discutées au cours de Système d’exploitation [4].
Au delà de la mémoire du programme (heap), ou des mémoires partagées, il y a la mémoire
persistante, les fichiers. Les connections à des bases de données sont aussi un exemple important
de ressource.

3.2 Pourquoi gérer une ressource ?


La gestion d’une ressource est très importante dans un programme. Les ressources sont externes
au programme. Dans la plupart des cas, elles seront partagées avec d’autres logiciels. Lorsque vous
avez besoin d’une ressource, vous devez l’acquérir. De cette manière, vous avertissez les autres que
vous l’utilisez. Au moment où vous n’avez plus besoin de votre ressource, vous devez la libérer.
A cet instant, vous rendez cette ressource disponible pour les autres logiciels. Si votre gestion est
défaillante, vous pouvez compromettre le bon fonctionnement d’un ensemble d’autres logiciels !

3.3 Le Principe RAII


Le principe RAII est l’acronyme de Resource Acquisition Is Initialization. Ce principe a été
énoncé par Bjarne Stroustrup, le fondateur du C++. Ce principe est orienté strictement POO.
La gestion d’une ressource est encapsulée dans une classe. Dans le constructeur de la classe, la
ressource est acquise. Elle sera libérée dans le destructeur.

19
20 CHAPITRE 3. GESTION DES RESSOURCES

Ce principe présente beaucoup d’avantages. Il assure la libération de la ressource dans tous


les cas de figure (en C++). Au-delà de cette sécurité, il permet d’éviter les erreurs humaines.
Un programmeur qui utilise la ressource au travers de cette classe ne doit plus se soucier de la
gestion de la celle-ci. Il se concentre uniquement sur son utilisation. Il est dès lors impossible qu’une
ressource ne soit pas libérée par une erreur humaine.

3.3.1 RAII en c++

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

6 // Constructeur qui reserve la memoire.


7 DataCollection(int count){
8 this.collection = new int[count];
9 }
10

11 // Destructeur qui libere la memoire


12 ~RationalParameter() {
13 delete[] this.collection;
14 }
15

16 private:
17 int *collection;
18 };

Listing 12 – Exemple d’une classe RAII en C++.

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 }

Listing 13 – Exemple d’utilisation d’une classe RAII en C++.

Lors de la fin de la méthode ”WorkWithDataCollection”, tous les objets construits dans le


contexte de cette méthode (entre l’ouverture et la fermeture de l’accolade de cette méthode), sont
détruits immédiatement. Cette étape est réalisée en deux temps : l’appel au destructeur des objets
concernés et la libération de la mémoire des variables ensuite. Au final, l’utilisateur de la classe
peut même ignorer l’existence de la ressource. Ce principe s’étend à tous les types de ressources.

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.

Lors de la destruction de l’objet, la première étape consiste à faire appel au destructeur. Le


nôtre s’occupe de libérer le tableau d’entier de la mémoire (heap) voir figure 3.2. La seconde étape
consiste à la destruction de l’objet lui-même et donc la libération de sa mémoire (stack).
22 CHAPITRE 3. GESTION DES RESSOURCES

Figure 3.2 – Etapes de libération de la mémoire lors de la destruction de l’objet myDataCollec-


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.

L’application du principe RAII nous protège efficacement de l’occurence de tel problème.


3.3. LE PRINCIPE RAII 23

3.3.2 RAII en Java


Le langage Java diffère du C++ sur la gestion de la mémoire. Du point de vue organisationnel,
tous les objets sont stockés dans le heap et seules les références sur ceux-ci sont stockées dans la
stack comme illustré sur la figure 3.4.

Figure 3.4 – Schéma de la mémoire Stack et Heap, en Java, après le constructeur de la classe
DataCollection.

La libération de la mémoire est déléguée à un programme spécifique de la machine virtuelle.


Celui-ci est communément appelé le ramasse-miette (Garbage Collector). Si un objet stocké dans
le heap ne possède plus de référence dans la stack, alors le ramasse-miette libérera sa mémoire
(voir Figure 3.5). L’avantage est que le programmeur ne doit pas se préoccuper de la libération de
la mémoire comme cela était le cas dans d’autres langages comme le C.

Figure 3.5 – Etapes de libération, en Java, de la mémoire lors de la destruction de l’objet


myDataCollection.

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

1 public class MyResource implements Closeable {


2 public MyResource() {
3 // Acquisition de la ressource
4 System.out.println("Acquisition de la ressource");
5 }
6

7 public void close() throws IOException {


8 System.out.println("Liberation de la ressource");
9 }
10 }

Listing 14 – Exemple d’une classe AutoCloaseable en Java.

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

1 public static void main(String[] args) {


2 try(MyResource myResource = new MyResource()) {
3 System.out.println("Try-With-Resources");
4 }
5 }

Listing 15 – Exemple d’utilisation d’une classe AutoCloseable.

Cette approche permet de garantir la fermeture de la ressource. Malheureusement, le code


final est plus lourd qu’en C++ et cette approche ne protège pas d’une erreur programmeur (par
exemple : oubli ou mauvaise utilisation du ”Try-With-Resources”).
3.3. LE PRINCIPE RAII 25

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

1 public class MyResource : IDisposable {


2 public MyResource() {
3 // Acquisition de la ressource
4 Console.WriteLine("Acquisition de la ressource");
5 }
6

7 public void Dispose() {


8 Console.WriteLine("Liberation de la ressource");
9 }
10 }

Listing 16 – Exemple d’une classe disposable en C#.

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

1 static void Main(string[] args) {


2 using(MyResource myResource = new MyResource()) {
3 System.out.println("Using statement context");
4 }
5 }

Listing 17 – 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

1 static void Main(string[] args) {


2 using MyResource myResource = new MyResource();
3 System.out.println("Using statement context");
4 }

Listing 18 – Exemple d’utilisation d’une classe disposable avec la nouvelle syntaxe.

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.

3.4 Ressources et exceptions


Une ressource est une source d’exceptions. En effet, que ce soit lors de l’acquisition, l’utilisation
ou la libération, des exceptions sont susceptibles d’être lancées.

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.

1 public static void main(String[] args) {


2 try(MyResource myResource = new MyResource()) {
3 System.out.println("Try-With-Resources");
4 } catch(MyResourceException ex) {
5 System.out.println("Erro while allocating/using MyResource !");
6 }
7 }

Listing 19 – Gestion des exceptions avec l’utilisation d’une classe Closeable.

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

1 static void Main(string[] args) {


2 try {
3 using(MyResource myResource = new MyResource())
4 System.out.println("Using statement context");
5 } catch(MyResourceException ex) {
6 Console.WriteLine("Erro while allocating/using MyResource !");
7 }
8 }

Listing 20 – Gestion des exceptions avec l’utilisation d’une classe IDisposable.

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

4.1 Qu’est ce qu’un fichier ?


Un fichier est une zone de la mémoire, persistante dans la grande majorité des cas. Cette
mémoire se trouve sur un support physique tel que le disque dur de votre machine, une clé USB
ou dans le cloud (disque dur d’une machine distante).
La particularité de cette mémoire est son identification. Un fichier sera identifié par un chemin
(Path). Celui-ci contient deux informations importantes : l’adresse, le dossier (Directory) où se
trouve le fichier et son nom.

4.2 Type de fichier


Un fichier est donc une zone mémoire. En d’autres mots, un fichier est un rassemblement
d’octets (Bytes). Ces octets représentent des données utiles.
Comment pouvons-nous interpréter les octets ? Il existe deux grandes familles de fichiers. Ceux
dont les octets peuvent être traduits en caractères au travers d’une table de conversion standard
sont appelés fichier texte. Les autres sont simplement nommés fichier binaire.

4.3 Fichier binaire


Les fichiers binaires représentent des données qui ne pourront être traitées correctement que
par les logiciels concernés.
Cette forme de fichier possède une faiblesse. Un entier sera toujours codé sur 4 octets. Mais
l’ordre des octets peut varier (endianness) en fonction de votre machine/système d’exploitation.
Ces deux approches s’appellent Little-endian et Big-endian. Voici un exemple concret.
Comme l’illustre le tableau 4.1, le nombre 4660 possède deux représentations différentes en
Big − Endian et Little − Endian. En cas de lecture dans la mauvaise Endianness, le nombre
devient alors 13330. Malheureusement, l’Endianness n’est pas embarqué dans le fichier. C’est à
celui qui va le lire de connaı̂tre l’Endianness utilisé par celui qui l’a créé.

29
30 CHAPITRE 4. FICHIERS

Data Big-endian Little-endian


int 4660 4660
hexa 0x1234 0x3412
bits 0001 0010 0011 0100 0011 0100 0001 0010
Big-endian read 4660 13330
Little-endian read 13330 4660

Table 4.1 – Big-endian and Little-endian comparison.

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.

Java Type Size


byte 1
short 2
int 4
long 8
float 4
double 8
char 2
boolean 1-4 (JVM dépendant)
C# Type Size
sbyte/byte 1
short/ushort 2
int/uint 4
long/ulong 8
float 4
double 8
decimal 16
char 2 (Unicode)
bool 4(local) / 1 (in array)

Table 4.2 – Liste des types primitifs du Java et C# avec leur taille en nombre d’octets.
4.4. FICHIER TEXTE 31

4.4 Fichier texte


Il existe un grand nombre de tables de conversion entre caractères et octets. Les sous-sections
suivantes présentent les tables les plus courantes.

4.4.1 ASCII et ISO-Latin1


ASCII est le l’acronyme de ”American Standard Code for Information Interchange”. Cette
table a été établie dans les années 60.

Figure 4.1 – Ensemble des caractères imprimables de la table ASCII [5].

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 ?

4.4.2 Universal character set Transformation Format


La norme UNICODE permet de prendre en compte un très grand nombre de caractères de
toutes origines. Pour ce faire, les caractères doivent être codés sur plusieurs octets.
Les tables de conversions sont appelées UTF. Un numéro accompagne cette nomenclature.
Celui-ci précise le nombre de bits minimum utilisé pour coder un caractère :
— UTF-8 : les caractères sont codés sur 1 à 4 octets (elle contient la table ASCII) ;
— UTF-16 : les caractères sont codés sur 2 à 4 octets ;
— UTF-32 : les caractères sont tous codés sur 4 octets
La table UTF-8 prend de plus en plus d’ampleur dans le monde de l’informatique. Elle est
gérée nativement en C# et Java. De plus, dans le monde du web, ce codage est devenu la norme
comme l’illustre le graphique 4.2.
32 CHAPITRE 4. FICHIERS

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

1 public class Sphere {


2 double x,y,z;
3 double radius;
4 }
5

6 public class Student {


7 String firstname, lastname;
8 int age;
9 }

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

Le nom CSV est l’acronyme de Comma-Separated Values. Ce standard propose de séparer


les données par des virgules. La variante française du CSV utilise des points-virgules comme sym-
bole séparateur. Chaque valeur représente un champ d’un enregistrement. Un enregistrement cor-
respond à une ligne. Le symbole séparateur des enregistrements est donc le retour à la ligne.
Chaque valeur peut être délimitée par des guillemets. Ceux-ci sont importants lorsque la valeur
contient un symbole séparateur. Le symbole guillemet dans une valeur devra être doublé pour être
différencié.
5.1. ORGANISATION 37

7.920000e-02 ;8.709800e-02 ;2.234380e-01 1.000000e-02


1.098240e-01 ;4.268000e-03 ;-7.849000e-02 ;1.000000e-02
7.990400e-02 ;-3.170200e-02 ;1.775190e-01 ;1.000000e-02
6.113800e-02 ;7.990400e-02 ;-8.121600e-02 ;1.000000e-02
1.059960e-01 ;1.300200e-02 ;8.930000e-02 ;1.000000e-02
-1.056880e-01 ;-1.936000e-02 ;-1.079120e-01 ;1.000000e-02
-3.082200e-02 ;4.683800e-02 ;1.007680e-01 ;1.000000e-02
9.185000e-02 ;-9.086000e-02 ;1.605520e-01 ;1.000000e-02
-8.822000e-03 ;4.576000e-02 ;-1.795400e-02 ;1.000000e-02
-1.566400e-02 ;9.794400e-02 ;1.470160e-01 ;1.000000e-02
-4.840000e-02 ;5.856400e-02 ;-7.520000e-03 ;1.000000e-02
-1.067000e-02 ;-9.550200e-02 ;-5.734000e-03 ;1.000000e-02

Listing 22 – Exemple de sauvegarde des données provenant d’instances de la classe Sphere au


format CSV (variante française).

Les listings 22 -23 présentent des données exemples issues des classes Sphere et Student au
format CSV.

Ludewig ;François ;39


Reip ;Vincent ;40
Hendrikx ;Nicolas ;35
Martin ;Vincent ;32
Swinnen ;Louis ;38
Mathy ;Christine ;45
Jadot ;Jean ;36

Listing 23 – Exemple de sauvegarde des données provenant d’instances de la classe Student au


format CSV (variante française).

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

Les fichiers en Java

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.

1 public static void main(String[] args) {


2 Path cheminAbsolu = Paths.get("c:","TEMP","in.txt");
3 Path cheminRelatif = cheminAbsolu.getParent().resolve("out.txt");
4 Path cheminAbsoluOuRelatif = Paths.get("temp.txt");
5

6 System.out.printf("Le chemin \%s existe ? \%b\n",


7 cheminAbsolu.toString(), Files.exists(cheminAbsolu));
8 System.out.printf("Le chemin \%s existe ? \%b\n",
9 cheminRelatif.toString(),Files.exists(cheminRelatif));
10 System.out.printf("Le chemin \%s existe ? \%b\n",
11 cheminAbsoluOuRelatif.toString(),Files.exists(cheminAbsoluOuRelatif));
12

13 System.out.printf("\%s en absolue == \%s\n", cheminAbsolu.toString(),


14 cheminAbsolu.toAbsolutePath().toString());
15 System.out.printf("\%s en absolue == \%s\n", cheminRelatif.toString(),
16 cheminRelatif.toAbsolutePath().toString());
17 System.out.printf("\%s en absolue == \%s\n",
18 cheminAbsoluOuRelatif.toString(),
19 cheminAbsoluOuRelatif.toAbsolutePath().toString());
20 }

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.

1 public static void main(String[] args) {


2 Path rscPath = Paths.get(getCurrentPath(), "src", "main", "resources", "in.txt");
3 System.out.printf("Le chemin %s existe ? %b\n",
4 rscPath.toString(), Files.exists(rscPath));
5 }
6

7 public static String getCurrentPath() {


8 Path currentRelativePath = Paths.get("");
9 return currentRelativePath.toAbsolutePath().toString();
10 }

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.

6.1 La nomenclature des classes


Java propose un grand nombre de classes pour la gestion des flux 1 . Afin de choisir l’outil
adéquat, il faut les connaı̂tre. La nomenclature utilisée pour nommer ces classes permet de com-
prendre leurs fonctions [11].
Nous avons déjà introduit la notion de fichier binaire et texte. Nous pouvons lire et écrire dans
les fichiers. Il existe donc 4 suffixes correspondant à ces caractéristiques. Le tableau 6.1 présente
les valeurs des suffixes.

Lecture Écriture
Binaire OutputStream InputStream
Texte Reader Writer

Table 6.1 – Les 4 suffixes en fonction du type de flux.

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.

6.2 Fichiers textes


6.2.1 Lecture
La figure 6.1 présente la hiérarchie des classes pour la lecture de texte proposée par Java.

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

1 public static void main(String[] args) {


2 Path cheminFichierLu = Paths.get(PathsFilesExample.getCurrentPath(), "src", "main", "resources", "inAN
3 try(FileReader fileReader = new FileReader(cheminFichierLu.toString())) {
4 displayData(fileReader);
5 } catch (IOException ex) {
6 System.out.println("The specific path is inaccessible");
7 ex.printStackTrace(System.err);
8 }
9 System.out.println("Goodbye!");
10 }

Listing 26 – Exemple de code de lecture dans un fichier texte : gestion de la ressources FileReader.

Le listing 27 illustre l’utilisation d’un FileReader au travers de l’interface Reader.

1 public static void displayData(Reader reader) throws IOException {


2 char[] tampon = new char[TAMPON_SIZE];
3 int nbrOctetsLus = reader.read(tampon);
4 while (nbrOctetsLus > 0) {
5 System.out.print(new String(tampon, 0, nbrOctetsLus));
6 clearTampon(tampon);
7 nbrOctetsLus = reader.read(tampon);
8 }
9 // The last '\n' doens't exist in the file
10 System.out.println("");
11 }
12

13 public static void clearTampon(char[] tampon) {


14 for(int i = 0 ; i < TAMPON_SIZE ; ++i)
15 tampon[i] = 0;
16 }

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.

1 public static void main(String[] args) {


2 Path cheminFichierLu = Paths.get(PathsFilesExample.getCurrentPath(), "src", "main", "resources", "inUTF8.tx
3 try (BufferedReader bufferedReader = Files.newBufferedReader(cheminFichierLu, StandardCharsets.UTF_8)) {
4 displayData(bufferedReader);
5 } catch (IOException ex) {
6 System.out.println("The specific path is inaccessible");
7 ex.printStackTrace(System.err);
8 }
9 System.out.println("Goodbye!");
10 }

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

1 public static void displayData(BufferedReader bufferedReader) throws IOException {


2 try {
3 String line = bufferedReader.readLine();
4 while (line != null) {
5 // '\n' is not read, it's a line separator for the readLine method.
6 System.out.println(line);
7 line = bufferedReader.readLine();
8 }
9 } catch (MalformedInputException ex) {
10 System.out.println("The file is not enconded in the expected charset.");
11 }
12 }

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.

1 public static void main(String[] args) {


2 Path cheminFichierLu = Paths.get(PathsFilesExample.getCurrentPath(), "src",
3 "main", "resources", "inANSI.txt");
4 Path cheminFichierEcrit = Paths.get(PathsFilesExample.getCurrentPath(), "src",
5 "main", "resources", "outANSI.txt");
6

7 try (Reader entree = new FileReader(cheminFichierLu.toString())) {


8 try(Writer sortie = new FileWriter(cheminFichierEcrit.toString())) {
9 copyFromTo(entree, sortie);
10 }
11 } catch (IOException ex) {
12 System.out.println("The specific path is inaccessible");
13 ex.printStackTrace(System.err);
14 }
15 System.out.println("Goodbye!");
16 }
17

18 public static void copyFromTo(Reader reader, Writer writer) throws IOException {


19 char[] tampon = new char[128];
20 int nbrOctetsLus = reader.read(tampon);
21 while (nbrOctetsLus > 0) {
22 String enMajuscules = new String(tampon, 0, nbrOctetsLus).toUpperCase();
23 writer.write(enMajuscules.toCharArray());
24 nbrOctetsLus = reader.read(tampon);
25 }
26 }

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.

1 public static void main(String[] args) {


2 Path cheminFichierLu = Paths.get(PathsFilesExample.getCurrentPath(), "src",
3 "main", "resources", "inUTF8.txt");
4 Path cheminFichierEcrit = Paths.get(PathsFilesExample.getCurrentPath(), "src",
5 "main", "resources", "outUTF8.txt");
6

7 try (BufferedReader bufferedReader = Files.newBufferedReader(cheminFichierLu,


8 StandardCharsets.UTF_8)) {
9 try(BufferedWriter bufferedWriter = Files.newBufferedWriter(cheminFichierEcrit,
10 StandardCharsets.UTF_8)) {
11 copyFromTo(bufferedReader, bufferedWriter);
12 }
13 } catch (IOException ex) {
14 System.out.println("The specific path is inaccessible");
15 ex.printStackTrace(System.err);
16 }
17 System.out.println("Goodbye!");
18 }
19

20 public static void copyFromTo(BufferedReader entree, BufferedWriter sortie) throws IOException {


21 String line = entree.readLine();
22 while (line != null) {
23 sortie.write(line.toUpperCase());
24 line = entree.readLine();
25 if(line != null)
26 sortie.write("\n");
27 }
28 }

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 ?

6.3 Fichiers binaires


Dans la continuité de la lecture et l’écriture de fichier texte, Java propose une famille de classes
dédiées à la lecture et l’écriture de fichiers binaires. Les figures 6.3 et 6.4 présentent la hiérarchie
des classes respectivement pour la lecture et l’écriture des fichiers binaires incluses dans le package
Java.io.

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

(OutOfMemoryError) qui provoquera l’arrêt immédiat du programme. Il n’est pas recommandé


d’utiliser cette fonction pour lire un fichier.

1 public static void main(String[] args) {


2 Path cheminFichierLu = Paths.get(PathsFilesExample.getCurrentPath(), "src", "Rsc", "data.txt");
3 Path cheminFichierEcrit = Paths.get(PathsFilesExample.getCurrentPath(), "src", "Rsc", "out.bin");
4

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

16 public static void writeTo(List<String> lines, Path path) throws IOException {


17 try(FileChannel canal = (FileChannel)Files.newByteChannel
18 (path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
19 ByteBuffer tampon = ByteBuffer.allocate(4*lines.size());
20

21 for (String line : lines) {


22 tampon.putInt(Integer.parseInt(line));
23 }
24 tampon.position(0); //Remise du pointeur à zéro
25 while(tampon.hasRemaining()) {
26 canal.write(tampon);
27 }
28 }catch (NumberFormatException ex) {
29 System.out.println("File contant doesn't correspond to the expected format.");
30 } catch(IOException ex) {
31 System.out.println("Error while writing in file");
32 }
33 }

Listing 32 – Exemple de code d’écriture dans un fichier binaire.

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.

1 public static void main(String[] args) {


2 Path cheminFichierLu = Paths.get(PathsFilesExample.getCurrentPath(), "src", "Rsc", "out.bin");
3

4 try(FileChannel canal = (FileChannel)Files.newByteChannel


5 (cheminFichierLu, StandardOpenOption.READ)) {
6 ByteBuffer tampon = ByteBuffer.allocate(4);
7 int octetsLus = canal.read(tampon);
8 while(octetsLus > 0) {
9 tampon.position(0);
10 int primeNumber = tampon.getInt();
11 System.out.printf("%d\n", primeNumber);
12 tampon.clear();
13 octetsLus = canal.read(tampon);
14 }
15 } catch(IOException ex) {
16 System.err.println("Error while reading in file");
17 }
18 System.out.println("Goodbye!");
19 }

Listing 33 – Exemple de code de lecture dans un fichier binaire.


54 CHAPITRE 6. LES FICHIERS EN JAVA

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.

1 public static int getNthPrimeNumber(int n) {


2 Path cheminFichierLu = Paths.get(PathsFilesExample.getCurrentPath(), "src", "Rsc", "out.bin");
3 try(FileChannel canal = (FileChannel)Files.newByteChannel
4 (cheminFichierLu, StandardOpenOption.READ)) {
5 canal.position((n-1)*4);
6 ByteBuffer tampon = ByteBuffer.allocate(4);
7 int octetsLus = canal.read(tampon);
8 if(octetsLus > 0) {
9 tampon.position(0);
10 return tampon.getInt();
11 }
12 } catch(IOException ex) {
13 System.err.println("Error while reading in file");
14 }
15 return -1;
16 }

Listing 34 – Exemple de code de lecture dans un fichier binaire.

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.

7.1 Architecture des classes


La classe Stream est une classe abstraite qui propose un accès à une séquence d’octets. Son
interface permet tant l’écriture que la lecture d’octets [16]. Les classes concrètes ont pour objectif
de lier cette séquence d’octets à une source/destination. Si nous souhaitons manipuler des octets
d’une zone mémoire nous utiliserons une instance de la classe MemoryStream, pour le réseau
NetworkStream et pour un fichier FileStream. La figure 7.1 présente la hiérarchie de ces classes.

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#

— BufferedStream : ajoute un buffer entre la source/destination et le client. De cette manière,


la communication avec la source est optimisée [17].
— CryptoStream : fait un lien entre le stream et un outil de cryptage. Elle permet le cryptage
à l’écriture et le décryptage à la lecture [18].
— GzipStream : fait un lien avec l’outil de compression gzip. Les données sont alors compressées
à l’écriture et décompressées à la lecture [19].
— DeflateStream : fait un lien avec l’outil de compression utilisant l’algorithme Deflate. Les
données sont alors compressées à l’écriture et décompressées à la lecture [20].
La figure 7.2 présente le schéma de classe avec héritages et compositions des décorateurs de la
classe Stream.

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.

Figure 7.3 – Schéma de classe de l’adaptateur pour la classe StreamReader.[Réalisé en PlantUML


[13]]

Il existe plusieurs adaptateurs, voici une partie de ceux-ci :


— StreamReader : permet la lecture de caractères dans un fichier [21]
— StreamWriter : permet l’écriture de caractères dans un fichier [22]
— BinaryReader : facilite la lecture d’octets dans un fichier [23]
— BinaryWriter : facilite l’écriture d’octets dans un fichier [24]
7.2. CRÉER UN STREAM 57

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.

7.2 Créer un stream


Le listing 35 illustre un exemple de construction d’une instance de classe FileStream. Les streams
accèdent à des ressources. Ils implémentent donc l’interface IDisposable. C’est pour cette raison
que la création d’une instance d’une classe héritant de Stream se fera toujours à l’aide du mot clé
using (voir chapitre 3).

1 public void CreateFileStream(string path)


2 {
3 using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)
4 //Use fs
5 }

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

Le listing 36 illustre un exemple d’utilisation des décorateurs et adaptateurs proposés par le


langage.

1 public void CreateCryptoFileStream(string path, ICryptoTransform encryptor)


2 {
3 using FileStream fs = new FileStream(path, FileMode.Append, FileAccess.Write);
4 using CryptoStream cfs = new CryptoStream(fs, encryptor, CryptoStreamMode.Write);
5 using StreamWriter streamWriter = new StreamWriter(cfs);
6 //Use streamWriter
7 }

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#

7.3 Fichiers binaires


Le listing 37 détaille un exemple d’écriture et lecture dans un fichier binaire. Le programme
convertit la chaine de caractères ”Bonjour à vous” en tableau d’octets. Ce tableau est écrit dans
le fichier via l’appel à la méthode Write. En delà de prendre le tableau d’octets en argument, elle
permet de préciser un offset et un count. Par ces paramètres nous pouvons préciser le nombre
d’octets à ignorer en début de tableau (offset) et le nombre maximum d’octet à écrire (count).

1 static void Main(string[] args)


2 {
3 using (FileStream output = new FileStream(@"c:\TEMP\SAD\data.txt", FileMode.Create))
4 {
5 byte[] asByte = Encoding.UTF8.GetBytes("Bonjour à tous");
6 output.Write(asByte, 0, asByte.Length);
7 }
8

9 using (FileStream input = new FileStream(@"c:\TEMP\SAD\data.txt", FileMode.Open))


10 {
11 byte[] bytesRead = new byte[input.Length];
12 input.Read(bytesRead, 0, bytesRead.Length);
13 Console.Write(\$"Lu : {Encoding.UTF8.GetString(bytesRead)}");
14 }
15 }

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

7.4 Fichiers textes


Le listing 38 propose une adaptation du code du listing 37. Pour faciliter le traitement des
chaines de caractères, le programme utilise les adaptateurs StreamWriter et StreamReader.

1 static void Main(string[] args)


2 {
3 using (TextWriter output = new StreamWriter(
4 new FileStream(@"c:\TEMP\SAD\data.txt", FileMode.Create),Encoding.UTF8))
5 {
6 output.Write("Bonjour à tous");
7 }
8

9 using (TextReader input = new StreamReader(


10 new FileStream(@"c:\TEMP\SAD\data.txt", FileMode.Open),Encoding.UTF8))
11 {
12 Console.Write(\$"Lu : {input.ReadLine()}");
13 }
14 }

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.

La figure 8.1 montre un diagramme de classe pour un Repository. L’interface DataRepository


propose un série de méthode pour le CRUD (Create/Read/Update/Delete) des données. Les deux
classes, qui en héritent, implémentent cette interface concrètement avec des fichiers XML (XML-
Repository) et JSON (JSONRepository) respectivement. Le client ne doit connaı̂tre que l’interface
du Repository. Ce dernier ignore alors la nature de la ressource qui contient les données (BD,
fichier JSON, fichier XML, ...). La nature et la gestion de la ressource des données ne sont pas de
la responsabilité du Client. En évitant d’ajouter un nouvelle responsabilité au client, nous veillons
au respect du principe SRP (Single Responsability Principle [28]). Il est possible d’ajouter de
nouvelles ressources sans impacter le code existant, en ajoutant une nouvelle classe concrète qui
implémente l’interface DataRepository. Cette dernière affirmation conduit au respect du principe
OCP (Open Close Principle [28]). Le programme est fermé au modification, car nous ne modifions
aucune classe existante, et ouvert à l’extension car nous pouvons ajouter de nouvelles ressources.

8.2 Librairies orientées objets


IL existe un grand nombre de librairies qui permettent de sérialiser un objet, une collection
d’objets et même un réseau d’objets de façon quasi automatique. Le sujet de cette section est
d’étudier l’intégration de tels outils dans un projet.

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

Intégration d’une BD dans une


application OO

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.

9.1 Orienté objet vs Base de Données


Tant en base de données que dans le design orienté objet, les données sont regroupées de façon
cohérente. Concrètement, les données sont regroupées dans des tables en base de données (BD) et
dans des classes en orienté objet (OO). Les ensembles de valeurs sont alors appelés tuples en BD
et objet en OO.

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.

Figure 9.3 – Exemple d’une hiérarchie de classes.

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.

Solution OCP DRY Perf.


A. une table X X V
B. une table par classe concrète V X V
C. une table par classe V V X

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

Table 9.2 – Liste des propriétés des compositions en OO et leurs valeurs.

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

tierce, les deux clés étrangères seront toujours ”N OT N U LL”.


— Optionnel
— Requise ajoute un ”N OT N U LL” à la clé étrangère.
La propriété d’intensité sera décisive dans le cas M anyT oM any. Si une composition est forte,
ce qui pour rappel implique que l’existence de l’objet est lié à l’objet qui le contient. La destruction
des données du tuple de la table ”contenue” doit être liée à celui de la table qui la contient.
Exercices récapitulatifs

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

(a) (b) (c) (d) (e) (f)

Figure 9.8 – Exemples non exhaustif des différentes associations OO.

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.

Les solutions orientées objets proposent de dialoguer directement en objets. L’utilisation du


langage SQL est alors cachée du programmeur. La mise en place et la configuration de ce type
d’approche est beaucoup plus complexe. Les performances peuvent être variables et dépendre de
la configuration ou de l’outil lui-même.
Conseil
Les solutions orientés objets vont donc dialoguer en objets. Soyez prudent quant à l’utilisation
de ces objets dans votre programme. Il est important de rendre votre programme indépendant
de la technologie choisie pour la sauvegarde de ses données.

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

Java DataBase Connectivity

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 }

Listing 39 – Extrait du fichier Build.gradle : gestion de la version de Java.

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 }

Listing 40 – Extrait du fichier Build.gradle : choix du répository.

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

5 // Use JUnit Jupiter Engine for testing.


6 testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
7

8 compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.6'


9 compile group: 'mysql', name: 'mysql-connector-Java', version: '8.0.11'
10 compile group: 'org.apache.derby', name: 'derby', version: '10.14.2.0'
11 }

Listing 41 – Extrait du fichier Build.gradle : déclaration des dépendances.

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.

1 static public String db = "jdbc:mysql://ip:port/BDName?useUnicode=true & "


2 + "useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false & "
3 + "serverTimezone=UTC&useSSL=false";
4 try(Connection connection = DriverManager.getConnection(db, username, password)){
5 // use connection
6 } catch (SQLException e) {
7 System.out.println("Failed to save members");
8 }

Listing 43 – Création d’une connection.

Une connexion est créée à l’appel de la méthode ”DriverManager.getConnection(db, username,


password)”. Le premier paramètre est l’URL 3 de la base de données. Cette URL contient le type
de SGBD, les informations nécessaires à la connexion avec le serveur hébergeant le SGBD (IP /
port). L’identifiant de la base de données (BDNAme). Le reste de l’URL contient des paramètres
non essentiels qui éliminent des alertes. La méthode prend deux autres paramètres qui représentent
l’accréditation de la base de données : identifiant et mot de passe.
2. pour une base de données local Derby : ”org.apache.derby.jdbc.EmbeddedDriver”
3. Universal Resource Locator
80 CHAPITRE 10. JAVA DATABASE CONNECTIVITY

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.

Figure 10.1 – Diagramme de classes pour un Storage de base de données.

La classe SQLDataStorageF actory a la responsabilité de connaitre toutes les informations


nécessaires à la connexion. Lors de l’appel à la méthode newStorage(), une nouvelle connexion
est créée et transmise au constructeur de SQLDataStorage. La fermeture de la classe est de la
responsabilité de SQLDataStorage. Pour ce faire, celle-ci applique le principe RAII adapté au
langage utilisé. L’appel à la méthode newStorage() de façon adéquate pour assurer l’utilisation
des méthodes de fermeture et garantir la fermeture de la connexion (voir chapitre 3).

10.3 Faire une requête : Statement


La librairie JDBC propose plusieurs classes pour créer des requêtes :
— Statement [32] ;
— P reparedStatement [33] ;
— CallalbleStatement [34]
Ces trois classes sont en lien avec la base de données. En tant qu’utilisateur d’une ressource,
elles implémentent AutoCloseable. Elles doivent donc être créées à l’aide d’un ”T ry − W ith −
Ressources”.
10.3. FAIRE UNE REQUÊTE : STATEMENT 81

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.

Le listing 44 présente des exemples d’utilisation de Statement et P reparedStatement. Les deux


instances de classe sont créées par des appels aux méthodes createStatement()
et prepareStatement(String) respectivement de la connexion.

1 try (Statement createStatement = connection.createStatement()) {


2 createStatement.executeUpdate("SQL query");
3 }
4

5 try (PreparedStatement insertStmt = connection.prepareStatement(


6 "SQL query use ? to place parameter")) {
7 insertStmt.setString(1,p.getName());
8 insertStmt.setString(2, p.getDescription());
9 insertStmt.executeUpdate();
10 }

Listing 44 – Exemple d’utilisation de Statement et PreparedStatement.

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.

4. Data Definition Language


5. Data Manipulation Language
10.4. MODIFICATIONS 83

1 try(Statement createStatements = connection.createStatement()) {


2 connection.setAutoCommit(false);
3 //Requ^
etes Sql formant la transaction
4 connection.commit();
5 } catch (SQLException ex) {
6 try {
7 connection.rollback();
8 } catch (SQLException e) {
9 throw new UnableToRollbackException(e);
10 }
11 throw new UnableToSetupException(ex);
12 } finally {
13 try {
14 con.setAutoCommit(true);
15 } catch(SQLException ex) {
16 throw new TransactionNotSupportedException(ex);
17 }
18 }

Listing 45 – Exemple d’implémentation d’une transaction.

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.

1 try (Statement createStatement = connection.createStatement()) {


2 createStatement.executeUpdate("CREATE TABLE PROJECT_ELEMENT(" +
3 "id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " +
4 "name VARCHAR(60) NOT NULL), " +
5 "description VARCHAR (1000) NOT NULL)"
6 );
7 }

Listing 46 – Exemple d’implémentation d’une requête de modification à l’aide d’une Statement.

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

1 try (PreparedStatement insertStmt = connection.prepareStatement(


2 "INSERT INTO PROJECT_ELEMENT(name,description) VALUES(?,?)",
3 Statement.RETURN_GENERATED_KEYS)) {
4 insertStmt.setString(1,p.getName());
5 insertStmt.setString(2, p.getDescription());
6 insertStmt.executeUpdate();
7

8 ResultSet generatedKeys = insertStmt.getGeneratedKeys();


9 if(generatedKeys.next()) {
10 p.setId(insertStmt.getGeneratedKeys().getInt(1));
11 }
12 }

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.

10.5 Lire des données


Les applications ont besoin de récupérer des données dans une base de données. Une requête
SELECT permet de réaliser cette opération. Lors de l’exécution d’une requête SELECT à l’aide
d’un Statement (quel qu’il soit), l’appel à la commande executeQuery retourne un objet du type
ResultSet voir listing 48.

1 try (Statement stmt = conn.createStatement()) {


2 ResultSet rs = stmt.executeQuery("select person_id, first_name, last_name, dob,
3 income from person");
4 } catch (SQLException) {
5 // Do what needs to be done
6 }

Listing 48 – Exemple de récupération d’un ResultSet lors de l’exécution d’une requête SELECT.

Un ResultSet représente un curseur sur le tableau des résultats. De ce point de vue, un


ResultSet est en lien avec la base de données et est donc une ressource. Il est important de
le traiter comme tel.
86 CHAPITRE 10. JAVA DATABASE CONNECTIVITY

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.

Figure 10.3 – Manipulation d’une ResultSet sur un tableau 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).

1. ActiveX Data Object

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

1 private static DbProviderFactory _factory;


2 public SQLMembersStorage()
3 {
4 try
5 {
6 _factory = DbProviderFactories.GetFactory("MySql.Data.MySqlClient");
7 }
8 catch (ArgumentException ex)
9 {
10 throw new ProviderNotFoundException(\$"Unable to load prodiver {providerName}",ex);
11 }
12 }

Listing 51 – Création d’une DbProviderFactory dans le cas d’un SGBD MySQL.

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 }

Listing 53 – Création d’une connection pour un SGBD MySQL .

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

4. Pour une base de données SQLServerLocale : ”DataSource = (localdb)


M sSqlLocalDb; InitialCatalog = dbL udF rT est; AttachDbF ilename = P athT oDataBaseF ile”
11.2. FAIRE UNE REQUÊTE : IDBCOMMAND 93

11.2 Faire une requête : IDbCommand

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 }

Listing 54 – Création d’une commande non paramétrée.

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#

1 private void Insert(Project project) {


2 const string insertQuery =
3 @"INSERT INTO PROJECT_ELEMENT(name,description, parent_id, dtype)
4 output INSERTED.ID
5 VALUES(@name,@description,null,@dtype)";
6 using (var insertCmd = _con.CreateCommand()) {
7 insertCmd.CommandText = insertQuery;
8 var nameParam = insertCmd.CreateParameter();
9 nameParam.ParameterName = "@name";
10 nameParam.Value = project.Name;
11 nameParam.DbType = DbType.String;
12

13 insertCmd.Parameters.Add(nameParam);
14 //...
15 }
16 }

Listing 55 – Création d’une commande paramétrée.

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.

11.3 Ajouter et Modifier des données


Pour la modification, il est parfois nécessaire de faire une transaction. La nature et la nécessité
d’une transaction ont déjà été discutées plus tôt dans syllabus, veuillez consulter la section 10.4.1.
ADO.N ET intègre la notion de transaction dès sa conception. De fait, la librairie propose une
interface IDbT ransaction. Son utilisation est extrêmement simple. Avant la création de la ou les
commandes faisant partie de la transaction, le début de celle-ci est marqué par un appel à la
méthode BeginT ransaction() sur la connexion. Cette méthode retourne un objet implémentant
l’interface IDbT ransaction. Pour que chaque commande connaisse sa transaction via sa propriété
T ransaction. Sa valeur doit être fixée égale la référence de l’objet IDbT ransaction créé par la
connexion. Lorsque l’ensemble des commandes ont été réalisée, l’appel à la méthode Commit() de
la transaction permet de les exécuter ensemble sur la base de données distante. En cas de problème,
l’annulation de toutes les commandes est concrétisée par un appel à la méthode Rollback() de la
transaction.
11.3. AJOUTER ET MODIFIER DES DONNÉES 95

1 IDbTransaction transaction = null;


2 try
3 {
4 using (IDbConnection con = NewConnection())
5 using (transaction = con.BeginTransaction())
6 using (IDbCommand dropCommand = con.CreateCommand())
7 {
8 dropCommand.CommandText = "DROP TABLE PROJECT_ELEMENT";
9 dropCommand.Transaction = transaction;
10 dropCommand.ExecuteNonQuery();
11 //Autres commandes
12 transaction.Commit();
13 }
14 }
15 catch (SqlException ex)
16 {
17 transaction?.Rollback();
18 Console.WriteLine(ex.Message);
19 }

Listing 56 – Exemple de transaction.

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 }

Listing 57 – Exemple de récupération des identifiants de base de données pour le SGBD :


SQLServer.

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#

qui expose la propriété LastInsertedId.

Exercices
Adapter le code du listing 57 dans le case d’utilisation de M ySQL.

11.4 Lire des données


La consultation des données en provenance d’une base de données est souvent réalisée par une
requête du type SELECT . Cette dernière se réalise comme les autres requêtes en ADO.N ET ex-
cepté pour son exécution. La méthode ExecuteReader() sera appelée au lieu de ExecuteN onQuery().
Cette méthode retourne un objet IDataReader permettant contenant la réponse de la requête.

Le IDataReader est à ADO.N ET ce qu’est le ResultSet à JDBC. Cet objet permet de


naviguer dans le tableau des résultats comme le ResultSet (voir section 10.5). La méthode read()
est le pendant de next() et permet de passer de ligne en ligne.

1 using (IDataReader reader = selectCmd.ExecuteReader())


2 {
3 while (reader.Read())
4 {
5 (string) reader["name"],
6 (string) reader["description"]
7 //...
8 }
9 }

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

1 using (IDataReader reader = selectCmd.ExecuteReader())


2 {
3 while (reader.Read())
4 {
5 reader.GetString(0), //nom
6 reader.GetString(1),//description
7 //...
8 }
9 }

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

Tout développement d’applications doit être accompagnée de tests. Dans la philosophie T DD 1 ,


les tests vont même précéder l’implémentation des méthodes. Dans le cadre de classe qui gère des
ressources externes au programme, nous parlerons de tests d’intégration. L’objectif de ce chapitre
est de discuter comment mettre en place des tests avec ressources.

12.1 Une ressource pour le test, pourquoi ?


La ressource qui est utilisée par le programme en fonctionnement pour les clients, sera appelé
ressource de production. Cette ressource se limitera à un fichier ou un accès à une base de données
dans le cadre de ce syllabus. Le premier point important est la création et l’utilisation de res-
source spécifique pour le test. En effet, les tests ont pour objectif de vérifier que le code effectue
la tâche pour laquelle il a été implémenté. De plus, il permet de placer des balises pour éviter
toute régression future. Il donc ”normal” et plus ou moins fréquent d’un test échoue. Si la res-
source utilisé pour les tests est partagé avec le programme, en cas d’échec de test, la ressource peut
être corrompue. L’échec d’un test en cours de développement d’une nouvelle fonctionnalité peut
alors générer un dysfonctionnement du programme chez les clients ! De plus certains tests peuvent
(doivent) modifier les données sauvegardées dans la ressource. Si ces données sont partagées avec
le programme, le comportement de ce dernier peut être altéré. Afin de prévenir tous ces cas de
figure, une ressource spécifique est utilisée pour les tests.

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.

1. Test Driven Development

99
100 CHAPITRE 12. TESTS ET RESSOURCE

12.2 Une ressource pour le test, concrètement !


Dans le projet de test, un dossier ressource sera créé. Les fichiers spécifiques aux tests se-
ront alors créés. Ces derniers devront respecter le format attendu et contenir des échantillons de
données pertinentes. Dans le cas de base de données, l’approche dépendra de la nature de la solu-
tion : JDBC en Java ou ADO.N ET en C#.

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.

12.3 Tester la lecture


Ces tests ne modifient pas les données présentes dans la ressource de test. Pour que ces tests
couvrent toutes les possibilités, il est important que les données intégrées à la ressource de test
soient représentatives. Néanmoins, la réussite des tests dépendra de l’implémentation du code
appelé et de l’intégrité de la ressource utilisée pour les tests. Pour limiter la dépendance à la
ressource, dans le cas de fichier, un fichier dédié uniquement à la lecture sera créé. Il est évidemment
trop lourd de faire de même dans le cas d’une base de données. Si des tables peuvent être modifiées
et lues par l’application, les tests de lectures resteront dépendant de l’intégrité de la ressource.

12.4 Tester l’écriture


Tester l’écriture est la partie la plus délicate et longue. En effet, pour que les tests réussissent,
il faut que les données contenues dans la ressource soient identiques à chaque lancement et ce pour
chaque test. Pour ce faire, les données doivent être initialisées avant chaque test. Des mots-clés
sont proposés dans les librairies de tests pour créer des méthodes appelées avant chaque test (ou
avant tous les tests de la classe).

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

Gestion d’erreur sans exception

1 public class Arithmetic {


2 public class ArithmeticResult {
3 private String message;
4 private int result;
5

6 public ArithmeticResult(int result) {


7 this.result = result;
8 this.message = "";
9

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

31 public ArithmeticResult divide(int a, int b) {


32 if(b == 0)
33 return new ArithmeticResult(new String("Cannot divide by zero !"));
34
105

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.

1 public static void main(String[] args) {


2 Arithmetic arithmetic = new Arithmetic();
3

4 Arithmetic.ArithmeticResult result = arithmetic.divide(10, 2);


5 System.out.println(result.toString());
6 result = arithmetic.divide(10, 0);
7 System.out.println(result.toString());
8 }

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

1. nested class en anglais


106 ANNEXE A. GESTION D’ERREUR SANS EXCEPTION
Annexe B

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.

Figure B.1 – Schéma de classe servant de structure au design pattern décorateur.[Réalisé en


PlantUML [13]]

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

Le listing suivant 62 présente un exemple de décorateur. La méthode main souligne un propriété


importante du design pattern décorateur. Ceux-ci peuvent être cumulés autant que souhaité. Cette
exemple réalise 3 décorations sur l’instance astonMartin de la classe Voiture. Il aussi possible de
cumuler des décorations à l’aide de décorateurs différents. De cette manière nous additionnons des
fonctionnalités proposées par chacun d’eux.
109

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

40 Console.WriteLine(astonMartin.Prix); // affiche 1110.99


41 }
42 }

Listing 62 – Exemple simple d’un décorateur en C# [38].


110 ANNEXE B. DÉCORATEUR
Annexe C

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

Figure C.1 – Schéma de classe servant de structure au design pattern adaptateur.[Réalisé en


PlantUML [13]]

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 }

Listing 63 – Exemple simple d’un adaptateur en C# : MainApp [39].

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 }

Listing 64 – Exemple simple d’un adaptateur en C# : classe But [39].


113

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 }

Listing 65 – Exemple simple d’un adaptateur en C# : classe Adapter [39].


114 ANNEXE C. ADAPTATEUR
Bibliographie

[1] Java10 doc exception, Accessed : 2019. https://docs.oracle.com/javase/10/docs/api/java/


lang/Exception.html.
[2] Csharp exception handling best practices, Accessed : 2019. https://stackify.com/
csharp-exception-handling-best-practices/.
[3] Microsoft doc .net, Accessed : 2019. https://docs.microsoft.com/en-us/dotnet/api/system.
exception?view=netframework-4.8/.
[4] Système d’exploitation, Accessed : 2019. https://learn.helmo.be/course/view.php?id=1009.
[5] wikipedia, Accessed : 2019. https://fr.wikipedia.org/wiki/American Standard Code for
Information Interchange.
[6] w3techs, Accessed : 2019. https://w3techs.com/technologies/overview/character encoding.
[7] Json, Accessed : 2020. https://www.json.org/json-en.html.
[8] Java 10 documentation - class path, Accessed : 2020. https://docs.oracle.com/javase/10/
docs/api/java/nio/file/Path.html.
[9] Java 10 documentation - class paths, Accessed : 2020. https://docs.oracle.com/javase/10/
docs/api/java/nio/file/Paths.html.
[10] Java 10 documentation - class files, Accessed : 2020. https://docs.oracle.com/javase/10/docs/
api/java/nio/file/Files.html.
[11] Développons en java, Accessed : 2020. https://www.jmdoudoux.fr/java/dej/chap-flux.htm.
[12] Java10 all classes documentation, Accessed : 2020. https://docs.oracle.com/javase/10/docs/
api/index.html?overview-summary.html.
[13] Plantuml, Accessed : 2020. https://www.planttext.com/.
[14] Standardopenoption, Accessed : 2020. https://docs.oracle.com/javase/10/docs/api/java/nio/
file/StandardOpenOption.html.
[15] Bytebuffer, Accessed : 2020. https://docs.oracle.com/javase/10/docs/api/java/nio/
ByteBuffer.html.
[16] Stream, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.io.stream?
view=netframework-4.8.
[17] Bufferedstream, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.io.
bufferedstream?view=netframework-4.8.

115
116 BIBLIOGRAPHIE

[18] Cryptostream, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.


security.cryptography.cryptostream?view=netframework-4.8.
[19] Gzipstream, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.io.
compression.gzipstream?view=netframework-4.8.
[20] Deflatestream, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.io.
compression.deflatestream?view=netframework-4.8.
[21] Streamreader, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.io.
streamreader?view=netframework-4.8.
[22] Streamwriter, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.io.
streamwriter?view=netframework-4.8.
[23] Binaryreader, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.io.
binaryreader?view=netframework-4.8.
[24] Binarywriter, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/system.io.
binarywriter?view=netframework-4.8.
[25] Stream architecture, Accessed : 2020. https://www.oreilly.com/library/view/c-60-in/
9781491927090/ch15.html.
[26] Ben Albahari and Albahari Jospeh. C# 6.0 in a Nutshell. O’Reilly Media, Inc., Sebastopol,
California, 2015.
[27] File class from system.io c#, Accessed : 2020. https://docs.microsoft.com/en-us/dotnet/api/
system.io.file?view=netframework-4.8.
[28] Robert C. Martin. Agile Software Development. Principles, Patterns and Practices. Pearson,
Essex, England, 2014.
[29] Service oriented architectur, Accessed : 2020. http://blog.octo.com/soa-par-la-pratique/.
[30] Maven, Accessed : 2020. https://maven.apache.org/guides/getting-started/
maven-in-five-minutes.html.
[31] Gradle, Accessed : 2020. https://docs.gradle.org/current/userguide/userguide.html.
[32] Statement, Accessed : 2020. https://docs.oracle.com/javase/7/docs/api/java/sql/Statement.
html.
[33] Preparedstatement, Accessed : 2020. https://docs.oracle.com/javase/7/docs/api/java/sql/
PreparedStatement.html.
[34] Callalblestatement, Accessed : 2020. https://docs.oracle.com/javase/7/docs/api/java/sql/
CallableStatement.html.
[35] Statement vs preparedstatement, Accessed : 2020. https://www.linkedin.com/pulse/
jdbc-java-statementpreparedstatement-youssef-najeh/.
[36] Ado.net, Accessed : 2020. https://docs.microsoft.com/fr-fr/dotnet/framework/data/adonet/.
[37] Erich Gamma, Richard Helm, Johnson Ralph, and John Vlissides. Design patterns : catalogues
de modèles de conceptions réutilisables. Addison-Wesley, Reading, Massachusetts, 1995.
BIBLIOGRAPHIE 117

[38] Exemple de décorateur, Accessed : 2020. https://fr.wikipedia.org/wiki/D%C3%A9corateur


(patron de conception)#Exemple en C#.
[39] Exemple d’adaptateur, Accessed : 2020. https://www.dofactory.com/net/
adapter-design-pattern.

Vous aimerez peut-être aussi