UP | HOME

Les entiers naturels - Une interface, plusieurs implémentations

On s'intéresse aux entiers naturels : 0, 1, 2, etc.. L'objectif est de proposer plusieurs implémentations de ce type de données et de permettre leur utilisation simultanée. Autrement dit, ces implémentations doivent être interopérables.

Table of Contents

Comment définir les entiers naturels

Deux points de vue sont possibles :

  • point de vue interne et
  • point de vue externe.

Point de vue externe : à quoi servent-ils ?

Ils servent :

  • à compter,
  • à faire des calculs (additions, etc.),

Le point de vue externe correspond à celui d'un utilisateur souhaitant utiliser des entiers naturels. Celui-ci s'intéresse à l'interface que lui présentent les entiers. Cette notion d'interface se retrouve dans de nombreux langages de programmation, notamment en Java. Une interface définit un contrat établi entre un utilisateur et un type de données.

Point de vue interne : comment les construire ?

Plusieurs constructions sont possibles.

  • Par récurrence : un entier naturel est ou bien nul, ou bien le successeur d'un entier naturel. \[\small n \ ::=\ 0 \mid \mathtt{S} n \]
  • Par restriction de l'ensemble des entiers relatifs (int) : un entier naturel est un entier relatif positif. \[\small n \in \mathbb{Z} \mid n \geq 0 \]
  • Par une représentation particulière, par exemple décimale (en base 10) : un entier est une suite finie de chiffres compris entre 0 et 9.

    \[\small \begin{array}{rcl} c & ::= & 1 \mid 2 \mid 3 \mid 4 \mid 5 \mid 6 \mid 7 \mid 8 \mid 9\\ d & ::= & 0 \mid c \\ n & ::= & 0 \mid c\,d^* \end{array} \]

  • Par un ensemble : un entier est l'ensemble vide, ou l'ensemble de ses prédécesseurs.

    \[\small n \ ::=\ \emptyset \mid n \cup \{ n \} \]

  • Etc.

Une première implémentation simple : restriction de int

  • Créer une nouvelle classe NatParInt dans un paquet session1.demo1.
  • Commencer par définir la méthode NatParInt somme(NatParInt x) en utilisant un accesseur public, la méthode int getInt() donnant la valeur du int associé.
  • En la définissant, utiliser les corrections proposées par Eclipse pour compléter la définition de la classe.
  • Veiller à garantir qu'un entier naturel est toujours construit avec un int positif.
  • Tester votre classe dans la fonction principale main d'une classe Test.
  • Peut-on additionner deux milliards à lui-même ? Pourquoi n'est-ce pas possible ?

On cherche à résoudre ce problème de dépassement en construisant une nouvelle classe.

Solution plus élaborée : représentation décimale

  • Créer une nouvelle classe NatDecimal dans le paquet session1.demo1.
  • Définir la méthode NatDecimal somme(NatDecimal x) en utilisant deux accesseurs publics,
    • la méthode int chiffre(int i) donnant la valeur du chiffre en position i dans la représentation décimale et
    • la méthode int taille() donnant le nombre total de chiffres dans la représentation.
  • Compléter la définition : définir l'état formé d'une chaîne de caractères (de type String) formée des chiffres décimaux représentant l'entier naturel ses accesseurs et les constructeurs utiles.
  • Veiller à garantir que la chaîne de caractères utilisée pour la représentation ne contient que des chiffres décimaux.
  • Tester votre classe.
  • Peut-on additionner deux milliards à lui-même ?

Les différentes couches d'une implémentation

A partir de ces deux constructions de classes, on peut mettre en évidence leur structure commune :

  • un état formé d'attributs,
  • des constructeurs initialisant les attributs et garantissant des propriétés sur l'état (appelées des invariants),
  • des accesseurs permettant d'observer l'état,
  • des services, méthodes utilisant les constructeurs et les accesseurs.
constructeurs
attributsservices
accesseurs

Problème de l'interopérabilité

Peut-on additionner un NatParInt et un NatDecimal ? La réponse est négative : il manque un type commun qui serait la réunion de ces deux types. En Java, la solution est de définir les deux classes comme des classes d'implémentation d'une interface commune.

  • Créer une nouvelle interface Nat contenant une méthode Nat somme(Nat x) ainsi que les accesseurs publics de NatParInt et de NatDecimal.
  • Préciser que les classes NatParInt et NatDecimal implémentent Nat.
  • Compléter ces classes.
  • Récrire les tests de manière à utiliser le type Nat. Réaliser l'addition de deux entiers naturels de classe différente.

Factorisation des tests

Avec plusieurs classes à tester, on constate de nombreuses répétitions : les tests ne diffèrent que par le nom de la classe apparaissant lors des invocations de constructeurs.

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 fabriques uniquement via les fabriques. Ainsi il suffit de changer la valeur d'une fabrique pour changer de constructeurs.

  • Créer une nouvelle interface FabriqueNat contenant les méthodes suivantes :
    • Nat creerNatAvecValeur(int x),
    • Nat creerNatAvecRepresentation(String repDecimale),
  • Créer deux classes de fabriques implémentant cette interface, FabriqueNatParInt et FabriqueNatDecimal.
  • Modifier la fonction principale de manière à factoriser les tests dans trois fonctions prenant comme arguments des fabriques :
    • void testerCourt(FabriqueNat fab) : test sur des petits entiers,
    • void testerGrand(FabriqueNat fab) : test sur des grands entiers,
    • void testerInteroperabilite(FabriqueNat fab1, FabriqueNat fab2) : test de l'interopérabilité.
  • Tester.

Bonus : une nouvelle implémentation par récurrence (ou induction).

Un entier naturel peut être défini par récurrence (ou induction) de la manière suivante : un entier naturel est soit nul, soit le successeur d'un entier naturel. On peut implémenter facilement de telles définitions inductives en Java : l'ensemble défini se traduit par une interface alors que chaque cas de définition se traduit par une classe implémentant cette interface.

Cf. le premier TP.

Conclusion : interfaces comme types essentiels, classes comme modèles concrets de données

A la lumière des exemples précédents, on peut tirer quelques conclusions.

  • Une interface sert à définir un type utilisable ensuite dans un programme. Elle définit le contrat réalisé par les données de ce type, un contrat étant l'ensemble des méthodes auxquelles répondent ces données.
interfacetype de retournomtype des arguments
méthode 1
méthode 2
etc.
  • Une classe sert à définir une implémentation d'une interface1 : elle réalise le contrat déclaré par l'interface, en définissant un modèle concret de données, à partir d'attributs, de constructeurs et de méthodes, dont celles déclarées dans l'interface. Elle est utilisée pour construire des objets, en qualifiant l'opérateur new, ce qui a pour effet d'appeler un constructeur de la classe. Pour réduire la dépendance relativement aux classes d'implémentation, une bonne pratique est de restreindre l'usage des constructeurs à des méthodes ou des classes particulières, appelées fabriques.
constructeurs
attributsservices (implémentant les méthodes de l'interface)
accesseurs

Footnotes:

1 A vrai dire, une classe peut implémenter une ou plusieurs interfaces simultanément.

Last Updated 2015-09-30T18:43+0200. Comments or questions: Send a mail.