La programmation objet

Poser un problème

Avant de se précipiter sur son clavier pour écrire un programme, il convient de réfléchir afin de poser correctement le problème... Or, la manière même dont le problème sera posé influe sur l'écriture du programme. D'où la notion de "paradigme de programmation". Or, s'il est possible d'implémenter tous les paradigmes en utilisant n'importe quel langage, cela sera plus ou moins facile selon le langage utilisé. Ainsi, il est possible de programmer en objet en utilisant le C... mais le C++, conçu dans cet objectif, supporte la programmation objet, par sa syntaxe d'une part, par les contrôles apportés tant au moment de la compilation que lors de l'exécution d'autre part.

top


La programmation objets expliquée aux programmeurs

Si vous êtes programmeur, mais habitué aux langages de programmation "procéduraux" (pascal, fortran, C, perl, etc.), ce chapitre est pour vous: il essaie d'expliquer comment on peut passer de la programmation procédurale à la programmation objet, via la programmation structurée.

Pour les terriens Mais si vous êtes débutant en programmation, vous êtes encore des "gens normaux", dans ce cas vous pouvez passer directement au chapitre suivant.apres

L'approche procédurale

Elle met l'accent sur l'action représentée par le programme: on doit "faire quelque chose", mais cette chose sera exécutée par étapes successives. Chaque étape elle-même peut être découpée. On arrive ainsi à des découpages de plus en plus fins, jusqu'à obtenir des fonctions élémentaires. Certaines de ces fonctions peuvent figurer dans des bibliothèques, cela permettra de les réutiliser plus tard pour d'autres projets.

Qu'est-ce qu'une fonction ?

Une fonction est un sous-programme caractérisé par:

Variables locales ou globales

Les variables peuvent être locales, mais aussi globales: si X est une variable globale, une fonction f1 peut modifier la valeur de X, mais une autre fonction f2 peut également modifier cette valeur. Un programme bien structuré aura le moins possible de variables globales... mais celles-ci ne pourront pas être totalement évitées.

top


L'approche modulaire

Supposons que dans notre programme, deux fonctions f1, f2 et f3 accèdent toutes deux aux variables globales A et B, mais ce sont les seules. Dans ces conditions, peut-on vraiment dire que A et B sont des variables globales ?
Il est tentant de regrouper ces fonctions et ces variables: c'est la notion de module. Nous pouvons regrouper les trois fonctions f1, f2 et f3 d'une part, les deux variables A et B d'autre part, dans un module qui est constitué de deux parties:

Interface publique...

Le module comporte une interface, c'est-à-dire un ensemble de fonctions (et éventuellement de variables), qui seules seront vues par l'utilisateur du module. Un soin particulier doit être apporté à l'écriture de l'interface, puisque la modification de celui-ci pourra avoir des conséquences sur le code utilisateur du module. La seule modification de type d'une variable de fonction, par exemple, peut entraîner une impossibilité de compilation de l'application.

...Implémentation cachée

Les algorithmes constituant le corps des fonctions seront cachés, en ce sens qu'ils ne seront pas visibles par les utilisateurs du module. En conséquence, il est possible de les modifier, par exemple pour améliorer leur performance ou pour corriger une erreur, sans que cela ait d'impact sur le reste du programme... à condition toutefois que l'interface reste inchangée (ou tout au moins qu'il y ait compatibilité entre l'ancienne et la nouvelle interface). Il est même possible de modifier le découpage en fonctions du module, en ajoutant ou en supprimant des fonctions: tant qu'on ne touche pas aux fonctions déclarées dans l'interface, pas de problème par rapport à l'extérieur.

Encapsulation des données

Les variables A et B étant cachées, on peut modifier leur type tout en limitant l'impact sur l'ensemble du programme. D'autre part, grâce à l'encapsulation, et en supposant que la fonction f4 n'est pas intégrée à ce module, on est sûr d'éviter le bogue suivant (pas toujours très simple à détecter):

void f4 (){
   int A1;
   A=0; /* ERREUR, on voulait écrire A1=0 */
}

Si A avait été une variable globale, la ligne A=0, qui est une erreur du point-de-vue du programmeur, n'aurait pas été signalée par le compilateur, puisque A est accessible. Si A est "cachée" (encapsulée dans un module), le compilateur détectera une erreur: il sera alors aisé de la corriger.

Prototypage

Le mécanisme d'encapsulation des données, lorsqu'il est supporté par le langage de programmation, permet de travailler aisément en équipe sur un même projet: chaque programmeur écrit un module différent: il faut que tout le monde soit d'accord sur l'interface de chaque module, mais le codage lui-même peut se faire par chacun de manière indépendante.
Il est également possible de travailler par prototypes: lors de la phase de prototypage, l'interface est écrit mais le code est incomplet. Cela permet toutefois d'utiliser l'interface du module dans d'autres parties de l'application, et ainsi de tester la cohérence du modèle.

top


L'approche objets

Il est possible de dépasser l'approche modulaire. Supposons que l'on veuille, dans un programme, définir une structure de pile de caractères. On pourra écrire un module, avec les deux fonctions interfaces suivantes:

char pop();
void push (char);

On peut dire qu'un tel module est un "archéo-objet". Mais nous aimerions répondre aux trois questions suivantes:

  1. Comment faire si nous avons besoin de plusieurs piles dans notre programme ?
  2. Comment classer nos objets par "familles d'objets" ?
  3. Comment faire si nous avons besoin de piles d'entiers ?

Des objets intégrés au système de typage

Donner un numéro d'identification aux piles:

On ajoute un paramètre aux fonctions pop et push définies ci-dessus, en l'occurence un numéro d'identification. Dès lors, on peut gérer autant de piles que l'on veut. Les fonctions interfaces deviennent:

char pop(int Id);
void push(int Id, char c);

La solution du C++

Le C++ apporte une solution beaucoup plus puissante: il permet au programmeur de définir un "un type de données qui se comporte [presque] de la même manière qu'un type prédéfini". apres Le module devient alors tout simplement une déclaration de type, on peut déclarer des variables de ce type; Les fonctions sont "attachées" à ces variables, de sorte qu'on écrira dans le code des lignes du style:

class stack {
   char pop();
   void push(char c);
}

stack A;
stack B;
stack C;

A.pop();
C.push(B.pop());

Opérations sur les objets

La phrase "type de données qui se comporte presque de la même manière qu'un type prédéfini" entraîne de nombreuses conséquences. Par exemple, on pourra déclarer un tableau d'objets de type pile de la manière suivante:

stack[10] Stacks;

Une variable pourra être initialisée:

stack S1=10;

On pourra recopier une variable dans une autre grâce à l'opérateur d'affectation:

stack A;
stack B;
...
B=A;

On pourra faire un transtypage (cast) d'un type dans un autre:

stack A;
int B;
...
B = (int) A;

On pourra même additionner deux piles avec l'opérateur +, les comparer avec >, etc. Le problème est bien sûr: quelle signification donner à ces opérations ? Si l'initialisation ne pose pas trop de problème, si l'affectation semble également évidente, que signifie additionner ou comparer deux piles ? Ces opérateurs étant définis par la personne qui définit l'objet, c'est également à elle de définir la signification précise des opérateurs du langage pour cet objet. C'est ce qu'on appelle la surcharge des opérateurs. Il n'est pas obligatoire de surcharger les opérateurs: si, dans l'exemple précédent, l'opérateur + n'est pas surchargé, l'opération A + B renverra tout simplement une erreur à la compilation.

top


Classer les objets

Nous avons maintenant à notre disposition autant de types de variables que nous voulons. Nous allons avoir rapidement besoin de définir une classification.

Un exemple tiré de la vie quotidienne

En effet, si nous prenons une comparaison avec la vie quotidienne, nous pouvons dire que nous avons à notre disposition: un couteau de cuisine, une Twingo, un tourne-vis, un couteau à beurre, une 205, un couteau à pain... et un raton laveur. On sent bien que nous aimerions écrire que nous avons des outils et des voitures; en l'occurrence un tourne-vis et plusieurs sortes de couteaux constituent les outils, alors que la 205 et la Twingo sont des voitures.

Un objet de type "shape"

Suposons que nous voulions définir une série d'objets permettant de dessiner des formes à l'écran ("circle", "triangle", "square"). Nous pouvons procéder de plusieurs manières:

La méthode "tout à plat"

C'est celle correspondant aux couteaux de cuisine ci-dessus: il suffit de définir trois objets différents, un pour chaque forme désirée. On aura alors des déclarations du style:

class circle;
class triangle;
class square;

Si mettre tout sur le même plan est stupide dans la vie quotidienne, ce n'est pas plus malin dans un programme informatique... mais en outre, cette méthode conduira à réécrire sans arrêt la même chose... justement ce que le C++ voudrait éviter.

La méthode étiquette

Déjà un peu mieux... elle consiste à considérer qu'un cercle, un triangle, un carré sont des formes: nous créons donc une classe appelée shape, définie de la manière suivante:

enum kind {circle, triangle, square};
class shape {
   point center;
   color col;
   kind k;
public:
   point where() {return center};
   void draw();
};

Du point-de-vue de la conception, cela revient à dire qu'un cercle est une forme ayant l'étiquette "circle"... pas mal, mais pas fameux, car cela revient aussi à dire qu'il n'y a pas plus de différence entre un cercle rouge et un cercle noir qu'entre un cercle et un carré... même sans être un as en géométrie, on sent bien que cela ne correspond pas à la réalité...
Du point-de-vue de l'écriture du code, la fonction draw va tester le champ kind, et suivant les cas dessinera un cercle, un triangle, un carré... cela présente quelques sérieux inconvénients:

Au fond, pour reprendre l'exemple concret précédent, tout se passe comme si les ingénieurs de Renault, lorsqu'ils ont conçu la Safrane, avaient réouvert le dossier de la Twingo et avaient modifié des dessins de celle-ci, en ajoutant des erreurs. Résultat: le jour de la sortie de la Safrane, les Twingo qui sortent de l'usine ont trois roues...

En fait, ce qui manque ici, c'est de distinguer entre propriétés génériques, communes à tous les objets de type shape, et propriétés spécifiques à certaines formes.

La méthode par l'héritage

L'héritage apres est précisément l'outil qui va nous permettre d'implémenter cette distinction: cela passera par la définition de 4 classes. Une classe "abstraite" comportant toutes les propriétés génériques, et trois classes comportant les propriétés spécifiques de chaque forme particulière. Voici le code de la classe de base:

class shape {
   point center;
   color col;
public:
   point where() {
      return center;
   };
   virtual void draw()=0;
}
La classe de base

La déclaration de fonction apres draw signifie qu'une forme doit pouvoir être dessinée, mais on ne sait pas encore, à ce stade, comment elle sera dessinée. Une conséquence de la présence de cette fonction est qu'il est impossible d'écrire dans un code quelque chose comme:

shape forme1;

Le compilateur refusera cela, parce que shape est une classe abstraite, c'est-à-dire une classe qui contient au moins une fonction virtuelle. Il ne sait pas quoi faire de cette fonction. En fait, cela est assez compréhensible: si je vous dis, "dessine-moi une forme"... et si vous êtes un ordinateur (donc complètement dépourvu d'imagination) vous ne saurez pas quoi dessiner comme forme. De même, si je dis "cette variable est de type forme", c'est à peu près comme si je disais "cette variable est un machin". Pas très précis... Par contre, rien ne m'empêche de passer à une fonction une variable de type shape:

void arriere_plan (shape s);

Je ne peux pas dire "Bien, aujourd'hui je vais créer un machin", mais n'importe qui a le droit de demander son machin à un ami...

Les classes dérivées

Un cercle, un triangle, un carré sont des formes ayant chacune leur particularité. Nous écrirons cela de la manière suivante:

class circle: public shape {
   int radius;
   public:
   void draw();
};

class triangle: public shape {
   ...;
   public:
      void draw();            // dessine-moi un triangle
      coord getheight(1);     // renvoie la hauteur principale  du triangle
};

class square: public shape {
   int cote;
public: void draw(); };

Ainsi, nos trois classes dérivées "héritent" des caractéristiques génériques de leur classe de base, mais ajoutent des caractéristiques particulières propres à chacune. Du point-de-vue de la modélisation, nous avons bien trois objets, qui sont un cercle, un triangle, un carré... mais ces trois objets sont aussi une forme. On a donc réussi à introduire un fort degré d'abstraction de données... tout en gardant, à l'intérieur des fonctions, du code C, donc "proche de la machine".
Du point-de-vue de l'implémentation, cela nous conduit à écrire trois versions différentes de la fonction draw. Lorsque je devrai rajouter une nouvelle forme, je n'aurai pas à revenir sur les formes déjà définies, il y a donc moins de risques d'erreurs.

Définir des piles de n'importe quoi

Revenons à notre histoire de pile de caractères: justement, c'est d'une pile d'entiers dont j'ai besoin. Et dans un autre programme, j'aurai besoin d'une pile d'autre chose... Ce cas peut-il se traiter, lui aussi, par la méthode d'héritage  ? Certainement pas: d'un point-de-vue conceptuel, on ne peut pas dire qu'une pile d'entiers et une pile de réels soient des cas particuliers d'un objet plus général, comme a pu le faire avec les cercles, les triangles ou les carrés... par contre, on peut dire qu'une pile est toujours une pile, et que si on peut empiler des caractères il n'y a aucune raison pour qu'on ne puisse pas empiler autre chose que des caractères, en réutilisant les mêmes algorithmes. De même dans la plupart des langages, on peut déclarer des tableaux d'entiers, de réels, de structures... Le C++ permettra d'implémenter ce concept par des types paramétrés (encore appelés des modèles. On pourra par exemple définir des piles de formes en déclarant: apres

stack<shape> pile-de-formes;

Héritage correct: faire du neuf avec du vieux

Une conséquence de ce qui précède est que nous allons être capables de "faire du neuf avec du vieux": puisque je sais passer à une fonction une variable de type shape, je peux dès la première version du programme écrire par exemple une fonction qui me tapisse mon écran avec toujours la même forme. Lors de l'appel de la fonction, je devrai bien sûr préciser si je veux dessiner des cercles, des carrés ou des triangles... mais surtout, si dans dix ans ils me prend la fantaisie de vouloir dessiner des ballons de rugby (des ellipses), il me suffira de créer un objet de type ellipse, et de recompiler le vieux programme qui, lui, restera inchangé. Le mécanisme d'héritage et de fonctions virtuelles me garantit que cela fonctionnera. D'où une énorme souplesse pour faire évoluer les programmes.

ATTENTIONCela fonctionnera uniquement à condition que les relations d'héritages soient "proprement" définies. En particulier, lorsqu'une classe hérite d'une autre, les fonctions virtuelles de la classe dérivée doivent faire au moins autant de choses que celles de la classe de base, et elles ne doivent pas avoir des exigences supérieures . En d'autres termes, la classe dérivée doit être une extension de sa classe de base, pas une restriction.

top


Des bibliothèques d'objets

Il est possible d'écrire des bibliothèques d'objets, qui utiliseront soit la généricité soit les relations d'héritages. Ces bibliothèques seront utilisables par les programmeurs, qui pourront éventuellement continuer à créer de nouveaux objets qui hériteront des objets de la bibliothèque. Nous verrons ici la stdlib

top


La programmation objets expliquée aux gens normaux

Ce chapitre s'adresse aux gens "normaux", c'est-à-dire aux personnes n'ayant pas d'expérience de programmation, quelque soit le langage utilisé. Les programmeurs chevronnés, eux, feraient mieux de lire le chapitre précédent avant

Une cafétéria informatisée

Imaginons que nous devons modéliser une cafétéria automatique, comme on en trouve souvent sur les aires de repos des autoroutes, constituée d'un certain nombre de cafetières en libre-service.

Pour corser la chose, il y a trois types différents de cafetières:

top


Le café en programmation procédurale

En programmation procédurale, on s'intéressera essentiellement à ce que la machine doit faire; en l'occurrence, on réfléchira à la manière de préparer le café lorsqu'un utilisateur l'aura demandé. Les objets de l'application (en l'occurrence les spécifications des différentes machines à café) n'occuperont pas une place centrale dans notre réflexion.

Ainsi, dans notre exemple, on écrira trois algorithmes différents, correspondant aux trois manières de faire le café suivant le type de machine. Cela pourra par exemple se traduire par une fonction du type:

int faire_le_cafe (int cafid, int caftyp, int cafforce);

cafid est un "identificateur de cafetière" (il faut bien donner une adresse ou un nom différents à chaque cafetière pour pouvoir les différentier), caftyp est le type correspondant de la cafetière, alors que cafforce est un nombre (de 1 pour du jus de chaussette à 10 pour du café italien) donnant une idée de la force du café désiré. La fonction faire_le_cafe cache sous un interface commun plusieurs algorithmes différents (au moins un pour chaque type de cafetière); il peut être astucieux d'ailleurs d'écrire trois fonctions différents, d'où la structure suivante pour la fonction faire_le_cafe:

int faire_le_cafe(int cafid, int caftyp, int cafforce) {
int rvl=0;
switch (caftyp) {
case CAFE_EN_GRAIN:
rvl = faire_le_cafe_en_grain(cafid,cafforce);
break;
case CAFE_MOULU:
rvl = faire_le_cafe_moulu(cafid,cafforce);
break;
case CAFE_SOLUBLE:
rvl = faire_le_cafe_soluble(cafid,cafforce);
break;
default:
rvl = 9; /* Erreur, type inconnu */
}
return rvl;
}

Il faut connaître pour chaque machine quel est son type, cela peut faire l'objet d'un tableau caf_types:

int caf_types[15];

Pour connaître le type de la machine numéro 10, il suffit de lire la valeur de caf_types[10].

Par ailleurs, on a besoin de connaître l'état des différentes machines à café; par exemple, un client a-t-il demandé un café ? On peut donc imaginer une fonction, appelée lire_etat, qui ira lire l'état de la machine à café. Elle renverra par exemple le code 0 pour dire "machine prête", et le code 1 pour dire "quelqu'un a demandé un café". On peut bien sûr imaginer encore d'autres codes pour dire par exemple que le réservoir d'eau est vide, etc.

Un programme déclenchant N machines afin qu'elles fassent toutes du café ressemblera en fin de compte à celui-ci:

for (int i=0; i < N; i++) {
if (lire_etat(i)==1) {
int rvl = faire_le_cafe(i,caf_types[i],cafforce);
if (rvl == 0) {
printf "le cafe est pret\n";
} else {
printf "Machine en panne\n";
};
}
}

top


Le café en programmation objet

Tout cela est bien beau, mais on voit d'emblée que cette manière de procéder peut poser quelques problèmes:

Modéliser une cafetière

Il est bien plus naturel de raisonner en termes d'objets, car c'est notre manière quotidienne de penser: il s'agit en effet de faire fonctionner un ensemble d'objets. Ceux-ci:

La programmation objets va permettre de modéliser cet ensemble d'objets et ainsi d'écrire un programme bien plus proche de la manière de penser humaine. D'autre part, la structure du programme est telle que la modification d'un objet ne devrait pas avoir d'impact sur les caractéristiques d'un autre objet, ou sur les caractéristiques générales du programme; le programme sera donc plus robuste, et plus facile à maintenir.

qu'est-ce qu'une cafetière ?

Nous dirons qu'une cafetière est un objet. Cet objet est un ensemble de composants:

Lorsque nous écrirons le programme, il nous suffira de représenter ces objets par des variables, ainsi il sera possible d'écrire:

cafetiere A;

de la même manière qu'on écrit en C:

int B;

Une cafetière est une cafetière, mais il y en a plusieurs sortes

Par ailleurs, nous avons vu qu'il pouvait exister plusieurs types de cafetières: or, même si une machine avec réserve de café en grain est de conception différente d'une machine avec réserve de café en poudre, nous savons parfaitement qu'il s'agit dans les deux cas de cafetières, c'est-à-dire que ces deux objets ont un grand nombre de caractéristiques communes. Nous allons exprimer cette relation dans notre programme objet, en utilisant les relations d'héritage: nous aurons donc un nouveau type de variable (cafetiere_grain), différent du type cafetiere, mais qui hérite de ce type un grand nombre de caractéristiques. Simplement, il ajoute de nouvelles caractéristiques à ce type. Ces caractéristiques nous permettront de préciser à la machine le procédé exact pour faire le café, alors que les caractéristiques communes permettent de dire à quelles conditions un objet peut légitimement être appelé cafetière.

Boutons marche-arrêt ou autres réglages

Lorsqu'on se trouve à l'extérieur de la cafetière, on n'a pas accès aux pièces composant la cafetière. De même, dans notre programme, un mécanisme permettra de cacher les objets situés à l'intérieur de l'objet cafetiere.
Par contre, on a un moyen de remplir le réservoir d'eau ou le réservoir de café, de savoir combien d'eau il reste dans le réservoir, combien de sucre dans la réserve de sucre, etc. De même, on a plusieurs boutons de réglage (café fort, moyen, faible), et on a un bouton pour déclencher la mise en route du café. Dans notre modèle, cela correspond à des fonctions que nous appellerons méthodes, ou encore fonctions-membres. Les fonctions-membres sont partie prenante de l'objet, de la même manière que le bouton de marche-arrêt de la (vraie) cafetière est une partie de la cafetière. Mais il s'agit de la partie "interface" avec le monde extérieur. Pour faire du café italien, nous pourrons alors écrire dans notre programme:

cafetiere_grain A;
A.force(10);
A.faire_le_cafe();

On voit que le style de programmation est considérablement différent de ce qui précède; alors que précédemment, le numéro de la cafetière, le type de cafetière, et peut-être encore d'autres informations sont passées en paramètre à une fonction faire_le_cafe, cette fois la fonction faire_le_cafe est directement intégrée à la variable A, ce qui est bien plus proche de la réalité: c'est bien la machine à café, qui intègre un mécanisme permettant de faire du café; de la même manière, l'algorithme expliquant à l'ordinateur comment le café devra être fait se trouve "intégré" à l'objet de type cafetiere.
Plus précisément, on va pouvoir dire au programme que toute cafetiere doit avoir au moins une fonction faire_le_cafe, mais cette fonction sera très différente suivant le type de cafetière. La seule chose de sûre, c'est que cette fonction devra exister.
Puisque la fonction est intégrée à l'objet cafetiere, celui-ci peut aller chercher les informations dont il a besoin directement à l'intérieur de l'objet: c'est ainsi que le paramètre cafforce de tout-à-l'heure a été retiré; il n'est plus nécessaire, car la force du café a été fixée par la fonction force(), qui est elle aussi une méthode de notre objet. A l'inverse de la fonction faire_le_cafe(), par contre, on peut très bien imaginer que pour certains types de machines la fonction force() est inexistante (dans ce cas la force du café est prédéfinie et ne peut être ajustée par l'utilisateur: c'est moins confortable, mais ça fait tout-de-même du café). Donc la fonction force() sera une méthode de cafetiere_grain, pas de cafetiere.

Voyants

La fonction appelée précédemment lire_etat devient, elle aussi, une fonction-membre: elle joue alors le rôle d'un voyant.

Programme principal

Nous pouvons maintenant réécrire l'ébauche de notre programme:

cafetiere C[100];

... initialiser C[i] avec des objets de type cafetiere_grain,
cafetiere_poudre, cafetiere_soluble ...

for (int i=0; i < N; i++) {
if (C[i].lire_etat()==1) {
int rvl = C[i].faire_le_cafe();
if (rvl == 0) {
printf "le cafe est pret\n";
} else {
printf "Machine en panne\n";
};
}
}

Des programmes plus robustes

Puisque les objets de type cafetiere sont tout simplement des variables comme les autres, il est possible, par exemple, de les ranger dans un tableau. Chaque élément d'un tableau sera donc une cafetiere, cependant certains éléments peuvent être un objet de type cafetiere_grain alors que d'autres peuvent être un objet de type cafetiere_soluble. De sorte que lorsque la fonction C[i].faire_le_cafe() est appelée, rien ne dit qu'il se passe en réalité la même chose à chaque itération du tableau. C'était déjà le cas en programmation fonctionnelle, la différence ici est tout simplement que la structure switch précédente a disparu: elle est remplacée par un mécanisme d'appel de fonctions intégré au système lui-même. Cela conduit à une totale séparation entre les différents types de cafetière, donc si je rajoute dans un ou deux siècles un nouveau type de cafetière, je ne risque plus d'ajouter des erreurs et de faire planter les autres types. Le code est à la fois plus clair, parce que plus proche de la pensée humaine, et plus robuste, en ce sens qu'une modification quelque part risque moins d'introduire des erreurs ailleurs. Ces deux caractéristiques conduisent à un code plus simple à maintenir.

Classes et objets

Une classe est une manière de décrire un type d'objets: on peut le voir comme un moule, qui servira à la fabrication des objets proprement dits. Une classe n'est donc pas un objet. Un objet se caractérise par trois choses:

L'état d'un objet:

L'état d'un objet est la combinaison de ses propriétés: celles-ci peuvent elles-mêmes être des objets. Par exemple, l'état de la machine à café sera:

L'état d'un objet dépend de son histoire: si la machine à café est plusieurs fois en fonctionnement, son réservoir finira par se vider.

Le comportement d'un objet:

Le comportement d'un objet est ce qu'il est capable de réaliser, la manière dont il réagira à des messages extérieurs, ... le comportement correspond donc au code qui sera inséré à l'intérieur de l'objet afin de la faire agir. Un changement d'état peut générer un comportement particulier (le réservoir à café se vide: l'objet téléphone à la maintenance), et réciproquement une action entraînera un changement d'état.

L'identité d'un objet:

Si l'on veut que plusieurs objets cohabitent dans le programe, il faudra bien les différentier d'une manière ou d'une autre: l'identité est là pour cela. Le programmeur n'a généralement pas à s'en préoccuper, elle est directement gérée par le système. L'identité de l'objet en C++ est simplement l'adresse de l'objet dans la mémoire.

Donner du sens à nos classes

On ne crée par de nouveau objets seulement pour s'amuser. Les objets qui apparaissent dans les programmes doivent avoir un sens. On distingue trois grands types d'objets:

Sémantique de valeur

Une classe a une sémantique de valeur si deux objets ayant le même état (c'est-à-dire que leurs données membres sont égales) sont considérés comme égaux. On pourra surcharger les opérateurs pour ces types d'objets, cela permettra d'écrire un code plus simple à lire. La classe string de la stl correspond bien à cette sémantique de valeur

Sémantique d'entité

Une classe a une sémantique d'entité si deux objets ayant le même état ne sont pas uniques pour autant: par exemple deux machines à café ayant la même quantité de café, d'eau etc. font tout de même deux machines à café différentes. Il s'agit ici de modéliser les objets de la "vie courante": il en résulte que les objets à sémantique d'entité seront essentiellement des objets "métiers". Ce sont sur ces objets qu'on voudra définir l'héritage, par contre nous ne chercherons pas à surcharger les opérateurs pour ces classes. Nous définirons une fonction de clônage, qui permettra de copier une entité sur une autre en tenant compte de la hiérarchie d'héritage.

Les gestionnaires de ressource

Une troisième sorte de classe est constituée par les gestionnaires de ressources. Il s'agit de classes dont l'unique raison d'être est de gérer (allouer et libérer) des ressources: mémoire, fichier etc. La stl contient plusieurs classes de ce type, elles vont nous faciliter la vie... si nous nous en servons.

Dans ce cours, nous étudierons les trois types d'objets, simplement pour savoir "comment ça marche". Mais dans la "vraie vie", nous définissions la plupart du temps des classes à sémantique d'entité, et nous utilisons en permanence des classes à sémantique de valeur ou gestionnaires de ressources, par le biais de la stl ou d'autres bibliothèques.

Des objets "métiers"

Les objets métiers sont des objets qui doivent être compris par toute personne du domaine concerné: par exemple, l'objet cafetière devrait être aisément compréhensible par tous ceux qui s'occupent réellement de la machine à café. Par contre, si on met les machines à café dans un tableau, qui lui-même sera un objet, ce tableau n'est utile que pour le déroulement du programme lui-même: les gens "du métier" n'ont aucune raison de s'y intéresser.

Relations entre objets

Les objets vont communiquer entre eux en s'envoyant des messages: dans la réalité, ces messages sont tout simplement des appels de fonctions, comme on l'a vu. Les objets sont en relation entre eux (un objet en relation avec aucun autre objet ne servirait à rien). On distingue plusieurs types de relations:

Association
C'est la plus courante, aussi la moins forte: c'est la relation qui lie un enseignant et ses étudiants, par exemple.
Agrégation
C'est la relation qui lie un objet et ses composants: la cafetière et son réservoir d'eau, de café, de gobelets, par exemple. Elle est plus forte que la relation d'association, toutefois une cafetière peut exister sans son réservoir à café.
Composition
La relation la plus forte (utilisée rarement). Un immeuble de 10 étages ne peut exister sans tous ses étages, et réciproquement: on exprimera donc dans le code le fait que si l'objet immeuble est détruit, tous les objets le composant seront détruits également, et réciproquement.
Une sorte de
On pourra exprimer le fait qu'une machine à café particulière est "une sorte de" machine à café.
Un tas de
Des objets particuliers (les conteneurs) nous permettront de mettre nos objets dans des structures de données: on pourra donc avoir "un tableau de cafetières", "une pile de cafetière", "une queue de cafetières", etc.

Les relations d'association, d'agrégation et de composition s'expriment en insérant des variables membres dans une définition de classe (apres). Dans le cas de la relation de composition, il convient de s'assurer que les objets sont construits ou détruits ensemble. La relation "est une sorte de" s'exprime grâce à l'héritage ( apres). Les autres relations s'expriment par les modèles (apres)

top


xhtml Licence Creative Commons Emmanuel Courcelle <emmanuel.courcelle@inp-toulouse.fr>