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.
- En utilisant la fabrique fab, créer un entier naturel n7 de
valeur 7 et un entier naturel n1 de valeur 1.
- 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.