Approche objet et types abstraits de données
Table of Contents
1 Consignes - Remarques préalables
Après avoir créé un projet Java ArchiLog16_eval, y importer à la racine l'archive (Import > General > Archive File > Zip à sélectionner). Développer ensuite dans les paquets indiqués ci-dessous. Fianlement, exporter votre projet (Export > General > Archive File) puis déposer sur Campus.
Remarque
- Le langage de programmation utilisé est Java, version 8.
- On utilise particulièrement le type Function <E, F> du paquet java.util.function. Il désigne les fonctions de E vers F. Il existe une méthode apply pour appliquer la fonction à un argument et une méthode compose pour la composition. Enfin, on peut définir une fonction avec la notation (x -> expression).
2 Un type abstrait de données pour les listes
On cherche à implémenter des listes suivant l'approche fonctionnelle puis l'approche objet. Les listes sont considérées comme un type abstrait de données muni des opérations suivantes. On note L le type des listes, E le type des éléments contenus dans la liste, boolean le type des booléens et String le type des chaînes de caractères (comme en Java).
// Constructeurs vide : L cons : E x L -> L // Sélecteur estVide : L -> boolean // Destructeurs tete : L -> E reste : L -> L // Foncteur application : L x (E -> E) -> L // Monoïde additif zero : L somme : L x L -> L // Egalité egal : L x L -> boolean // Représentation representation : L -> String
A ces opérations sont associées de nombreuses équations, tout à fait habituelles, dont voici deux exemples permettant de définir l'application et la somme.
application(vide, f) = vide application(cons(e, l), f) = cons(f(e), application(l, f)) somme(vide, l) = l somme(cons(e, l), k) = cons(e, somme(l, k))
Pour implémenter les listes, on utilisera le patron de conception "Composite", qui traduit directement la définition inductive suivante : une liste est
- soit vide
- soit une liste construite à partir d'un élément et d'une autre liste,
- soit l'application d'une fonction à une liste.
En choisissant cette définition inductive, on opte pour une définition paresseuse de l'opération d'application, exactement comme pour le type Stream en Java 8 : l'application n'est réalisée que quand on observe la tête de liste.
3 Approche fonctionnelle
(Développer dans le paquet fonction pour cette partie.)
Avec l'approche fonctionnelle, on définit des modules, qui regroupent les opérations sur une structure élémentaire (sans opérations) de données.
Plusieurs interfaces sont fournies.
- paquet structuresAlgebriques.fonction : les structures algébriques, avec en particulier l'interface générique ModuleMonoideAdditif.
- paquet fonction :
- interface générique Sequence<L, E> définissant les accesseurs (sélecteurs et destructeurs),
- interface générique FabriqueSequences<L, E> définissant les fabriques (associées aux constructeurs),
- interface générique ModuleFoncteurListe<L, E> définissant l'application (d'une fonction à tous les éléments d'une liste).
3.1 Diagramme des types
Voici le diagramme des types à obtenir.
Module fonctionnel et listes paresseuses
3.2 Une interface pour les listes
Définir l'interface générique ListeParesseuse<E> par héritage de Sequence (à instancier correctement). Y déclarer également les méthodes suivantes.
// Sélecteur boolean estEvaluee(); // Destructeurs Function<E, E> fonction(); ListeParesseuse<E> support();
Une liste n'est pas évaluée si elle est égale à une application d'une fonction à une liste ; elle est évaluée si elle est vide ou construite. Lorsqu'elle n'est pas évaluée, les destructeurs donnent respectivement la fonction et la liste à laquelle la fonction est appliquée.
3.3 Implémentation suivant le patron Composite
Définir trois classes génériques d'implémentation de l'interface ListeParesseuse.
- ListeVide : classe singleton représentant la liste vide (et évaluée)
- ListeCons : classe représentant les listes construites (et évaluées)
- ApplicationListe : classe représentant l'application d'une fonction à une liste (non évaluée, en mode paresseux)
Suivre les indications suivantes.
- On lancera une exception UnsupportedOperationException lorsque les méthodes tete et reste sont appeléee sur une liste vide, et lorsque les méthodes fonction et support sont appellées sur des listes évaluées.
- Pour implémenter la classe singleton, on utilisera la méthode traditionnelle : classe finale (sans sous-classe), constructeur privé, constante statique publique de type Liste<?>.
3.4 Un module pour les listes
Définir l'interface générique ModuleListes<E, Rep> par héritage de FabriqueSequences, ModuleFoncteurListe et ModuleMonoideAdditif (à instancier correctement). Cette interface est paramétrée par un type E représentant le type des éléments et par un type Rep sous-type de Sequence<Rep, E>. L'étendre avec deux méthodes, boolean egal(Rep x, Rep y) et String representation(Rep x), analogues aux méthodes equals et toString.
3.5 Implémentation du module
Définir une classe générique ModuleListesParesseuses<E> implémentant ModuleListes<E, ListeParesseuse<E>>
Suivre les indications suivantes.
- Définir récursivement les méthodes somme et egal.
- Implémenter la méthode representation par une boucle. La liste sera représentée sous la forme \([e_0, \ldots, e_n]\).
- Pour implémenter la méthode application, procéder ainsi :
- si la liste est évaluée, créer une nouvelle application (de type ApplicationListe),
- si la liste n'est pas évaluée et est donc une application, créer une nouvelle application de même support et de fonction la composition des deux fonctions.
- Définir un constructeur privé.
- Définir une fonction (statique) d'abstraction ainsi.
public static <E> ModuleListes<E, ?> abstraction() { return new ModuleListesParesseuses<E>(); }
Expliquer en commentaire à quoi elle sert.
3.6 Test
Tester l'implémentation du module en décommentant le corps de la classe TestListesParesseuses.
4 Approche objet
(Développer dans le paquet objet pour cette partie.)
Avec l'approche objet, on définit des données qui encapsulent les opérations.
Plusieurs interfaces sont fournies.
- paquet structuresAlgebriques.objet : les structures algébriques, avec en particulier l'interface générique MonoideAdditif.
- paquet objet : interface générique FoncteurListe<E> définissant l'application (d'une fonction à tous les éléments d'une liste).
4.1 Diagramme des types
Voici le diagramme des types à obtenir.
Interface objet pour les listes et adaptation du module
4.2 Interface pour les listes
Définir l'interface générique Liste par héritage de Sequence, FabriqueSequences, FoncteurListe et de MonoideAdditif (à instancier correctement).
4.3 Implémentation des listes par délégation
On implémente l'interface Liste en déléguant les calculs à une liste élémentaire (sans opérations) et à un module permettant d'opérer sur cette liste. On réalise donc une adaptation du module à la nouvelle interface.
Précisément, définir une classe ListeAvecModule<E, Rep> implémentant l'interface Liste<E>. Le paramètre Rep représente le type des listes élémentaires.
Suivre les indications suivantes.
- Le paramètre Rep doit être un sous-type de Sequence<Rep, E>>.
- La classe possède deux attributs, un module et une liste.
private ModuleListes<E, Rep> module; private Rep liste;
- Pour convertir de Liste<E> vers Rep, utiliser la
méthode concretiser ainsi définie.
private Rep concretiser(Liste<E> l){ if(l.estVide()) return module.vide(); return module.cons(l.tete(), concretiser(l.reste())); }
- Définir la méthode equals en utilisant les méthodes estVide, tete et reste.
- Définir la méthode toString en utilisant la méthode representation.
4.4 Test
Dans la classe TestListes, décommenter le code et définir la fonction (statique) fabrique qui prend comme argument un module et renvoie une fabrique. Veiller à réaliser la capture du joker (?) en quantifiant universellement la définition.
5 Conclusion : intra-opérabilité contre interopérabilité
Lorsqu'on implémente les calculs dans le module ModuleListesParesseuses, on se restreint à des listes du même type, ListeParesseuse, dont on connaît la structure. Il est impossible d'opérer avec des listes d'un type différent : c'est l'intra-opérabilité. Avec une implémentation de l'interface Liste, il est possible d'opérer avec toute autre implémentation de l'interface Liste : c'est l'interopérabilité. L'interopérabilité a un coût, celui de la conversion d'une liste quelconque en une liste équivalente appartenant à l'implémentation : cf. la méthode concretiser.