Activité Gérons nos items, relation « est un » et principe ouvert-fermé

Objectifs

À la fin de ce travail, vous devez :

  1. Connaître la relation « est un »
  2. Connaître le principe ouvert-fermé

Consigne

Le but de cette activité est de découvrir la notion d’interface en Java, la relation « est un » et le second principe SOLID, le principe ouvert-fermé.

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

Ressources

Logiciel :

  • maven
  • Visual Studio Code

Documents :

Situation

Vous êtes développeur dans un projet de logiciel de gestion de bibliothèque. Une réunion a été organisée pour faire le point avec le client à propos du projet de gestion de bibliothèque. Durant la discussion, vous apprenez que la bibliothèque permet d’emprunter des objets qui ne sont pas des livres. Il y a notamment des publications périodiques (journaux, magazine, etc.) et des films documentaires sous forme de DVD. Vous subodorez que ces documents n’ont pas exactement les mêmes caractéristiques que les livres. Pour en avoir le cœur net, vous demandez au client de préciser comment ceux-ci devraient être décrits.

À la fin de la discussion, il ressort de celle-ci que les périodiques sont caractérisés par :

  • un nom
  • un éditeur (publisher)
  • un numéro de volume (volume)
  • un numéro (issue)
  • une date de parution (publication date)
  • un ISSN (International Standard Serial Number)

Les films documentaires sont quant à eux caractérisés par :

  • un titre
  • une date de sortie (launch date)
  • un ou plusieurs réalisateurs (directors)
  • un ou plusieurs auteurs (writers)

À la fin de la réunion, le client ajoute que pour tous les types d’objets (livres, périodiques et documentaires), on doit pouvoir enregistrer la langue et une description.

De retour à votre bureau, vous entreprenez la modification du modèle de classe de votre application pour tenir compte de ces nouvelles informations. Vous commencez par ajouter les classes Periodical et Documentary, puis vous modifiez la classe Book. La figure 1 montre le résultat de ce travail.

Fig. 1 – Ajout des classes et correction
Fig. 1 – Ajout des classes et correction

C’est alors que vous rencontrez une difficulté. En effet, dans le modèle actuel, la classe Loan représente un emprunt et cette classe est associée à la classe Copy par l’attribut copy de type Copy. La classe Copy étant elle-même associée à la classe Book par l’attribut book de type Book, vous vous rendez compte qu’avec ce modèle, il est impossible de louer un film ou un périodique.

Pour résoudre ce problème, vous envisagez une première approche qui consiste à renommer les classes Loan et Copy en BookLoan et BookCopy, puis à créer deux nouvelles paires de classes : PeriodicalLoan et PeriodicalCopy, et DocumentaryLoan et DocumentaryCopy, comme illustré par le diagramme de la figure 2.

Fig. 2 – Première approche
Fig. 2 – Première approche

Toutefois, vous remarquez d’une part que cette solution nécessite la duplication de la classe LoanManager et, d’autre part, que les trois classes Loan et les trois classes Copy ne diffèrent que par le type de l’association, tout le reste étant parfaitement identique. De plus, pour enregistrer un emprunt à la bibliothèque, il suffit que l’objet ait un identifiant sous la forme d’une puce RFID ou d’un code QR. Le fait que l’objet soit un livre, un périodique ou un DVD n’a pas d’importance. Il n’y a donc pas de raison d’avoir plusieurs types d’emprunts et plusieurs types d’exemplaires.

Vous imaginez donc une autre solution. Vous vous dites qu’au lieu de créer trois classes Book, Documentary et Periodical, on pourrait créer une seule classe LibraryItem qui aurait à la fois les propriétés des livres, des films documentaires et des publications périodiques (voir figure 3), mais vous renoncez vite à ce projet. Vous savez que cette solution va clairement à l’encontre du principe de responsabilité unique (le S de SOLID). En effet, la classe LibraryItem aurait au moins trois raisons de changer : on peut modifier la manière de décrire (1) un livre (2) un documentaire (3) un périodique.

Fig. 3 – Deuxième approche
Fig. 3 – Deuxième approche

Vous envisagez donc une troisième approche qui consiste à dupliquer non pas les classes, mais les attributs de la classe Copy comme le monte le diagramme ci-après (figure 4).

Fig. 4 – Troisième approche
Fig. 4 – Troisième approche

Vous décidez de présenter votre travail à l’architecte du projet. Celui-ci reconnait que votre solution est astucieuse, mais elle viole l’un des principes SOLID : le principe ouvert-fermé (open-closed principle, le O de SOLID). Il vous dit que ce principe stipule qu’une classe doit être à la fois ouverte pour l’extension et fermée pour la modification. Or, poursuit-il, pour ajouter une nouvelle sorte d’item, il est nécessaire de modifier la classe LibraryItem. Il termine en disant que la solution à ce problème réside dans la relation « est un ». Comme vous ne semblez pas savoir de quoi il parle, il entreprend de vous l’expliquer.

Vous connaissez déjà la relation « a un ». Par exemple, un Book « a un » Author. Il s’agit d’une relation entre des objets, une instance de la classe Book est liée à une instance de la classe Author. Une telle relation est généralement réalisée à l’aide d’une variable membre. La relation « est un » est une relation entre des types. Avec une relation « est un », on indique qu’une instance d’un certain type S se comporte conformément à la spécification d’un autre type T. On dit alors que S est un sous-type de T et, par symétrie, que T est un super-type de S. Si S est un sous-type de T, alors on peut utiliser une expression de type S partout où une expression de type T est attendue. Par exemple, si Cat est un sous-type de Animal, alors on peut utiliser une expression de type Cat partout où une expression de type Animal est attendue. L’inverse n’est pas vrai. Une expression de type Animal peut faire référence à une instance de n’importe quel sous-type. Si Dog est également un sous-type de Animal, une expression de type Animal peut faire référence à une instance de type Dog. Il serait alors malvenu d’utiliser cette expression là où une expression de type Cat est attendue.

En UML, la relation est « est un » est représentée par une grande flèche fermée du sous-type (S) vers le super-type (T) et se lit : S est un sous-type de T ou T est un super-type de S. En Java, lorsque le super-type est une interface, on réalise cette relation avec le mot clé implements dans la déclaration de la classe. Le mot clé implements exprime l’idée que le sous-type implémente les méthodes du super-type et que, par conséquent, il se conforme à sa spécification. Les figures 5 et 6 montrent le même exemple en UML et en Java.

Fig. 5 – Relation « est un » (S « est un » T)
Fig. 5 – Relation « est un » en UML (S « est un » T)
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Définition du super-type T dans le fichier T.java.
//
// On définit le super-type à l’aide d’une interface.
//
// En Java, une interface est la description d’un type
// défini comme un ensemble d’opérations. Une interface
// contient uniquement la signature des méthodes, sans
// implémentation.
//
// Une interface ne contient  ni variables membres,
// ni constructeur et ne peut donc pas être instanciée
// avec un new.
//
public interface T {
    public String getSomeProperty(); // pas d’accolades,
                                     // pas d’implémentation
    public void doSomeAction();
}


// Définition de sous-type S dans le fichier S.java
//
// À l’aide du mot clé implements, on indique que la
// classe est un sous-type de l’interface. C’est-à-dire
// qu’une instance de la classe se comporte conformément
// à la spécification du type définit par l’interface.
//
public class S implements T {

    private String someMember;
    private int someOtherMember;

    public S(String val, int otherVal)  {
        this.someMember = val;
        this.someOtherMember = otherVal;
    }

    @Override
    public String getSomeProperty() {
        return this.someMember;
    }

    @Override
    public void doSomeAction() {
        // fait quelque chose ...
    }

    public int getSomeOtherProperty() {
        return this.someOtherMember;
    }

    public void doSomeOtherAction() {
        // fait autre chose ...
    }
}

// Définition de la classe App dans le fichier App.java
//
public class App
{
    public static void main( String[] args )
    {
        // Instancie un objet de type S et
        // affecte-le à une variable de type T.
        // Ok car un objet de type S « est un » objet de type T.
        T t = new S("val", 0);

        t.doSomeAction();
        String val = t.getSomeProperty();

        t.doSomeOtherAction(); // ERREUR : doSomeOtherAction n’est
                               //          pas une méthode du type T !

        T t = new T(); // ERREUR : le type T ne peut pas être instancié !
    }
}
Fig. 6 – Relation « est un » en Java (S « est un » T)

Après ces explications, vous envisagez une nouvelle approche. Vous reprenez l’idée d’une classe LibraryItem mais cette fois, plutôt que d’y mettre toutes les propriétés de Book, de Documentary et de Periodical, vous utilisez la relation « est un » pour dire qu’une instance de Book est un LibraryItem, qu’une instance de Documentary est un LibraryItem et qu’une instance de Periodical est un LibraryItem. Le type LibraryItem ne définit que les propriétés communes des trois classes. Le résultat de votre travail est le diagramme UML présenté à la figure 7.

Fig. 7 – Diagramme de classes révisé
Fig. 7 – Diagramme de classes révisé

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/gerons-nos-items-relation-est-un.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 gerons-nos-items-relation-est-un et lancer la commande code -r . pour ouvrir le projet.

À vous de jouer !

Tâches

  1. Modifiez le projet pour qu’il corresponde au diagramme UML de la figure 7. Remarques : Lorsque les arguments des constructeurs ne sont pas détaillés dans le diagramme, le constructeur prend un argument pour chaque variable membre. Les classes n’ont plus de setters.
  2. Vous devriez avoir remarqué que les méthodes qui revoient les liste d’auteurs, de réalisateurs et de scénaristes sont de type List<T>. Expliquez comment cela est possible alors que le type de l’attribut est ArrayList<T>.

Demandez de l’aide en cas de besoin, mais essayez d’abord par vous-même et respectez toujours le code d’honneur !