Activité Introduction aux patrons de conception : patrons de création d'objets (partie 1/3)
Objectifs
À la fin de ce travail, vous devez :
- Connaître la notion de patron de conception (design pattern)
- Connaître la notion de patron de conception de création (creational design pattern)
- Être capable d’expliquer le patron de conception « Static Factory Method »
- Être capable d’expliquer le patron de conception « Abstract Factory »
Consigne
Pour ce travail on vous demande de prendre connaissance de la situation et de réaliser les tâches proposées. Si vous ne parvenez pas à terminer le travail en classe, il vous appartient de prendre sur votre temps pour l’achever.
Le code des tests unitaires doit être utilisé tel quel et ne doit pas être modifié. Le code des classes doit être conforme à la spécification fournie sous forme de tests unitaires et de diagramme UML.
Le travail est individuel. Vous pouvez communiquer en respectant le code d’honneur.
Résultat attendu
- Un projet Maven contenant les classes décrites
- Un petit document où vous aurez décrit ce que vous avez fait et les difficultés que vous aurez rencontrées.
Ressources
Logiciel :
- Machine virtuelle de développement
- Visual Studio Code
Documents :
- Capsule de théorie : Classes et instances
- Capsule de théorie : Héritage et sous‑typage
- Capsule de théorie : Polymorphisme
Situation
Vous êtes affecté à un projet de gestion de contacts et vous êtes chargé de réaliser un « Data Mapper ». Il s’agit d’un objet qui joue le rôle d’intermédiaire entre la couche « domaine » (domain layer) dans laquelle se trouvent les objets « métiers » (ici les objets de type Person) et la couche « source de données » (data source layer) où se trouvent des objets qui représentent des éléments techniques (fichiers, interpréteur xml, etc.).
Mise en route
Lancez la machine virtuelle de développement avec Vagrant. Lancez Visual Studio Code, connectez-vous à la machine virtuelle et ouvrez la fenêtre de terminal intégré.
Rendez-vous dans le répertoire de projets (~/projects
) et lancez la commande suivante :
1
git clone https://gitlab.epai-ict.ch/m226/contact-manager.git --single-branch
Lorsque le système vous y invite, saisissez votre nom et votre mot de passe I-FR.
Déplacez-vous dans le répertoire contact-manager
et lancer la commande code -r .
pour ouvrir le projet.
À vous de jouer !
Tâches
Créer une classe Person
Commencez par implémenter interface Person et une classe PersonImpl selon le diagramme ci-dessous.
Charger une liste de personne à partir d’un fichier CSVs
Dans le répertoire de votre projet, vous trouvez un fichier CSV qui contient une liste de personnes. Chaque ligne correspond à un enregistrement. Les champs de données sont séparés par des points-virgules. Chaque enregistrement contient les champs suivants :
- id
- firstname
- lastname
- street
- postalCode
- city
Il s’agit de réaliser une classe qui permet de charger ces données dans une instance de ArrayList selon le diagramme de la figure 2. La procédure readFile
doit être appelée dans le constructeur et doit charger la liste depuis le fichier passé en paramètre.
La figure 3 montre un extrait de code qui devrait vous aider à réaliser ce travail.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void readFile(String fileName) throws Exception {
// Lit toutes les lignes d’un fichier de texte et les stocke dans une
// liste de chaîne.
List<String> lines = Files.readAllLines(Paths.get(fileName), StandardCharsets.UTF_8);
// Itère la liste liste de chaîne (pour chaque chaîne de la liste)
for (String line : lines) {
// Scinde la ligne en un tableau. Le paramètre de split est le caractère
// utilisé pour séparer les champs (ici le caractère de tabulation).
String[] fields = line.split("\t");
int id = Integer.parseInt(fields[0]);
String firstName = fields[1];
// TODO : utiliser les champs pour créer un objet de type PersonImpl et l'ajouter à la liste.
}
}
Le code de la figure 4 est un programme de test pour vos classes. Son exécution affiche le nom et le prénom des personnes dans la sortie standard.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main( String[] args ) {
String fileName = "person.csv";
try {
PersonDataMapper dataMapper = new PersonDataMapperCsv(fileName);
Iterable<Person> persons = dataMapper.getAll();
for (Person p : persons) {
System.out.printf("%s %s\n", p.getFirstName(), p.getLastName());
}
} catch (Exception ex) {
System.out.println("Une erreur est survenue.");
}
}
Charger une liste de personnes à partir d’un fichier XML
Votre supérieur vous demande maintenant d’ajouter une nouvelle classe PersonDataMapperXml similaire à la classe PersonDataMapperCsv mais qui permet de charger des données depuis un fichier XML.
Dans le répertoire de votre projet, vous trouvez un fichier XML qui contient la même liste de personnes. La figure 5 montre une manière de charger cette liste en utilisant la bibliothèque de classes de Java (JCL).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
...
import java.io.File;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
...
private void readFile(String fileName) throws Exception {
File xmlFile = new File(fileName);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(xmlFile);
doc.getDocumentElement().normalize();
NodeList personNodeList = doc.getElementsByTagName("person");
for (int i = 0; i < personNodeList.getLength(); i++) {
Node node = personNodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element personElement = (Element)node;
int id = Integer.parseInt(personElement.getElementsByTagName("id").item(0).getTextContent());
String firstName = personElement.getElementsByTagName("firstname").item(0).getTextContent();
String lastName = personElement.getElementsByTagName("lastname").item(0).getTextContent();
String street = personElement.getElementsByTagName("address").item(0).getTextContent();
// TODO : utiliser les champs pour créer un objet de type PersonImpl et l'ajouter à la liste
}
}
}
Le code de la figure 7 est un programme de test pour le Data Mapper XML. Son exécution affiche le nom et le prénom des personnes dans la sortie standard.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main( String[] args ) {
String fileName = "person.xml";
try {
PersonDataMapper dataMapper = new PersonDataMapperXml(fileName);
Iterable<Person> persons = dataMapper.getAll();
for (Person p : persons) {
System.out.printf("%s %s\n", p.getFirstName(), p.getLastName());
}
} catch (Exception ex) {
System.out.println("Une erreur est survenue.");
}
}
Instancier la bonne classe en fonction de l’extension du fichier
Vous avez réalisé deux classes de type PersonDataMapper. Un client de votre bibliothèque peut désormais choisir entre deux formats de fichier pour cela, il n’a qu’à instancier l’une ou l’autre des classes en fonction de ses besoins. Mais votre supérieur aimerait qu’un client de votre bibliothèque n’ait pas à choisir lui-même la bonne classe en fonction de l’extension du fichier. En effet, si plusieurs clients ont besoin d’instancier un Data Mapper, le code de sélection doit être répété à chaque fois.
Il vous explique que pour résoudre ce problème, on peut avoir recours à une combinaison des patrons de conception « Static Factory Method » et « Abstract Factory ».
Le patron « Abstract Factory » consiste à définir un type abstrait dont chaque opération sert à instancier un certain type d’objets. Dans notre cas, on définit une interface DataMapperFactory avec une méthode createPersonDataMapper
qui prend un nom de fichier en paramètre et permet de créer un objet de type PersonDataMapper. Nous devons ensuite réaliser une factory concrète, c’est-à-dire une implémentation de cette interface (DataMapperFactoryImpl) qui pourra créer une instance de PersonDataMapperCsv ou PersonDataMapperXml en fonction de l’extension (.csv ou .xml).
Pour utiliser la factory abstraite (DataMapperFactory), un client doit obtenir une instance de la factory concrète (DataMapperFactoryImpl). Si le client utilise l’opérateur new
, il devient dépendent de la classe concrète DataMapperFactoryImpl, ce qui n’est pas souhaitable. Pour éviter cela, on peut mettre en oeuvre le patron « Static Factory Method » en implémentant dans l’interface DataMapperFactory, une méthode statique newInstance
qui renvoie une instance d’une factory concrète (DataMapperFactoryImpl).
La figure 8 montre le diagramme des classes que vous devez réaliser. Notez la notation de la méthode statique (méthode de classe) newInstance()
dans l’interface DataMapperFactory.
En Java, la classe String dispose d’une méthode endWith
qui peut sans doute vous être utile pour trouver l’extension de fichier.
Utiliser de la bibliothèque
Vous pouvez maintenant écrire un petit programme pour mettre en oeuvre le système que vous avez réalisé. La figure 9 vous présente un exemple. Notez que le client (la classe App
) ne dépend d’aucune classe concrète de votre bibliothèque.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
String fileName = "person.xml"; // ou "person.csv" les deux fonctionnent
DataMapperFactory factory = DataMapperFactory.newInstance();
try {
PersonDataMapper dataMapper = factory.createPersonDataMapper(fileName);
Iterable<Person> persons = dataMapper.getAll();
for (Person p : persons) {
System.out.printf("%s %s\n", p.getFirstName(), p.getLastName());
}
} catch (Exception ex) {
System.out.println("Une erreur est survenue.");
}
}