Activité : Introduction aux patrons de conception : patrons de création d'objets (partie 1/3)

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, mais en respectant le code d’honneur.

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

Objectifs

À la fin de ce travail, vous devez :

  1. Connaître la notion de patron de conception (design pattern)
  2. Connaître la notion de patron de conception de création (creational design pattern)
  3. Être capable d’expliquer le patron de conception « Static Factory Method »
  4. Être capable d’expliquer le patron de conception « Abstract Factory »

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 :

  • maven
  • Visual Studio Code

Documents :

Mise en route

Rendez-vous dans le répertoire de vos projets, lancez la commande ci-après et lorsque le système vous le demande, saisissez votre nom et votre mot de passe I-FR.

1
git clone https://gitlab.epai-ict.ch/m226/contact-manager.git --single-branch

Déplacez-vous dans le répertoire contact-manager et lancer la commande code . pour ouvrir le projet dans VSC (Visual Studio Code).

Création d’une classe Person

Commencez par implémenter interface Person et une classe PersonImpl selon le diagramme ci-dessous.

Fig. 1 – Diagramme de classe : Person
Fig. 1 – Diagramme de classe : Person

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
  • email

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.

Fig. 2 – Diagramme de classe : Person
Fig. 2 – Diagramme de classe : PersonDataMapper

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.
        }
    }
Fig. 3 – Lecture d’un fichier de type CSV

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("Un erreur est survenue.");
        }
    }
Fig. 4 – Programme de test

Charger une liste de personne à 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 
            }
        }
    }
Fig. 6 – Lecture d’un fichier de type XML

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("Un erreur est survenue.");
        }
    }
Fig. 7 – Programme de test

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’a 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ée 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.

Fig. 2 – Diagramme de classe : Person
Fig. 8 – Diagramme de classe : 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.

Utilisation 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("Un erreur est survenue.");
        }
    }
Fig. 9 – Exemple d'utilisation