UP | HOME

Immutabilité - Interfaces et classes d'implémentations

Table of Contents

Session 1 - Travaux dirigés - 4 mai 2015 - 16h30-17h45 - 1h15

  • A préparer à l'avance (temps de travail : trente minutes).
  • On procèdera ainsi pendant la séance :
    • Exercice 1 : pas de programmation sous Eclipse,
    • Exercice 2 : programmation sous eclipse.
  • Correction : cf. la solution.

Préalable : terminer les exemples du cours du 27 avril

Une interface et des classes d'implémentation pour les entiers naturels

Paquet cm1.demo1

  • Interface Nat
  • Classes d'implémentation NatParInt etr NatDecimal
  • Interface FabriqueNat
  • Classes d'implémentation FabriqueNatParInt et FabriqueNatDecimal
  • Test des classes d'implémentation

Une hiérarchie de structures algébriques

Paquet cm2.demo2

  • Hiérarchie des onze interfaces correspondant aux structures algébriques

Exercice 1 : sur l'immutabilité

Revenons aux entiers naturels. On a vu deux implémentations qui partagent une propriété commune, celle de définir des objets dits immutables : une fois créés, les entiers naturels conservent leur état initial, si bien que chaque calcul se traduit par la création d'un nouvel objet pour stocker le résultat.

// Implémentation de NatParInt
public Nat somme(Nat x){
  return new NatParInt(this.getInt() + x.getInt());
}
// Implémentation de NatDecimal
public Nat somme(Nat x){
  // ...
  int retenue = 0;
  String chiffres = "";
  for(int i=0; i < max; i++){
    int result = retenue + this.chiffre(i) + x.chiffre(i);
    //...
    chiffres = result + chiffres;
  }
  // ...
  return new NatDecimal(retenue == 1 ? "1" + chiffres : chiffres);
}

On peut imaginer des implémentations alternatives, qui modifient en place les entiers naturels sans créer de nouveaux objets à chaque calcul : l'état peut alors varier après la construction. On dit alors que les objets sont mutables.

Réalisation d'une implémentation mutable

  • Dans un paquet td1.mutabilite, créer une nouvelle classe NatParInt implémentant cm1.demo1.Nat.

    Rappel : l'interface Nat

    public interface Nat {
          Nat somme(Nat x);
          int getInt();
          int chiffre(int i);
          int taille();
    }
    
  • Récupérer le code de la classe NatParInt définie dans le paquet cm1.demo1. Voici le squelette de la classe dont seuls les éléments pertinents sont conservés.
    public class NatParInt implements Nat {
          // unique attribut
          private int i;
          // constructeur initialisant l'attribut
          public NatParInt(int i) {
                  ...
                  this.i = i;
          }
          @Override
          public Nat somme(Nat x){
                  return new NatParInt(this.getInt() + x.getInt());
          }
          @Override
          public int getInt() {
                  return this.i;
          }
          // autres méthodes
          ...
    }
    
  • Ajouter un accesseur privé en écriture void setInt(int i).
  • Modifier la méthode somme de manière à modifier en place l'objet cible (celui référencé par this).
  • Réaliser une transformation analogue pour l'autre implémentation, en définissant une nouvelle classe td1.mutabilite.NatDecimal. Pour cela, récupérer le code de la classe NatDecimal définie dans le paquet cm1.demo1, y ajouter un accesseur en écriture et modifier la méthode somme. Voici le squelette de la classe NatDecimal dont seuls les éléments pertinents sont conservés.
    public class NatDecimal implements Nat {
          // unique attribut (suite de chiffres décimaux (entre 0 et 9)   
          private String chiffres;
    
          public NatDecimal(String chiffres) {
                  ...
                  this.chiffres = chiffres;
          }
          @Override
          public Nat somme(Nat x){
                  String c = "";
                  ...
                  return new NatDecimal(c);
          }
          // autres méthodes
    }
    
  • Définir deux fabriques, une par classe d'implémentation. Chaque fabrique implémente l'interface cm1.demo1.FabriqueNat, rappelée ci-dessous.
    public interface FabriqueNat {
      Nat gen(int x);
      Nat gen(String repDecimale);
    }
    

    Note (et rappel du cours) : Il est possible d'abstraire la construction des objets, en utilisant des fabriques. Une fabrique est un objet particulier dont le rôle est de construire des objets : précisément, elle contient des méthodes dont l'implémentation se réduit à l'invocation d'un constructeur. Une bonne pratique de programmation est de n'utiliser les constructeurs que dans les fabriques et de construire les objets hors fabrique uniquement via les fabriques. Ainsi il suffit de changer la valeur d'une fabrique pour changer de constructeurs. On verra l'intérêt des fabriques dans le test qui va suivre.

Immutabilité contre mutabilité : comment choisir

  • En préalable, pour pouvoir comparer les entiers naturels, définir dans toutes les classes d'implémentation de Nat une méthode equals spécifiée ainsi : si l'argument n'est pas de type Nat, renvoyer false, sinon tester l'égalité des Nat en comparant les int associés. Réutiliser le même code dans toutes les classes.
  • Créer une classe de test dans le paquet td1.mutabilite.
  • Réaliser le test suivant dans une fonction void comparerMutabilite(FabriqueNat fab).
    • En utilisant la fabrique fab, créer un entier naturel n7 de valeur 7 et un entier naturel n1 de valeur 1.

      Rappel : l'interface FabriqueNat

      public interface FabriqueNat {
        Nat gen(int x);
        Nat gen(String repDecimale);
      }
      
    • Comparer n7 à n7.somme(n1) en utilisant la méthode equals, et afficher le résultat de la comparaison.
  • Appeler la fonction comparerMutabilite avec les quatre fabriques, celles permettant de créer des entiers immutables et celles permettant de créer des entiers mutables. Qu'observe-t-on lors de l'exécution ?
  • Analyser les résultats.

Exercice 2 : un type pour les logs

Souvent, un système informatique conserve des logs de toutes les actions qu'il réalise. Nous allons définir un type de données pour les logs. On suppose qu'une interface Action est donnée.

  • Définir les accesseurs suivants dans l'interface Log.
    • Action getPremiereAction()
    • Log getActionsRestantes()
    • Action getAction(int i)
    • int taille()
  • On peut munir l'ensemble des logs d'une addition, réalisant la concaténation des logs. Faire hériter l'interface Log de la bonne structure algébrique.

    Rappel : voici quelques interfaces décrivant des structures algébriques. Elles sont génériques (paramétrées par un type) car elles expriment des propriétés de types.

    public interface SemiGroupeAdditif<T> {
          T somme(T x);
    }
    public interface MonoideAdditif<T> extends SemiGroupeAdditif<T> {
          T zero();
    }
    public interface GroupeAdditif<T> extends MonoideAdditif<T> {
          T oppose();
    } 
    
  • Implémenter l'interface Log par une classe LogParListe utilisant une liste de type List<Action>. Définir trois constructeurs, l'un sans argument, le second prenant une action et un log, le troisième une liste d'actions. Indication pour le traitement des listes : on pourra utiliser la méthode add de l'interface générique List.
  • Proposer une seconde implémentation. On pourra utiliser deux classes, l'une pour représenter l'élément neutre de l'addition, l'autre pour représenter les autres logs.
  • Tester les implémentations.

Last Updated 2015-05-11T08:00+0200. Comments or questions: Send a mail.