Programmation orientée objetCe cours présente les concepts de la programmation orientée objet ainsi que leur mise en place en Java

Introduction

L'objectif de la programmation orientée objet est d'organiser le code de manière à le rendre plus simple à comprendre et à faire évoluer. Dans les langages non orientés objet, nous organisons le code en utilisant des fonctions, en créant des fichiers différents appelés modules et en les placant dans des sous dossiers. Cette organisation est performante mais l'utilisation de concepts objets permet d'aller plus loin dans la structuration des développements

La modélisation objet consiste à créer une représentation informatique des éléments du monde réel auxquels on s'intéresse, sans se préoccuper de l'implémentation, ce qui signifie indépendamment d'un langage de programmation. Il s'agit donc de déterminer les objets présents et d'isoler leurs données et les fonctions qui les utilisent.

Nous allons utiliser un exemple permettant d'illustrer les différents concepts objet. Nous allons modéliser certains personnages de "Games of thrones", modélisation qui pourrait être réalisée pour programmer un jeu vidéo par exemple

Les classes

On appelle classe la structure d'un objet, c'est-à-dire la déclaration de l'ensemble des entités qui composeront un objet. Un objet est donc « issu » d'une classe, c'est le produit qui sort d'un moule. En réalité on dit qu'un objet est une instanciation d'une classe, c'est la raison pour laquelle on pourra parler indifféremment d'objet ou d'instance.

Composition

Une classe est composée de deux parties :

  • Les attributs (parfois appelés données membres) : il s'agit des données représentant l'état de l'objet
  • Les méthodes (parfois appelées fonctions membres): il s'agit des opérations applicables aux objets

Les constructeurs permettent d'instancier les classes en valorisant les paramètres. Il s'agit d'une méthode sans signature qui porte le nom de la classe

Les classes sont liées entre elles, une classe peut en contenir une autre.

Exemple

Pour modéliser les données des personnages de Game of thrones, nous pouvons retenir la modélisation suivante :

Les objets

Un objet est caractérisé par plusieurs notions :

  • Les attributs : Il s'agit des données caractérisant l'objet. Ce sont des variables stockant des informations d'état de l'objet
  • Les méthodes : Les méthodes d'un objet caractérisent son comportement, c'est-à-dire l'ensemble des actions que l'objet est à même de réaliser. Ces opérations permettent de faire réagir l'objet aux sollicitations extérieures (ou d'agir sur les autres objets). De plus, les opérations sont étroitement liées aux attributs, car leurs actions peuvent dépendre des valeurs des attributs, ou bien les modifier
  • L'identité : L'objet possède une identité, qui permet de le distinguer des autres objets, indépendamment de son état. On construit généralement cette identité grâce à un identifiant découlant naturellement du problème (par exemple un produit pourra être repéré par un code, une voiture par un numéro de série, etc.)
L'encapsulation

Le concept d'encapsulation

L'encapsulation est un mécanisme consistant à rassembler les données et les méthodes au sein d'une structure en cachant l'implémentation de l'objet, c'est-à-dire en empêchant l'accès aux données par un autre moyen que les services proposés. L'encapsulation permet donc de garantir l'intégrité des données contenues dans l'objet.

L'encapsulation revient à exposer des méthodes réalisant un service sans exposer leur fonctionnement

Le masquage des informations

Le masquage des informations permet au développeur qui créé l'objet de s'assurer que celui-ci est utilisé de la bonne manière en exposant des services qu'il aura documenté.

L'encapsulation permet de définir des niveaux de visibilité des éléments de la classe. Ces niveaux de visibilité définissent les droits d'accès aux données selon que l'on y accède par une méthode de la classe elle-même, d'une classe héritière, ou bien d'une classe quelconque. Il existe trois niveaux de visibilité:

  • publique : les fonctions de toutes les classes peuvent accéder aux données ou aux méthodes d'une classe définie avec le niveau de visibilité public. Il s'agit du plus bas niveau de protection des données
  • protégée : l'accès aux données est réservé aux fonctions des classes héritières, c'est-à-dire par les fonctions membres de la classe ainsi que des classes dérivées
  • privée : l'accès aux données est limité aux méthodes de la classe elle-même. Il s'agit du niveau de protection des données le plus élevé

Exemple concret

Nous avons créé les différentes classes permettant de modéliser les personnages de Game of Thrones, nous souhaitons nous assurer que :

  • Les maisons ne changeront jamais de nom ou de blason
  • Les personnages ne peuvent pas changer de nom ou de prénom mais peuvent changer de maison ou devenir roi
  • Les dragons ne peuvent pas changer de nom
  • Les dragons ont dorénavant un attribut supplémentaire qui est donné à la création et ne changera jamais : la puissance. Cet attribut est utilisé dans la méthode cracherFeu() mais ne pourra pas être utilisé ailleurs
  • Nous pouvons accéder à toutes les informations : nom, prénom, blason, etc. de tous les objets

Pour faire cela, nous allons jouer sur la visibilité des différentes variables et méthodes.

  • Toutes les méthodes (combattre() et cracherFeu()) seront publiques : on peut les utiliser à tout moment
  • Tous les attributs seront privés : on ne pourra pas les modificer directement en faisant johnSnow.nom="Stark"; par exemple. Nous ne pourrons même pas accéder en lecture à l'attribut
  • Pour accéder aux attributs en lecture ou écriture, nous allons créer des accesseurs et mutateurs :
    • Les accesseurs ou Getters sont des méthodes permettant de d'accéder au contenu d'une variable. Ces méthodes se nomment get[NomDeLaVariable]() et retournent la valeur de la variable
    • Les mutateurs ou Setters sont des méthoes permettant de modifier le contenu d'une variable. Ces méthodes se nomment set[NomDeLaVariable](TypeDeLaVariable t) et modifient la valeur de celle-ci

De cette manière, nous aurons le contrôle sur ce que nous allons exposer de la classe

L'héritage

La notion d'héritage

L'héritage (en anglais inheritance) est un principe propre à la programmation orientée objet, permettant de créer une nouvelle classe à partir d'une classe existante. Le nom d'"héritage" provient du fait que la classe dérivée contient les attributs et les méthodes de sa superclasse (la classe dont elle dérive). L'intérêt majeur de l'héritage est de pouvoir définir de nouveaux attributs et de nouvelles méthodes pour la classe dérivée, qui viennent s'ajouter à ceux et celles héritées.

Par ce moyen on crée une hiérarchie de classes de plus en plus spécialisées. Cela a comme avantage majeur de ne pas avoir à repartir de zéro lorsque l'on veut spécialiser une classe existante. De cette manière il est possible d'acheter dans le commerce des librairies de classes, qui constituent une base, pouvant être spécialisées à loisir (on comprend encore un peu mieux l'intérêt pour l'entreprise qui vend les classes de protéger les données membres grâce à l'encapsulation...).

Hiérarchie des classes

Il est possible de représenter sous forme de hiérarchie de classes, parfois appelée arborescence de classes, la relation de parenté qui existe entre les différentes classes. L'arborescence commence par une classe générale appelée superclasse (parfois classe de base, classe parent, classe ancêtre, classe mère ou classe père, les métaphores généalogiques sont nombreuses). Puis les classes dérivées (classe fille ou sous-classe) deviennent de plus en plus spécialisées. Ainsi, on peut généralement exprimer la relation qui lie une classe fille à sa mère par la phrase "est un".

Mise en place d'héritage sur notre exemple

Nous allons à présent ajouter la notion de marcheur blanc à notre modèle de base. Le marcheur blanc est un personnage particulier qui peut convertir() les nouveaux nés en marcheurs blancs. Tous les personnages n'auront pas cette faculté en revanche, le marcheur blanc sait faire tout ce que peut faire un personnage classique. Il hérite donc du comportement des personnages

Nous voulons également indiquer un royaume pour les rois, or la modélisation actuelle ne nous permet pas de le faire simplement. Nous allons donc créer une nouvelle classe Roi qui héritera de Personnage et qui en plus définiera un royaume.

Enfin, il est possible (pas nécéssairement souhaitable) de définir les dragons comme étant des personnages ayant une capacité supplémentaire : cracher du feu

On obtient alors le modèle suivant :

Le polymorphisme

Définition du polymorphisme

Le polymorphisme est pour les méthodes la faculté de prendre plusieurs implémentations différentes. Cette caractéristique est un des concepts essentiels de la programmation orientée objet. Alors que l'héritage concerne les classes (et leur hiérarchie), le polymorphisme est relatif aux méthodes des objets.

On distingue généralement trois types de polymorphisme :

  • La surcharge
  • La généricité
  • La redéfinition

Nous allons maintenant tenter de définir plus précisément tout cela, mais il est important de noter que beaucoup de confusions existent lorsqu'il s'agit de différencier tous ces types de polymorphisme.

La surcharge

La surcharge permet d'avoir des fonctions de même nom, avec des fonctionnalités similaires, dans des classes sans aucun rapport entre elles (si ce n'est bien sûr d'être des filles de la classe objet). Par exemple, la classe complexe, la classe image et la classe lien peuvent avoir chacune une fonction "afficher". Cela permettra de ne pas avoir à se soucier du type de l'objet que l'on a si on souhaite l'afficher à l'écran.

Le polymorphisme ad hoc permet ainsi de définir des opérateurs dont l'utilisation sera différente selon le type des paramètres qui leur sont passés. Il est donc possible par exemple de surcharger l'opérateur + et de lui faire réaliser des actions différentes selon qu'il s'agit d'une opération entre deux entiers (addition) ou entre deux chaînes de caractères (concaténation).

La généricité

La généricité, représente la possibilité de définir plusieurs fonctions de même nom mais possédant des paramètres différents (en nombre et/ou en type). Le polymorphisme paramétrique rend ainsi possible le choix automatique de la bonne méthode à adopter en fonction du type de donnée passée en paramètre.

Ainsi, on peut par exemple définir plusieurs méthodes homonymes addition() effectuant une somme de valeurs.

  • La méthode int addition(int, int) pourra retourner la somme de deux entiers
  • La méthode float addition(float, float) pourra retourner la somme de deux flottants
  • La méthode char addition(char, char) pourra définir au gré de l'auteur la somme de deux caractères

On appelle signature le nombre et le type (statique) des arguments d'une fonction. C'est donc la signature d'une méthode qui détermine laquelle sera appelée.

La redéfinition

La possibilité de redéfinir une méthode dans des classes héritant d'une classe de base s'appelle la spécialisation. Il est alors possible d'appeler la méthode d'un objet sans se soucier de son type intrinsèque : il s'agit du polymorphisme d'héritage. Ceci permet de faire abstraction des détails des classes spécialisées d'une famille d'objet, en les masquant par une interface commune (qui est la classe de base).

Imaginons un jeu d'échec comportant des objets roi, reine, fou, cavalier, tour et pion, héritant chacun de l'objet piece. La méthode mouvement() pourra, grâce au polymorphisme d'héritage, effectuer le mouvement approprié en fonction de la classe de l'objet référencé au moment de l'appel. Cela permettra notamment au programme de dire piece.mouvement sans avoir à se préoccuper de la classe de la pièce.

Exemple de mise en place

Si nous souhaitons mettre en place un service de combat entre les personnages, le service a besoin de deux combattants et d'appeler leur fonction combat. Qu'il s'agisse d'un dragon, d'un marcheur blanc, d'un humain, le service s'en moque, il souhaite juste les faire combattre.

Nous allons modifier le programme de manière à créer une couche d'abstraction entre les différentes classes définissant des combattants. Pour celà, nous allons créer une interface spécifique Combattant qui définiera la méthode combattre(). Toutes les classes implémentant cette interface devront redéfinir la méthode de combat

Nous allons créer un module de combat, la méthode combattre va être modifée, elle va générer un nombre qui correspondra, dans un combat, le vainqueur sera celui qui obtient le nombre le plus élevé. En cas d'égalité, il y a match nul (comme dans le combat entre La montagne et Oberyn Martell)