Script CM7

Slide 00

Dans ce cours nous allons achever la présentation des mécanismes d'abstraction, en poussant un cran plus loin et en présentant les notions de classe abstraite et d'interface. Nous nous appuyerons sur un exemple, et ce sera aussi l'occasion de revenir sur l'héritage et les énumérations.

Slide 01

Le cas exemple sur lequel nous allons nous pencher est celui d'une fonderie.

Le code source complet est à disposition sous forme de projet Eclipse sur la page associée à ce cours. Vous êtes donc invités à importer ce projet pour pouvoir le consulter car nous y ferons référence par la suite.

La fonderie reçoit de la matière première sous forme de pavés, un pavé étant caractérisé par les dimensions de ses 3 arêtes (largeur, longueur, hauteur). Le pavé est mesuré avant sa fusion, pour estimer le volume de matière fondue.

Slide 02

Le modèle objet de cet exemple est pour l'instant assez simple, le code l'est aussi.

La classe Block représente le pavé et dispose pour cela de 3 attributs (length, width, height, entiers pour simplifier), d'un constructeur prenant en paramètre leurs valeurs initiales, et expose une méthode publique getVolumeInCm3 permettant d'obtenir le volume du pavé (par une simple multiplication des valeurs des 3 attributs).

La classe Foundryreprésente une fonderie, ce qui nous intéresse ici est de pouvoir fondre des pavés (par série) et de savoir quel volume total a été fondu depuis la mise en service de la fonderie. Ce total est stocké dans un attribut totalVolumeOfMeltedBlocksInCm3. Le constructeur ne prend pas de paramètre et met en service une nouvelle fonderie. La méthode meltBlocks prend un tableau de références de type Block correspondant à la série de pavés à fondre, calcule le volume fondu, l'ajoute au total et le retourne. Dans cette méthode, le volume fondu est la somme des volumes des pavés de la série, le volume de chaque pavé étant obtenu par appel à la méthode getVolumeInCm3 sur l'objet Block correspondant. Enfin, un accesseur en lecture permet d'obtenir le volume fondu total.

La méthode main de l'application crée des instances de type Block, les rassemble dans un tableau, crée une instance de type Foundry et invoque la méthode metlBlocks sur l'objet Foundry en lui passant en paramètre le tableau.

Slide 03-04

Imaginons maintenant que les pièces à fondre ne soient pas uniquement des pavés, mais aussi des cubes.

Dans ce cas, on peut tirer parti de l'héritage et écrire la classe Cube comme une sous-classe de Block. La classe Cube ne définit qu'un constructeur à un seul paramètre, qui s'appuie sur le super-constructeur en l'appelant avec 3 fois ce paramètre puisqu'un cube est un pavé dont les 3 dimensions sont égales.

Nul besoin de redéfinir la méthode calculant le volume puisque celle de Block fait très bien son travail. Pas besoin non plus de modifier la classe Foundry puisque la méthode permettant de fondre une série de pavés prend un tableau de références de type Block. Par polymorphisme, le tableau peut tout à fait contenir à la fois des références vers des objets de type Blockou Cube, le comportement à l'exécution sera adapté.

La méthode main de l'application crée des instances de type Block et Cube, les rassemble dans un même tableau, crée une instance de type Foundry et invoque la méthode metlBlocks sur l'objet Foundry en lui passant en paramètre le tableau.

Slide 05

Le cas précédent était relativement simple à gérer, mais admettons maintenant que les pieces à fondre puissent être des pavés, des cubes, mais aussi des cylindres ou des sphères.

Dans ce cas, on ne peut pas construire les classes représentant les cylindres et les sphères par extension de la classe Block car elles n'ont pas les mêmes caractéristiques de base : un pavé et un cube sont caractérisés par largeur/longueur/hauteur, un cylindre par un rayon et une hauteur, et une sphère par un rayon...

Pas possible non plus de trouver un concept concret commun à toutes ces pièces particulières, dont les classes pourraient hériter.

Pas de panique, l'abstraction vient à notre secours.

Slide 06

Une classe abstraite est une classe à trous, dénotée en Java via le mot-clé abstract et en UML en ajoutant le prototype << abstract >> au dessus du nom de la classe.

Une classe abstraite modèlise un concept de façon incomplète. Le concept que l'on modélise avec ce type de classe est si général que son comportement ne peut pas être complètement défini concrêtement, ce qui consiste donc à laisser des trous dans l'implémentation, autrement dit à prévoir l'existence de méthodes mais sans en donner le code.

Une méthode abstraite est dénotée avec le mot clé abstract, comme sur l'exemple, et la signature se termine par un ;. Attention, une méthode abstraite n'a pas de code, ce qui est différent d'un code vide, que l'on exprimerait par { }.

L'intérêt de la classe abstraite est tout d'abord de pouvoir regrouper sous un super-type commun des concepts concrêts similaires qui peuvent néanmoins avoir une structure différente. Ces types concrêts sont définis par extension du type abstrait et les instances peuvent être manipulées indifféremment par polymorphisme à travers leur type abstrait commun.

Ensuite, une classe abstraite peut factoriser des attributs et méthodes qui seraient amenés à exister dans toutes les sous-classes, où les méthodes abstraites seraient redéfinies et des attributs supplémentaires pourraient être ajoutées.

Par exemple, on peut définir un type abstrait List qui possède un attribut size (représentant le nombre d'éléments de la liste) et une méthode non abstraite getSize() permettant d'obtenir cette propriété. Les méthodes abstraites seraient ici les méthodes d'ajout, de retrait, de test d'appartenance, qui ne peuvent pas être implémentées si on ne connait pas la façon dont sont stockés les éléments. Le type Listest donc abstrait, et ses sous-classes ArrayList et LinkedList seraient des spécialisations concrêtes du concept de liste ayant fait le choix d'une stratégie de représentation interne (tableau, chaine).

Toute classe qui possède au moins une méthode abstraite est abstraite et doit être identifiée comme telle.

Une classe abstraite peut posséder des constructeurs, utilisés pour initialiser les attributs. Ces constructeurs ne peuvent néanmoins pas être appelés en dehors de la classe et de ses sous-classes. Il est en effet interdit d'instancier une clase abstraite. Si c'était le cas, dans la mesure où des méthodes n'ont pas de code, cela conduirait à une impasse et un échec de l'exécution. Les constructeurs peuvent être de visibilité public, ou protected, ils sont avant tout là pour que les sous-classes puissent s'y appuyer depuis leurs propres constructeurs.

A noter que pour qu'une sous-classe d'une classe abstraite ne soit plus abstraite, elle doit donner l'implémentation de toutes les méthodes abstraites.

Slide 07

Dans le cas de la fonderie, la notion de classe abstraite va nous permettre de regrouper tous les types particuliers de pièces (pavé, cube, cylindre, sphère) sous un même super-type commun abstrait Piece.

Ici, le type abstrait ne factorise rien et sert juste à pouvoir tirer parti du polymorphisme en remplaçant le tableau de références de type Block par un tableau de références de type Piece dans la méthode meltBlocks de la classe Foundry (cette méthode devenant au passage meltPieces).

La classe abstraite Piece ne définit pas d'attribut ni de méthode concrête, elle ne déclare que la méthode abstraite getVolumeInCm3 invoquée par la méthode meltPieces et dont le comportement à l'exécution sera celui redéfinit par la sous-classe (Block, Cylinder, Sphere) correspondant au type réel de l'objet manipulé.

Les classes Block, Cylinder et Sphere héritent de Piece, définissent les attributs qui les caractérisent, les constructeurs qui initialisent ces attributs, et redéfinissent la méthode abstraite pour réaliser le comportement attendu.

La méthode main de l'application crée des instances de type Block, Cube, Cylinderet Sphere, les rassemble dans un même tableau, crée une instance de type Foundry et invoque la méthode metlPieces sur l'objet Foundry en lui passant en paramètre le tableau. Le comportement observé à l'exécution dépend du type réel de l'objet sur lequel la méthode getVolumeInCm3 est invoquée.

A noter que la classe Foundry est sans aucune modification prête à utiliser n'importe quelle nouvelle sous-classe modélisant un nouveau type de pièce, dans la mesure où elle ne voit les pièces qu'à travers le type abstrait. Cela facilite la réutilisation.

Slide 08-09

Admettons maintenant que les pièces puissent être faites de métaux différents parmi 3 (Plomb, Cuivre, Fer), et que l'on puisse fondre ensemble des pièces de formes et de métaux variés (pour faire des alliages par exemple).

Il ne suffit plus de connaitre le volume total de pièces fondues mais aussi le prix total que représentent ces pièces fondues.

Nous allons donc modifier la conception de cette application de sorte de pouvoir prendre en compte la variété de métaux.

Les métaux, puisqu'ils sont en nombre limité, peuvent ici être représentés par l'énumération Metal.

On associe à chaque constante énumérée :

Toutes les pièces étant caractérisées par le métal qui les constitue, on peut factoriser l'attribut metal au niveau abstrait, de même que l'accesseur en lecture correspondant.

Plus intéressant, on peut définir intégralement la méthode getPrice qui calcule le prix de la pièce en multipliant son volume par le prix par cm3 du métal correspondant. On peut remarquer que l'implémentation de cette méthode s'appuye sur un appel à une méthode abstraite. C'est là aussi l'intérêt de l'abstraction. On peut écrire une méthode en s'appuyant sur un comportement abstrait car on sait que si l'on peut appeler cette méthode c'est qu'elle est définie dans une sous-classe qui n'est plus abstraite et donc pour laquelle le comportement abstrait a été redéfini.

Slide 10

La notion d'interface pousse celle de classe abstraite à l'extrême. Une interface ne contient aucune implémentation de méthode, ni attribut, ni constructeur.

Comme pour une classe abstraite, l'instanciation d’objet est impossible à partir d’une interface mais la création de nouveaux objets est possible à partir d'une classe déclarant implémenter l’interface.

Une classe implémente une interface si et seulement si elle définit l’implémentation de toutes les méthodes déclarées par l’interface. L'extension d'une classe par une classe était indiquée par le mot-clé extends, l'implémentation d'une interface par une classe sera indiquée par le mot-clé implements.

Le schéma illustre les relations interface/classe/objet. Un objet possède une relation est du type avec la classe dont il est une instance. Lorsque cette classe implémente une interface, alors l'objet possède la même relation avec l'interface. Par polymorphisme, un objet peut donc être vu à travers les types définis par les classes appartenant à son arbre d'héritage, mais également à travers tous les types définis par les interfaces que ces différentes classes implémentent.

Slide 11

La notion d'interface est liée à celle de service.

Un service met en relation 2 entités : le fournisseur et son client

Le contrat de service permet aux 2 entités de s'accorder sur la nature du service, sur ce qui est rendu et les modalités d'accès (dit autrement, le quoi) La réalisation du service est sa mise en oeuvre par le fournisseur (dit autrement, le comment). Les détails de la réalisation n'ont pas à être connus du client.

Un exemple de service est une horloge parlante. Le contrat indique que sur simple appel à un numéro de téléphone donné une voix nous indiquera l'heure. La réalisation peut quant à elle être diverse et se baser sur l'utilisation d'un serveur vocal, sur des opérateurs humains dans un centre d'appels, ...

En Java, le service est incarné par 2 éléments :

La vue du client (l'objet à gauche sur le schéma) se limite à la connaissance de l'interface, il reste indépendant des détails de l'implémentation qui lui sont cachés. Dit autrement, l'objet client ne sait pas quel est le type exact de l'objet réalisant le service dont il a besoin. On parle alors de couplage faible, c'était déjà le cas pour les classes abstraites, par opposition au couplage fort vu jusque là.

Slide 12

Plusieurs classes peuvent implémenter l'interface, en d'autres termes il peut y avoir différentes manières de réaliser le même service (comme dans l'exemple de l'horloge parlante).

Dans la mesure où la vue de l'objet client se limite à l'interface, ceci permet de tirer pleinement partie du polymorphisme et de pouvoir utiliser à l'exécution n'importe quelle implémentation disponible.

Séparer le contrat de la réalisation permet de ne pas avoir à réécrire une seule ligne de code de l'objet client si une nouvelle implémentation du service voit le jour.

Slide 13

L’implementation partielle d’une interface par une classe abstraite permet de factoriser attributs, constructeurs et méthodes entre des implémentations similaires pour en faciliter l’écriture par extension (voir le schéma).

Les méthodes de l’interface qui sont définies dans la classe abstraite (par leur signature et leur code) peuvent être (ou pas) redéfinies dans les sous-classes.

Les méthodes de l’interface non définies dans la classe abstraite (c'est à dire ni par leur signature ni par leur code) doivent nécessairement être définies dans les sous-classes.

Slide 14

Comme nous l'avons vu auparavant, le client du service est écrit en se basant uniquement sur l'interface et donc de manière indépendante de implementation.

A l'exécution, l'objet client doit néanmoins intéragir avec un objet d'un type concret, même s'il le voit à travers le type défini par l'interface que sa classe déclare implémenter.

Sur l'exemple illustré par le schéma, l'objet client ServiceUser possède un attribut du type defini par l'interface AServiceInterface. On peut supposer que dans la méthode useService de la classe ServiceUser, on trouve un appel à la méthode use de l'interface. Cette interface possède 2 implémentations différentes correspondant aux classes AServiceImplementation et AnotherServiceImplementation. L'objet ServiceUser pourrait de manière transparente, à travers l'interface, manipuler une instance de l'une ou l'autre de ces 2 classes. La référence de l'objet fournisseur du service à utiliser est passée en paramètre du constructeur de l'objet client afin que celui-ci initialise son attribut. Une autre solution consisterait à définir une méthode dans la classe ServiceUser pour pouvoir faire passer cette référence.

Ici, c'est le main qui crée l'objet fournisseur, puis l'objet client en lui passant la référence du premier.

L'objet client est donc indépendant de l'objet fournisseur à la conception mais dépendant à l'exécution. L'établissement du lien à l'exécution s'appelle une injection de dépendance.

Slide 15

En Java, l'utilisation d'interfaces est un recours à l'absence d'héritage multiple.

Sur l'exemple illustré sur le schéma, nous pouvons définir le concept particulier de VehiculeAmphibie qui peut à la fois être vu comme un véhicule terrestre et maritime. Cette classe peut par exemple être écrite sous la forme d'une extension d'une classe VehiculeTerrestre et d'une implémentation de l'interface VehiculeMaritime. Ainsi, la classe VehiculeAmphibie hérite de la méthode roule (qu'ele peut redéfinir si besoin) et doit donner la définition de la méthode navigue.

Slide 16

En Java, il est possible pour une classe d'implémenter plusieurs interfaces, c'est à dire de réaliser plusieurs contrats.

Sur l'exemple illustré sur le schéma, la classe ProducteurConsommateur implémente les interfaces Producteur et Consommateur, et doit donc donner la définition des méthodes produit et consomme. Par polymorphisme, un objet du type ProducteurConsommateur peut être vu à travers les types ProducteurConsommateur, Producteur, Consommateur et Object.

Slide 17

Nous avons achevé la présentation des mécanismes d'abstraction, en présentant les notions de classe abstraite et d'interface. Il nous reste à nous pencher sur le mécanisme de gestion des exceptions et la structuration en paquetages/modules pour clore cette seconde partie du module. La troisième et dernière partie sera quant à elle consacrée à l'étude des bibliothèques standards les plus communes.