La programmation par objets est une technique de programmation. Un langage de programmation qui supporte un style de programmation particulier fournit des facilités qui permettent d'utiliser ce style de manière aisée ainsi que des garde-fous nécessaires au contrôle du style de programmation. Ainsi, Java est un langage adapté à la programmation objet, même si (au prix d'un plus gros effort), les langages procéduraux comme C ou Pascal permettent également d'utiliser ce style de programmation.
long pgcd(long x, long y) { unsigned long r ; if (x < y) { r = x ; x = y ; y = x ; } do { r = x % y // r est le reste de la division entière de x et y x = y ; y = r } // réitérer ce processus en échangeant (x, y) par (y, r) while (r != 0) ; // tant que le reste de la division entière x et y est non nul return x ; // retourne le pgcd }
Une fois cette fonction donnée, il est possible à présent d'utiliser cette fonction dans toute autre fonction.
void une_fonction(void) { ... x = pgcd(4356, y) ; ... }
Les utilisateurs de ce module ignorent parfaitement les détails d'implantation de cette pile. En particulier, s'agit-il d'un tableau ou d'une liste chaînée ? Seul le programmeur de ce module le sait. L'utilisation de ce module par d'autres se fait alors de la manière suivante :
// Interface du module PILE de caractères (pile.h) void empiler(char) ; char depiler(void) ; // Fin de l'interface
Cette première tentative de modularité ne permet l'utilisation que d'une seule pile. Il est souvent nécessaire de disposer de plusieurs piles dans un même programme. La solution consiste à fournir une interface plus sophistiquée dans laquelle un nouveau type de donnée appelé id_de_pile sera défini.
// Module XXX utilisant une pile de caractère #include "pile.h" void une_fonction() { short c1, c2 ; ... empiler('a') ; empiler('b') ; ... c1 = depiler() ; if (c1 == ERREUR) erreur(" la pile est vide ") ; c2 = depiler() ; if (c2 == ERREUR) erreur(" la pile est vide ") ; ... } // Fin du module XXX
Idéalement, on aurait souhaiter que le type abstrait id_de_pile se comporte comme un type prédéfini. Or, cela n'est pas le cas : il appartient à l'utilisateur de ce module de s'assurer de la bonne utilisation. En particulier, celui-ci devra allouer les variables nécessaires pour manipuler ces piles, s'assurer qu'elles sont désallouées. Il n'existe pas de convention claire pour les passages des arguments de type définis par un module particulier. Bref, on ne dispose pas d'aide à la programmation.
// Interface du module PILE de caractères typedef id_de_pile ... id_de_pile creer_pile() ; void empiler(id_de_pile, char) ; char depiler(id_de_pile) ; void detruire_pile(id_de_pile) ; // Fin de l'interface
// Module XXX utilisant une pile de caractère #include "pile.h" void une_fonction(void) { id_de_pile p1, p2 ; char c1, c2 ; ... p1 = creer_pile() ; // p2 non créée empiler(p1, 'a') ; c1 = depiler(p1) ; if (c1 == EOF) erreur(" la pile est vide ") ; detruire_pile(p2) ; p1 = p2 ; // p2 utilisée après sa destruction // p1 non détruit ... } // Fin du module XXX
Une fois un type de donnée abstrait défini, il interfère très peu avec le reste de programme ; pourtant toute modification ou enrichissement de ce type de donnée entraîne une refonte complète du programme. Imaginons que l'on définit un type de donnée forme.
enum type {cercle, triangle, carree} ; typedef struct forme { point centre ; type t ; } forme ;
Le programmeur devra connaître toutes les formes manipulées pour implanter la fonction dessiner :
void dessiner(forme f) { switch(t.type) { case cercle : ... break ; case triangle : ... break ; case carree : ... break ; } }
Ainsi, la suppression d'une forme ou l'ajout d'une nouvelle forme force le programmeur à reprendre l'ensemble des fonctions et à les adapter. Programmation par objets : l'héritage Le mécanisme d'héritage de la programmation objet apporte une solution élégante au problème soulevé dans la section précédante. On définit tout d'abord une classe qui possède les propriétés générales de toutes les formes :
class forme { private point centre ; ... public point position() { return center ; } public void deplacer(point vers) { center = vers ; dessiner() ; } public abstract void dessiner() ; public abstract void tourner(int) ; ... }
Les fonctions dont l'implantation (ou la mise en oeuvre) est spécifique pour chaque forme sont marquées par le mot clé abstract. Pour définir effectivement ces fonctions virtuelles, on commencera par définir une classe dérivée de la classe forme :
class cercle extends forme { private int rayon ; public void dessiner() { ... } public void tourner(int) { } }
Considérons un type nouvellement défini (que l'on appellera tableau) qui est constitué de sa taille et d'un pointeur vers les éléments du tableau. La création d'un objet de type tableau se fait en définissant une variable de la classe tableau. Que doit-on créer à l'initialisation ? Faut-il allouer la place nécessaire pour coder le tableau ? etc. Les réponses à ces questions ne peuvent être fournies que par l'utilisateur ayant défini le type tableau.
Une première solution consiste à se définir une fonction init que l'utilisateur se forcera à appeler avant toute utilisation d'un objet d'un type utilisateur.
Cette solution est peu agréable et Java fournit un mécanisme plus astucieux pour faire l'initialisation : l'appel de la fonction d'initialisation se fait automatiquement à la définition de la variable. La fonction d'initialisation se nomme constructeur . Par contre, contrairement aux langages de programmation objet habituels, il n'est pas nécessaire de fournir un destructeur pour les objets d'un type défini par l'utilisateur. Cette destruction se fera automatiquement et une récupération mémoire se charge de restituer au système la place mémoire rendue libre.
class tableau { private int [] t ; public void init(int taille) ; ... } ; void une_fonction() { tableau t ; ... t.init(2) ; // n'utiliser t qu'après son initialisation }
class tableau { private int [] t ; tableau(int s) { if (s<=0) erreur(...) ; t = new int[n] ; } ; }
Les objets dont nous allons étudier sont similaires aux objets du monde réel qui nous entoure en ce sens, qu'eux également, sont dans un certain état et possèdent un certain nombre de comportements. L'état d'un objet est représenté par des attributs et les comportements sont définis par des méthodes.
Un objet est une collection d'attributs munis de méthodesOn peut représenter les objets du monde réel par des objets informatiques comme c'est le cas dans un programme d'animation. On peut également représenter des concepts abstraits ; une manipulation graphique est un objet dans un environnement graphique.
Un objet qui modélise un objet du monde réel (un vélo, par exemple) possède des attributs qui indiquent l'état de celui-ci (la vitesse, la cadence de pédalage etc.). Le comportement de cet objet est constitué par des méthodes (accélération, freinage, changement de vitesse, etc.). Les seules actions possibles sur les objets sont celles définies par les méthodes. Les changements d'état (modification des variables d'attributs) ne sont permis qu'à travers les méthodes. Cette manière de protéger les attributs par des méthodes s'appelle encapsulation.
L'encapsulation des données a pour objectif de cacher les détails d'implantation d'un objet pour les autres objets. Pour reprendre l'exemple du vélo, lorsqu'on change de vitesse, il n'est nullement nécessaire de connaître le mécanisme du levier de vitesse ; la seule chose qui nous intéresse, c'est de savoir comment changer de vitesse. De même, il n'est pas utile de connaître comme un objet ou une classe d'objets est implanté, il nous suffit juste de savoir quelles méthodes utiliser.
Dans beaucoup de langages objets, un objet peut décider de rendre publics (donc accessibles sans passer par des méthodes) certains de ses attributs. Ces attributs peuvent alors être ``visibles'' de l'extérieur et (plus grave) peuvent être modifiés.
A l'opposé, toutes les méthodes ne sont pas forcément utilisables par les autres objets. Un objet peut choisir de cacher certaines méthodes i.e. les rendre privées. Bref, dans les langages de type C++, les objets peuvent rendre publiques des attributs et rendre privées des méthodes.
Certains messages doivent contenir des informations complémentaires pour réaliser la tâche demandée. Le message changer de vitesse à notre fameux vélo doit s'accompagner de l'information lui spécifiant s'il faut passer à la vitesse supérieure ou inférieure. Ces informations complémentaires accompagnent un message grâce aux paramètres de la méthode.
Un message est donc constitué de trois parties :
De même, dans la programmation orientée objet, on pourra avoir plusieurs objets de même nature, un moule commun permettant de tirer partie des caractéristiques communes des ces objets. Ce moule est appelé classe.
Une classe est un prototype qui définit des attributs et des méthodes communes à tous les objets d'une certaine nature.Les valeurs des attributs d'instances (attributs d'une instance d'une classe d'objets) sont distinguées. Ainsi, une fois la classe créée, il faut créer une instance de cette classe pour en faire un objet effectivement utilisable. Lors de la création d'une instance d'une certaine classe, on crée un objet de la nature spécifiée et le système alloue de la mémoire pour les attributs d'instances définis dans la classe. C'est alors que l'on pourra faire appel aux méthodes de cet objet pour réaliser quelque chose.
Contrairement aux attributs d'instance, les objets d'une même classe utilisent la même méthode d'instance. Il n'y a pas un exemplaire de chaque méthode pour chaque instance d'une classe.
Outre les attributs d'instance et des méthodes, les classes peuvent également définir des attributs de classes et de méthodes de classes. Ces attributs et méthodes de classes peuvent être accessibles soit par l'intermédiaire d'une instance de classe, soit directement par l'intermédiaire de la classe elle-même (il n'est pas nécessaire de disposer d'une instance d'une classe pour utiliser des attributs et méthodes de classes).
Le système ne crée qu'un seul exemplaire des attributs de classe et toutes les instances d'une même classe partagent ces attributs. Pour en revenir à notre fameux vélo, supposons que tous les vélos aient un même nombre de vitesse. Il serait alors inefficace de définir un attribut par instance de vélo codant cette information sur le nombre de vitesses (chaque instance disposerait d'une copie de cette variable d'attribut). Dans de ce cas, il serait naturel (et surtout plus efficace) de définir un attribut de classe qui contient ce nombre de vitesses ; toutes les instances partageant la même variable. Une modification par une instance de cette classe d'un attribut de classe, entraînerait la modification pour toutes les autres instances de cette classe.
DESSIN !!!
Dans les langages orientés objets, il est permis de définir des classes en fonction d'autres classes. Par exemple, les vélos de course, les vélos tout terrain et les tandems sont des classes d'objets qui partagent des caractéristiques communes ; ce sont tous des vélos avec quelques caractéristiques supplémentaires. On dira que les vélos de course, les vélos tout terrain et les tandems sont sous-classes de la classe des vélos et que la classe des vélos est une classe de base des classes de vélos de course, de vélos tout terrain et des tandems.
Chaque sous-classe hérite de toutes les caractéristiques de la classe père : les attributs et les méthodes. Les sous-classes ne sont condamnées à ne contenir que les seules caractéristiques de la classe père ; elles peuvent s'enrichir de nouveaux attributs et de nouvelles méthodes. Ces sous classes sont donc définies par les attributs et les méthodes de la classe père, mais également des attributs et méthodes propres à la sous classe.
Les sous-classes peuvent également modifier les méthodes héritées d'une classe père et proposer une version spécialisée de cette méthode. L'héritage (comme dans la vie courante) se propage de génération en génération. Une classe père peut elle-même être une sous-classe d'une autre classe et ainsi de suite... La hiérarchie des classes peut être aussi profond que l'on souhaite. Les attributs et les méthodes sont transmis vers d'une classe vers la sous-classe et ce autant de fois que nécessaire.
Le programmeur peut définir des super classes abstraites qui définissent des comportements génériques. Ces comportements peuvent être implantés, partiellement implantés ou même pas du tout implantés. Lorsque des comportements génériques ne sont pas implantés, il appartient à d'autres de définir des sous-classes de cette super classe dans lesquelles des comportements spécialisés seront spécifiés.