UP | HOME

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.

medias/exam_functionalApproach.png

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.

medias/exam_objectApproach.png

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.

Last Updated 2015-11-04T14:51+0100. Comments or questions: Send a mail.