[EYROLLES] Lorienté objet - Cours et Exercices en UML2

L’orienté objetCours et exercices en UML 2, avec Java 5, C# 2, C++, Python et PHP 5 Hugues Bersini L’orienté objetCours et exercices en UML 2 avec Jav...

18 downloads 497 Views 7MB Size

Hugues Bersini

3 eé dit ion

L’orienté L’orienté

objet objet

Cours exercices UML2,2 Cours et et exercices enen UML

avec Java 5, C# 2, C++, PythonetetPHP PHP55 avec Java 5, C# 2, C++, Python

-PSJFOUÏ

PCKFU édition 32e édition

Dans

la même collection

X Blanc, I. Mounier. – UML 2 pour les développeurs. N°12029, 2006, 202 pages A. Tasso. – Le livre de Java premier langage. N°11994, 4e édition 2006, 472 pages, avec CD-Rom. P. Roques. – UML 2 par la pratique. N°12014, 5e édition 2006, 385 pages. Chez le même éditeur P. Roques. – UML 2. Modéliser une application web. N°11770, 2006, 236 pages (collection Cahiers du programmeur). P. Roques, F. Vallée. – UML 2 en action. De l’analyse des besoins à la conception. N°12104, 4e édition 2007, 382 pages. E. Puybaret. – Cahier du programmeur Swing. N°12019, 2007, 500 pages (coll. Cahiers du programmeur) E. Puybaret. – Java 1.4 et 5.0. (coll. Cahiers du programmeur) N°11916, 3e édition 2006, 400 pages S Powers. – Débuter en JavaScript. N°12093, 2007, 386 pages T. Templier, A. Gougeon. – JavaScript pour le Web 2.0. N°12009, 2007, 492 pages J. Zeldman. – Design web : utiliser les standards, CSS et XHTML. N°12026, 2e édition 2006, 444 pages. X. Briffault, S. Ducasse. – Programmation Squeak. N°11023, 2001, 328 pages. H. Sutter (trad. T. Petillon). – Mieux programmer en C++. N°09224, 2001, 215 pages. P. Haggar (trad. T. Thaureaux). – Mieux programmer en Java. N°09171, 2000, 225 pages. J.-L. Bénard, L. Bossavit , R.Médina , D. Williams. – L’Extreme Programming, avec deux études de cas. N°11051, 2002, 300 pages. I. Jacobson, G. Booch, J.Rumbaugh. – Le Processus unifié de développement logiciel. N°9142, 2000, 487 pages. P. Rigaux, A. Rochfeld. – Traité de modélisation objet. N°11035, 2002, 308 pages. B. Meyer. – Conception et programmation orientées objet. N°9111, 2000, 1223 pages.

)VHVFT#FSTJOJ

-PSJFOUÏ

PCKFU 3e2eédition édition

Avec la contribution d’Ivan Wellesz

ÉDITIONS EYROLLES 61, bd Saint-Germain 75240 Paris Cedex 05 www.editions-eyrolles.com

Le code de la propriété intellectuelle du 1er juillet 1992 interdit en effet expressément la photocopie à usage collectif sans autorisation des ayants droit. Or, cette pratique s’est généralisée notamment dans les établissements d’enseignement, provoquant une baisse brutale des achats de livres, au point que la possibilité même pour les auteurs de créer des œuvres nouvelles et de les faire éditer correctement est aujourd’hui menacée. En application de la loi du 11 mars 1957, il est interdit de reproduire intégralement ou partiellement le présent ouvrage, sur quelque support que ce soit, sans autorisation de l’éditeur ou du Centre Français d’Exploitation du Droit de Copie, 20, rue des Grands-Augustins, 75006 Paris. © Groupe Eyrolles, 2002, 2004, 2007, ISBN : 978-2-212-12084-4

Table des matières Avant-propos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1

L’orientation objet en deux mots . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Objectifs de l’ouvrage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Plan de l’ouvrage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . À qui s’adresse ce livre ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

2 5 5 6

CHAPITRE 1

Principes de base : quel objet pour l’informatique ? . . . . . . . . . . . . . . . . . .

9

Le trio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stockage des objets en mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . L’objet dans sa version passive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . L’objet dans sa version active . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introduction à la notion de classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Des objets en interaction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Des objets soumis à une hiérarchie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Héritage bien reçu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

10 11 15 17 19 21 23 25 26 27

CHAPITRE 2

Un objet sans classe… n’a pas de classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

29

Constitution d’une classe d’objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La classe comme module fonctionnel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La classe comme garante de son bon usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La classe comme module opérationnel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

30 33 36 37

VI

L’orienté objet

Un premier petit programme complet dans les cinq langages . . . . . . . . . . . . . . . . La classe et la logistique de développement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

39 50 52

CHAPITRE 3

Du faire savoir au savoir-faire… du procédural à l’OO . . . . . . . . . . . . . . . .

57

Objectif objet : les aventures de l’OO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mise en pratique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Conception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Impacts de l’orientation objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

58 60 60 62 62

CHAPITRE 4

Ici Londres : les objets parlent aux objets . . . . . . . . . . . . . . . . . . . . . . . . . . . .

65

Envois de messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Association de classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dépendance de classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Réaction en chaîne de messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

66 67 68 70 70

CHAPITRE 5

Collaboration entre classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

73

Pour en finir avec la lutte des classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La compilation Java : effet domino . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . En C#, en Python, PHP 5 et en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . De l’association unidirectionnelle à l’association bidirectionnelle . . . . . . . . . . . . . Auto-association . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Package et namespace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

74 76 77 79 82 83 86

CHAPITRE 6

Méthodes ou messages ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

87

Passage d’arguments prédéfinis dans les messages . . . . . . . . . . . . . . . . . . . . . . . . . Passage d’argument objet dans les messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Une méthode est-elle d’office un message ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La mondialisation des messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

88 95 102 104 105

Table des matières

VII

CHAPITRE 7

L’encapsulation des attributs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

109

Accès aux attributs d’un objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Encapsulation : pourquoi faire ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

110 115 120

CHAPITRE 8

Les classes et leur jardin secret . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

123

Encapsulation des méthodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les niveaux intermédiaires d’encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Afin d’éviter l’effet papillon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

124 127 131 134

CHAPITRE 9

Vie et mort des objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

135

Question de mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . C++ : le programmeur est le seul maître à bord . . . . . . . . . . . . . . . . . . . . . . . . . . . En Java, C#, Python et PHP 5 : la chasse au gaspi . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

136 145 148 154

CHAPITRE 10

UML 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

159

Diagrammes UML 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Représentation graphique standardisée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Du tableau noir à l’ordinateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Programmer par cycles courts en superposant les diagrammes . . . . . . . . . . . . . . . Diagrammes de classe et diagrammes de séquence . . . . . . . . . . . . . . . . . . . . . . . . Diagramme de classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les bienfaits d’UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Diagramme de séquence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

161 162 163 164 165 165 196 199 205

CHAPITRE 11

Héritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

211

Comment regrouper les classes dans des superclasses . . . . . . . . . . . . . . . . . . . . . . Héritage des attributs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

212 213

VIII

L’orienté objet

Héritage ou composition ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Économiser en rajoutant des classes ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Héritage des méthodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La recherche des méthodes dans la hiérarchie . . . . . . . . . . . . . . . . . . . . . . . . . . . . Encapsulation protected . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Héritage et constructeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Héritage public en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Le multihéritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

219 220 220 229 230 231 237 238 249

CHAPITRE 12

Redéfinition des méthodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

253

La redéfinition des méthodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Beaucoup de verbiage mais peu d’actes véritables . . . . . . . . . . . . . . . . . . . . . . . . . Un match de football polymorphique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

254 255 256 288

CHAPITRE 13

Abstraite, cette classe est sans objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

299

De Canaletto à Turner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Des classes sans objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Du principe de l’abstraction à l’abstraction syntaxique . . . . . . . . . . . . . . . . . . . . . Un petit supplément de polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

300 300 301 308 313

CHAPITRE 14

Clonage, comparaison et assignation d’objets . . . . . . . . . . . . . . . . . . . . . . . .

325

Introduction à la classe Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Décortiquons la classe Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Test d’égalité de deux objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Le clonage d’objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Égalité et clonage d’objets en Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Égalité et clonage d’objets en PHP5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Égalité, clonage et affectation d’objets en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . En C#, un cocktail de Java et de C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

326 329 331 336 339 341 343 353 359

Table des matières

IX

CHAPITRE 15

Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

361

Interfaces : favoriser la décomposition et la stabilité . . . . . . . . . . . . . . . . . . . . . . . Java, C# et PHP5 : interface via l’héritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les trois raisons d’être des interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les Interfaces dans UML 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . En C++ : fichiers .h et fichiers .cpp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaces : du local à Internet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

363 363 364 376 377 380 381

CHAPITRE 16

Distribution gratuite d’objets : pour services rendus sur le réseau . . . . .

385

Objets distribués sur le réseau : pourquoi ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . RMI (Remote Method Invocation) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Corba (Common Object Request Broker Architecture) . . . . . . . . . . . . . . . . . . . . . Rajoutons un peu de flexibilité à tout cela . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les services Web sur .Net . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

386 389 395 402 408 418

CHAPITRE 17

Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

421

Informatique séquentielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implémentation en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implémentation en C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implémentation en Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . L’impact du multithreading sur les diagrammes de séquence UML . . . . . . . . . . . Du multithreading aux applications distribuées . . . . . . . . . . . . . . . . . . . . . . . . . . . Des threads équirépartis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Synchroniser les threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

423 425 426 428 430 431 432 432 434 441

CHAPITRE 18

Programmation événementielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

445

Des objets qui s’observent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . En Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . En C# : les délégués . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

446 447 450

X

L’orienté objet

En Python : tout reste à faire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Un feu de signalisation plus réaliste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

456 459 461

CHAPITRE 19

Persistance d’objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

463

Sauvegarder l’état entre deux exécutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Simple sauvegarde sur fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sauvegarder les objets sans les dénaturer : la sérialisation . . . . . . . . . . . . . . . . . . . Les bases de données relationnelles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Réservation de places de spectacles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les bases de données relationnelles-objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les bases de données orientées objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

464 465 472 477 488 493 497 499

CHAPITRE 20

Et si on faisait un petit flipper ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

501

Généralités sur le flipper et les GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Retour au Flipper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

503 512

CHAPITRE 21

Les graphes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

525

Le monde regorge de réseaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tout d’abord : juste un ensemble d’objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Liste liée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La généricité en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La généricité en Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Passons aux graphes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

526 528 529 536 539 544 549

CHAPITRE 22

Petites chimie et biologie OO amusantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

553

Pourquoi de la chimie OO ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les diagrammes de classe du réacteur chimique . . . . . . . . . . . . . . . . . . . . . . . . . . Quelques résultats du simulateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La simulation immunologique en OO ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

554 555 569 571

Table des matières

XI

CHAPITRE 23

Design patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

577

Introduction aux design patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les patterns « truc et ficelle » . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Les patterns qui se jettent à l’OO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

578 580 587

Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

599

Avant-propos Dans les tout débuts de l’informatique, le fonctionnement « intime » des processeurs décidait toujours, en fin de compte, de la seule manière efficace de programmer un ordinateur. Alors que l’on acceptait tout programme comme une suite logique d’instructions, il était admis que l’organisation du programme et la nature même de ces instructions ne pouvaient s’éloigner de la façon dont le processeur les exécutait : pour l’essentiel, des modifications de données mémorisées, des déplacements de ces données d’un emplacement mémoire à un autre, et des opérations d’arithmétique et de logique élémentaire. La mise au point d’algorithmes complexes, dépassant les simples opérations mathématiques et les simples opérations de stockage et de récupérations de données, obligea les informaticiens à effectuer un premier saut dans l’abstrait, en inventant un style de langage dit procédural, auquel appartiennent les langages Fortran, Cobol, Basic, Pascal, C... Ces langages permettaient à ces informaticiens de prendre quelques distances par rapport au fonctionnement intime des processeurs (en ne travaillant plus directement à partir des adresses mémoire et en évitant la manipulation directe des instructions élémentaires) et d’élaborer une écriture de programmes plus proches de la manière naturelle de poser et de résoudre les problèmes. Les codes écrits dans ces langages devenant indépendants en cela des instructions élémentaires propres à chaque type de processeur. Ces langages cherchaient à se positionner quelque part entre l’écriture des instructions élémentaires et l’utilisation tant du langage naturel que du sens commun. Il est incontestablement plus simple d’écrire : c = a + b qu’une suite d’instructions telles que : "load a, reg1", "load b, reg2", "add reg3, reg1, reg2", "move c, reg3", ayant pourtant la même finalité. Une opération de traduction automatique, dite de compilation, se charge alors de traduire le programme, écrit au départ dans ce nouveau langage, dans les instructions élémentaires, seules comprises par le processeur. Cette montée en abstraction permise par ces langages de programmation présente un double avantage : une facilitation d’écriture et de résolution algorithmique, ainsi qu’une indépendance accrue par rapport aux différents types de processeur existant aujourd’hui sur le marché. Plus les problèmes à affronter gagnaient en complexité – comptabilité, jeux automatiques, compréhension et traduction des langues naturelles, aide à la décision, bureautique, conception et enseignement assistés, programmes graphiques, etc. –, plus l’architecture et le fonctionnement des processeurs semblaient contraignants, et plus il devenait vital d’inventer des mécanismes informatiques simples à mettre en œuvre, permettant une réduction de cette complexité et un rapprochement encore plus marqué de l’écriture des programmes des manières humaines de poser et de résoudre les problèmes. Avec l’intelligence artificielle, l’informatique s’inspira de notre mode cognitif d’organisation des connaissances, comme un ensemble d’objets conceptuels entrant dans un réseau de dépendance et se structurant de manière taxonomique. Avec la systémique ou la bio-informatique, l’informatique nous révéla qu’un ensemble d’agents au fonctionnement élémentaire, mais s’influençant mutuellement, peut produire un comportement émergent d’une surprenante complexité. La complexité affichée par le comportement d’un système observé dans sa globalité ne témoigne pas systématiquement d’une complexité équivalente lorsque l’attention est portée sur

2

L’orienté objet

chacune des parties composant ce système et prise isolément. Dès lors, pour comprendre jusqu’à reproduire ce comportement par le biais informatique, la meilleure approche consiste en une découpe adéquate du système en ses parties et en une attention limitée au fonctionnement de chacune d’entre elle. Tout cela mis ensemble : la nécessaire distanciation par rapport au fonctionnement du processeur, la volonté de rapprocher la programmation du mode cognitif de résolution de problème, les percées de l’intelligence artificielle et de la bio-informatique, le découpage comme voie de simplification des systèmes apparemment complexes, conduisit graduellement à un deuxième style de langage de programmation, un tout petit peu plus récent, bien que fêtant ses 45 ans d’existence (l’antiquité à l’échelle informatique) : les langages orientés objets, tels Simula, Smalltalk, C++, Eiffel, Java, C#, Delphi, Power Builder, Python et bien d’autres...

L’orientation objet en deux mots À la différence de la programmation procédurale, un programme écrit dans un langage objet répartit l’effort de résolution de problèmes sur un ensemble d’objets collaborant par envoi de messages. Chaque objet se décrit par un ensemble d’attributs (partie statique) et un ensemble de méthodes portant sur ces attributs (partie dynamique). Certains de ces attributs étant l’adresse des objets avec lesquels les premiers collaborent, il leur est possible de déléguer certaines des tâches à leurs collaborateurs. Le tout s’opère en respectant un principe de distribution des responsabilités on ne peut plus simple, chaque objet s’occupant de ses propres attributs. Lorsqu’un objet exige de s’informer ou de modifier les attributs d’un autre, il charge cet autre de s’acquitter de cette tâche. Cette programmation est fondamentalement distribuée, modularisée et décentralisée. Pour autant qu’elle respecte également des principes de confinement et d’accès limité (dit d’encapsulation) que nous décrirons dans l’ouvrage, cette répartition modulaire a également l’insigne avantage de favoriser la stabilité des développements, en restreignant au maximum l’impact de modifications apportées au code au cours du temps. Ces impacts seront limités aux seuls objets qu’ils concernent et à aucun de leurs collaborateurs, même si le comportement de ces derniers dépend en partie des fonctionnalités affectées. Ces améliorations, résultant de la prise de conscience des problèmes posés par l’industrie du logiciel ces dernières années, complexité accrue et stabilité dégradée, ont enrichi la syntaxe des langages objet. Un autre mécanisme de modularisation inhérent à l’orienté objet est l’héritage qui permet à la programmation de refléter l’organisation taxonomique de notre connaissance en une hiérarchie de concepts du plus au moins général. À nouveau, cette organisation modulaire en objets génériques et plus spécialistes est à l’origine d’une simplification de la programmation, d’une économie d’écriture et de la création de zone de code aux modifications confinées. Aussi bien cet héritage que la répartition des tâches entre les objets permettent une décomposition plus naturelle des problèmes, une réutilisation facilitée des codes déjà existants, et une maintenance facilitée et allégée de ces derniers. L’orientation objet s’impose, non pas comme une panacée universelle, mais comme une évolution naturelle, au départ de la programmation procédurale, qui facilite l’écriture de programmes, les rendant plus gérables, plus compréhensibles, plus stables et réexploitables. L’orienté objet inscrit la programmation dans une démarche somme toute très classique destinée à affronter la complexité de quelque problème qui soit : une découpe naturelle et intuitive en des parties plus simples. A fortiori, cette découpe sera d’autant plus intuitive qu’elle s’inspire de notre manière « cognitive » de découper la réalité qui nous entoure. L’héritage, reflet fidèle de notre organisation cognitive, en est le témoignage le plus éclatant. L’approche procédurale rendait cette découpe moins naturelle, plus « forcée ». Si de nombreux adeptes de la programmation procédurale sont en effet conscients qu’une manière incontournable de simplifier le développement d’un programme complexe est de le découper physiquement, ils souffrent de l’absence d’une prise en compte naturelle et syntaxique de cette découpe dans les langages de programmation utilisés. Dans un programme imposant, l’OO permet de tracer les pointillés que les ciseaux doivent suivre là où il semble le

Avant-propos

3

plus naturel de les tracer : au niveau du cou, des épaules ou de la ceinture, et non pas au niveau des sourcils, des biceps ou des mollets. De surcroît, cette pratique de la programmation incite à cette découpe suivant deux dimensions orthogonales : horizontalement, les classes se déléguant mutuellement un ensemble de services, verticalement, les classes héritant entre elles d’attributs et de méthodes installés à différents niveaux d’une hiérarchie taxonomique. Pour chacune de ces dimensions, reproduisant fidèlement nos mécanismes cognitifs de conceptualisation, en plus de simplifier l’écriture des codes, il est important de faciliter la récupération de ces parties dans de nouveaux contextes et d’assurer la robustesse de ces parties aux changements survenus dans d’autres. Un code OO, idéalement, sera aussi simple à créer qu’à maintenir, récupérer et faire évoluer. Il est parfaitement inconséquent d’opposer le procédural à l’OO car, in fine, toute programmation des méthodes (c’est-à-dire la partie active des classes et des objets) reste totalement tributaire des mécanismes procéduraux. On y rencontre des variables, des arguments, des boucles, des arguments de fonction, des instructions conditionnelles, tout ce que l’on trouve classiquement dans les boîtes à outils procédurales. L’OO ne permet en rien de faire l’économie du procédural, simplement, il complémente celui-ci, en lui superposant un système de découpe plus naturel et facile à mettre en œuvre. Il n’est guère surprenant que la plupart des langages procéduraux comme le C, Cobol ou, plus récemment, PHP, se soient relativement aisément enrichis d’une couche dite OO sans que cette addition ne remette sérieusement en question l’existant procédural. Cependant, l’impact de cette couche additionnelle ne se limite pas à quelques structures de données supplémentaires afin de mieux organiser les informations manipulées par le programme. Il va bien au-delà. C’est toute une manière de concevoir un programme et la répartition de ses parties fonctionnelles qui est en jeu. Les fonctions et les données ne sont plus d’un seul tenant mais éclatées en un ensemble de modules reprenant, chacun, une souspartie de ces données et les seules fonctions qui les manipulent. Il faut réapprendre à programmer en s’essayant au développement d’une succession de micro-programmes et au couplage soigné et réduit au minimum de ces micro-programmes. En substance, la programmation OO pourrait reprendre à son compte ce slogan devenu très célèbre parmi les adeptes des courants altermondialistes : « agir localement, penser globalement ». Se pose alors la question de la stratégie pédagogique, question très controversée dans l’enseignement de l’informatique aujourd’hui, sur l’ordre chronologique à donner au procédural et à l’OO. De nombreux enseignants de la programmation, soutenus en cela par de très nombreux manuels de programmation, considèrent qu’il faut d’abord passer par un enseignement intensif et une maîtrise parfaite du procédural, avant de faire le grand saut vers l’OO. Quinze années d’enseignement de la programmation à des étudiants de tout âge et de toutes conditions (de 7 à 77 ans, issus des sciences humaines ou exactes) nous ont convaincus qu’il n’y a aucun ordre à donner. De même qu’historiquement, l’OO est né quasiment en même temps que le procédural et en complément de celui-ci, l’OO doit s’enseigner conjointement et en complément du procédural. Il faut enseigner les instructions de contrôle en même temps que la découpe en classe. Tout comme un cours de cuisine s’attardant sur quelques ingrédients culinaires très particuliers parallèlement à la manière dont ces ingrédients doivent s’harmoniser, ou un cours de mécanique automobile se focalisant sur quelques pièces ou mécanismes en particulier en même temps que le plan et le fonctionnement d’ensemble, l’enseignement de la programmation doit mélanger à loisir la perception « micro » des mécanismes procéduraux à la vision « macro » de la découpe en objets. Aujourd’hui, tout projet informatique de dimension conséquente débute par une analyse des différentes classes qui le constituent. Il faut aborder l’enseignement de la programmation tout comme débute la prise en charge de ce type de projet, en enseignant au plus vite la manière dont ces classes et les objets qui en résultent opèrent à l’intérieur d’un programme. L’orienté objet s’est trouvé à l’origine ces dernières années, compétition oblige, d’une explosion de technologies différentes, mais toutes intégrant à leur manière les mécanismes de base de l’OO : classes, objets, envois de messages, héritage, encapsulation, polymorphisme... Ainsi sont apparus une multitude de langages de programmation, qui intègrent ces mécanismes de base à leur manière, à partir d’une syntaxe dont les différences

4

L’orienté objet

sont soit purement cosmétiques, soit légèrement plus subtiles. Ils sont autant de variations sur le ou les thèmes créés par leurs trois principaux précurseurs : Simula, Smalltalk et C++. L’OO a également permis de repenser trois des chapitres les plus importants de l’informatique de ces deux dernières décennies. Tout d’abord, le besoin d’une méthode de modélisation graphique débouchant sur un niveau d’abstraction encore supplémentaire (on ne programme plus, on dessine un ensemble de diagrammes, le code étant généré automatiquement à partir de ceux-ci) (rôle joué par UML 2) ; ensuite, les applications informatiques distribuées (on ne parlera plus d’applications distribuées mais d’objets distribués, et non plus d’appels distants de procédures mais d’envoi de messages à travers le réseau) ; enfin, le stockage des données qui doit maintenant compter avec les objets. Chaque fois, plus qu’un changement de vocabulaire, un changement de mentalité sinon de culture s’impose. Aujourd’hui, force est de constater que l’OO constitue un sujet d’une grande attractivité pour tous les acteurs de l’informatique. Microsoft a développé un nouveau langage informatique purement objet, C#, et a très intensément contribué au développement d’un système d’informatique distribuée, basé sur des envois de messages d’ordinateur à ordinateur, les services web. Tous les langages informatiques intégrés dans sa nouvelle plate-forme de développement, Visual Studio .Net (aux dernières nouvelles, ils seraient 22), visent à une uniformisation (y compris les nouvelles versions de Visual Basic et Visual C++) en intégrant les mêmes briques de base de l’OO. Aboutissement considérable s’il en est, il devient très simple de faire communiquer ou hériter entre elles des classes écrites dans des langages différents. Quelques années auparavant, Sun avait créé Java, une création déterminante car à l’origine de ce nouvel engouement pour une manière de programmer qui pourtant existait depuis toujours sans que les informaticiens dans leur ensemble en reconnaissent l’utilité et la pertinence. Depuis, en partant de son langage de prédilection, Sun à créé RMI, Jini, et sa propre version des services web, tous basés sur les technologies OO. Ces mêmes services web font l’objet de développements tout autant aboutis chez HP ou IBM. À la croisée de Java et du Web, originellement, la raison sinon du développement de Java du moins de son succès, on découvre une importante panoplie d’outils de développement et de conception de sites web dynamiques. IBM et Borland, en rachetant respectivement Rational et Together, mènent la danse en matière d’outil d’analyse du développement logiciel, avec la mise au point de puissants environnements UML, technologie OO comme il se doit. Au départ des développements chez IBM, la plate-forme logicielle Eclipse est sans doute, à ce jour, l’aventure Open Source la plus aboutie en matière d’OO. Comme environnement de développement Java, Eclipse est aujourd’hui le plus prisé et le plus usité et gagne son pari : éclipser tous les autres. Borland a rendu Together intégrable tant dans Visual Studio.Net que dans Eclipse comme outil de modélisation UML synchronisant au mieux et au plus la programmation et la réalisation des diagrammes UML. Enfin, l’OMG, organisme de standardisation du monde logiciel, n’a pas comme lettre initiale de son acronyme la lettre O pour rien. UML et Corba sont ses premières productions : la version OO de l’analyse logicielle et la version OO de l’informatique distribuée. Cet organisme plaide de plus en plus pour un développement informatique détaché des langages de programmation ainsi que des plates-formes matérielles, par l’utilisation intensive des diagrammes UML. Au départ de ces mêmes diagrammes, les codes seraient générés automatiquement dans un langage choisi et en adéquation avec la technologie voulue. Par le nouveau saut dans l’abstraction qu’il autorise, UML se profilerait comme le langage de programmation de demain. Il jouerait à ce titre le même rôle que jouèrent les langages de programmation au temps de leur apparition, en reléguant ceux-ci à la même place que le langage assembleur auquel ils se sont substitués jadis : un pur produit de traduction automatisée. Au même titre qu’Unix pour les développements en matière de système d’exploitation, l’OO apparaît donc comme le point d’orgue et de convergence de ce qui se fait de plus récent en matière de langages et d’outils de programmation.

Avant-propos

5

Objectifs de l’ouvrage Toute pratique économe, fiable et élégante de Java, C++, C#, Python, .Net ou UML requiert, pour débuter, une bonne maîtrise des mécanismes de base de l’OO. Et, pour y pourvoir, rien de mieux que d’expérimenter les technologies OO dans ces différentes versions, comme un bon conducteur qui se sera frotté à plusieurs types de véhicule, un bon skieur à plusieurs styles de ski et un guitariste à plusieurs modèles de guitare. Plutôt qu’un voyage en profondeur dans l’un ou l’autre de ces multiples territoires, ce livre vous propose d’explorer plusieurs d’entre eux, mais en tentant à chaque fois de dévoiler ce qu’ils recèlent de commun. Car ce sont ces ressemblances qui constituent en dernier ressort les briques fondamentales de l’OO, matière de base, qui se devrait de perdurer encore de nombreuses années, y compris sous de nouveaux déguisements. Nous pensons que la mise en parallèle de C++, de Java, C#, Python, PHP 5 et UML est une voie privilégiée pour l’extraction de ces mécanismes de base. Il nous a paru pour cette raison indispensable de discuter et comparer la façon dont ces cinq langages de programmation gèrent, par exemple, l’occupation mémoire par les objets ou leur manière d’implémenter le polymorphisme, pour en comprendre, in fine, toute la problématique et les subtilités indépendamment de l’une ou l’autre implémentation. Rajoutez une couche d’abstraction, ainsi que le permet UML, et cette compréhension ne pourra s’en trouver que renforcée. Chacun de ces cinq langages offre des particularités amenant les praticiens de l’un ou l’autre à le prétendre mordicus supérieur aux autres : la puissance du C++, la compatibilité Windows et l’intégration XML de C#, l’anti-Microsoft et le leadership de Java en matière de développement web, les vertus pédagogiques et l’aspect « scripting » de Python, le succès incontestable de PHP 5 pour la mise en place de solution web dynamique et capable de s’interfacer aisément avec les bases de données. Nous nous désintéresserons ici complètement de ces guerres de religion (qui partagent avec les guerres de langages informatiques pas mal d’irrationalité), a fortiori car notre projet pédagogique nous conduit bien davantage à nous pencher sur ce qui les réunit plutôt que ce qui les différencie. C’est leur multiplicité qui a présidé à cet ouvrage et qui en fait, nous l’espérons, son originalité. Nous n’allons pas nous en plaindre et défendons en revanche l’idée que le choix définitif de l’un ou l’autre de ces langages dépend davantage d’habitude, d’environnement professionnel ou d’enseignement, de questions sociales et économiques et surtout de la raison concrète de cette utilisation (pédagogie, performance machine, adéquation Web ou base de données, …). De plus, le succès d’UML, assimilable à un langage universel OO à l’intersection de tous les autres et automatiquement traduisible dans chacun, ou des efforts, tels ceux de Microsoft, d’homogénéisation des langages OO, rend ces discordes quelque peu obsolètes et un peu dérisoires, tant il va devenir facile de passer de l’un à l’autre. Enfin, nous souhaitions que cet ouvrage, tout en étant suffisamment détaché de toutes technologies, couvre l’essentiel des problèmes posés par la mise en œuvre des objets en informatique, y compris le problème de leur stockage sur le disque dur et leur interfaçage avec les bases de données, de leur fonctionnement en parallèle, et leur communication à travers Internet. Un ouvrage donc qui découvrirait l’OO de très haut, ce qui lui permet évidemment de balayer très large, et qui accepte ce faisant de perdre un peu en précision, perte dont il nous apparaît nécessaire de mettre en garde le lecteur.

Plan de l’ouvrage Les 23 chapitres de ce livre peuvent se répartir en cinq grandes parties. Le premier chapitre constitue une partie en soi car il a pour importante mission d’introduire aux briques de base de la programmation orientée objet, sans aucun développement technique : une première esquisse, teintée de sciences cognitives, et toute en intuition, des éléments essentiels de la pratique OO.

6

L’orienté objet

La deuxième partie intègre les quatorze chapitres suivants. Il s’agit pour chacun d’entre eux de décrire, plus techniquement cette fois, ces briques de base que sont : objet, classe (chapitres 2 et 3), messages et communication entre objets (chapitres 4, 5 et 6), encapsulation (chapitres 7 et 8), gestion mémoire des objets (chapitre 9), modélisation objet (chapitre 10), héritage et polymorphisme (chapitres 11 et 12), classe abstraite (chapitre 13), clonage et comparaison d’objets (chapitre 14), interface (chapitre 15). Chacune de ces briques est illustrée par des exemples en Java, C#, C++, Python, PHP 5 et UML. Nous y faisons le pari que cette mise en parallèle est la voie la plus naturelle pour la compréhension des mécanismes de base : extraction du concept par la multiplication des exemples. La troisième partie reprend, dans le droit fil des ouvrages dédiés à l’un ou l’autre langage objet, des notions jugées plus avancées : les objets distribués, Corba, RMI, Services web (chapitre 16), le multithreading ou programmation parallèle (ou concurrentielle, chapitre 17), la programmation événementielle (chapitre 18) et enfin la sauvegarde des objets sur le disque dur, y compris l’interfaçage entre les objets et les bases de données relationnelles (chapitre 19). Là encore, le lecteur se trouvera le plus souvent en présence de plusieurs versions dans les quatre langages de ces mécanismes. La quatrième partie décrit plusieurs projets de programmation totalement aboutis, tant en UML qu’en Java. Elle inclut d’abord le chapitre 20, décrivant la modélisation objet d’un petit flipper et les problèmes de conception orientée objet que cette modélisation pose. Le chapitre 21, lié au chapitre 22, décrit la manière dont les objets peuvent s’organiser en liste liée ou en graphe, mode de mise en relation et de regroupement des objets que l’on retrouve abondamment dans toute l’informatique. Le chapitre 22 marie la chimie et la biologie à la programmation OO. Il contient tout d’abord la programmation d’un réacteur chimique générant de nouvelles molécules à partir de molécules de base, et ce, tout en suivant à la trace l’évolution de la concentration des molécules dans le temps. La chimie – une chimie élémentaire acquise bien avant l’université – nous est apparue comme une plate-forme pédagogique idéale pour l’assimilation des concepts objets. Nous ne surprendrons personne en affirmant que les atomes et les molécules sont deux types de composants chimiques, et que les secondes sont composées des premiers. Dans ce chapitre, nous traduisons ces connaissances en UML et en Java. Dans la suite de la chimie, nous proposons aussi dans le chapitre une simulation élémentaire du système immunitaire, comme nouvelle illustration de combien l’informatique OO se prête facilement à la reproduction informatisée des concepts de science naturelle, tels ceux que l’on rencontre en chimie ou en biologie. Enfin la dernière partie se ramène au seul dernier chapitre, le chapitre 23, dans lequel est présenté un ensemble de recettes de conception OO, solutionnant de manière fort élégante un ensemble de problèmes récurrents dans la réalisation de programme OO. Ces recettes de conception, dénommées Design Pattern, sont devenues fort célèbres dans la communauté OO. Leur compréhension accompagne une bonne maîtrise des principes OO et s’inscrit dans la suite logique de l’enseignement des briques et des mécanismes de base de l’OO. Elle fait souvent la différence entre l’apprenti et le compagnon parmi les programmeurs OO. Nous les illustrons en partie sur le flipper, la chimie et la biologie des chapitres précédents.

À qui s’adresse ce livre ? Cet ouvrage ayant pour objet de traiter de nombreuses technologies, nul doute qu’il est destiné à être lu par un public assez large. En clair, il s’adresse à tous les adeptes de chacune de ces technologies : industriels, enseignants et étudiants qui pourront le confronter utilement à l’état de l’art en la matière. La vocation première de cet ouvrage n’en reste pas moins une initiation à la programmation orientée objet, prérequis indispensable à l’assimilation de nombreuses autres technologies.

Avant-propos

7

Ce livre sera un compagnon d’étude utile et, nous l’espérons, enrichissant pour les étudiants qui comptent la programmation objet dans leur cursus d’étude (et toutes technologies s’y rapportant : Java, C++, C#, Python, PHP, Corba, RMI, Services Web, UML). Il devrait les aider, le cas échéant, à évoluer de la programmation procédurale à la programmation objet, pour aller ensuite vers toutes les technologies s’y rapportant. Nous ne pensons pas, en revanche, que ce livre peut seul prétendre à une même porte d’entrée dans le monde de la programmation tout court. Comme dit précédemment, nous pensons qu’il est idéal d’aborder les mécanismes OO en même temps que procéduraux. Pour des raisons évidentes de place, et parce que les librairies informatiques en regorgent déjà, nous avons fait l’économie d’un enseignement de base des mécanismes procéduraux : variables, boucles, instructions conditionnelles, éléments fondamentaux et compagnons indispensables à l’assimilation de l’OO. Nous pensons, dès lors, que ce livre sera plus facile à aborder pour des lecteurs ayant déjà un peu de pratique de la programmation dite procédurale, et ce, dans un quelconque langage de programmation. Aujourd’hui, l’informatique est un sujet si vaste, existant à tant de niveaux d’abstraction, et pour tant de raisons différentes, qu’il n’est pas étonnant qu’il faille l’aborder muni de plusieurs guides. Ce livre en est un. Il n’a rien d’exhaustif, ne se spécialise dans aucune des technologies évoquées, mais fournit les bases nécessaires à l’assimilation d’un grand nombre d’entre elles et de celles à venir.

1 Principes de base : quel objet pour l’informatique ? Ce chapitre a pour but une introduction aux briques de base de la conception et de la programmation orientée objet (OO). Il s’agit pour l’essentiel des notions d’objet, de classe, de message et d’héritage. À ce stade, aucun approfondissement technique n’est effectué. Les quelques bouts de code seront écrits dans un pseudo langage très proche de Java. De simples petits exercices de pensée permettent une mise en bouche, toute en intuition, des éléments essentiels de la pratique OO.

Sommaire : Introduction à la notion d’objet — Introduction à la notion et au rôle du référent — L’objet dans sa version passive et active — Introduction à la notion de classe — Les interactions entre objets — Introduction aux notions d’héritage et de polymorphisme

Doctus — Tu as l’air bien remonté, aujourd’hui ! Candidus — Je cherche un objet, mon vieux ! C’est l’objet que je cherche partout. Doc. — Ce n’est pourtant pas ce qui manque… Tiens, prends donc ma valise… Cand. — Non, je cherche un objet autrement plus encombrant… C’est ce sacré objet logiciel dont tout le monde parle. Il me fait penser au Yéti… Je me demande si quelqu’un en a vraiment rencontré un… Doc. — Quelle idée, il n’a rien d’aussi mystérieux notre objet.. Il s’agit simplement de petits soldats qui vont nous libérer de bien des contraintes du monde procédural. Cand. — Justement ! Dis-moi ce qu’est cette guerre Procédural contre Objet. Doc. — Au commencement… il y avait l’ordinateur, avec toutes ses faiblesses de nouveau-né. C’est nous qui étions à son service pour le pouponner. Il fallait être sacrément malin pour en tirer quelque chose. Cand. — Et maintenant il a grandi et je parie qu’il veut jouer avec des petits objets. Doc. — Il a effectivement pris du plomb dans la tête et il comprend beaucoup mieux ce qu’on attend de lui. On peut lui parler en adulte, lui expliquer les choses d’une façon plus structurée… Cand. — …Veux-tu dire qu’il serait capable de comprendre ce que nous voulons sous forme de spécification ?

10

L’orienté objet

Doc. — Doucement ! Je dis simplement que nous ne passerons plus tout notre temps à considérer ce que nos processeurs attendent pour faire le travail demandé. C’est la première des étapes que nous avons déjà franchies. Cand. — Quelles sont les autres étapes ? Doc. — Et bien notre bébé est aujourd’hui capable de manipuler lui-même les informations qu’on lui confie. Il a ses propres méthodes d’utilisation et de rangement. Il ne veut même plus qu’on touche à ses jouets.

Un rapide coup d’œil par la fenêtre et nous apercevons… des voitures, des passants, des arbres, un immeuble, un avion… Cette simple perception est révélatrice d’un ensemble de mécanismes cognitifs des plus subtils, dont la compréhension est une porte d’entrée idéale dans le monde de l’informatique orientée objet. En effet, pourquoi n’avoir pas cité « l’air ambiant », la « température », la « bonne ambiance » ou, encore, « la lumière du jour », que l’on perçoit tout autant ? Pourquoi les premiers se détachent-ils de cette toile de fond parcourue par nos yeux ? Tout d’abord, leur structure est singulière, compliquée, ils présentent une forme alambiquée, des dimensions particulières, parfois une couleur uniforme et distincte du décor qui les entoure. Nous dirons que chacun se caractérise par un ensemble « d’attributs » structuraux, prenant pour chaque objet une valeur particulière : une des voitures est rouge, plutôt longue, ce passant est grand, assez vieux, courbé, etc. Ces attributs structuraux – et leur présence conjointe dans les objets – sont la raison première de l’attrait perceptif qu’ils exercent. C’est aussi la raison de la segmentation et de la nette séparation perceptive qui en résulte, car si les voitures et les arbres se détachent en effet de l’arrière plan, nous ne les confondons en rien.

Le trio Nous avons l’habitude de décrire le monde qui nous entoure à l’aide de ce trio que les informaticiens se plaisent à nommer : , par exemple : , , , . Si ce sont les entités et non pas leurs attributs qui vous sautent aux yeux, c’est bien parce que chacune de ces entités ou objets, la voiture et le passant, se caractérise par plusieurs de ces attributs, couleur, âge, taille, prenant une valeur particulière, uniforme « sur tout l’objet ». L’objet est perçu, de fait, car il est au croisement de ces différents attributs. Il naît à partir de leur rencontre. Les attributs en tant que tels ne sont pas des objets, puisqu’ils servent justement à la caractérisation de ces objets, à les faire exister et à les rendre prégnants. La nature des attributs est telle qu’ils se retrouvent attribut d’un nombre important d’objets, pourtant très différents. La voiture a une taille comme le passant. L’arbre a une couleur comme la voiture. Le monde des attributs est beaucoup moins diversifié que le monde des objets. C’est une des raisons qui nous permettent de regrouper les objets en classes et les classes en différentes sous-classes, comme nous le découvrirons plus tard. Que ce soit comme résultat de nos perceptions ou dans notre pratique langagière, les attributs et les objets jouent des rôles très différents. Les attributs structurent nos perceptions et ils servent, par exemple, sous forme d’adjectifs, à qualifier les noms qui les suivent ou les précèdent. La première conséquence de cette simple faculté de découpage cognitif sur l’informatique d’aujourd’hui est la suivante : Objets, attributs, valeurs Il est possible dans tous les langages informatiques de stocker et de manipuler des objets en mémoire, comme autant d’ensembles de couples attribut/valeur.

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

11

Stockage des objets en mémoire Dans la figure qui suit, nous voyons apparaître ces différents objets dans la mémoire de l’ordinateur. Chacun occupe un espace mémoire qui lui est propre et alloué lors de sa création. De façon à se faciliter la vie, les informaticiens ont admis un ensemble de « types primitifs » d’attribut, dont ils connaissent à l’avance la taille requise pour encoder la valeur. Figure 1-1

Les objets informatiques et leur stockage en mémoire.

Il s’agit, par exemple, de types comme réel qui occupera 64 bits d’espace (dans le codage des réels en base 2 et selon une standardisation reconnue) ou entier, qui en occupera 32 (là encore par sa traduction en base 2), ou finalement caractère qui occupera 16 bits dans le format « unicode » (qui code ainsi chacun des caractères de la majorité des écritures répertoriées et les plus pratiquées dans le monde). Dans notre exemple, les dimensions seraient typées en tant qu’entier ou réel. Tant la couleur que la marque pourraient se réduire à une valeur numérique (ce qui ramènerait l’attribut à un entier) choisie parmi un ensemble fini de valeurs possibles, indiquant chacune une couleur ou une marque. Dès lors, le mécanisme informatique responsable du stockage de l’objet « saura », à la simple lecture structurelle de l’objet, quel est l’espace mémoire requis par son stockage.

12

L’orienté objet

La place de l’objet en mémoire Les objets seront structurellement décrits par un premier ensemble d’attributs de type primitif, tels qu’entier, réel ou caractère, qui permettra, précisément, de déterminer l’espace qu’ils occupent en mémoire.

Types primitifs À l’aide de ces types primitifs, le stockage en mémoire de ces différents objets se transforme comme reproduit dans la figure 1-2. Ce mode de stockage de données est une caractéristique récurrente en informatique, présent dans pratiquement tous les langages de programmation, et se retrouvant dans les bases de données dites relationnelles. Dans ces bases de données, chaque objet devient un enregistrement. Les voitures sont stockées à l’aide de leurs couples attribut/valeur dans des bases encodant des voitures, et gérées, par exemple, par un concessionnaire automobile (comme montré à la figure 1-3). Figure 1-2

Les objets avec leur nouveau mode de stockage où chaque attribut est d’un type dit « primitif » ou « prédéfini », comme entier, réel, caractère…

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

13

Figure 1-3

Table d’une base de données relationnelle de voitures, avec quatre attributs et six enregistrements.

Marque

Modèle

Série

Numéro

Renault

18

RL

4698 SJ 45

Renault

Kangoo

RL

4568 HD 16

Renault

Kangoo

RL

6576 VE 38

Peugeot

106

KID

7845 ZS 83

Peugeot

309

chorus

7647 ABY 82

Ford

Escort

Match

8562 EV 23

Bases de données relationnelles Il s’agit du mode de stockage des données sur support permanent le plus répandu en informatique. Les données sont stockées en tant qu’enregistrement dans des tables, par le biais d’un ensemble de couples attribut/valeur dont une clé primaire essentielle à la singularisation de chaque enregistrement. Des relations sont ensuite établies entre les tables par un mécanisme de jonction entre la clé primaire de la première table et la clé dite étrangère de celle à laquelle on désire la relier. Le fait, par exemple, qu’un conducteur puisse posséder plusieurs voitures se traduit en relationnel par la présence dans la table voiture d’une clé étrangère qui reprend les valeurs de la clé primaire présente dans la table des conducteurs. La disparition de ces clés dans la pratique OO fait de la sauvegarde des objets dans ces tables un problème épineux de l’informatique d’aujourd’hui, comme nous le verrons au chapitre 19.

Les arbres, quant à eux, chacun également avec leurs couples attribut/valeur, s’enregistrent dans des bases de données gérées par un botaniste. Cette façon de procéder n’a rien de novateur et n’est en rien à l’origine de cette pratique informatique désignée comme orientée objet. La simple opération de stockage et manipulation d’objet en soi et pour soi n’est pas ce qui distingue fondamentalement l’informatique orientée objet de celle désignée comme « procédurale » ou « fonctionnelle ». Nous la retrouvons dans pratiquement tous les langages informatiques. Patience ! Nous allons y venir… De tout temps également, les mathématiciens, physiciens, ou autres scientifiques, ont manipulé des objets mathématiques caractérisés par un ensemble de couples attribut/valeur. Ainsi, un point dans un espace à trois dimensions se caractérise par les valeurs réelles prises par ses attributs x,y,z. Lorsque ce point bouge, on peut y adjoindre trois nouveaux attributs pour représenter sa vitesse. Il en est de même pour une espèce animale, caractérisée par le nombre de ses représentants, d’un atome, caractérisé par son nombre atomique, et d’une molécule par le nombre d’atomes qui la composent et par sa concentration au sein d’un mélange chimique. Même chose pour la santé économique d’un pays, caractérisée par le PIB par habitant, la balance commerciale ou le taux d’inflation.

Le référent d’un objet Observons à nouveau les figures 1-1 et 1-2. Chaque objet est nommé et ce nom doit être unique. Le nom de l’objet est son seul et unique identifiant. Comme c’est en le nommant que nous accédons à l’objet, il est clair que ce nom ne peut être partagé par plusieurs objets. En informatique, le nom correspondra de manière univoque à l’adresse physique de l’objet en mémoire. Rien de plus unique qu’une adresse, sauf à supposer que deux objets puissent occuper le même espace mémoire. Pas d’inquiétude pour eux, nos objets ont les moyens de ne pas squatter la mémoire ! Le nom « première-voiture-vue-dans-la-rue » est en fait une variable informatique, que nous appellerons référent par la suite, stockée également en mémoire, mais dans un espace dédié

14

L’orienté objet

uniquement aux noms symboliques. À cette variable on assigne comme valeur l’adresse physique de l’objet que ce nom symbolique désigne. En général, dans la plupart des ordinateurs aujourd’hui, l’adresse mémoire se compose de 32 bits, ce qui permet de stocker jusqu’à 232 informations différentes. Un référent est donc une variable informatique particulière, associée à un nom symbolique, codée sur 32 bits, et contenant l’adresse physique d’un objet informatique. Espace mémoire Le référent contient l’adresse physique de l’objet, codée sur 32 bits dans la plupart des ordinateurs aujourd’hui. Le nombre d’espaces mémoire disponibles est lié à la taille de l’adresse de façon exponentielle. Ces dernières années, de plus en plus de processeurs, tant chez Sun, Apple ou Intel, ont fait le choix d’une architecture à 64 bits, ce qui implique notamment une révision profonde de tous les mécanismes d'adressage dans les systèmes d'exploitation. Depuis, les informaticiens peuvent voir l'avenir avec confiance et se sentir à l'aise pour des siècles et des siècles face à l'immensité de l’espace d'adressage qui s’ouvre à eux. Celui-ci devient de 264, soit 18.446.744.073.709.551.616 octets. Excusez du peu. Aura-t-on jamais suffisamment de données et de programmes à y installer ?

Référent vers un objet unique Le nom d’un objet informatique, ce qui le rend unique, est également ce qui permet d’y accéder physiquement. Nous appellerons ce nom le « référent de l’objet ». L’information reçue et contenue par ce référent n’est rien d’autre que l’adresse mémoire où cet objet se trouve stocké.

Plusieurs référents pour un même objet Un même objet peut-il porter plusieurs noms ? Plusieurs référents, qui contiennent tous la même adresse physique, peuvent-ils désigner en mémoire un seul et même objet ? Oui, s’il est nécessaire de nommer, donc d’accéder à l’objet, dans des contextes différents et qui s’ignorent mutuellement. Dans la vie courante, rien n’interdit à plusieurs personnes, tout en désignant le même objet, de le nommer de manière différente. Le livre que vous vous devez d’acheter en vingt exemplaires pour le faire connaître autour de vous, le livre dont tous les informaticiens raffolent, le best-seller de l’année, le chef-d’œuvre absolu, autant de référents différents pour désigner cet unique ouvrage que vous tenez précieusement entre les mains. Les noms des objets seront distincts, car utilisés dans des contextes distincts. C’est aussi faisable en informatique orientée objet, grâce à ce mécanisme puissant et souple de référence informatique, dénommé adressage indirect par les informaticiens qui permet, sans difficulté, d’offrir plusieurs voies d’accès à un même objet mémoire. Comme la pratique orienté objet s’accompagne d’une découpe en objets et que chacun d’entre eux peut être sollicité par plusieurs autres qui « s’ignorent » entre eux, il est capital que ces derniers puissent désigner ce premier à leur guise en lui donnant un nom plus conforme à l’utilisation qu’ils en feront. Adressage indirect C’est la possibilité pour une variable, non pas d’être associée directement à une donnée, mais plutôt à une adresse physique d’un emplacement contenant, lui, cette donnée. Il devient possible de différer le choix de cette adresse pendant l’exécution du programme, tout en utilisant naturellement la variable. Et plusieurs de ces variables peuvent alors pointer vers un même emplacement. Une telle variable est dénommée un pointeur, en C et C++.

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

15

Plusieurs référents pour un même objet Dans le cours de l’écriture d’un programme orienté objet, on accédera couramment à un même objet par plusieurs référents, générés dans différents contextes d’utilisation. Cette multiplication des référents sera un élément déterminant de la gestion mémoire associée à l’objet. On acceptera à ce stade-ci qu’il est utile qu’un objet séjourne en mémoire tant qu’il est possible de le référer. Sans référent un objet est bon pour la poubelle puisque inaccessible. Vous êtes mort, je jour où vous n’êtes même plus un numéro dans aucune base de données. Figure 1-4

Plusieurs référents désignent un même objet grâce au mécanisme informatique d’adressage indirect.

Nous verrons dans la section suivante qu’un attribut peut servir de référent vers un autre objet, et qu’il y a là un mécanisme idéal pour permettre à ces deux objets de communiquer, par envoi de messages, du premier vers le deuxième. Mais n’allons pas trop vite en besogne…

L’objet dans sa version passive L’objet et ses constituants Voyons plus précisément ce qui amène notre perception à privilégier certains objets plutôt que d’autres. Certains d’entre eux se révèlent être une composition subtile d’autres objets, objets évidemment tout aussi présents que les premiers, et que, pourtant, il ne vous est pas venu à l’idée de citer lors de notre première démonstration.

16

L’orienté objet

Vous avez dit « la voiture » et non pas « la roue de la voiture » ou « sa portière », vous avez dit « l’arbre », et non pas « la branche » ou « le tronc de l’arbre ». De nouveau, c’est l’agrégat qui vous saute aux yeux, et non pas toutes ses parties. Vous savez pertinemment que l’objet « voiture » ne peut fonctionner en l’absence de ses objets « roues » ou de son objet « moteur ». Néanmoins, pour citer ce que vous observiez, vous avez fait l’impasse sur les différentes parties constitutives des objets relevés. Première distinction : ce qui est utile à soi et ce qui l’est aux autres L’orienté objet, pour des raisons pratiques que nous évoquerons par la suite, encourage à séparer, dans la description de tout objet, la partie utile pour tous les autres objets qui y recouront de la partie nécessaire à son fonctionnement propre. Il faut séparer physiquement ce que les autres objets doivent savoir d’un objet donné, afin de solliciter ses services, et ce que ce dernier requiert pour son fonctionnement, c’est-à-dire la mise en œuvre de ces mêmes services.

Objet composite En tant que banal utilisateur de l’objet « voiture », vous vous préoccuperez des roues et du moteur comme de l’an 40. À moins que vous en ayez un besoin direct et incontournable, le garagiste se chargera bien tout seul de vous ruiner ! Que les objets s’organisent entre eux, en composite et composant, est une donnée de notre réalité que les informaticiens ont jugé important de reproduire. Comme indiqué dans la figure suivante, un objet stocké en mémoire peut être placé à l’intérieur de l’espace mémoire réservé à un autre. Figure 1-5

L’objet moteur devient un composant de l’objet voiture.

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

17

Son accès ne sera dès lors possible qu’à partir de celui qui lui offre cette hospitalité et, s’il le fait, c’est qu’il sait que l’existence de l’hôte est totalement conditionnée par l’existence de l’hôte (la langue française est ainsi faite qu’elle permet cette ambiguïté terminologique !). L’objet moteur, dans ce cas, n’existe que comme seul attribut de l’objet voiture. Si vous vous débarrassez de la voiture, vous vous débarrasserez dans le même temps de son moteur. Une composition d’objets Entre eux, les objets peuvent entrer dans une relation de type composition, où certains se trouvent contenus dans d’autres et ne sont accessibles qu’à partir de ces autres. Leur existence dépend entièrement de celle des objets qui les contiennent.

Dépendance sans composition Vous comprendrez aisément que ce type de relation entre objets ne suffit pas pour permettre une description fidèle de la réalité qui nous entoure. En effet, si la voiture possède bien un moteur, occasionnellement elle contient également des passagers, qui n’aimeraient pas être portés disparus lors de la mise à la casse de la voiture... D’autres modes de mise en relation entre objets devront être considérés, qui permettent à un premier de se connecter facilement à un deuxième, mais sans que l’existence de celui-ci ne soit entièrement conditionnée par l’existence du premier. Mais nous en reparlerons plus tard.

L’objet dans sa version active Activité des objets Afin de poursuivre cette petite introspection cognitive dans le monde de l’informatique orientée objet, jetez à nouveau un coup d’œil par la fenêtre et décrivez-nous quelques scènes observées : « une voiture s’arrête à un feu rouge », « les passants traversent la route », « un passant entre dans un magasin », « un oiseau s’envole de l’arbre ». Que dire de toutes ces observations bouleversantes que vous venez d’énoncer ? D’abord, que les objets ne se bornent pas à être statiques. Ils bougent, se déplacent, changent de forme, de couleur, d’humeur, et ce, souvent, suite à une interaction directe avec d’autres objets. La voiture s’arrête car le feu est devenu rouge, et elle redémarre dès qu’il passe au vert. Les passants traversent car les voitures s’arrêtent. L’épicier dit « bonjour » au client qui ouvre la porte de son magasin. Les objets inertes sont par essence bien moins intéressants que ceux qui se modifient constamment. Certains batraciens ne détectent leur nourriture favorite que si elle est en mouvement. Placez-la, immobile, devant eux, et l’animal ne la verra simplement pas. Ainsi, l’objet sera d’autant plus riche d’intérêt qu’il est sujet à des transitions d’états nombreuses et variées.

Les différents états d’un objet Les objets changent donc d’état, continûment, mais tout en préservant leur identité, en restant ces mêmes objets qu’ils ont toujours été. Les objets sont dynamiques, la valeur de leurs attributs change dans le temps, soit par des mécanismes qui leur sont propres (tel le changement des feux de signalisation), soit en raison d’une interaction avec un autre objet (comme dans le cas de la voiture qui s’arrête au feu rouge). Du point de vue informatique, rien n’est plus simple que de modifier la valeur d’un attribut. Il suffit de se rendre dans la zone mémoire occupée par cet attribut et de remplacer la valeur qui s’y trouve actuellement stockée par une nouvelle valeur. La mise à jour d’une partie de sa mémoire, par l’exécution d’une instruction appropriée, est une des opérations les plus fréquentes effectuées par un ordinateur. Le changement d’un attribut n’affecte

18

L’orienté objet

en rien l’adresse de l’objet, et donc son identité. Tout comme vous, qui restez la même personne, humeur changeante ou non. L’objet, en fait, préservera cette identité jusqu’à sa pure et simple suppression de la mémoire informatique. Pour l’ordinateur : « Partir, c’est mourir tout à fait. ». L’objet naît, vit une succession de changements d’états et finit par disparaître de la mémoire. Et voilà expédié le résumé d’une vie, qu’elle soit digne d’un roman ou d’un simple fait divers. Pas vraiment enviable la vie des objets dans ce monde impitoyable de l’informatique ! Changement d’états Le cycle de vie d’un objet, lors de l’exécution d’un programme orienté objet, se limite à une succession de changements d’états, jusqu’à sa disparition pure et simple de la mémoire centrale.

Les changements d’état : qui en est la cause ? Mais qui donc est responsable des changements de valeur des attributs ? Qui a la charge de rendre les objets moins inertes qu’ils n’apparaissent à première vue ? Qui se charge de les faire évoluer et, ce faisant, de les rendre un tant soit peu intéressants ? Reprenons l’exemple des feux de signalisation évoqué plus haut, et comme indiqué à la figure 1-6, stockons-en un, « celui-que-vous-avez-vu-dans-la-rue », dans la mémoire de l’ordinateur (nous supposerons que la couleur est bien représentée par un entier ne prenant que les valeurs 1, 2 ou 3). Installons dans cette même mémoire, mais un peu plus loin, une opération, qui sera responsable du changement de couleur. Dans la mémoire dite centrale, RAM ou vive, d’un ordinateur, ne se trouvent toujours installés que ces deux types d’information, des données et des instructions qui utilisent et modifient ces données, rien d’autre. Nous appellerons la simple opération de changement de couleur : « change ». Comme chaque attribut, chaque opération se doit d’être nommée afin de pouvoir y accéder. Il s’agira pour elle, le plus banalement du monde, d’incrémenter l’entier couleur et de ramener sa valeur à 1 dès que celui-ci atteint 4. C’est cette opération triviale qui mettra un peu d’animation dans notre feu de signalisation. Figure 1-6

Le changement d’état du feu de signalisation par l’entremise de l’opération « change ».

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

19

Comment relier les opérations et les attributs ? Alors que nous comprenons bien l’installation en mémoire, tant de l’objet que de l’opération qui pourra le modifier, ce qui nous apparaît moins évident, c’est la liaison entre les deux. Comment l’opération et les deux instructions de changement (l’incrémentation et le test), installées dans la mémoire à droite, savent-elles qu’elles portent sur le feu de signalisation installé, lui, dans la mémoire à gauche ? Plus concrètement encore, comment l’opération change, qui incrémente et teste un entier, sait-elle que cet entier est, de fait, celui qui code la couleur du feu et non « l’âge du capitaine » ? La réponse à cette question vous fait entrer de plain-pied dans le monde de l’orienté objet… Mais vous êtes sans doute un peu ému. Alors, avant de vous donner la réponse tant attendue, permetteznous de vous souhaiter un chaleureux welcome dans ce monde de l’OO, car vous n’êtes pas près de le quitter.

Introduction à la notion de classe Méthodes et classes Place à la réponse. Elle s’articule en deux temps. Dans un premier temps, il faudra que l’opération change – que nous appellerons dorénavant méthode – ne soit attachée qu’à des objets de type feu de signalisation. Seuls ces objets possèdent cet entier à l’endroit où ils le possèdent, et sur lesquels peut s’exercer cette méthode. Appliquer la méthode change sur tout autre type d’objet, tel que la voiture ou l’arbre, n’a pas de sens, car on ne saurait de quel entier il s’agit. De surcroît, ce double incrément pour revenir à la valeur de base est totalement dénué de signification pour ces autres objets. Le subterfuge qui permet d’associer, à jamais, les méthodes avec les objets qui leur correspondent consiste à les unir tous deux par les liens, non pas du mariage, mais de la « classe ». Classe Une nouvelle structure de données voit le jour en OO : la classe, qui, de fait, a pour principale raison d’être d’unir en son sein tous les attributs de l’objet et toutes les opérations qui y accèdent et qui portent sur ceux-là. Opération que l’on désignera par le nom de méthode, et qui regroupe un ensemble d’instructions portant sur les attributs de l’objet. Pour les programmeurs en provenance du procédural, les attributs de la classe sont comme des arguments implicites passés à la méthode ou encore des variables dont la portée d’action se limite à la seule classe.

La classe devient ce contrat logiciel qui lie à vie les attributs de l’objet et les méthodes qui utilisent ces attributs. Par la suite, tout objet devra impérativement respecter ce qui est dit par sa classe, sinon gare au compilateur ! Comme c’est l’usage en informatique s’agissant de variables manipulées, on parlera dorénavant de l’objet comme d’une instance de sa classe, et de la classe comme du type de cet objet. Chacun des attributs de l’objet sera « typé » comme il est indiqué dans sa classe, et toutes les méthodes affectant l’objet seront uniquement celles prévues dans sa classe. D’où l’intérêt, bien sûr, de garder une définition de la classe séparée mais partagée par toutes les instances de celles-ci. Non seulement c’est la classe qui déterminera les attributs sur lesquels les méthodes pourront opérer mais, plus encore, et nous accroîtrons dans les prochains chapitres la sévérité de ce principe, seules les méthodes déclarées dans la classe pourront de facto manipuler les attributs des objets typés par cette classe. La classe Feu-de-signalisation pourrait être définie plus ou moins comme suit : class Feu-de-signalisation { int couleur ; change() { couleur = couleur + 1 ; if (couleur ==4) couleur = 1 ; } }

20

L’orienté objet

Définition : type entier Le type primitif entier est souvent appelé dans les langages de programmation int (pour integer), le type réel double ou float, le caractère char. Eh oui ! l’anglais reste l’espéranto de l’informatique.

Chaque objet feu de signalisation répondra de sa classe, en faisant en sorte, dès sa naissance, de n’être modifié que par les méthodes déclarées dans sa classe (ici la seule méthode change, mais il pourrait y en avoir bien d’autres comme met-le-feu-en-stand-by, change-la-durée-d’une-des-couleurs, et il faudrait alors rajouter quelques attributs comme la durée de chaque couleur). Gardez bien à l’esprit ce principe fondateur de l’OO qu’aucune autre méthode, jamais, que celles prévues par la classe, ne pourra s’aventurer à changer la valeur des attributs des objets de cette classe. Ce qui permet aux objets de se modifier leur est aussi propre que les attributs qui les décrivent structurellement. Un objet existe, par l’entremise de ses attributs, et se modifie, par l’entremise de ses méthodes (et nous disons bien « ses » et non « ces » ou vous risquez d’être bouté à jamais hors du royaume merveilleux de l’OO).

Sur quel objet précis s’exécute la méthode Dans un second temps, il faudra signaler à la méthode change (qui maintenant, grâce à la définition de la classe, sait qu’elle n’opère exclusivement que sur des feux de signalisation) lequel, parmi tous les feux possibles et stockés en mémoire, est celui qu’il est nécessaire de changer. Cela se fera par le simple appel de la méthode sur l’objet en question, et, plus encore, par l’écriture d’une instruction de programmation de type : feu-de-signalisation-en-question.change()

Nous appliquons la méthode change() (nous expliquerons plus tard la raison d’être des parenthèses) sur l’objet feu-de-signalisation-en-question. C’est le point dans cette instruction qui permet ici la liaison entre l’objet précis et la méthode à exécuter sur cet objet. N’oubliez pas que le référent feu-de-signalisationen-question possède effectivement l’adresse de l’objet et, de là, automatiquement, de l’attribut entier/couleur sur lequel la méthode change() doit s’appliquer. Lier la méthode à l’objet On lie la méthode f(x) à l’objet « a », sur lequel elle doit s’appliquer, au moyen d’une instruction comme : a.f(x). Par cette écriture, la méthode f(x) saura comment accéder aux seuls attributs de l’objet, ici les attributs de l’objet « a », qu’elle peut manipuler.

Différencier langage orienté objet et langage manipulant des objets De nombreux langages de programmation, surtout de scripts pour le développement web (JavaScript, VB Script), rendent possible l’exécution de méthodes sur des objets dont les classes préexistent au développement. Le programmeur ne crée jamais de nouvelles classes mais se contente d’exécuter les méthodes de celles-ci sur des objets. Supposons par exemple que vous vouliez agrandir un « font » particulier de votre page web. Vous écrirez f.setSize(16) mais jamais dans votre code, vous n’aurez créé la classe Font (vous utilisez l’objet « f » issu de cette classe) et sa méthode setSize(). Vous vous limitez à les utiliser comme vous utilisez les bibliothèques d’un quelconque langage de programmation. Les classes inclues dans ces librairies auront été développées par d’autres programmeurs et mis à votre disposition.

Voilà, c’est aussi simple que cela, et c’est le départ d’un grand voyage dans le monde de l’OO, un voyage qui nous réserve encore de nombreuses surprises.

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

21

Des objets en interaction Parmi les saynètes évoquées plus haut, certaines décrivaient une interaction entre deux objets, comme le feu qui, passant au vert, permet à la voiture de démarrer, ou l’épicier qui salue le nouveau client. Ce serait bien qu’à l’instar de cette réalité observée, les objets informatiques puissent interagir de la sorte. Vous allez être contents. C’est non seulement possible, mais c’est la base de l’informatique OO. Rien d’autre, en effet, ne se passe dans cette informatique-là que des objets interagissant entre eux. Dans le monde, un objet esseulé n’est pas grand-chose, en OO également. C’est ensemble, mais aussi et paradoxalement chacun pour soi (ce paradoxe sera résolu plus tard), qu’ils commencent à nous être utiles. Tentons d’imaginer comment la première scène, décrivant l’effet du changement de couleur du feu sur le démarrage de la voiture, pourrait être reproduite informatiquement. Nous considérerons que les deux objets, feu-de-signalisation et voiture-vue-dans-la-rue, instance pour le premier d’une classe Feu-de-signalisation (notez la majuscule de la première lettre) et pour le deuxième d’une classe Voiture, sont chacun caractérisés par un attribut, l’entier couleur pour le feu, et, pour la voiture, un entier vitesse pouvant prendre jusqu’à 130 valeurs possibles (en tout cas en France, le traducteur allemand s’adaptera). Figure 1-7

Comment l’objet « feu-designalisation » parle-t-il à l’objet « voiture-vue-dansla-rue » ?

Comment les objets communiquent Faisons simple, quitte à faire irréaliste, en supposant que le changement de couleur du feu induise dans la voiture l’accélération de la vitesse de 0 à 50. Observez la figure 1-7, comme pour la couleur du feu, dont les seules modifications ne sont permises que par la méthode change ; le changement de vitesse de la voiture relèvera, également, exclusivement de l’exécution d’une méthode, que nous dénommerons changeVitesse(int nV) (car d’autres attributs de la voiture pourraient également faire l’objet de changement). Nous constatons, par ailleurs, qu’il est prévu que cette méthode reçoive un argument de type entier, qui lui permette d’affiner son effet en fonction de la valeur de cet argument.

22

L’orienté objet

Les plus informaticiens d’entre vous noteront que l’écriture, la syntaxe et le mode de fonctionnement d’une méthode sont en tous points semblables aux routines ou procédures dans un quelconque langage de programmation. Elles peuvent recevoir des arguments, qu’elles utiliseront dans le corps de leur définition, et ce afin de paramétrer leur fonctionnement, comme ici pour la méthode changeVitesse(int nV). de la classe Voiture. C’est la raison d’être des parenthèses qui, même lorsqu’elles ne contiennent aucun argument, sont obligées d’apparaître dans l’appel de la méthode. Méthode Il s’agit d’un regroupement d’instructions semblable aux procédures, fonctions et routines rencontrés dans tous les langages de programmation, à ceci près qu’une méthode s’exécute toujours sur un objet précis (comme si celui-ci lui était, implicitement, passé comme un argument additionnel).

Envoi de messages D’ores et déjà, vous en aurez déduit que la seule manière pour deux objets de communiquer, c’est que l’un demande à l’autre d’exécuter une méthode qui lui est propre. Ici, le feu de signalisation demande à la voiture d’exécuter sa méthode changeVitesse(50), qui lui permet de modifier son attribut vitesse et de le porter à 50. Rappelez-vous qu’il serait impropre que le feu s’en charge directement, étant donné que seules les méthodes de la classe Voiture peuvent se permettre de modifier l’état de cette dernière. En se référant à la figure 1-7, vous constatez que le moyen utilisé par l’objet feu pour déclencher la méthode changeVitessse est de prévoir dans le corps de sa propre méthode une instruction telle que voitureDevant.changeVitesse(50). Le feu s’adresse donc à un référent particulier dénommé voitureDevant, sur lequel il déclenche la méthode changeVitesse. Envoi de message Le seul mode de communication entre deux objets revient à la possibilité pour le premier de déclencher une méthode sur le second, méthode déclarée et définie dans la classe de celui-ci. On appellera ce mécanisme de communication un « envoi de message » du premier objet vers le second. Cette expression se justifie par la présence de ces deux objets : l’expéditeur dans le code duquel l’envoi se produit et le destinataire à qui le message est destiné. Elle se justifie davantage encore pour des objets s’exécutant sur des ordinateurs très éloignés géographiquement, situation entraînant réellement un envoi physique de message d’un point à l’autre du globe.

Identification des destinataires de message On comprend bien la démarche de l’objet feu, mais tout informaticien restera quelque peu interloqué par la brutalité d’exécution. Ça marche cette recette ? Non, bien sûr ! Il nous manque quelques ingrédients indispensables. Le premier est de signaler au feu que ce référent voitureDevant est bien de type classe Voiture et que, de ce fait, cette demande d’exécution de méthode est tout à fait légitime. Afin de typer ce référent-là, on pourrait tout simplement le passer comme argument de la méthode change pour le feu. Cependant, ce que nous rencontrerons bien plus souvent, c’est le procédé qui consiste à faire de ce référent un attribut à part entière de la classe Feu-de-signalisation, un attribut de type Voiture. La classe Feu-de-signalisation se définirait alors comme ceci : class Feu-de-signalisation { int couleur ; Voiture voitureDevant;

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

23

change() { couleur = couleur + 1 ; if (couleur ==4) couleur = 1 ; if (couleur ==1) voitureDevant.changeVitesse(50) ; } }

Plus rien n’est vraiment choquant dans cette écriture, car le référent voitureDevant étant en effet de type classe Voiture, il peut recevoir le message changeVitesse(50). Le compilateur prendra garde de vérifier qu’il existe bel et bien à proximité une classe Voiture contenant la méthode changeVitesse(int). Nous reviendrons sur ce mécanisme dans les prochains chapitres. Syntaxiquement, cette écriture est parfaitement correcte, y compris en l’absence pour l’instant du moindre objet voiture. Ce qui est simplement dit à ce stade de l’écriture de la classe, est que tout objet feu de signalisation se voit associer un objet voiture auquel il peut envoyer le message changeVitesse(x). Une association est ici réalisée entre les classes Feu-de-signalisation et Voiture et elle va dans le sens du Feu vers la Voiture. Le deuxième ingrédient indispensable est de relier ce référent à l’objet en question, celui que nous désirons voir démarrer quand le feu passe au vert, et donc d’assigner à ce référent la même adresse physique que celle contenue dans le référent voiture-vue-dans-la-rue. Nous décrirons par la suite différentes manières d’y parvenir. Mais si nous adoptons l’écriture de la classe indiquée plus haut, une simple manière consistera à prévoir, au cours de la création de l’objet feu-de-signalisation, une instruction telle que : voitureDevant = voiture-vue-dans-la-rue, qui permettra à l’adresse physique de la voiture en question d’être directement transmise au feu-de-signalisation. Dorénavant l’attribut de la classe Feu ayant pour mission de référer l’objet voiture avec lequel l’objet feu se doit d’interagir possédera en effet l’adresse mémoire de celui-ci. Gestion d’événement Lorsqu’il passe au vert, plutôt qu’un envoi de message du feu destiné à toutes voitures qui lui font face (et dont il ignore la nature et le nombre), il serait sans doute plus réaliste de considérer que les voitures sont susceptibles d’observer la transition de couleur du feu et de réagir en conséquence, c’est-à-dire démarrer, sans en être explicitement « ordonné » par le feu. Ce mécanisme d’observation et de gestion d’événement est également un « plus » de la programmation OO et le chapitre 18, qui y est consacré, vous indiquera comment, en effet, les voitures pourraient réagir au quart de tour au changement de couleur, sans recevoir le moindre message explicite du feu.

Des objets soumis à une hiérarchie Dernier petit détour du côté de la fenêtre (dernier, promis !) et, rassurez-vous, vous n’aurez plus à regarder quoi que ce soit, mais plutôt à vous interroger sur la manière dont, tout à l’heure, vous avez nommé les objets.

Du plus général au plus spécifique Vous avez parlé de « voiture », « passant », « arbre », « immeuble ». Prenons le premier de ces objets. Vous avez dit « voiture » mais êtes-vous vraiment sûr qu’il s’agissait d’une voiture ? Ne s’agissait-il pas plutôt, pour être précis, d’une Peugeot, plus encore d’une 206, ou, de surcroît, dans sa version turbo ou cabriolet. Qu’est-ce que cela peut bien changer, direz-vous ? Pas grand-chose ici, mais beaucoup pour l’informatique OO, car ce seul et même objet, celui que vous avez vu, est, en fait, tout cela à la fois. Ainsi, pour être tout à fait complet, il aurait également pu être qualifié de « moyen de transport » ou « juste un objet de ce monde ». Il l’est d’ailleurs. Il est bien ces six ou sept

24

L’orienté objet

concepts, tout à la fois. Tous ces différents concepts existent et forment entre eux une hiérarchie ou taxonomie : du plus général au plus spécifique. Et c’est ce qui nous permet de les utiliser de la manière la plus adaptée et la plus économique qui soit. Héritage et taxonomie Une pratique clé de l’orienté objet est d’organiser les classes entre elles de manière hiérarchique ou taxonomique, des plus générales aux plus spécifiques. On parlera d’un mécanisme « d’héritage » entre les classes. Un objet, instance d’une classe, sera à la fois instance de cette classe mais également de toutes celles qui la généralisent et dont elle hérite. Tout autre objet ayant besoin de ses services choisira de le traiter selon le niveau hiérarchique le plus approprié. Pour vous lecteurs, nous ne sommes que de pauvres objets enseignants de la chose informatique. Si vous nous connaissiez mieux, vous découvririez des natures autrement plus raffinées, mais à quoi cela vous servirait-il de mieux nous connaître ?

Vous allez être surpris en apprenant que, tout bien pensé, il n’existe dans ce monde aucune voiture, tout comme il n’existe aucun arbre. D’ailleurs, nous expliquerons plus tard pourquoi, malgré l’existence possible de classes Arbre ou Voiture dans le logiciel OO que nous pourrions réaliser, il serait souhaitable que ces classes ne donnent naissance à aucun objet. En revanche, il existe des Peugeot 206, des Renault Kangoo, des Fiat Uno, des Volkswagen Golf. Il existe des peupliers, des cerisiers, et même des cerisiers du Japon. Pourquoi alors ce concept de voiture, si rien de ce que nous percevons ne s’y rapporte vraiment ? C’est parce que l’usage que l’immense majorité des êtres humains font de leur objet voiture et les événements les plus fréquents qu’ils narrent, liés à ce même objet, ne requièrent aucunement d’en connaître la marque : « J’ai pris la voiture pour partir en voyage », « Ma voiture est en panne », « J’ai eu un accident de voiture ». Ce serait la même histoire, le même scénario, si vous remplaciez la voiture par sa marque. Dès lors, cette précision devient inutile car elle n’apporte rien de plus à la conversation, et risque même de détourner le sens premier de vos propos. En outre, le même traitement est souvent réservé à toutes les voitures, quelle que soit leur marque. Il est bien commode de pouvoir dire, dans une seule et même phrase : « Les voitures font la queue devant la station service », « L’accident a impliqué cinq voitures », « Après deux ans, votre voiture doit passer au contrôle technique ».

Dépendance contextuelle du bon niveau taxonomique Dans l’emploi que vous faites du concept « voiture », lors de conversations, rêveries, écritures, ce simple mot « voiture » suffit largement à véhiculer tout le sens qui est nécessaire à ces contextes. De même, généraliser d’un cran ce concept, et parler de « moyen de transport » en lieu et place de « voiture », risque, là encore, de dénaturer le sens de vos propos. Car ce niveau intègre également des objets comme le train et l’avion, et votre interlocuteur ne pourra manquer de généraliser vos propos à ces autres objets. Le niveau taxonomique que vous utilisez dépend bien évidemment du contexte. Dialoguant avec un garagiste, il y a fort à parier que vous serez contraint à un moment ou à un autre de lui préciser la marque de la voiture, mais cela se produira rarement dans la grande majorité des interactions sociales. Croyez-nous ou croyez Wittgenstein (c’est plus sûr), qui est une des figures intellectuelles les plus marquantes de ce siècle : c’est l’utilisation que vous en faites, plus que la réalité qu’ils dépeignent, qui sous-tend le sens des mots. Les mots sont d’abord des outils au service de nos interactions sociales ou de nos élucubrations mentales. Comme dans la majorité de celles-ci, le mot « voiture » suffit, non seulement à vous véhiculer, mais également à véhiculer tout ce qu’il vous est important de signifier à son propos, d’abord à vous puis aux autres, c’est pour cela que vous nous avez parlé de voiture, tout à l’heure, en regardant par la fenêtre.

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

25

Wittgenstein Cette figure de légende de la philosophie, né à Vienne en 1889 et mort à Cambridge en 1951, a vécu mille vies, toutes plus extraordinaires les unes que les autres, et bâtit deux ouvrages philosophiques parmi les plus marquants et illustres du xxe siècle. Il est issu d’une des familles les plus riches d’Autriche, cadet d’une famille de huit enfants, tous marqués par un destin cruel. Jeune et brillantissime, il se destine à une carrière d’ingénieur aéronautique prometteuse, mais finit par s’en détourner pour, au contact des mathématiciens et des philosophes de Cambridge, tel Russel, se lancer dans sa première œuvre philosophique, consacrée à la nature du langage et de la pensée : le Tractacus. Dans cette œuvre, il accorde au langage des vertus figuratives, le disséquant pour le présenter en une composition d’objets atomiques, isomorphes au monde, et rentrant dans des relations structurelles, tout aussi fidèle à la réalité qu’il cherche à dépeindre. Ce premier Wittgenstein est un précurseur de nos objets informatiques et des liens relationnels qu’ils maintiennent entre eux. Ensuite, héros de la Première Guerre mondiale (il s’adonne à ses écrits philosophiques entre deux obus), maintenant avec le monde universitaire un rapport haine/amour, des allées et venues incessantes, assistant à Cambridge, instituteur dans d’austères villages de montagne, jardinier de couvent, architecte pour la maison de sa sœur, il se décide à remettre complètement en question la vision du langage présentée dans ses premiers écrits, et qui, pourtant, fait autorité dans son cercle universitaire. C’est le deuxième Wittgenstein, auteur des Recherches philosophiques, et qui retire dorénavant au langage ces mêmes vertus figuratives dont il l’a paré dans une première vie, pour, à la place, l’envisager comme un outil d’interaction sociale, dont la quintessence sémantique est à puiser dans son usage plutôt que dans la réalité qu’il dépeint. Ce deuxième Wittgenstein nous permet de comprendre l’intérêt des mécanismes d’héritage, qui est à trouver davantage dans la manière et les contextes d’utilisation de nos mots que dans les objets du réel que ces mots désignent. Il n’y a, de fait, ni voiture ni arbre dans le monde, pas plus du temps de Wittgenstein qu’aujourd’hui, mais il nous est bien utile de pouvoir recourir à ces mots dans tant de situations. Il continuera cette vie dissolue, entre le monde académique, les salles d’hôpitaux où il est homme à tout faire, pour terminer sa vie dans une hutte de pêcheur, où il commence à s’éteindre, se débattant dans la maladie, la solitude et la tourmente mentale. Entre-temps, cet incontestable génie se sera débarrassé au profit d’artistes en peine et de plus pauvres de l’immense fortune héritée de son père. Il passera l’essentiel de ses loisirs au cinéma, à voir des polars et des westerns qu’il privilégie à toute autre stimulation mentale. Il vivra très difficilement son homosexualité. Étrange destin décidément que celui de Wittgenstein qui, partageant, petit, les bancs d’école avec un certain Adolphe, aurait non seulement été (selon certains) à l’origine de la haine que son camarade d’école voua au peuple juif, mais fut un héros de la résistance et de l’espionnage britannique, pendant la Seconde Guerre mondiale. Cela vaut bien ce petit encart, non ?

Héritage Dans notre cognition et dans nos ordinateurs, le rôle premier de l’héritage est de favoriser une économie de représentation et de traitement. La factorisation de ce qui est commun à plusieurs sous-classes dans une même superclasse offre des avantages capitaux. Vous pouvez omettre d’écrire dans la définition de toutes les sous-classes ce qu’elles héritent des superclasses. Il est de bon sens que, moins on écrit d’instructions, plus fiable et plus facile à maintenir sera le code. Si vous apprenez d’une classe quelconque qu’elle est un cas particulier d’une classe générale, vous pouvez lui associer automatiquement toutes les informations caractérisant la classe plus générale et ce, sans les redéfinir. De plus, vous ne recourrez à cette classe plus spécifique que dans des cas bien plus rares, où il vous sera essentiel d’exploiter les informations qui lui sont propres.

Polymorphisme Plusieurs voitures patientent, le moteur ronronnant et l’automobiliste la bave aux lèvres, devant le feu. Dès que le feu passe au vert, c’est avec rage que tous ces moteurs s’emballent et propulsent leur voiture. Elles démarrent toutes, de fait, mais pas de la même manière. La pauvre 2-CV, après quelques soubresauts et quelques

26

L’orienté objet

protestations mécaniques, cale péniblement. La Twingo s’avance tranquillement dans le carrefour, le temps pour son conducteur de sourire par la fenêtre au malheureux conducteur de la 2-CV. La BMW la double férocement et traverse le carrefour en moins de temps qu’il ne faut pour le dire. En réalité, toutes ces voitures ont bien reçu le même message de démarrage, envoyé par le feu, mais se sont empressées de l’interpréter différemment. Et c’est tant mieux pour le feu, qui serait bien en peine de différencier le message en fonction des voitures à qui il les adresse. Notre conceptualisation du monde, par héritage et généralisation, est ainsi faite, que nous retrouvons la même dénomination pour des activités partagées par un ensemble d’objets, mais dont l’exécution se particularise en fonction de la vraie nature de ces objets. Cela permet à un premier objet, interagissant avec cet ensemble d’objets, dont il sait qu’ils sont à même d’exécuter ce message, de le leur adresser sans se préoccuper de cette nature intime. L’objet feu n’a que faire dans son fonctionnement de la marque des voitures avec lesquelles il communique. Pour lui, il s’agit là uniquement d’objets de la classe voiture, objets qui peuvent tous démarrer, un point c’est tout. Une grande économie de conception et un gage de stabilité sont permis par ce mécanisme (ajouter une nouvelle sous-classe de voiture devant le feu ne changera rien au comportement de ce dernier), dont le nom vous permettra de briller dans les salons : polymorphisme. Prenez la souris de votre PC, cliquez partout sur votre écran et regardez ce qui se passe : des menus se déroulent, des fenêtres s’ouvrent, d’autres se ferment, des icônes s’inscrivent, des petits « Einstein » vous cassent les pieds. Pourtant, tous les objets de votre écran reçoivent ce même clic, mais tous l’interprètent différemment. Un seul clic et autant de réaction à celui-là qu’il n’y a de types d’objets sur votre écran. Vous, objets lecteurs et apprentis informaticiens, nous, auteurs, nous vous incitons à lire ce livre, en prévoyant que vous le lirez, tous à votre rythme, et en l’appréciant différemment, suivant vos prérequis, votre enthousiasme à la lecture et votre goût pour l’informatique. Y compris ceux qui le ferment à l’instant même et le jettent au bout du lit, vous êtes polymorphes et soyez-en fiers ! Polymorphisme, conséquence directe de l’héritage Le polymorphisme, conséquence directe de l’héritage, permet à un même message, dont l’existence est prévue dans une superclasse, de s’exécuter différemment, selon que l’objet qui le reçoit est d’une sous-classe ou d’une autre. Cela permet à l’objet responsable de l’envoi du message de ne pas avoir à se préoccuper dans son code de la nature ultime de l’objet qui le reçoit et donc de la façon dont il l’exécutera.

Héritage bien reçu Et c’est avec ce mécanisme d’héritage que nous terminons notre entrée dans le monde de l’OO, muni, comme vous vous en serez rendu compte, de notre petit manuel de psychologie. Rien de bien surprenant à cela. Les sciences cognitives et l’intelligence artificielle prennent une large place dans le faire-part de naissance de l’informatique OO. Dans les sciences cognitives, cette idée d’objet est largement répandue, déguisée sous les traits des schémas piagétiens, des noumènes kantiens (et en avant pour la confiture…), des paradigmes kuhniens. Tous ces auteurs se sont efforcés de nous rappeler que notre connaissance n’est pas aussi désorganisée qu’elle n’y paraît. Que des blocs apparaissent, faisant de notre cognition un cheptel d’îles plutôt qu’un océan uniforme, blocs reliés entre eux de manière relationnelle et taxonomique. Cette structuration cognitive reflète, en partie, la réalité qui nous entoure, mais surtout notre manière de la percevoir et de la communiquer, tout en se soumettant à des principes universaux d’économie, de simplicité et de stabilité.

Principes de base : quel objet pour l’informatique ? CHAPITRE 1

27

Exercices Exercice 1.1 Prenez comme exemple une de vos activités sportives, culturelles, artistiques ou sociales, et faites une liste des objets impliqués dans cette activité. Dans un premier temps, créez ces objets sous formes de couples attribut/ valeur. Dans un deuxième temps, réfléchissez au lien d’interaction existant entre ces objets ainsi qu’à la manière dont ils sont capables de s’influencer mutuellement. Dans un troisième temps, identifiez pour chaque objet une possible classe le caractérisant.

Exercice 1.2 Répondez aux questions suivantes : • un même référent peut-il désigner plusieurs objets ? • plusieurs référents peuvent-ils désigner un même et seul objet ? • un objet peut-il faire référence à un autre ? si oui, comment ? • pourquoi l’objet a-t-il besoin d’une classe pour exister ? • un objet peut-il changer d’état ? si oui, comment ? • que signifie cette écriture a.f(x) ? • où doit être déclarée f(x) pour que l’instruction précédente s’exécute sans problème ? • qu’appelle-t-on un envoi de message ? • comment un premier objet peut-il agir de telle sorte qu’un deuxième objet change d’état suite à cette action ?

Exercice 1.3 Placez dans un arbre taxonomique, du plus général au plus spécifique, les concepts suivants : • humain, footballeur, avant-centre, sportif, skieur, spécialiste du slalom géant ; • guitare, instrument de musique, trompette, instrument à vent, instrument à corde, violon, saxophone, voix.

Exercice 1.4 Réfléchissez à quelques objets de votre entourage : livre, ordinateur, portefeuille, collègues, téléphone, et interrogez-vous à chaque fois sur le niveau taxonomique que vous privilégiez dans la manière de les désigner. Pourquoi celui-là ? Par exemple, pourquoi dites-vous simplement livre et pourquoi pas le livre d’Eyrolles intitulé « L’orienté objet » ?

Exercice 1.5 Dans les couples d’objets suivants : voiture/chauffeur, footballeur/ballon, guitare/guitariste, télévision/ télécommande, lequel des deux est l’expéditeur et lequel est le destinataire des messages ?

2 Un objet sans classe… n’a pas de classe Ce chapitre a pour but d’introduire la notion de classe : de quoi une classe est-elle faite et quel rôle joue-telle, durant le développement du programme, sa compilation, sa structuration finale, et surtout son découpage. On verra que les classes servent à la fois de modèle à respecter stricto sensu par les objets, ainsi que de modules idéaux pour l’organisation logicielle.

Sommaire : Classe — Méthode — Signature de méthode — La classe, vigile de son bon usage — Méthodes et attributs statiques — La découpe logicielle en classe Candidus — Enfin, vas-tu m’expliquer le mode d’emploi de ton bébé, oui ou non ? Doctus — La classe… Cand. — Eh bien, il grandit vite ! Doc. — Bon ! Allons-y à fond dans le genre imagé. Le mode procédural consistait à chatouiller bébé au bon endroit pour le forcer à faire ce qu’on attendait de lui, le mode orienté objet consiste maintenant à lui fournir des moyens d’agir qui lui sont accessibles. Si nous nous arrangeons pour organiser son environnement comme un ensemble de modules simples et complets, il nous fera tout un tas de petits miracles. Cand. — Et on sait bien que beaucoup de programmes marchent par miracle. Doc. —… Je continue. Ces modules ne seront pas autre chose qu’un ensemble de pièces avec leurs règles d’utilisation. Cand. — J’imagine que tous ces petits modules sont en fait les différents composants d’une structure. Ce n’est que la vision globale de l’ensemble qui laissera voir la complexité du travail de bébé. Ça semble génial… Tu viens de faire la même découverte que Descartes quand il voulait tout expliquer en réduisant tout ce qui lui apparaissait complexe en des parties plus simples ! Doc. — Je ne prétends pas tout expliquer! Je parle d’une simple orientation de l’effort à fournir. C’est toi qui devras tout expliquer quand il te faudra réaliser un programme particulier. Ce qu’il faut retenir de cette orientation est que ton effort devra être basculé de la phase de développement vers la phase de conception. Tes données ne seront plus ces choses inertes avec lesquelles tu jonglais en te servant de fonctions bien trop complexes, ce seront des acteurs à part entière de ton programme. De leur plein gré, elles sauront quoi faire et avec qui le faire. Cand. — Là, tu commences à m’intéresser ! Doc. — Ah ! parce que ce n’est qu’à ce chapitre que tu trouves ça intéressant !

30

L’orienté objet

Constitution d’une classe d’objets Le premier chapitre a apporté une première justification à la nécessité de faire précéder toute manipulation d’objets d’une structure de données, associant aux attributs de l’objet les seules méthodes qui peuvent y avoir accès. Dorénavant, chaque objet créé le sera à partir d’une classe à laquelle il sera tenu de se conformer tout au long de son existence. Rien n’interdit pourtant dans la réalité, un objet de changer de statut ou de comportement, par exemple un étudiant de devenir professeur ou un professeur d’informatique de devenir sommelier. C’est possible dans la réalité mais pas dans l’OO d’aujourd’hui, peut-être de demain, l’objet est coincé par sa classe, même s’il s’y sent parfois à l’étroit. Dans les langages OO, la classe est le modèle à respecter stricto sensu, comme une maison le fait du plan de l’architecte, et la robe du patron du couturier. La classe se décrit au moyen de trois informations, (voir figure 2-1). Figure 2-1

Un exemple d’une classe et des trois types d’information qui la composent.

Les trois informations constitutives de la classe Toute définition de classe inclut trois informations : d’abord, le nom de la classe, ensuite ses attributs et leur type, enfin ses méthodes.

Le nom de la classe, ici : FeuDeSignalisation, le nom des attributs : couleur, position et hauteur, et leur type : int , int (il s’agit de deux entiers) et double (il s’agit d’un réel), enfin, le nom des méthodes change, clignote, avec la liste des arguments et le type de ce que les méthodes retournent.

Définition d’une méthode de la classe : avec ou sans retour Une méthode retourne quelque chose si le corps de ses instructions se termine par une expression telle que « return x ». Si c’est le cas, son nom sera précédé du type de ce qu’elle retourne. Par exemple, la méthode change, modifiant la couleur du feu, pourrait se définir comme suit : int change() { couleur = couleur + 1 ; if couleur == 4 couleur = 1; return couleur ; /* la méthode retourne un entier */ } Commentaires

/* …. */ encadre des commentaires à l’intérieur d’un code. Lorsque les commentaires restent sur une seule ligne, on peut également utiliser //. Toute écriture mise en commentaire est désactivée dans le code. Nous utiliserons beaucoup les commentaires dans nos codes, de manière à expliquer ceux-ci sans pour autant modifier la façon dont ils s’exécutent. Pour Python, en revanche, tous les commentaires débutent par le dièse #.

Un objet sans classe… n’a pas de classe CHAPITRE 2

31

La couleur étant représentée par un entier, le retour de la méthode est de type entier. La rencontre du mot return met fin à l’exécution de la méthode en replaçant celle-ci dans le code qui l’appelle par la valeur de ce retour. La différence entre une méthode qui retourne quelque chose et une méthode qui ne retourne rien (void précède alors le nom de la méthode) se marque uniquement dans le contexte d’exécution de la méthode. La seconde méthode de la classe, clignote(), ne retourne rien. Son appel dans un corps d’instruction se fera indépendamment d’un contexte opératoire spécifique, alors que l’appel de la méthode change() pourra (car elle pourrait être également appelée comme une méthode qui ne retourne rien) se produire à l’intérieur d’une expression. Dans cette expression, l’appel de cette méthode, dans son entièreté, pourrait être remplacé par un simple entier, comme dans : if (change() == 1) print (« le feu est vert »)

ou encore : int b = change() + 2 ; Fonctions et procédures Les praticiens des langages de programmation procéduraux retrouveront là la distinction faite généralement dans ces langages entre une fonction (déclarée avec retour comme toute fonction mathématique f(x) en général) et une procédure (déclarée sans retour et qui se borne à modifier des données du code sans que cette action soit intégrée à l’intérieur même d’une instruction).

De même, une méthode, comme toute opération informatique (fonction ou procédure), peut recevoir un ensemble d’arguments entre les parenthèses, qu’elle utilisera dans le cours de son exécution. Dans l’exemple ci-après, l’argument entier « a » permet de calibrer la boucle présente dans la méthode. Le corps de cette méthode fait clignoter le feu deux fois et peut, en fonction de la valeur de « a », adapter la durée des phases éteintes et allumées. void clignote(int a) { System.out.println("deuxieme maniere de clignoter"); /* Affichage de texte à l’écran */ for(int i=0; i<2; i++) { for (int j=0; j
Identification et surcharge des méthodes par leur signature Ensemble, le nom de la méthode ainsi que la liste et la nature de ses arguments, constituent la signature de cette méthode. Tout envoi de message est conditionné par cette signature. Si un objet parle à un autre, le langage d’interaction sera la liste des signatures des méthodes disponibles chez cet autre. Cette signature est associée

32

L’orienté objet

de manière unique au corps d’instructions qui composent la méthode et qui, in fine, l’exécuteront. Cette signature est à rapprocher du référent des objets, car il s’agit à nouveau d’un mode d’accès. Cette signature peut faire référer à un autre corps d’instructions dès lors que, tout en conservant son nom, elle modifie quoi que ce soit dans la liste ou dans le type de ses arguments. Si pour un même nom, on modifie dans une nouvelle définition de méthode la seule liste des arguments, on parle alors de surcharge de méthodes. Une modification dans cette liste des arguments (changement du nombre ou du typage de ceux-ci) donnera lieu à une nouvelle méthode, car lors de l’appel de la méthode, le choix de la version dépendra du nombre et du type des arguments. Surcharge de méthode La manœuvre consistant à surcharger une méthode revient à en créer une nouvelle, dont la signature se différencie de la précédente, uniquement par la liste ou la nature des arguments.

Il n’est pas possible d’avoir deux méthodes qui possèdent la même signature, c’est-à-dire le même nom et la même liste d’arguments, et qui se différencient par le contexte opérationnel dans lequel elles sont appelées, comme le type de « retour ». Et ce parce que, au moment précis de l’appel d’une des deux méthodes, elles resteront indistinguables. Signature de méthode La signature de la méthode est ce qui permet de la retrouver dans la mémoire des méthodes. Elle est constituée du nom, de la liste, ainsi que du type des arguments. Toute modification de cette liste pourra donner naissance à une nouvelle méthode, surcharge de la précédente. La nature du return ne fait pas partie de cette signature dans la mesure où deux méthodes ayant le même nom et la même liste d’arguments ne peuvent différer par leur return.

Dans le code qui suit, la classe FeuDeSignalisation surcharge deux fois sa méthode clignote(), selon que l’on spécifie ou non dans les arguments les durées des phases allumées et éteintes. class FeuDeSignalisation { void clignote() { System.out.println("premiere manière de clignoter"); for(int i=0; i<2; i++) { for (int j=0; j<3; j++) System.out.println("je suis eteint"); for (int j=0; j<3; j++) System.out.println("je suis allume"); } } void clignote(int a) { System.out.println("deuxieme maniere de clignoter"); for(int i=0; i<2; i++) { for (int j=0; j
Un objet sans classe… n’a pas de classe CHAPITRE 2

33

int clignote(int a, int b) { System.out.println("troisieme maniere de clignoter"); for(int i=0; i<2; i++) { for (int j=0; j
La classe comme module fonctionnel Différenciation des objets par la valeur des attributs L’existence de la classe nous épargnera de préciser, pour chaque objet, le nombre et le type de ses attributs, ainsi que la signature et le corps des méthodes qui manipulent ces derniers, économie d’écriture non négligeable, et dont l’effet va croissant avec le nombre d’objets issus d’une même classe. En général, tout programme OO manipulera un grand nombre d’objets d’une même classe, comme des « voitures », des « feux » ou des « passants ». Ces objets seront stockés dans des ensembles informatiques particuliers, que l’on dénomme des collections. Il pourra s’agir d’ensembles extensibles, comme des listes, ou non, comme des tableaux. Les objets d’une même classe se différenciant entre eux uniquement par la valeur de leurs attributs, la seule information qu’il restera à préciser lors de la création de l’objet est, effectivement, la valeur initiale de ses attributs. C’est bien parce qu’il s’agit de l’unique information à compléter qu’une méthode particulière portant le même nom que la classe s’y emploiera. On appelle cette méthode singulière le constructeur.

Le constructeur Figure 2-2

Addition de deux constructeurs surchargés dans la classe FeuDeSignalisation.

34

L’orienté objet

Le constructeur Le constructeur est une méthode particulière, portant le même nom que la classe, et qui est définie sans aucun retour. Il a pour mission d’initialiser les attributs d’un objet dès sa création. À la différence des autres méthodes qui s’exécutent alors qu’un objet est déjà créé et sur celui-ci, il n’est appelé que lors de la construction de l’objet, et une version par défaut est toujours fournie par les langages de programmation. La recommandation, classique en programmation, est d’éviter de se reposer sur le « défaut », et, de là, toujours prévoir un constructeur pour chacune des classes créées, même s’il se limite à reproduire le comportement par défaut. Au moins, vous aurez « explicité » celui-ci. Le constructeur est souvent une des méthodes les plus surchargées, selon les valeurs d’attributs qui sont connues à la naissance de l’objet et qui sont passées comme autant d’arguments.

Ainsi le constructeur de la classe FeuDeSignalisation pourrait se définir comme suit : FeuDeSignalisation(int positionInit, double hauteurInit) { /* pas de retour pour le constructeur */ position = positionInit ; hauteur = hauteurInit ; couleur = 1 ; }

Une surcharge de ce constructeur pourrait être imaginée (comme dans la figure 2-2), si seule la position était connue. Ce nouveau constructeur ne recevrait alors qu’un argument. Si aucun constructeur n’est spécifié dans la classe, un constructeur par défaut est fourni par les langages de programmation OO. Cependant, dès qu’un constructeur est défini dans la classe, et pour autant qu’il reçoive un ou plusieurs arguments, il ne sera plus possible de créer un objet en n’indiquant aucune valeur d’argument (sauf si le constructeur est explicitement surchargé par un autre qui ne reçoit aucun argument). Comme il est de bonne pratique en informatique de toujours avoir la maîtrise de l’initialisation des objets qu’on utilise, prenez l’habitude, pour éviter toute surprise, de toujours définir un constructeur. Muni de ce constructeur, l’instruction de toute création d’objet devrait maintenant vous paraître limpide : FeuDeSignalisation unNouveauFeu = new FeuDeSignalisation(1, 3) ;

La dernière partie de l’instruction consiste en l’appel du constructeur. Notez que cette même instruction pourrait se décomposer en deux parties comme suit : FeuDeSignalisation unNouveauFeu; // Création du seul référent initialisé à null

À la fin de cette première instruction, seul le référent est créé, créé et typé. Il n’est pas incorrect aux yeux du compilateur d’envoyer un message à même ce référent (comme unNouveauFeu.change();) bien que l’objet ne soit pas créé. Bien évidemment, cela plantera à l’exécution et produira une exception de type NullPointer, une des erreurs les plus fréquentes en Java et C# (et que le compilateur aussi attentif soit-il ne peut anticiper). Cela démontre que le compilateur ne s’intéresse jamais à la partie new des instructions de création d’objet et limite son attention à la déclaration statique. Le reste, la création à proprement parler, ne se déroule que pendant l’exécution. unNouveauFeu = new FeuDeSignalisation(1, 3); /* Création de l’objet et assignation de l’adresse de l’objet comme valeur du référent */

Un objet sans classe… n’a pas de classe CHAPITRE 2

35

La figure 2-3 illustre les trois étapes de la construction d’objet déclenchées par la seule instruction : FeuDeSignalisation unNouveauFeu = new FeuDeSignalisation(1, 3) ; Figure 2-3

Les trois étapes de la construction d’un objet par le truchement du « new ».

On peut légitimement se demander pourquoi est-il nécessaire d’indiquer deux fois le nom de la classe dans cette instruction, lors du typage de l’objet et lors de l’appel du constructeur. La raison en est très simple, mais il vous faudra attendre de comprendre le mécanisme d’héritage pour la découvrir. La classe renseignée à gauche pourrait être différente de la classe renseignée à droite. Plus précisément, le type de l’objet pourrait être une superclasse de la classe référée par le constructeur. Vous verrez dans quelques chapitres ce qu’est une superclasse et pourquoi peut différer le typage à gauche et à droite de l’instruction de création d’objet.

Mémoire dynamique, mémoire statique À l’époque des tous premiers langages de programmation, toute réservation de l’espace mémoire nécessaire au stockage des données traitées par le programme, se faisait au départ du programme. À l’issue de la compilation, il y avait moyen de prévoir de quelle quantité de mémoire vive le programme aurait besoin pour son exécution. Lors de cette exécution, le programme se bornait à modifier les valeurs des variables stockées dans cette mémoire. Rien ne se créait et rien ne se perdait, du vrai « Lavoisier ». Ensuite, les langages ont autorisé l’allocation de mémoire au cours de l’exécution du code, mais toujours sous contrôle et dans un espace de

36

L’orienté objet

mémoire dédié et particulier dont la gestion s’effectue selon un principe dit de « mémoire pile » et toujours d’actualité dans la plupart des langages d’aujourd’hui. La gestion pile revient à empiler et dépiler les données à mémoriser en respectant un mécanisme de type dernier rentrant premier sortant, en fonction du début et de la fin des blocs d’instructions dans lesquels ces données opèrent. Ce mode de gestion mémoire est également décrit comme « statique ». Le petit mot réservé, new, et bien connu des informaticiens, a chamboulé tout cela et est apparu le jour où ceux-ci ont accepté qu’un programme serait autorisé, au cours de son exécution, non seulement à allouer de l’espace mémoire pour y placer de nouvelles variables (ici, cela se réduit aux seuls objets). mais également de les disposer n’importe où dans cette mémoire et sans contraindre leur apparition et disparition de leur seule présence dans les blocs d’instructions. Ce mode alternatif de gestion mémoire est également taxé de « dynamique ». Ainsi, lorsqu’en C++, digne héritier de cette tradition et langage capable des deux modes de gestion mémoire, la création d’un objet, instance de FeuDeSignalisation, se fait par la simple instruction suivante, en l’absence de new : FeuDeSignalisation unNouveauFeu(1,3); // ici pas de classes à droite et à gauche !

Ce nouvel objet ne sera plus créé dans une zone mémoire, à découvrir pendant l’exécution (appelée mémoire tas et rediscutée plus en détail dans le chapitre 9), mais dans une zone mémoire identifiée pendant la compilation et gérée à la manière d’une mémoire pile. Dans de nombreux programmes, les objets apparaissent et disparaissent de manière non synchronisée à l’ouverture et la fermeture des blocs d’instructions, et établir l’espace mémoire au départ du programme à gérer de manière pile est, de fait, un peu trop contraignant. De plus, comme ces objets s’installent n’importe où dans la mémoire, il devient quasi impossible de les gérer à la manière d’une pile car, où se trouve le dessus de la pile, on vous le demande ? Mais, vous l’aurez compris, l’informatique est un sport extrême, et cette limitation fut levée par l’introduction du new et par l’existence des « référents ». Ces référents reçoivent comme valeur, dès la création de tout objet, l’adresse physique de ce dernier, quel que soit l’endroit où il se logera dans la mémoire. La disparition de cet objet est entièrement à repenser et le chapitre 9 s’y consacrera essentiellement.

La classe comme garante de son bon usage Le fait que les objets ne puissent vivre sans leur classe, et que toute création et manipulation d’objet soient entièrement tributaires de ce qui est prévu dans sa classe, confère à la pratique de la programmation orientée objet une liste plutôt conséquente d’avantages. Tout d’abord, nous avons vu que la seule existence de la classe permet à tous les objets, sans que cela soit reprécisé pour chacun, de savoir automatiquement de quoi ils sont faits et ce qu’ils font. Ensuite, les informaticiens détestent les imprévus. Ils sont d’une susceptibilité telle qu’ils ne supportent pas, quand un programme s’exécute, que celui-ci ne se comporte pas comme prévu, ou, pire encore, comble de l’humiliation, que celui-ci « se plante ». Ils confient donc à un compilateur, à partir du logiciel qu’ils ont écrit dans un langage de programmation OO tel que C++, Smalltalk, Java ou C#, le soin de le traduire dans les instructions élémentaires du processeur (seul langage que le processeur comprend). Python et PHP 5 se singularisent ici (et ils se singulariseront encore souvent) car ce sont des langages dits de script, qui s’exécutent sans étape préalable de compilation afin de vérifier que le code est, dans son ensemble, correctement écrit. La traduction dans le langage du processeur se fait instruction par instruction et les instructions élémentaires qui sont produites sont exécutées au fur et à mesure. En Java, C++ et C#, comme le compilateur a pour fonction critique de générer un code « exécutable », et que son utilisateur exige le moins inattendu possible, il prendra garde de vérifier que rien de ce qui est écrit par le

Un objet sans classe… n’a pas de classe CHAPITRE 2

37

programmeur ne puisse être source d’imprévu et d’erreur. Et c’est là que la classe joue de nouveau un rôle considérable, en permettant au compilateur de se transformer en un véritable cerbère, et de s’assurer que ce qui est demandé aux objets (essentiellement l’exécution de messages) est de l’ordre du possible. La classe est comme un texte contractuel. Elle disparaît lors de l’exécution pour donner place aux objets, tout en s’étant assurée par avance que tout de ce que feraient ses objets est conforme à ce qui est spécifié dans le contrat. Et ce contrat est passé avec le compilateur. On ne peut envoyer sur l’objet un message qui ne soit pas une des méthodes prévues par sa classe. On dit des langages qui permettent cette vérification, comme Java, C++ ou C#, qu’ils sont fortement typés. Le programmeur est à ce point contraint et tenu lors de l’écriture du logiciel (mais il faut croire qu’ils aiment ça) que, si ça passe à la compilation, il y a de fortes chances que cela passe aussi à l’exécution. Dans tous les cas, on n’est pas trop loin du but. De leur côté, Python et PHP 5, faisant l’impasse sur l’étape de compilation, laissent à l’exécution le soin de découvrir les instructions erronées, par le simple fait que celles-ci se planteront à l’exécution. Ne pas recourir à l’étape de compilation permet un gain indéniable en vitesse et en productivité, mais délègue à l’étape d’exécution (souvent critique) la responsabilité de repérer les dysfonctionnements. Malheureusement, à l’exécution, c’est parfois trop tard. Il n’y a plus grand-chose après ! C’est la différence entre s’informer au mieux sur la qualité d’un livre de programmation avant de l’acquérir et de le parcourir ou d’attendre d’ouvrir les premières pages pour se fixer les idées. Procéder sans vérification préalable permet d’aller plus vite mais… quelquefois au « casse-pipe ». Pour celui-ci, ça marche, mais c’est souvent risqué… reconnaissons-le.

Langage fortement typé Un langage de programmation est dit fortement typé quand le compilateur vérifie que l’on ne fait avec les objets et les variables du programme que ce qui est autorisé par leur type. Cette vérification a pour effet d’accroître la fiabilité de l’exécution du programme. Java, C++ et C# sont fortement typés. L’étape de compilation y est essentielle. Ce n’est pas le cas, comme nous le verrons plus loin, de Python et PHP 5.

La classe comme module opérationnel Mémoire de la classe et mémoire des objets À propos des deux caractéristiques de la classe, on pourrait très synthétiquement dire de la première qu’elle est sa partie passive – représentée par les attributs qui sont associés aux objets (vu que chaque objet contiendra son propre ensemble d’attributs) –, et de la seconde qu’elle est sa partie active – représentée par les méthodes, en tant qu’associées à la classe, car les méthodes sont communes à tous les objets d’une même classe. Nous avons, dans le chapitre précédent, sciemment forcé cette séparation, en installant les attributs et les méthodes dans des espaces mémoires bien distincts. Supposez maintenant que tous les feux de signalisation évoqués dans le chapitre précédent mesurent la même hauteur ou que, dans une application logicielle particulière, toutes les voitures soient de la même marque. Il n’est plus nécessaire d’installer ces deux attributs dans l’espace mémoire alloué à chaque objet, vu que leur valeur est commune à tous les objets. Il serait plus naturel, à l’instar des méthodes, de les installer dans les espaces mémoire dédiés aux classes. On qualifie ce type d’attribut particulier, dont les valeurs sont partagées par tous les objets et deviennent de ce fait plutôt attributs de classe que d’objet, d’attribut statique. Dans la figure 2-4, on retrouve ces attributs dans la zone mémoire adéquate.

38

L’orienté objet

Figure 2-4

Comment les attributs statiques « hauteur » de la classe FeuDeSignalisation et « marque » de la classe Voiture se retrouvent dans la mémoire associée aux classes et non plus aux objets.

Méthodes de la classe et des instances Certaines méthodes peuvent également être déclarées statiques. Quel intérêt, alors, de forcer leur association à la classe plutôt qu’aux instances, constaterez-vous avec raison ? Les méthodes ne sont-elles pas toujours des méthodes de classe, ne le sont-elles par principe ? C’est très bien vu, mais vous vous serez rendu compte également que, bien qu’associée à la classe, toute méthode a, jusqu’à présent, toujours été exécutée sur un objet, ou simplement appelée à partir d’un objet, par une instruction semblable à a.f(x), où a est le référent de l’objet, et f(x) la méthode. Une méthode statique aura la possibilité de pouvoir s’exécuter sans le moindre objet créé, uniquement à partir de sa classe, par une simple instruction comme Class1.f(x) (Class1 étant le nom d’une classe) (nous verrons d’ailleurs qu’un langage comme C# n’accepte un appel de méthode statique qu’à partir de sa classe, ce qui est très logique). Une méthode statique pourra s’exécuter dès que la classe qui la contient est chargée en mémoire, y compris en l’absence de toute création d’objet. C’est par exemple le cas de toutes les méthodes mathématiques définies dans la classe Math en Java et C#, et qui s’appellent de la manière suivante : Math.sin(45) ou Math.pow(2,1). On ne voit pas vraiment l’utilité de créer un objet de type Math. Les praticiens de Java ou de C# connaissent tous, quelle que soit leur maîtrise de ces langages, une célèbre méthode statique, totalement inévitable, même par les plus novices d’entre eux: la méthode main(). La première méthode à s’exécuter lors du démarrage d’un programme se trouve définie, comme toute méthode (pas de traitement de faveur pour la « principale »), à l’intérieur d’une classe, mais une classe qui n’a pas forcément besoin de donner naissance à un objet. En C++, lourd tribut payé au C, et participant à rendre ce langage moins OO que les précédents, le « main » reste une procédure existant en dehors de toute classe. En Java et

Un objet sans classe… n’a pas de classe CHAPITRE 2

39

C#, le « main » est une méthode statique, car il n’est, en effet, pas nécessaire de lancer cette méthode à partir d’un objet. Dans le cas contraire, il faudrait toujours s’assurer de la création d’un objet issu de la classe principale (qu’en général, rien ne force dans la conception de l’application) avant de déclencher le main. Comme une méthode statique peut s’exécuter uniquement à partir de la classe, sans objet, les données que celle-ci manipulera se devront également de pouvoir exister sans objet. Toute méthode nécessite, lors de son exécution, l’adresse physique des données qu’elle manipule. Pour un objet, elle retrouve cette adresse à partir de son référent. Quand la méthode est statique, les données qu’elle utilise devront forcément se trouver dans l’espace mémoire réservé aux classes, et, par là même, se transformer en statique. Statique Les attributs d’une classe dont les valeurs sont communes à tous les objets, et qui deviennent ainsi directement associés à la classe, ainsi que les méthodes pouvant s’exécuter directement à partir de la classe, seront déclarés comme statiques. Ils pourront s’utiliser en l’absence de tout objet. Une méthode statique ne peut utiliser que des attributs statiques, et ne peut appeler en son sein que des méthodes également déclarées comme statiques.

Un premier petit programme complet dans les cinq langages Ayant défini la manière de réaliser le main, nous avons tous les éléments en main (non, en main pas en main), pour réaliser un premier petit programme dans les quatre langages. Ce programme possédera une classe FeuDeSignalisation. Dans le main, il créera deux objets de cette classe, à l’aide de deux constructeurs surchargés. Il interrogera ensuite ces objets quant à la valeur de leur attribut statique hauteur, qu’il modifiera de plusieurs manières. Il finira par exécuter sur un de ces objets la méthode clignote, dans ses trois versions surchargées, passant comme argument les durées des phases éteintes et allumées. Pour l’instant, n’accordez aucune importance à la présence des mots-clés public et private, qu’il est nécessaire de spécifier, mais dont la signification sera longuement discutée dans la suite. Les cinq codes aboutissent au même résultat à l’écran (présenté sous le code Java).

En Java Nous allons écrire le code Java dans un seul fichier, bien qu’il contienne deux classes et qu’une pratique bien meilleure consiste à séparer les classes par fichier. Nous le faisons ici pour des raisons de facilité et de simplicité au vu de la petitesse des codes présentés. Le fichier Principale.java /* Il est obligatoire en Java que la seule classe publique contenue dans le fichier porte le même nom que celui-ci, ici Principale. C’est ce qui permet à Java de faire des liaisons dynamiques entre les classes contenues dans des fichiers différents dès lors que chacun des fichiers ne contient qu’une classe */ class FeuDeSignalisation { private int couleur; private int position ; private static double hauteur; /* attribut statique */

40

L’orienté objet

public FeuDeSignalisation(int couleurInit) { /* un premier constructeur */ couleur = couleurInit; position = 0 ; } public FeuDeSignalisation(int couleurInit, double hauteurInit) { /* le constructeur est surchargé */ couleur = couleurInit; hauteur = hauteurInit; position = 0 ; } public void setHauteur(double nouvelleHauteur) { hauteur = nouvelleHauteur; } public static void getHauteur() { /* méthode statique qui accède à l’attribut statique */ System.out.println("la hauteur du feu est " + hauteur); } public void clignote() { System.out.println("premiere maniere de clignoter"); for(int i=0; i<2; i++) { for (int j=0; j<2; j++) System.out.println("je suis eteint"); for (int j=0; j<2; j++) System.out.println("je suis allume"); } } public void clignote(int a) { // première surcharge de la méthode System.out.println("deuxieme maniere de clignoter"); for(int i=0; i<2; i++) { for (int j=0; j
Un objet sans classe… n’a pas de classe CHAPITRE 2

41

FeuDeSignalisation.getHauteur(); /* appel de la méthode statique à partir de la classe */ unAutreFeu.setHauteur(10.6); /* tous les feux voient leur hauteur modifiée */ unFeu.getHauteur(); /* appel de la méthode statique à partir de l’objet */ System.out.println("********** CLIGNOTEMENT **********"); unFeu.clignote(); unFeu.clignote(3); int b = unFeu.clignote(2,3); } }

Résultats la hauteur du feu est 8.9 la hauteur du feu est 10.6 ********** CLIGNOTEMENT ****** premiere maniere de clignoter je suis eteint (écrit deux fois) je suis allume (écrit deux fois) je suis eteint (écrit deux fois) je suis allume (écrit deux fois) deuxieme maniere de clignoter je suis eteint (écrit trois fois) je suis allume (écrit trois fois) je suis eteint (écrit trois fois) je suis allume (écrit trois fois) troisieme maniere de clignoter je suis eteint (écrit deux fois) je suis allume (écrit trois fois) je suis eteint (écrit deux fois) je suis allume (écrit trois fois)

EN C# Le fichier Principal.cs Le code C# est si proche du code Java que vous pourriez jouer au jeu des sept erreurs. D’ailleurs, il doit y en avoir moins. Parmi ces dernières: le Main() qui peut s’exécuter sans argument, les noms des méthodes débutant par une majuscule (dont le Main). C# choisit de débuter le nom des méthodes par une majuscule, contrairement à Java. Oui, on est d’accord, c’est un peu mesquin… Plus conséquent et plus logique (un bon point en faveur de C#), une méthode statique ne peut être appelée qu’à partir de sa classe et non plus à partir de ses instances (Java et C++ offrent les deux possibilités). Enfin, si vous ajoutez par mégarde un return devant le constructeur, tout comme en C++, cela provoquera une erreur lors de la compilation. En Java, vous aurez juste déclaré une nouvelle méthode, qui joue un rôle autre que celui de constructeur. /* Bien que cela soit une excellente habitude surtout dans le cas recommandé où vous n’installez qu’une classe par fichier, C# n’oblige pas, comme Java, à donner au fichier le même nom que la classe qu’il contient */ using System; /* comme nous utiliserons dans le code l’instruction « Console.WriteLine », celle-ci ➥se trouve dans l’assemblage « System » qu’il est nécessaire de spécifier */

L’orienté objet

42

class FeuDeSignalisation { private int couleur; private int position ; private static double hauteur; public FeuDeSignalisation(int couleurInit) { couleur = couleurInit; position = 0 ; } public FeuDeSignalisation(int couleurInit, double hauteurInit) { couleur = couleurInit; hauteur = hauteurInit; position = 0 ; } public void setHauteur(double nouvelleHauteur) { hauteur = nouvelleHauteur; } public static void getHauteur() { Console.WriteLine("la hauteur du feu est " + hauteur); /* la manière d’écrire sur l’écran en C# */ } public void clignote() { Console.WriteLine("premiere maniere de clignoter"); for(int i=0; i<2; i++) { for (int j=0; j<2; j++) Console.WriteLine("je suis eteint"); for (int j=0; j<2; j++) Console.WriteLine("je suis allume"); } } public void clignote(int a) { Console.WriteLine("deuxieme maniere de clignoter"); for(int i=0; i<2; i++) { for (int j=0; j
Un objet sans classe… n’a pas de classe CHAPITRE 2

43

public class Principale { public static void Main() { /* voici le Main en C# */ FeuDeSignalisation unFeu = new FeuDeSignalisation(1,3.5); FeuDeSignalisation unAutreFeu = new FeuDeSignalisation(1); unFeu.setHauteur(8.9) ; FeuDeSignalisation.getHauteur(); unAutreFeu.setHauteur(10.6); /* unFeu.getHauteur(); impossible en C# */ Console.WriteLine("********** CLIGNOTEMENT **********"); unFeu.clignote(); unFeu.clignote(3); int b = unFeu.clignote(2,3); } }

En C++ Le fichier Principal.cpp En C++, de très nombreuses différences apparaissent. Dans la suite, nous aurons l’occasion de revenir sur nombre d’entre elles. Parmi les plus notables, main est une procédure ou une fonction, mais plus une méthode. Elle peut ou non retourner quelque chose. Dans le code, nous supposons qu’elle peut retourner, comme il est classique en C++, un code d’erreur si quelque chose se passe mal lors de l’exécution du programme. #include "stdafx.h" #include "iostream.h" /* afin de pouvoir utiliser le cout */ class FeuDeSignalisation { private: /* le public et le private sont mis en évidence */ int couleur; int position; static double hauteur; public: FeuDeSignalisation (int couleurInit) { couleur = couleurInit; position = 0 ; } FeuDeSignalisation (int couleurInit, int positionInit):couleur(couleurInit),position(positionInit) { /* le constructeur peut initialiser les attributs directement à partir de * la déclaration de sa signature */ } void setHauteur(double nouvelleHauteur) { hauteur = nouvelleHauteur; } void static getHauteur() { cout << "la hauteur du feu est " << hauteur << endl; /* la manière d'écrire sur l'écran en C++ */ } void clignote() { cout <<"premiere maniere de clignoter"<< endl; for(int i=0; i<2; i++) { for (int j=0; j<2; j++) cout << "je suis eteint" << endl;

44

L’orienté objet

for (int k=0; k<2; k++) cout <<"je suis allume" << endl; } } void clignote(int a) { cout << "deuxieme maniere de clignoter" << endl; for(int i=0; i<2; i++) { for (int j=0; jsetHauteur(10.6); /* le point se transforme en flèche pour des objets sur le tas */ unAutreFeu->getHauteur(); cout << "********** CLIGNOTEMENT **********" << endl; unFeu.clignote(); unFeu.clignote(3); int b = unFeu.clignote(2,3); return 0; }

C++ autorise l’hybridation des deux modes de programmation : procédural et objet, et, pour le main, on n’a pas vraiment le choix. La référence à une classe se fait toujours par Classe::méthode comme, dans le code, pour l’appel de la méthode statique, quand cet appel se fait à partir de la classe. Les objets peuvent être créés sur la pile, sans le new, ou dans le tas, avec le new. Lorsqu’ils sont créés dans le tas, les objets sont alors adressés

Un objet sans classe… n’a pas de classe CHAPITRE 2

45

par une variable de type pointeur, faisant ici office de référent (nous reviendrons largement sur la gestion mémoire dans le chapitre 9). Pointeur Un pointeur est une variable dont la valeur, comme le référent, est l’adresse d’une autre variable. Il fonctionne par adressage indirect. En C++, les pointeurs ne sont pas typés comme les référents. On peut par exemple les traiter comme des entiers, les incrémentant ou les décrémentant. Si peu contrainte, leur utilisation comporte de nombreux risques, car on voyage dans la mémoire sans le filet de sécurité assuré par les langages plus typés. Le typage plus strict des référents, en Java et en C#, les force à ne pointer, toujours, que sur des objets existants, d’une classe donnée.

L’évocation des méthodes sur le pointeur se fait en remplaçant le point par la flèche. Dans la procédure main, les deux objets, l’un dans la mémoire pile (qu’on associe aux méthodes avec le « . ») et l’autre dans la mémoire tas (qu’on associe aux méthodes avec le « -> »), sont utilisés, par la suite, de manière indifférenciée.

En Python Le fichier Principal.py class FeuDeSignalisation: __couleur=0 #il s’agit d'office d’un attribut de la classe __position=0 #donc statique __hauteur=0 #ici aussi statique def __init__(self, couleurInit): self.__couleur=couleurInit #L'utilisation de self indique que l'on peut également #référencer un attribut à partir d'un objet. #C'est en utilisant self qu'un attribut de classe #deviendra ici un attribut d'objet. #Une première manière très simple de réaliser un constructeur capable d’une certaine forme #de surcharge est : def __init__(self, couleurInit=None,hauteurInit=None): self.__couleur=couleurInit self.__hauteur=hauteurInit #Une autre manière détournée mais plus sophistiquée d’opérer une surcharge. #On redéfinit une méthode statique pouvant faire office de constructeur. def other__init(couleurInit, hauteurInit): result=FeuDeSignalisation(couleurInit) result.__couleur=couleurInit result.__hauteur=hauteurInit return result #On en fait une méthode statique car elle ne peut s'appeler #qu'à partir de la classe. other__init=staticmethod(other__init)

46

L’orienté objet

def setHauteur(self,nouvelleHauteur): FeuDeSignalisation.__hauteur=nouvelleHauteur def getHauteur(): print "la hauteur du feu est %s" % (FeuDeSignalisation.__hauteur) #Ici également on transforme cette méthode en statique. getHauteur = staticmethod(getHauteur) #Pas de surcharge, ici aussi on s'arrange comme on peut. def clignote(self,a='omitted',b='omitted'): if a == 'omitted' and b == 'omitted': print ("premiere maniere de clignoter") i=0 while i<2: j=0 while j<2: print "je suis eteint" j+=1 j=0 while j<2: print "je suis allume" j+=1 i+=1 elif a!='omitted' and b=='omitted': print ("deuxieme maniere de clignoter") i=0 while i<2: j=0 while j
Un objet sans classe… n’a pas de classe CHAPITRE 2

47

unFeu=FeuDeSignalisation.other__init(1,3.5) #Par le premier type de surcharge du constructeur, on aurait également pu directement et plus #simplement faire appel au constructeur officiel #unFeu=FeuDeSignalisation(1,3.5) #Ici, c'est bien l'appel de ce constructeur #à proprement parler. unAutreFeu=FeuDeSignalisation(1) unFeu.setHauteur(8.9) FeuDeSignalisation.getHauteur() unAutreFeu.setHauteur(10.6) unFeu.getHauteur() print "****** CLIGNOTEMENT ******" unFeu.clignote() unFeu.clignote(3) b=unFeu.clignote(2,3)

Python est un langage ayant recherché dès son origine une grande simplicité d’écriture, tout en conservant tous les mécanismes de programmation OO de haut niveau. Il cherche à soulager au maximum le programmeur de problèmes syntaxiques non essentiels aux fonctionnalités clés du programme. Les informaticiens parlent souvent à son compte d’un excellent langage de prototypage qu’il faut remplacer par un langage plus « solide » tel Java, les langages .Net ou C++, lorsqu’on arrive aux termes de l’application. Sa syntaxe de base, par les raccourcis qu’elle autorise, est donc assez différente de celle des trois autres langages. Ainsi, par rapport aux langages précédents, une surprise de taille nous attend, surtout pour ceux qui ont vu passer des kyrielles de langages de programmation : les accolades et les points-virgules ont disparu. Python détecte les limites des blocs d’instructions grâce à l’indentation des lignes, indentation qui devient dès lors capitale. Cette nouvelle règle syntaxique a fait d’une pratique souvent recommandée une obligation. Python est un langage OO, mais tout comme C++, il n’oblige pas à la pratique OO. En témoigne ici l’absence d’une classe principale et même de la méthode main, car il suffit d’écrire le programme appelant au même niveau que la définition des classes. Reconnaissez que, de la sorte, un programme est bien plus simple à démarrer que par le très laborieux public void static main (String[] args) de Java. Le sempiternel hello world se fait simplement par print "hello world". Tentez de faire plus simple et vous conviendrez aisément des ambitions pédagogiques de Python. Une autre différence essentielle, visible dans la déclaration des attributs, est que Python n’est pas un langage typé, du moins en ce qui concerne les variables et les attributs. Il est dit « typé dynamiquement » en ce sens que le type de la variable est alloué en fonction de ce qu’elle contient et peut ainsi changer au fil des affectations. C’est une pratique très discutable qui permet une économie d’écriture, mais peut occasionner quelques comportements indésirables lors de l’exécution. Comme nous avons déjà eu l’occasion de le dire, le rôle du typage est inséparable de celui du compilateur et permet à ce dernier de détecter des erreurs de substitution entre variables. Python ne compilant pas, ce typage explicite ne s’avère plus autant nécessaire, au risque que certains problèmes ne se produisent qu’à l’exécution. Pour déclarer un attribut privé, il suffit de faire précéder son nom de deux underscores. Une faiblesse additionnelle est que Python ne supporte pas la surcharge de méthodes. C’est assez compréhensible vu l’absence de typage explicite ; difficile de distinguer deux signatures de méthode par le type de leurs arguments. Dans le code ci-dessus, certaines astuces sont adoptées pour contourner cette limitation. Le constructeur doit avoir le nom de __init__. Nous discuterons du self, indispensable dans la définition des méthodes s’exécutant à partir des objets (c’est-à-dire non statiques), par la suite.

48

L’orienté objet

En PHP 5 Le fichier Principal.php Classe Feu de Signalisation

Classe feu de signalisation



public function __construct() { /* définition d’un constructeur surchargeable assez proche de Python*/ $num_args=func_num_args(); switch ($num_args) { case '0': $this->couleur = $this->position = 0; /* $this est indispensable pour les attributs d’objet*/ self::$hauteur = 0; /* self l’est pour les attributs statiques */ break; case '1': $this->couleur = func_get_arg(0); $this->position = 0; self::$hauteur = 0; break; case '2': $this->couleur =func_get_arg(0); $this->position = 0; self::$hauteur=func_get_arg(1); break; } } public function setHauteur($nouvelleHauteur) { self::$hauteur = $nouvelleHauteur; } static public function getHauteur() { print ("la hauteur du feu est ". self::$hauteur . "
\n"); }

Un objet sans classe… n’a pas de classe CHAPITRE 2

public function clignote() { // définition d’une méthode clignote surchargeable $num_args=func_num_args(); $b=0; switch ($num_args) { case '0': print("premiere maniere de clignoter
\n"); for ($i=0;$i<2;$i++){ for ($j=0;$j<2;$j++) print("je suis eteint
\n"); for ($j=0;$j<2;$j++) print("je suis allume
\n"); } break; case '1': print("deuxieme maniere de clignoter
\n"); $a=func_get_arg(0); for ($i=0;$i<2;$i++){ for ($j=0;$j<$a;$j++) print("je suis eteint
\n"); for ($j=0;$j<$a;$j++) print("je suis allume
\n"); } break; case '2': print("troisieme maniere de clignoter
\n"); $a=func_get_arg(0); $b=func_get_arg(1); for ($i=0;$i<2;$i++){ for ($j=0;$j<$a;$j++) print("je suis eteint
\n"); for ($j=0;$j<$b;$j++) print("je suis allume
\n"); } return $b; } } } $unFeu = new FeuDeSignalisation(1,3.5); $unAutreFeu = new FeuDeSignalisation(1); $unFeu->setHauteur(8.9); FeuDeSignalisation::getHauteur(); $unAutreFeu->setHauteur(10.6); $unFeu->getHauteur(); print("********** CLIGNOTEMENT ********
\n"); $unFeu->clignote(); $unFeu->clignote(3); $b=$unFeu->clignote(2,3); ?>

49

50

L’orienté objet

PHP 5 est devenu le langage de prédilection pour nombre de maîtres toileurs (webmestres) qui lui trouvent de nombreux avantages pour la conception de sites web dynamiques. PHP est un langage d’écriture de script qui s’exécute sur un serveur web et permet de mêler assez simplement les informations de structuration d’un site web (exprimé dans le langage HTML) et les instructions de programmation permettant de rendre ce même site dynamique et interactif. Créé en 1995, il est devenu, dans sa cinquième version (début des années 2000), pleinement orienté objet, ce qui a contribué davantage encore à son succès et l’a rendu responsable du bon fonctionnement et de l’attrait de plusieurs millions de sites web. Ceux-ci également, comme tout type de logiciel aujourd’hui, tentent à s’enrichir de plus en plus de nombreuses fonctionnalités : complexification croissante, dont la programmation et la modularisation OO contribuent à adoucir les effets dévastateurs sur la santé mentale des programmeurs. Ce livre n’a en aucun cas l’ambition d’aborder, même un tant soi peu, la conception de sites web. Le sujet s’avère suffisamment riche pour dédier un livre sinon une librairie entière à son seul traitement. Ne nous intéressera donc dans PHP que le côté « langage de programmation OO », et nullement la manière dont il s’harmonise avec les informations de structuration et de contenu propres à tout site web. On considérera aussi comme résolus, tous les problèmes d’installation de serveur web indispensables à l’utilisation et au bon fonctionnement du PHP. Néanmoins, le cadre d’exécution des scripts PHP étant un navigateur Internet, le code PHP 5 se trouve forcément, comme dans l’exemple précédent, imbriqué dans un environnement HTML. À la suite de quelques instructions HTML, le code débute par la balise . Le reste devrait vous paraître assez familier. Étant conçu comme un langage de script, PHP se passe, tout comme Python (c’est une espèce d’hybride entre la famille C++/Java et les langages de script tels Perl ou Python), de compilateur et de typage explicite (avec les mêmes avantages et inconvénients épinglés pour Python). Si vous avez « encaissé » les codes Java et Python, celui du PHP 5 ne devrait pas vous poser de problème particulier, sinon que vous dire…. Retour à la case Java !

La classe et la logistique de développement Classes et développement de sous-ensembles logiciels Nous aurons souvent l’occasion de revenir sur l’avantage suivant: la classe permet un découpage logiciel des plus naturels. Un programme, quel qu’il soit, se compose toujours d’une structure de données et d’un ensemble d’opérations portant sur ces données. Or, un programme, cela peut devenir bien vite très gros, des millions de lignes de code dit-on pour Windows (bien que nous reconnaissions ne pas les avoir comptées). La préoccupation pour le programmeur ou, plus souvent, l’équipe de programmeurs devient de trouver un moyen simple et naturel de découper le programme en un ensemble de modules gérables et suffisamment indépendants entre eux. Vous nous voyez venir avec nos gros sabots. Mais oui, bien sûr, pourquoi ne pas découper tout le logiciel en ses classes, puisque chacune d’entre elles, tout comme un petit programme à part entière, se retrouve avec sa structure de données et ses opérations? Pour l’informaticien quelles équations de rêve que les suivantes : une classe = un type = un module = un fichier, un programme = un ensemble de classes en interaction = un ensemble de fichiers automatiquement liés. C’est donc autour de la classe que l’informaticien, idéalement, tracera les traits pointillés qui lui permettront de découper son code en fichiers. C’est, parmi tous les langages que nous découvrons dans ce livre, Java qui a poussé cette logique à son paroxysme, en insistant pour placer une classe par fichier (si vous ne le faites pas, il le fait pour vous à la compilation) et en donnant au fichier le même nom que celui de sa classe.

Un objet sans classe… n’a pas de classe CHAPITRE 2

51

Classes, fichiers et répertoires De même que vous organisez l’emplacement et la gestion des fichiers à l’aide de répertoires imbriqués selon les thèmes repris par ces fichiers, les classes pourront également être organisées en assemblage, selon, là encore, de simples critères sémantiques. Les classes portant sur un domaine semblable se regrouperont dans un même assemblage. L’organisation des classes en assemblage sera transposée de manière isomorphe dans une organisation des fichiers qui contiennent ces classes en répertoire. Les assemblages s’organiseront entre eux, tous comme les répertoires, de manière hiérarchique. Toute dépendance entre classes par l’envoi de message débouchera sur une liaison des plus simples à mettre en œuvre entre les fichiers qui contiennent ces classes. Aucune liaison dynamique, autre que celle directement prévue par les déclarations des classes, n’apparaîtra comme nécessaire. Idéalement, si un objet de la classe A nécessite de connaître la classe B pour s’exécuter, cela sera inscrit noir sur blanc dans le code et n’appellera aucune instruction additionnelle, au niveau du système d’exploitation, pour relier les deux fichiers. Liaison naturelle et dynamique des classes La classe, par le fait qu’elle s’assimile à un petit programme à part entière, constitue un module idéal pour le découpage du logiciel en ses différents fichiers. La liaison sémantique entre les classes, rendue possible si la première intègre en son code un appel à la seconde, devrait suffire à relier de façon dynamique, pendant la compilation et l’exécution du code, les fichiers dans lesquels ces classes sont écrites. C’est principalement dans Java que cette logique de découpe et d’organisation sémantique du code en ses classes isomorphes à la découpe et l’organisation physique en fichiers sont le plus scrupuleusement forcées par la syntaxe. C’est un très bon point en faveur de Java.

Ces différents rôles, endossés par les classes, ont été disséqués en profondeur par Bertrand Meyer dans son remarquable ouvrage Conception et programmation orientées objet. Citons-le : « Dans les approches non OO, les concepts de module et de type restent distincts. La propriété la plus remarquable de la notion de classe est qu’elle généralise ces deux concepts, les fusionnant en une seule construction logique. Une classe est un module, ou une unité de décomposition logicielle ; mais c’est aussi un type… ». Bertrand Meyer Bertrand Meyer est un personnage incontournable du monde de l’OO, une de ses plus grosses pointures. Formé en France, il se partage ces dernières années entre la Suisse (il est professeur à l’ETH de Zurich), les États-Unis et l’Australie. Il est toujours extrêmement actif, et, plus récemment, s’est beaucoup investi dans la plate-forme .Net de Microsoft. Pour nous, ici, il est surtout le père du langage de programmation Eiffel (il a fondé la compagnie Eiffel Software à Santa Barbara en Californie), un langage OO clef qui, même s’il ne rivalisera sans doute jamais, en termes de popularité, avec Java, C++ et C#, est une espèce d’idéal à atteindre par tous les langages OO. On le retrouve d’ailleurs intégré dans .Net. On trouve dans Eiffel toutes les bonnes choses de l’OO, le tout objet, l’encapsulation, le polymorphisme, l’héritage simple et multiple, la généricité, le ramasse-miettes…, et plus encore. Ce langage est décrit en profondeur dans une deuxième réalisation remarquable de Meyer, l’ouvrage Conception et programmation orientées objet (Eyrolles), qui en est à sa deuxième édition. C’est une référence indispensable pour qui cherche à s’aventurer au plus profond dans les questions et les méandres de la pratique OO. Les réponses que vous ne trouverez pas ici, vous devriez, au risque d’une lecture un peu plus corsée (on n’a rien sans mal), les trouver dans l’ouvrage de Meyer. Ce livre n’est pas toujours d’un abord facile, mais l’effort déployé à comprendre ce qui y est dit est souvent très gratifiant. Depuis plusieurs années, Meyer essaie d’imposer une vision à la fois très personnelle et très formelle de la programmation OO, qui rajoute à toutes les bonnes choses déjà connues et reprises dans son ouvrage, ainsi que dans le nôtre, la mise en pratique de la « conception par contrat ». Très schématiquement, si les classes sont à même de fournir des prestations pour des clients, elles devraient le faire sous une forme de contrat, clairement établi et explicité.

52

L’orienté objet

Pour autant que soit garanti un ensemble de pré-conditions nécessaires à la bonne exécution du contrat, dans ce dernier la classe prestataire du service s’engage à fournir un ensemble de post-conditions remplissant les attentes du client. De plus, chaque classe a la responsabilité personnelle de préserver son intégrité, en respectant, quoi qu’elle fasse, un ensemble d’invariants. Ce qu’il faut comprendre ici, c’est que, même si cette pratique peut être, à coup de triturations d’écriture, implémentée dans tous les langages, Meyer estime que ces invariants devraient plus intimement et plus naturellement être intégrés dans la syntaxe de ces langages, ce qu’Eiffel accomplit (et le « tour » est joué). La mise en pratique de ces différentes additions (pré et post conditions ainsi que les invariants) devrait assurer le développement de logiciels plus fiables et plus faciles à maintenir et à faire évoluer. En décembre 2005, Bertrand Meyer fut victime d’une plaisanterie assez macabre qui pourrait provenir de l’un de ses étudiants. L’encyclopédie online Wikipédia annonça son décès (le lendemain de la publication des résultats d’un de ses examens au polytechnique de Zurich), et il fallut quelques jours pour corriger cette fausse mauvaise nouvelle. Cela permit à certains de dénoncer le fonctionnement et l’existence même de Wikipédia qui avait pourtant trouvé en Bertrand Meyer l’un de ses défenseurs les plus ardents. Version web de l’arroseur arrosé.

Exercices Exercice 2.1 Réalisez la classe voiture avec un changement de vitesse, en y installant, tout d’abord, deux méthodes ne retournant rien, mais permettant, l’une d’incrémenter la vitesse, et l’autre de décrémenter la vitesse. Surchargez ensuite la méthode d’incrémentation de vitesse, en lui passant, en argument, le nombre de vitesses à incrémenter.

Exercice 2.2 Soit la déclaration de la méthode suivante : public void test(int a) {}

Quelles sont les surcharges admises entre ces différentes possibilités ? public public public public

void test() {} void test(double a) {} void test(int a, int b) {} int test(int a) {}

Exercice 2.3 Les fichiers Java, A.java, et C#, A.cs, suivants ne compileront pas, et ce, malgré leur grande ressemblance, pour des raisons différentes. Expliquez pourquoi. A.java

public class A { void A(int i) { System.out.println("Hello"); } public static void main(String[] args) { A unA = new A(5); } }

A.cs

using System; public class A { void A(int i) { Console.WriteLine("Hello"); } public static void Main() { A unA = new A(5); } }

Un objet sans classe… n’a pas de classe CHAPITRE 2

53

Exercice 2.4 Réalisez le constructeur de la classe voiture, initialisant la vitesse à 0. Surchargez ce constructeur si l’on connaît la vitesse initiale.

Exercice 2.5 Parmi les attributs suivants de la classe Renault Kangoo, la version avec toutes les options possibles, séparez ceux que vous déclareriez comme statiques des autres : vitesse, nombre de passagers, vitesse maximale, nombre de vitesses, capacité du réservoir, âge, puissance, prix, couleur, nombre de portières.

Exercice 2.6 Les trois codes suivants ne trouveront pas grâce aux yeux du constructeur. Une seule erreur s’est glissée dans chacun d’eux. Corrigez-les. Code 1 : Fichier Java : PrincipalTest.java

class Test { int a; Test (int b) { a = b; } } public class PrincipalTest { public static void main(String[] args) { Test unTest = new Test(); } }

Code 2 : Fichier C# : PrincipalTest.cs

class Test { private int private int public Test a = b; } public Test a = e; c = f; } }

a; c; (int b) { (int e, int f) {

public class PrincipalTest { public static void Main(){ Test unTest = new Test(5); Test unAutreTest = new Test(5, 6.5); } } Code 3 : Fichier C++ : PrincipalTest.cpp

#include "stdafx.h" class Test { private: int a, b; public: Test (int c, int d) { a = c; b = d; } Test (int c):a(c) {} }; int main(int argc, char* argv[]) { Test unTest(5); Test *unAutreTest = new Test(6,10);

L’orienté objet

54

Test unTroisiemeTest; return 0; }

Exercice 2.7 Les trois codes suivants ne trouveront pas grâce aux yeux du compilateur. Une seule erreur s’est glissée dans chacun d’eux. Corrigez-les. Code Java : fichier PrincipalTest.java

class Test { int a; int c; Test (int b) { a = b; } static int donneC() { return c; } }

public class PrincipalTest { public static void main(String[] args) { Test unTest = new Test(5); } } Code C++ : PrincipalTest.cpp

#include "stdafx.h" class Test { private: int a, b; static int c; public: Test (int e, int f) { a = e; c = f; } Test (int e):a(e) {} static int donneC() { return c; } }; int main(int argc, char* argv[]) { Test unTest(5);

Code C# : fichier PrincipalTest.cs

class Test { private int a; static private int c; public Test (int b) { a = b; } public Test (int e, int f) { a = e; c = f; } public static int donneC() { return c; } } public class PrincipalTest { public static void Main() { Test unTest = new Test(5); unTest.donneC(); } }

Un objet sans classe… n’a pas de classe CHAPITRE 2

55

Test *unAutreTest = new Test(6,10); unAutreTest->donneC(); return 0; }

Exercice 2.8 Réalisez en Java et en C#, un programme contenant une classe Point, avec ses trois coordonnées dans l’espace x,y,z, et que l’on peut initialiser de trois manières différentes (selon les valeurs initiales connues des trois coordonnées, on connaît soit x, soit x et y , soit x et y et z). Ensuite, intégrez dans la classe une méthode translate() qui est surchargée trois fois, dépendant également desquelles des trois valeurs des translations sont connues.

Exercice 2.9 Créez deux objets de la classe Point à peine réalisée, et testez le bon fonctionnement du programme quand vous translatez ces points.

3 Du faire savoir au savoir-faire… du procédural à l’OO Ce chapitre distingue l’approche dite procédurale, axée sur les grandes activités de l’application, de l’approche objet, axée sur les acteurs de la simulation et la manière dont ils interagissent. Nous illustrons cette distinction à l’aide de la simulation d’un petit écosystème.

Sommaire : Procédural versus OO — Activité versus acteurs — Dépendance fonctionnelle mais indépendance dans le développement — Introduction à la relation interclasses ou client-fournisseur — Acteurs collaborant

Candidus – Bon ! Maintenant qu’on a donné des jouets à bébé, il faut lui expliquer comment tout cela fonctionne. J’aurais envie de mettre tout à sa portée, bien rangé sur la table, mais ne va-t-il pas tout mélanger ? Doctus – Mieux vaut lui présenter chacun des petits puzzles l’un après l’autre. Cand. – Si je comprends bien, tu veux lui faire construire un gros truc sans qu’il s’en rende compte. Pourtant je rêve d’un ordinateur qui me comprendrait à demi-mot ! Doc. – Je propose de diviser une structure complexe en sous-ensembles simples. Par exemple, selon la méthode classique, nos voitures se présentaient sous forme de pièces détachées devant être assemblées et contrôlées et c’était à toi d’en vérifier l’intégrité avant chaque usage. Avec l’OO, ta voiture est toujours prête, tu montes dedans et c’est parti ! Cand. — Normalement, c’est le programmeur qui se charge de réaliser ce qui a été envisagé lors de la phase de conception… Doc. — Oui, mais la programmation objet a un rôle à jouer autant à la phase d’analyse, qu’à la conception et même à l’exécution. Les pièces de notre puzzle ne sont plus de simples morceaux de carton, elles participent activement à notre jeu ! Chaque pièce sera indissociable de son mode d’emploi ! Cand. — Tu veux dire que les données et les fonctions seront scotchées les unes aux autres ? Mais où vais-je donc pouvoir mettre mes goto alors ? Et que deviennent les procédures de notre programme ? Doc. – Il s’agit pour schématiser de les remplacer par un jeu de transactions entre les différents acteurs. Cand. — Ça c’est fort ! Les jouets vont jouer les uns avec les autres ! Mais comment faire, lorsqu’il y a plusieurs fabrications de jouets, pour créer des interfaces ? C’est sûr que les cotes et les formes des pièces du puzzle devront être bien définies pour que bébé puisse jouer sans s’énerver. Doc. – Chaque programmeur – pardon, chaque fabricant de jouet – aura toute une panoplie de moyens pour mettre en place les permissions ou interdictions qu’il jugera utiles.

58

L’orienté objet

Cand. – Hm… Ne s’agit-il pas en fin de compte d’un nouveau modèle dogmatique qui restera en vogue jusqu’à ce qu’un nouveau ne le remplace dans quelques années ? Doc. – Si on considère qu’il atteint son objectif, à savoir se rapprocher d’une organisation naturelle, on peut lui présager un futur à la hauteur !

Objectif objet : les aventures de l’OO L’addition des méthodes dans la classe, dès que celles-ci portent sur un des attributs de la classe, transforme ces dernières de simples récipients d’information en véritables acteurs : l’objet fait plus qu’il n’est. Il ne se borne pas simplement à stocker son état ; il est surtout le premier responsable des modifications que celui-ci subit. Il sait et il fait, tout à la fois. Qu’un second objet, quelconque, désire se renseigner sur l’état du premier ou d’entreprendre de modifier cet état, il devra passer par les méthodes de ce premier objet qui, seules, ont la permission de lire ou transformer cet état. L’objet devra toujours être accompagné de son mode d’emploi que nous verrons plus loin, défini dans une structure de donnée à part : son interface. L’orienté objet est loin d’être une pratique neuve en informatique, puisque le premier langage OO important, Simula, remonte à 1966. Cela permet aux vieux grisards de l’informatique, et qui cherchent à faire de leurs rides et de leurs cheveux blancs (on les attrape, paraît-il, beaucoup plus tôt en informatique), plus qu’une incitation au respect, un atout majeur, de prétendre que tout ce qui se fait d’apparemment neuf du côté de l’ordinateur n’est qu’un hoquet du passé, et que rien de vraiment novateur ne s’est produit depuis von Neuman ou Turing. Kristen Nygaard et Ole-Johan Dahl : Simula Simula est l’ancêtre de tous les langages orientés objet. Un ancêtre vieux seulement de 38 ans, car il a été proposé par deux chercheurs norvégiens, Kristen Nygaard et O-J. Dahl, en 1966, alors qu’ils étaient tous deux chercheurs au Norwegian Computing Center (NCC), à Oslo. Malgré son âge, il n’a pas pris une ride car tout y est de ce qui fait la force de l’OO : classe, objet, encapsulation, envoi de messages, typage fort, héritage, polymorphisme, multithreading, gestion mémoire, etc. Il fut à l’origine mis au point pour faciliter la conception et l’analyse des systèmes à temps discret, mais très vite évolua vers le langage de programmation fondateur de l’OO. Si le succès ne fut pas au rendez-vous, la raison en est simple : Simula était trop en avance sur son temps. Simula était la réponse logicielle la plus adéquate trouvée par ces chercheurs pour affronter la simulation de processus industriels complexes. La décomposition modulaire en classes suivait la décomposition structurelle du processus en ses différents composants. L’approche répondait à cette simple intuition : pourquoi baser la décomposition du logiciel sur un mode différent que celui qui vous est proposé par le monde ? Il semblait évident à ces deux chercheurs qu’écrire un programme c’est d’abord et avant tout réaliser un modèle de la réalité que l’on cherche à maîtriser à l’aide de celui-ci (d’où le nom de Simula). Nygaard et Dahl furent extrêmement créatifs et productifs dans le département informatique de l’université d’Oslo et continuent à innover dans le développement des applications distribuées ou de langages OO plus compréhensibles. Nygaard est aussi très connu en Norvège comme un activiste politique et social des plus influents. Parallèlement à ses apports technologiques, il n’a pas cessé de se questionner sur l’impact des technologies de l’information dans la société et les systèmes d’éducation. Il fut très engagé dans les mouvements de protection de la nature et a été, surtout, le porte-étendard de la croisade qui enjoignit la Norvège à ne pas joindre l’Union européenne. Ce n’est qu’assez récemment, en 2001 et 2002, que la communauté informatique a reconnu l’apport décisif de ces deux chercheurs dans l’informatique d’aujourd’hui, en leur décernant les prestigieux prix IEEE John Von Neuman et ACM Alan Turing, prix qui sont à l’informatique ce que le prix Nobel est aux autres sciences. Ces deux génies se sont suivis dans la créativité comme dans la mort. Ole-Johan Dahl nous a quittés le 29 juin 2002 à 70 ans, juste quelques semaines avant Kristen Nygaard parti, lui, le 9 août 2002 à 76 ans. L’AITO, « Association Internationale pour les Technologies Objets » a, en 2004, créé le prix Dahl-Nygaard récompensant les informaticiens les plus importants dans l’évolution du monde OO. Il fut décerné en 2005 à Bertrand Meyer et en 2006 au « Gang des quatre », auteurs des Design Patterns présentés au chapitre 23.

Du faire savoir au savoir-faire… du procédural à l’OO CHAPITRE 3

59

Que les programmeurs en herbe se rassurent, on entend dire la même chose de la philosophie qui ne serait autre que des bas de page aux écrits de Platon, du jazz depuis Charlie Parker, et certainement de l’architecture, la peinture et de bien d’autres passe-temps humains. Vous aurez tôt fait de rétorquer à ces rabat-joie qu’il en va de même en matière de ringardise, où rien n’a plus vraiment évolué depuis les jérémiades du premier ringard…

Argumentation pour l’objet Il est vrai que toutes les époques ne peuvent être aussi créatives, et que les années 1950 et 60 – époque charnière et témoin de la naissance de l’informatique – ont été inévitablement plus génératrices de nouveauté (il est plus commode d’innover à partir de rien). Néanmoins, il suffit de lorgner du côté de la bio-informatique ou de l’informatique quantique pour aisément se rendre compte que l’ordinateur est loin d’avoir épuisé ce potentiel de créativité qu’il suscite. Aujourd’hui, les progrès en matière de développement logiciel cherchent à rendre la programmation de plus en plus distante du fonctionnement intime des microprocesseurs, et de plus en plus proche de notre manière spontanée de poser et résoudre les problèmes. Il est loin le temps où la maîtrise de l’assembleur était le signe distinctif de ceux qui savaient programmer. La simplicité de conception, l’accessibilité, l’adaptabilité, la fiabilité et la maintenance facilitée sont, bien plus que l’optimisation du programme en temps calcul et en espace mémoire, les voies du progrès. Ce qu’on perd en temps CPU, plusieurs indicateurs tendent à montrer qu’on le regagne aisément en espèces sonnantes et trébuchantes. Tout en informatique semble se conformer à la loi de Moore, d’un doublement de performance tous les 18 mois, tout … sauf les programmeurs. Plus l’application à réaliser est complexe et fait intervenir de multiples acteurs en interaction, plus il devient bénéfique de prendre ses distances par rapport aux contraintes imposées par le processeur, pour faire du monde qui nous entoure la principale source d’inspiration.

Transition vers l’objet L’OO est une de ces étapes, qui ne demandent qu’à être poursuivies, à la croisée des progrès en génie logiciel, de l’amélioration des langages de programmation et des sciences cognitives. Mais foin de toute démagogie et de spéculation hasardeuse, et revenons à des considérations plus terriennes. Il est indéniable qu’il existe aujourd’hui deux manières de penser les développements logiciels : la manière dite « procédurale », et représentée par les langages de programmation de type procédural, comme C , Pascal, Fortran, Cobol, Basic, et la manière dite « orientée objet », et représentée par les langages de programmation de type orienté objet, comme C++, Java, Smalltalk, Eiffel, C#, Python. Aujourd’hui, la seconde semble inéluctablement prendre le pas sur la première. Lors d’un entretien pour un emploi d’informaticien, répondez OO à toutes les questions, et vos chances d’embauche sont multipliées par 100. Une fois en place, programmez comme vous voulez et sans risque apparent car, alors que l’on peut mesurer votre degré d’alcoolémie, rien n’existe de semblable pour tester votre respect de la bonne pratique de l’OO. Hélas, dirons-nous, car à l’issue des prochains chapitres nous espérons que vous serez convaincus des nombreux avantages objectifs qu’il y a à adopter la pratique OO dans le développement d’applications logicielles un tant soit peu conséquentes. De manière à différencier ces deux approches le plus pratiquement qui soit, nous nous glisserons dans la peau d’un biologiste qui désire réaliser la simulation d’un petit écosystème dans lequel, comme indiqué dans la figure 3-1, évoluent un prédateur (le lion), une proie (l’oiseau) et des ressources, eau et plante, nécessaires à la survie des deux animaux.

60

L’orienté objet

Mise en pratique Simulation d’un écosystème Figure 3-1

Programmation en Java d’un petit écosystème.

Décrivons brièvement le scénario de cette petite simulation. La proie se déplace vers l’eau, vers la plante, ou afin de fuir le prédateur ; elle agit en fonction du premier de ces objets qu’elle repère. La vision, tant de la proie que du prédateur, est indiquée par une ligne, qui part du coin supérieur de l’animal et balaie l’écran. Quand la ligne de vision traverse un objet, quel qu’il soit, l’objet est considéré comme repéré, la vision ne quitte plus l’objet, et l’animal se dirige vers la cible ou la fuit. Le prédateur se déplace vers l’eau ou poursuit la proie, là aussi en fonction du premier des deux objets perçus. L’énergie selon laquelle les deux animaux se déplacent décroît au fur et à mesure des déplacements, et conditionne leur vitesse de déplacement. Dès que le prédateur rencontre la proie, il la mange. Dès que le prédateur ou la proie rencontre l’eau, ils se ressourcent (leur énergie augmente) et l’eau diminue de quantité (visuellement, la taille de la zone d’eau diminue). Dès que la proie rencontre la plante, elle se ressource (son énergie augmente également) et la plante diminue de quantité (sa taille diminue). Enfin, la plante pousse lentement avec le temps, alors que la zone d’eau, au contraire, s’évapore.

Analyse Analyse procédurale Adoptons dans un premier temps la pratique procédurale. Tous les objets de notre programme seront créés dès le départ, et stockés en mémoire des objets comme indiqué à la figure 3-2. Dans la phase d’élaboration structurelle des objets, rien ne distingue vraiment l’approche procédurale de l’approche OO. Les deux pratiques commencent à se démarquer par le fait que, dans la vision procédurale, les objets constituent un ensemble global de données du programme que de grands modules procéduraux affecteront tour à tour. En « procédural », l’analyse et la décomposition du programme se font de manière procédurale ou fonctionnelle, c’est-à-dire que l’on découpe le code en ses grandes fonctions. Programmation procédurale La programmation procédurale s’effectue par un accès collectif et une manipulation globale des objets, dans quelques grands modules fonctionnels qui s’imbriquent mutuellement, là où la programmation orientée objet est confiée à un grand nombre d’objets, se passant successivement le relais, pour l’exécution des seules fonctions qui leur sont affectées.

Du faire savoir au savoir-faire… du procédural à l’OO CHAPITRE 3

61

Fonctions principales Figure 3-2

Voici le découpage logiciel, avec ses grands blocs fonctionnels, de l’approche procédurale.

Quelles sont les fonctions que tous les objets doivent accomplir ici ? Tout d’abord, la proie et le prédateur doivent se déplacer. On commencera donc à penser et coder les déplacements de la proie et du prédateur, ensemble. Le fait de les traiter ensemble, même s’il s’agit de deux entités différentes, est ici très important. Tant pour le déplacement de la proie que du prédateur, il faudra préalablement que chacun des animaux repère une cible. La fonctionnalité de « déplacement » devra faire appel à une nouvelle fonctionnalité, de « repérage », qui, elle également, concerne tous les objets. En effet, les objets doivent se repérer entre eux : le prédateur cherche la proie, la proie cherche la plante et à éviter le prédateur, et tous deux cherchent l’eau. Le repérage se fait à l’aide de la vision, un bloc procédural constitué de deux fonctionnalités semblables, que l’on associera à chaque animal, et qui n’agira que pendant ce repérage. Une deuxième grande fonctionnalité consiste dans le ressourcement de la proie et du prédateur. Ce ressourcement concernera à nouveau tous les objets car, pour la proie comme pour le prédateur, il fonctionne différemment selon la ressource rencontrée. Par exemple, lorsque la proie rencontre la plante, elle la mange et la plante s’en trouve diminuée. Lorsque le prédateur rencontre la proie, il la mange, et la proie, morte, disparaît de l’écran. La troisième et dernière fonctionnalité est l’évolution dans le temps des ressources, la plante poussant et l’eau s’évaporant. Le programme principal se trouvera finalement constitué de tous les objets du problème, et ensuite de trois grandes procédures : « déplacement », « ressourcement » et « évolution des ressources ». La première d’entre elles fait appel à une nouvelle procédure de repérage entre les objets. Cette décomposition fonctionnelle est représentée dans la figure 3-2.

62

L’orienté objet

Conception Conception procédurale Le déplacement porte sur tous les objets, le repérage les confronte deux à deux. Le ressourcement porte également sur tous les objets. L’évolution des ressources ne porte que sur deux des objets. On comprend par cet exemple comment la pratique procédurale découpe l’analyse du problème en de grandes fonctions, imbriquées, et portant, toutes, sur l’essentiel des données du problème. En « procédural », les grandes fonctions accomplies par le logiciel, en s’imbriquant l’une dans l’autre, sont la voie de la modularité et de l’évolution de toute l’approche. On perçoit aisément, tant par le partage collectif des objets que par l’interpénétration des fonctions, qu’il est plus difficile de parvenir à une décomposition naturelle du problème en des modules relativement indépendants.

Conception objet Pour sa part, la pratique orientée objet cherche d’abord à identifier les acteurs du problème et à les transformer en classe, regroupant leurs caractéristiques structurelles et comportementales. Les acteurs ne se limitent pas à exister structurellement, ils se remarquent surtout par le comportement adopté, par ce qu’ils font, pour eux et pour les autres. Ce ne sont plus les grandes fonctions qui guident la construction modulaire du logiciel, mais bien les classes/acteurs eux-mêmes. Les acteurs ici sautent aux yeux. Il s’agira de la proie, du prédateur, de l’eau et de la plante. Une fois que l’on a établi les attributs de chacun, la grande différence avec la démarche précédente consistera à réaliser une nouvelle analyse fonctionnelle, mais particularisée à chaque acteur, cette fois. Que font, pris individuellement, la proie, le prédateur, l’eau et la plante ? Commençons par les plus simples. La plante pousse et peut diminuer sa quantité, l’eau s’évapore et peut également diminuer sa quantité. La proie peut se déplacer, et doit pour cela repérer les autres objets. À cette fin, elle utilisera, comme le prédateur, une nouvelle classe vision, constituée d’une longueur, et à même de repérer quelque chose dans son champ. Mais la proie se devra d’interagir avec les autres classes. La proie peut boire l’eau et manger la plante. L’interaction avec les autres classes se poursuit. Le prédateur, à son tour, se déplacera en fonction des cibles, et peut boire l’eau et manger la proie. Les liens entre objets et méthodes sont maintenant représentés comme dans la figure 3-3.

Impacts de l’orientation objet Les acteurs du scénario Ici, le découpage logiciel s’effectue à partir des grands acteurs de la situation, et non plus des grandes activités propres à la situation. Un avantage évident est que le premier découpage apparaît bien plus naturel à mettre en œuvre que le second et, comme vous le savez sans doute, si vous chassez le naturel, il revient à l’OO. Il est plus intuitif de séparer le programme à réaliser en ces quatre acteurs qu’en ses trois grandes activités. Vous ne percevez pas la jungle comme la réunion de trois grandes activités : migratoire, évolutive et alimentaire, vous la percevez, d’abord et avant tout, comme un regroupement d’animaux et de végétaux. Un autre avantage, que nous étaierons par la suite, est que, dans l’approche procédurale, toutes les activités impliquent tous les objets. Cela a pour effet d’accroître les difficultés de maintenance et de mise à jour du logiciel. Changez quoi que ce soit dans la description d’un objet, et vous risquez d’avoir à ré-éplucher l’entièreté du code. Au contraire, dans l’approche OO, si vous changez la description structurelle de l’eau, au pire, c’est la seule méthode évaporer qu’il faudra ré-examiner.

Du faire savoir au savoir-faire… du procédural à l’OO CHAPITRE 3

63

Figure 3-3

Dans l’approche OO, le découpage du logiciel se fait entre les classes qui regroupent les descriptions structurelles avec les activités les concernant. Les classes interagissent entre elles.

Indépendance de développement et dépendance fonctionnelle Le renforcement de l’indépendance entre les classes est une volonté majeure de la programmation OO, et nous reviendrons largement sur ce point dans les prochains chapitres. Cependant, il est important de distinguer d’emblée l’indépendance dans le développement logiciel des classes de la dépendance fonctionnelle entre ces mêmes classes, et qui reste la base de l’OO. Nous verrons dans les prochains chapitres qu’en encapsulant les classes, on favorise leur développement de manière indépendante bien que, lors de leur passage à l’action, ces classes soient fonctionnellement dépendantes. Dépendance fonctionnelle versus indépendance logicielle Alors que l’exécution d’un programme OO repose pour l’essentiel sur un jeu d’interaction entre classes dépendantes, tout est syntaxiquement mis en œuvre lors du développement logiciel pour maintenir une grande indépendance entre les classes. Cette indépendance au cours du développement favorise tant la répartition des tâches entre les programmeurs que la stabilité des codes durant leur maintenance, leur réexploitation dans des contextes différents et leur évolution.

Ainsi, la dépendance entre les classes, quand dépendance fonctionnelle il y a, par exemple ici, entre la classe prédateur et la classe proie, se réalise directement entre les deux classes en question, sans un détour obligé par un module logiciel, plus global, les regroupant en son sein. Le prédateur devra repérer la proie, puis la rattraper pour finalement la dévorer. Il le fera en activant un certain nombre de ses méthodes, qui, de leur côté, nécessiteront de lancer l’exécution de méthodes directement sur la proie. De manière semblable,

64

L’orienté objet

lorsque la proie boira l’eau, elle activera pour ce faire la méthode de la classe Eau, responsable de sa diminution de quantité. En substance, les dépendances entre classes se pensent au coup par coup et deux à deux, et ne sont pas noyées dans la mise en commun de toutes les classes dans les modules d’activité logicielle. Cela conduit à une conception de la programmation sous la forme d’un ensemble de couples client-fournisseur (ou serveur), dans laquelle toute classe sera tour à tour client et fournisseur d’une ou de plusieurs autres. Une conception où les classes se rendent mutuellement des services, ou se délèguent mutuellement des responsabilités, pointe à l’horizon. Une conception bien plus modulaire que la précédente, car le monde contient plus d’acteurs que de fonctions possibles. Les grandes fonctions génériques sont redéfinies pour chaque acteur.

Petite allusion (anticipée) à l’héritage Ce dernier point conduira très naturellement à la pratique de l’héritage et du polymorphisme, pratique dont la non-invocation ici, pour la programmation du petit écosystème, nous amène à différer de quelques chapitres sa modélisation complète en orienté objet (nous la reprendrons au chapitre 11). En effet, si vous êtes déjà passés sur les fonts baptismaux de l’OO, le fait que cette simulation soit propice à une mise en pratique des mécanismes d’héritage ne vous aura pas échappé. Du moins nous l’espérons, sinon, point de regret, ce livre était un investissement nécessaire. Rassurez-vous, cela ne nous a pas échappé non plus et, dans un prochain chapitre, nous reviendrons sur cet écosystème, en multipliant les objets qui le constituent, mais surtout en rajoutant quelques superclasses du côté des animaux et des ressources.

La collaboration des classes deux à deux Un ensemble de classes, agissant par paire et par envoi de messages, ici encore, cela permet de favoriser l’éclatement du programme, en forçant autant que faire se peut les classes à devenir indépendantes entre elles, car, deux par deux, c’est toujours mieux que toutes avec toutes. Plus le programme est décomposable, plus facile sera l’attribution de ses modules à différents programmeurs, et plus réduit sera l’impact provoqué par le changement d’un de ses modules sur les autres. Quoi de plus naturel, dès lors, que de décomposer un logiciel, en collant au mieux à la manière dont notre appareil perceptif et nos facultés cognitives découpent le monde. Une perception qui, pour l’essentiel, sépare les objets, et qui, pendant quelques minutes (au parloir), leur permet de s’échanger quelques messages. OO comparé au procédural en performance et temps calcul Il est parfaitement incorrect de clamer haut et fort que les performances en consommation des ressources informatiques (temps calcul et mémoire) des programmes OO sont supérieures en général à celles de programmes procéduraux remplissant les mêmes tâches. Tout concourt à faire des programmes OO de grands consommateurs de mémoire (prolifération des objets, distribués n’importe où dans la mémoire violant les principes de « localité » informatique) et de temps calcul (retrouver à l’exécution les objets et les méthodes dans la mémoire, puis traduire, au dernier moment, les méthodes dans leur forme exécutable sans parler du garbage collector et autres « casseur » de performance). Les seuls temps et ressources réellement épargnés sont ceux des programmeurs, tant lors du développement que lors de la maintenance et de l’évolution de leur code. L’OO considère simplement, à juste titre nous semble-t-il, que le temps programmeur est plus précieux que le temps machine.

4 Ici Londres : les objets parlent aux objets Ce chapitre illustre et décrit le mécanisme d’envoi de messages qui est à la base de l’interaction entre les objets. Cette interaction exige que les classes dont sont issus ces objets entrent dans un rapport de composition, d’association ou de dépendance. Ces messages peuvent s’enclencher de manière récursive.

Sommaire : Envoi de messages — Composition, association et dépendance entre classes — De la sévérité du compilateur — Réaction en chaîne d’envois de messages

Candidus — Alors, y’en a plus que pour les objets, les procédures passent donc à la trappe, c’est bien ça ? Doctus — Pas tout à fait. Chaque objet a ses voyants et ses boutons : les voyants affichent la valeur des données à chaque instant ; les boutons actionnent ses méthodes. Ces méthodes ne sont pas autre chose que nos anciennes procédures. Cand. — Ainsi, la nourrice incite notre bébé à activer les bons boutons et c’est lui qui déclenche la suite des événements ? Doc. — Le déclenchement des méthodes d’un objet est effectivement conditionné par la disponibilité des boutons de commande correspondants. Mais nous aurons également affaire à quelques mécanismes plus subtils. Les jouets les plus complexes contiendront des mécanismes internes que le fabricant du jouet n’a pas mis à portée de main. Ils ne concernent que le fonctionnement intime de notre objet. Par ailleurs, certains de nos objets peuvent être combinés de telle sorte que le fonctionnement de l’un en mette un ou plusieurs autres en action. Ig — On aurait donc aussi des interfaces apparentes pour les connexions entre objets. Ne deviennent-ils pas complètement dépendants les uns des autres ? Doc. — Oui, on aboutit à un circuit de dépendance. Ce qui amène la question suivante : comment allons-nous nous assurer du bon cloisonnement entre les différents objets sans nous interdire de les combiner quand c’est possible ? Cand. — Hm… Tu voudrais faire des usines à gaz sans trop de tuyaux quoi !

66

L’orienté objet

Envois de messages David A. Taylor : Object Technology: A Manager’s Guide (Addison-Wesley) Cet ouvrage existe en français sous le titre Technologie orienté objet chez Vuibert. C’est une excellente introduction à l’orienté objet, qui réussit sans aucun approfondissement technique, à communiquer tout l’esprit de la conception et de la programmation OO. L’ouvrage est agrémenté d’un ensemble de dessins originaux, spirituels et surtout didactiques. La biologie et les systèmes complexes y sont largement à l’honneur (ce qui, cela va sans dire, n’est pas pour nous déplaire). C’est notre porte d’entrée favorite dans le monde de l’OO, construite par un auteur enthousiaste, cultivé, s’adressant aux néophytes et à tous ceux qui, bien que concernés par l’OO, ne mettront peut-être jamais les mains dans du code. Non seulement les mécanismes clefs de l’OO y sont abordés, mais l’auteur s’attaque avec la même superficialité brillante à la problématique des objets distribués et de leur stockage dans les bases de données objet ou objet-relationnelle (voir chapitre 19).

Dans les chapitres précédents, nous avons discuté de la nécessité de faire interagir l’objet feu de signalisation avec l’objet voiture, quand le passage au vert du premier déclenche le démarrage du second. De même, nous avons retrouvé un type semblable d’interaction quand le lion, s’abreuvant, provoque la diminution de la quantité d’eau. Le lion ne peut directement diminuer la quantité d’eau, quelle que soit l’ampleur de sa soif, qu’il meure ou non ce soir. Il le fera par un appel indirect à la méthode, responsable pour l’eau, de la diminution de sa quantité. L’eau gère sa quantité, pas le lion, qui, lui, a déjà fort à faire avec la gestion de son énergie et de ses déplacements. Les objets interagissent, et comme tout ce qu’ils font doit être prévu dans leurs classes, celles-ci se doivent d’interagir également. C’est cette interaction entre objets, lorsqu’un d’entre eux demande à l’autre d’exécuter une méthode, qui constitue le mécanisme clé et récurrent de l’exécution d’un programme OO. Un tel programme n’est finalement qu’une longue et barbante litanie d’envois de messages entre les objets agrémentés ici et là de quelques mécanismes procéduraux (tests conditionnels, boucles…). Nous allons, à l’aide d’un exemple minimal de programmation, illustrer ce principe de communication entre deux objets. Supposons un premier objet o1, instance d’une classe O1, contenant la méthode jeTravaillePourO1() , et un deuxième objet o2, instance d’une classe O2, contenant la méthode jeTravaillePourO2(). Dans le logiciel, o1 interagira avec o2, si la méthode jeTravaillePourO1() contient une instruction comme: o2.jeTravaillePourO2(). C’est au moment précis de l’exécution de cette instruction que l’objet o1 interférera avec l’objet o2. Mais, pour que o1 puisse exécuter quoi que ce soit, il faudra d’abord déclencher la méthode jeTravaillePourO1()sur o1. Nous supposerons que la méthode main s’en charge et débute l’exécution du programme en déclenchant la méthode jeTravaillePourO1() sur l’objet o1. Le reste suivra, comme indiqué à la figure 4-1. Figure 4-1

L’objet o1 parle à l’objet o2.

Ici Londres : les objets parlent aux objets CHAPITRE 4

67

Dans ce diagramme (il s’agit en fait d’un diagramme de séquence UML que nous préciserons au chapitre 10 mais nous pensons qu’il est compréhensible en l’état), le petit bonhomme fait office de main, en envoyant le premier message jeTravaillePourO1() sur l’objet o1. Par la suite, nous voyons que l’objet o1 interrompt l’exécution de sa méthode, le temps pour o2 d’exécuter la sienne. Finalement, le programme reprendra normalement son cours, là où il l’a abandonné, en redonnant la main à o1. L’envoi de message s’accompagne d’un passage de relais de l’objet o1 à l’objet o2, relais qui sera restitué à o1 une fois qu’o2 en aura terminé avec sa méthode.

Association de classes Nous avons déjà rencontré ce chien de garde sévère qu’est le compilateur, et qui ne laisse rien passer, si ce que font les objets n’a pas été prévu dans leur classe. Ainsi, si o1 déclenche la méthode jeTravaillePourO2() sur l’objet o2, c’est que la classe responsable de o1 sait que cet objet o2 est à même de pouvoir exécuter cette méthode. Figure 4-2

Les deux classes O1 et O2 en interaction par un lien fort et permanent dit « d’association ». Les messages sont envoyés de la classe O1 vers la classe O2.

Pour ce faire, la classe O1 se doit d’être informée, quelque part dans son code, sur le type de l’objet o2, c’està-dire la classe O2. Une première manière pour la classe O1 de connaître la classe O2 est celle indiquée par la figure 4-2 (il s’agit à nouveau d’un petit diagramme UML, cette fois de classe, que nous préciserons aussi au chapitre 10, mais nous pensons là encore qu’il est très compréhensible en l’état). On dira dans ce cas que les deux classes sont associées, et que la connaissance de O2 devient une donnée structurelle à part entière de la classe O1. En langage de programmation, O2 devient purement et simplement le type d’un attribut de O1, comme dans le petit code de la classe O1 qui suit : class O1 { O2 lienO2 ; /*la classe O2 type un attribut de la classe O1 */ void jeTravaillePourO1() { lienO2.jeTravaillePourO2() ; } }

Cela peut être le cas si les deux objets qui interagissent entrent dans une relation de composition. Si telle est leur manière d’être ensemble, chaque objet de la classe O1 contiendra « physiquement » un objet de la classe O2, et ces deux objets deviendront alors indéfectiblement liés dans la vie comme dans la mort. Mais cela peut être plus simplement le cas si o1 désire pouvoir, partout dans son code (à l’intérieur de toutes ses méthodes), envoyer des messages à o2. Comme tout autre attribut de type « primitif » (entier, réel, booléens…), cet attribut lienO2 devient accessible dans l’entièreté de la classe. En cela, on peut dire que cet attribut d’un type un peu particulier fait de la liaison entre les deux classes une vraie propriété structurelle de la première. Il s’agit en fait d’une espèce nouvelle d’attribut dit « attribut référent », typé, non plus par un type primitif, mais bien par la classe qu’il réfère. Dans l’espace mémoire réservé à o1, l’attribut lienO2 contiendra, comme indiqué dans la figure 4-3, l’adresse de l’objet o2.

68

L’orienté objet

Figure 4-3

Comment o1 possède l’adresse de o2.

Cela permettra à o1 de connaître parfaitement l’adresse de son destinataire, ce qui est souhaitable lorsqu’on désire lui faire parvenir un message. Cette notion de message prend tout son sens en présence de ces deux acteurs essentiels que sont l’objet expéditeur et l’objet destinataire. Comme conséquence d’une association dirigée entre la classe O1 et la classe O2, tout objet de type O1 sera lié à un objet de type O2, avec lequel il pourra communiquer à loisir, indépendamment de son état, et quelles que soient les activités entreprises. Le compilateur, en compilant O1, devra être informé de l’emplacement physique d’O2, tout comme à l’exécution. Association de classes La manière privilégiée pour permettre à deux classes de communiquer par envoi de message consiste à rajouter aux attributs primitifs de la première un attribut référent du type de la seconde. La classe peut donc, tout à la fois, contenir des attributs et se constituer en nouveau type d’attribut. C’est grâce à ce mécanisme de typage particulier que, partout dans son code, la première classe pourra faire appel aux méthodes disponibles de la seconde.

Dépendance de classes Mais il existe d’autres manières, non persistantes cette fois, pour la classe O1 de connaître le type O2. Comme dans le code qui suit, la méthode jeTravaillePourO1(O2 lienO2) pourrait recevoir comme argument l’objet o2, sur lequel il est possible de déclencher la méthode jeTravaillePourO2(). class O1 { void jeTravaillePourO1(O2 lienO2) { lienO2.jeTravaillePourO2() ; } }

Le compilateur acceptera le message car le destinataire est bien typé par la classe O2. Cependant, dans un cas semblable, le lien entre O1 et O2 n’aura d’existence que le temps d’exécution de la méthode jeTravaillePourO1(), et on parlera, entre les deux classes, plutôt que d’un lien d’association, d’un lien de dépendance, plus faible et surtout passager (voir figure 4-4).

Ici Londres : les objets parlent aux objets CHAPITRE 4

69

Figure 4-4

Les deux classes O1 et O2 en interaction par un lien faible et temporaire dit de « dépendance ».

La raison en est que, lors de l’appel de toute méthode, se crée un espace de mémoire temporaire, dans lequel sont stockés les arguments transmis à la méthode. À la fin de l’exécution de la méthode, cet espace est perdu et restitué au programme, en conséquence de quoi le lien entre les deux objets s’interrompt immédiatement. Les deux classes ne se connaissent dès lors que durant le temps d’exécution de la méthode. Un lien de dépendance est maintenu entre les deux classes, car toute modification de la classe qui fournit la méthode à utiliser pourrait entraîner une modification de la classe qui fait appel à cette méthode. Il s’agit à proprement parler plutôt d’une dépendance de type logicielle, car elle reste au niveau de l’écriture logicielle des deux classes, alors que le lien d’association va bien au-delà de cette dépendance, et représente une véritable connexion structurelle et fonctionnelle entre ces deux classes. Une autre liaison passagère pourrait se produire, si, comme dans le code ci-après, la méthode jeTravaillePourO1() décide, tout de go, de créer l’objet lienO2, le temps de l’envoi du message. class O1 { void jeTravaillePourO1() { O2 lienO2 = new O2(); lienO2.jeTravaillePourO2() ; } }

Au sortir de la méthode, l’objet lienO2 sera irrémédiablement perdu, de même que cette liaison passagère qui, là encore, n’aura duré que le temps d’exécution de la méthode jeTravaillePourO1(). Alors que, dans la première dépendance, c’est la liaison entre les deux objets qui s’interrompait à la fin de la méthode, ici l’objet destinataire du message disparaîtra également à la fin de cette méthode. Autant que faire se peut, on privilégiera entre les classes des liens de type fort, d’association, faisant des relations entre les classes une donnée structurelle de chacune d’entre elles. Il vaut mieux, dès lors qu’elles envisagent la moindre communication, que les classes se connaissent, non pas intimement, comme nous le verrons, mais suffisamment, pour échanger quelques bribes de conversation. Les fichiers contenant les classes en interaction, et pour autant qu’on informe les phases de compilation et d’exécution de l’emplacement de ces fichiers, s’en trouveront automatiquement liés également. De même, ce lien structurel entre les classes et leurs instances sera préservé lorsque ces objets seront sauvegardés de manière permanente. Y compris sur le disque dur, les objets posséderont parmi leurs attributs des référents connaissant l’adresse physique sur le disque des objets avec lesquels ils sont en relation (comme nous le verrons plus en détail au chapitre 19). Communication possible entre objets Deux objets pourront communiquer si les deux classes correspondantes possèdent entre elles une liaison de type composition, association ou dépendance, la force et la durée de la liaison allant décroissant avec le type de liaison. La communication sera dans les deux premiers cas possible, quelle que soit l’activité entreprise par le premier objet, alors que dans le troisième cas elle se déroulera uniquement durant l’exécution des seules méthodes du premier objet, qui recevront de façon temporaire un référent du second.

70

L’orienté objet

Réaction en chaîne de messages Finalement, il sera très fréquent que l’objet o2 lui-même, durant l’exécution de sa méthode jeTravaillePourO2(), envoie un message vers un objet o3, qui lui-même enverra un message vers un objet o4, etc. On assiste donc, comme dans la figure 4-5, à une succession d’envois de messages en cascade. Toutes les méthodes successivement déclenchées seront, dans une approche séquentielle traditionnelle (où seule une méthode à la fois peut requérir le processeur – nous évoquerons d’autres approches, où plusieurs messages peuvent s’exécuter en parallèle, au chapitre 17), imbriquées les unes dans les autres. Le flux d’exécution passera de l’une à l’autre pour, à la fin de l’exécution de la dernière, refaire le chemin dans le sens inverse, et achever successivement toutes les méthodes enclenchées. Figure 4-5

Un envoi de messages en cascade.

Bertrand Meyer compare cela au déploiement successif de toutes les pièces d’un feu d’artifice géant et complexe, résultant directement ou indirectement de l’allumage initial d’une minuscule étincelle. Dans le chapitre 10, nous racontons comment la réalisation et les conséquences d’un but lors d’une simulation orientée objet d’un match de football peut occasionner la participation et l’interaction de nombreux objets de classe différente : Ballon, Joueurs, Arbitre, Filets, Score, Terrain… sans pour cela que l’écriture du code ne s’en trouve inutilement compliquée. Réaction en chaîne Tout processus d’exécution OO consiste essentiellement en une succession d’envois de messages en cascade, d’objet en objet, messages qui, selon le degré de parallélisme mis en œuvre, seront plus ou moins imbriqués.

Exercices Exercice 4.1 Considérez les deux classes suivantes : Interrupteur et Lampe, telles que, quand l’interrupteur est allumé, la lampe s’allume aussitôt. Réalisez dans un pseudo-code objet le petit programme permettant cette interaction.

Exercice 4.2 Tentez d’envisager dans quelles circonstances il est préférable de privilégier entre deux classes en interaction une relation de type « dépendance » plutôt qu’une relation de type « association ».

Ici Londres : les objets parlent aux objets CHAPITRE 4

71

Exercice 4.3 Réalisez un petit programme exécutant l’envoi de message entre deux objets, et ce lorsque les deux classes dont ils sont issus entretiennent entre elles une relation de type « association » dans le premier cas, et de « dépendance » dans le second.

Exercice 4.4 Écrivez un squelette de code réalisant un envoi de messages en cascade entre trois objets.

Exercice 4.5 Considérez les deux classes suivantes : Souris et Fenêtre, telles que, quand la souris clique sur un point précis de la fenêtre, celle-ci se ferme. Réalisez un pseudo-code objet permettant cette interaction. On favorisera une relation de type dépendance entre les objets concernés.

5 Collaboration entre classes Le but de ce chapitre est de poursuivre la découpe d’une application OO en ses classes, classes jouant, tout à la fois, le rôle de module, fichier et type. Java favorise de façon très pratique cette vision, étendant la relation qui existe entre les classes jusqu’aux étapes de compilation et d’exécution. C++, Python, PHP 5 et C# rendent cette mise en œuvre moins immédiate, les trois premiers en raison d’un souci de compatibilité avec le monde procédural y compris le C, le dernier avec Windows et les exécutables d’avant.

Sommaire : Les classes comme fichiers reliés — La compilation dynamique — Association de classes — Auto-association de classes — Les paquetages Candidus — J’aimerais bien faire une visite chez le fabricant de jouets, histoire de voir comment il se débrouille avec tout ça, comment il construit puis emballe ce qu’il livre à notre bébé… Doctus — L’organisation des fichiers en différentes catégories permet de rechercher les objets comme dans un jeu de pistes. Ils auront le même nom que l’objet qu’ils contiennent. Bébé n’aura alors qu’à utiliser des règles simples pour trouver tout ce qui lui sera nécessaire. Si le fichier contenant l’objet ne se trouve pas là où il devrait être, il ne sera pas content et nous le fera savoir ! Cand. — Ne risque-t-on pas d’avoir des conflits avec des noms d’objets homonymes ? Doc. — Bien sûr que si, c’est pourquoi on ne peut pas avoir plusieurs fichiers portant le même nom dans un même répertoire ! La solution consiste à utiliser les packages, pour que chaque fabricant ait un répertoire d’entrée différent… et le tour est joué ! Candidus — Bébé utilise donc les mêmes règles que le fabricant pour déduire la position des objets. Lorsqu’une pièce du puzzle en appelle une autre, par son nom, il n’y aura qu’à suivre la piste indiquée pour mettre la main dessus. Doc. — Oui, et par-dessus le marché, ces règles simples font que le fabricant peut s’assurer que tous les liens entre les pièces de ses jouets figurent bien dans sa livraison. Cand. — L’informatique… mais c’est très simple ! Doc. — Mieux encore : même les moules qu’utilisent les fabricants pour créer les objets sont organisés ainsi. Ces derniers seront tout simplement déployés dans une structure de répertoires identique, les noms de fichiers ne seront distingués que par un suffixe différent pour le moule et l’objet fini. Cand. — Tous ces liens peuvent nous faire un sacré labyrinthe si un grand nombre d’objets se connaissent l’un l’autre. Lors de la fabrication, si un objet est combiné à un autre qui n’est pas encore sorti de son moule, comment le fabricant s’y prend-il ? Il lui faudra manipuler toute une série de moules en même temps avant d’en avoir terminé avec cet objet composite. Ça semble bien compliqué tout ça !

74

L’orienté objet

Doc. — Il faut tout simplement se rappeler la chose suivante : pour savoir utiliser un objet accessoire, peu importe qu’il soit ou non immédiatement disponible si son mode d’emploi est bien défini. Il nous suffira qu’il soit effectivement livré quand bébé en aura besoin pour faire fonctionner son puzzle animé. Cand. — Ce qui fait qu’un objet peut avoir une existence virtuelle avant d’être vraiment réalisé – ça ressemble à l’histoire de la poule et de l’œuf ! Doc. — Exactement, mais ce n’est plus un problème dans notre cas. Il s’agit simplement de savoir en quoi consistera chaque objet et ce qu’on pourra en attendre pour connaître entièrement ce qu’on doit en savoir. Ce n’est qu’au moment de l’emballage qu’il s’agira de vérifier que tous les liens s’emboîteront correctement.

Pour en finir avec la lutte des classes Nous avons vu dans les chapitres précédents que ce qu’il y a sans doute de plus capital en orienté objet, le Capital, c’est d’en finir avec la lutte des classes. Les classes ne sont pas de simples structures d’accueil d’information, des « inforooms », elles sont à notre service pour la réalisation de certaines tâches mais, plus encore, elles sont chacune au service des autres. Elles le sont, car elles ne peuvent déléguer à aucune autre la responsabilité de l’évolution de leur état. Si les autres nécessitent une modification de l’état d’une classe, elles doivent impérativement s’adresser à elle. On ne le répétera jamais assez, la programmation orientée objet se conçoit, essentiellement, comme une société de classes en interaction, se déléguant mutuellement un ensemble de services. Les classes, lors de leur conception, prévoient ces services, pour que le compilateur s’assure de la cohérence et de la qualité de cette conception, et traduise le tout en une forme exécutable. Par la suite, ces services s’exécuteront en cascade, quand les objets, instances de ces classes, occuperont la mémoire et se référenceront mutuellement, afin de réaliser le programme anticipé par la structure relationnelle des classes. Java , James Gosling et Bill Joy James Gosling, d’origine canadienne et aujourd’hui directeur technologique chez Sun Microsystems, aurait été bien incapable, il y a de cela une douzaine d’années, de pressentir le succès extraordinaire que rencontrerait le langage de programmation sur lequel il travaillait. Le projet « Green », qui donnera naissance à ce langage, langage appelé « Oak » à l’origine, avait pour finalité la programmation de petits appareils électriques et électroniques de grande consommation, comme de l’électroménager, télévision, chaînes hifi, et autres, tous dotés de processeurs de conception différente. Il fallait un langage simple, sûr, portable. La portabilité fut empruntée au langage Pascal, pour lequel Niklaus Wirth avait déjà, à l’époque, imaginé le principe de la machine virtuelle, et conçu celle adaptée au Pascal. Il fallait un langage d’utilisation simple et intuitive, l’OO s’imposait mais, préservant la culture Unix chère à tous les informaticiens, la syntaxe C/ C++ s’imposait. Le projet Green ne rencontra pas le succès escompté, et Java aurait pu sombrer dans les oubliettes des créations informatiques sans le succès soudain et explosif du Web. Le Web était en 1995 uniquement statique, une page web se bornant à apparaître sans aucune possibilité d’interaction. Le navigateur « Mosaic » commençait à largement se répandre, mais sans solutionner l’austère inertie des pages web. Gosling raconte que, lors d’une conférence dédiée à Internet, il présenta un nouveau concept de navigateur sur lequel il travaillait. Il fit d’abord apparaître une molécule dans une page web classique. L’assistance resta de marbre. Mais quelle ne fut la surprise de ce même public lorsque, à l’aide de la souris, il se mit à bouger la molécule, la faisant tourner, la déplaçant en avant et en arrière. Une applet Java s’exécutait dans le navigateur, qui permettait cela. L’engouement pour Java démarra ce même jour J (comme Java). L’avantage essentiel de Java dans le monde d’Internet, un monde informatiquement hétérogène (n’importe quelle plate-forme informatique pouvant devenir un nœud du réseau), était sa capacité à s’exécuter d’une seule et même manière quel que soit le couple processeur/OS sur lequel l’applet s’exécutait. La décision de Netscape de rendre la version 2.0 compatible avec Java (d’y intégrer une machine virtuelle Java) ne fit qu’accroître son succès. On connaît la suite.

Collaboration entre classes CHAPITRE 5

75

Il est étonnant de voir que le succès de ce langage tient au début, non pas à ses qualités syntaxiques propres, mais en ce qu’il propose une solution technique pour rendre les pages Web plus dynamiques et interactives. Depuis, bien d’autres technologies permettent aux pages Web d’exécuter du code en local, telles que VBScript, JavaScript. Java reste pourtant le langage privilégié des applications Internet, de par l’exploitation de sa technologie RMI (voir chapitre 16), dont les objets bénéficient pour se rendre mutuellement des services à travers le Web. Aujourd’hui, force est de constater que le succès de Java a largement dépassé ce premier cadre d’application. Il est devenu, tout comme pour nous ici, le langage idéal pour l’enseignement de l’OO, au détriment du C++, dont la complexité est par trop rébarbative pour une première entrée dans le monde de l’OO. Il s’impose d’ailleurs comme tel dans un nombre croissant de centres d’enseignement de l’informatique (plus de la moitié, dit-on, à ce jour). Il est aussi le langage de programmation le plus utilisé et semble le plus apprécié lorsque des sondages sont effectués auprès des informaticiens. Toutes les qualités reconnues de ce langage et reprises dans le livre blanc de Java : langage simple, OO, portable, distribué, fiable, performant, multithread, dynamique, étaient au départ destinées à son exploitation dans un appareillage varié, fragile, aux capacités informatiques modestes et dont il fallait préserver une grande facilité d’utilisation. Ce sont ces mêmes qualités qui ont fait de ce langage un moyen idéal d’assimiler les principes de l’OO. Même les plus ardents défenseurs du C++, à commencer par Bjarne Stroustrup lui-même (et qui a toujours su qu’il existait dans C++ un petit noyau syntaxique plus compact et plus cohérent qui ne demandait qu’à germer), ont salué avec enthousiasme l’élégance et les qualités de ce langage. Gosling a vu dans la luxuriance syntaxique du C++ et les multiples degrés de liberté qui lui sont inhérents une source d’erreurs et de maladresses. On comprend, vu la cible première, non plus les fiers ordinateurs, mais des appareils ménagers, qu’il ait préféré délester le programmeur d’une part de son contrôle lors de l’exécution du programme (ainsi l’absence de pointeurs explicites et de gestion mémoire manuelle). Si son pari initial, « Green », ne fut pas gagné à l’époque, des projets comme « Jini » tentent de le repositionner en ligne de mire. En effet, qu’est-ce que Jini (évoqué chapitre 19) si ce n’est le développement d’une plate-forme de programmation pour tout type de processeur embarqué dans tout type d’appareil connecté à Internet, ordinateur, caméra, radio ou frigo. Gossling travaille de manière intensive sur l’interconnectivité de ces différents types d’appareils : vous filmez et ce que vous filmez apparaît derechef sur le bac à glaçons de votre réfrigérateur ou sur l’écran de votre télévision. Vous conduisez et êtes informé en ligne de tous les problèmes de trafic via Internet. Il s’investit aussi dans de nouvelles manières de programmer, plus visuelles, et donnant plus d’importance à des représentations graphiques, de type arborescentes, de la structure et du fonctionnement des programmes. Une petite mention supplémentaire pour Bill Joy, co-fondateur, à l’âge de 28 ans, de Sun Microsystems avec Scott McNealy (son patron actuel) et un des acteurs très importants de la mise au point du langage Java (il fut également le créateur de BSD, la version de Berkeley de l’OS Unix). Il y a quelques années de cela, à l’orée du deuxième millénaire, il joua les Cassandre en rédigeant un article extrêmement pessimiste dans le célèbre magazine Wired Magazine, militant en faveur d’un « ralentissement » de la recherche scientifique. Son article, intitulé joyeusement « Pourquoi l’avenir n’a pas besoin de nous », se préoccupe des risques à long terme induits par le développement à tout crin actuel des nanotechnologies, de la génétique et de la robotique. En substance, il craint que de nouveaux artefacts issus de ces développements ne nous dépassent et n’échappent complètement à notre contrôle. C’est pas la « Joy ». Aujourd’hui le langage Java en est à sa sixième version (Java 6), La cinquième version fut surtout remarquée pour l’introduction de modifications de base importantes comme la généricité, les énumérations ou encore de nouveaux types de boucle for que nous traiterons au chapitre 21. Nous ne pourrions terminer cet encart sans vous indiquer une petite liste, loin d’être exhaustive bien sûr, et risquant l’obsolescence à la sortie du livre (si le choix se présente, optez toujours pour la dernière version de ces mêmes ouvrages), de nos références bibliographiques en programmation Java : – Cahier du programmeur Java 1.4 et 5.0, Puybaret, Eyrolles 2004. – Au cœur de Java, : vol. 1 et 2, HORSTMAN et CORNELL, CampusPress. – Comment programmer en Java, DEITTEL et DEITTEL, Goulet, et autres ouvrages Java cosignés par ces deux auteurs. – Beginning Java 3, Ivor HORTON, Wrox. – Thinking in Java 1 & 2, Bruce ECKEL, www.mindview.net/Books/TIJ, Prentice Hall. – Java for Practitioners, John HUNT, Springer Verlag.

76

L’orienté objet

La compilation Java : effet domino L’interaction entre classes, ainsi que la modularisation recommandée de ces classes en fichiers, permettent d’obtenir l’impression d’écran présentée à la figure 5-1, qui correspond à la situation décrite ci-après. Figure 5-1

Deux fichiers Java contenant chacun une classe, et les liens dynamiques qui s’établissent lors de la compilation.

Deux fichiers Java séparés, O1.java et O2.java, contiennent, respectivement, l’un le code de la classe O1, l’autre le code de la classe O2. Dans la fenêtre DOS, vous les voyez apparaître tous deux dans leur répertoire. Ce sont deux fichiers source Java qui portent l’extension « java ». Nous compilons le premier à l’aide de l’instruction de compilation javac : javac O1.java

Automatiquement, deux nouveaux fichiers apparaissent. Non seulement, comme prévu, l’exécutable issu du premier fichier : O1.class, mais également, de manière plus surprenante, la version exécutable du deuxième fichier : O2.class, alors que nous n’avons jamais demandé sa compilation. La raison en est, bien sûr, que le compilateur s’est aperçu que la classe O1 nécessite pour sa réalisation la classe O2, et a, de ce fait, pris l’initiative heureuse de compiler aussi la classe O2. Dès que le compilateur découvre une dépendance entre les classes, il se charge de toutes les compilations nécessaires. Cela tient de la magie, nous direz-vous ? Comment savait-il où trouver le code de la classe O2 ? Il manque un détail clé à l’explication. Nous avons nommé, comme il est classique de le faire en Java, le fichier du même nom que la classe. La classe O2 se trouve dans le fichier O2.java, et le tour est joué, le compilateur sait maintenant comment trouver le code de la classe O2, afin de le relier à O1. Il en sera de même lors de l’exécution. En partant de la seule exécution du fichier contenant la méthode main, toutes les classes dépendantes entre elles seront reliées dynamiquement lors de cette exécution. La découpe et l’association une classe-un fichier découlent naturellement de ce mécanisme de liaison dynamique.

Collaboration entre classes CHAPITRE 5

77

Vous constatez qu’il n’y a point besoin d’effectuer une liaison explicite entre les fichiers qui doivent se connaître mutuellement. Les classes ont besoin l’une de l’autre. En nommant les fichiers par le nom des classes qu’ils contiennent, automatiquement, les fichiers sauront comment se trouver et se lier, tant pendant la phase de compilation que pendant la phase d’exécution. Ce mécanisme de découverte automatique du code de la classe O2, en reprenant le nom de la classe pour le fichier, est propre à Java, et disparaît dans les autres langages de programmation OO, comme C++, C#, Python et PHP 5. Cela n’en reste pas moins une manière de procéder aussi élégante qu’efficace. En Java, la classe, dans sa version exécutable .class, est toujours isolée dans un fichier, le nom du fichier devenant automatiquement le nom de la classe compilée qu’il contient. Même si, au départ, plusieurs classes sont codées dans un seul fichier source, la compilation créera autant de fichiers qu’il y a de classes distinctes. Cette modularisation forcée, mais parfaitement adéquate, disparaît des autres langages, pour des raisons de compatibilité avec les technologies les ayant précédés, souci non partagé par les ingénieurs de Sun, lesquels avaient décidé à l’époque de repartir de zéro. Faire de chaque classe un fichier source séparé devient, de fait, une pratique tendant à se répandre dans tous les langages OO, que ces langages l’encouragent ou non par leur syntaxe et leur fonctionnement propres. Une classe, un fichier Dans sa pratique, et bien plus que les trois autres langages, Java force la séparation des classes en fichiers distincts. Si vous ne le faites pas lors de l’écriture des sources, il le fera pour vous, comme résultat de la compilation de ces sources. En conséquence de quoi, autant le précéder, par une écriture des classes séparée en fichiers. Cette bonne pratique tend à se généraliser à tous les développements OO, quel que soit le langage de programmation utilisé.

En C#, en Python, PHP 5 et en C++ En C#, il est nécessaire, pour que la classe O1 (installée dans le fichier O1.cs) puisse se rattacher à la classe O2 (installée dans le fichier O2.cs), de faire d’abord de la classe O2 une librairie « dll » (dynamic link library, extension qui ne surprendra en rien les habitués de Windows), et ce au moyen de l’instruction suivante : csc /t:library /out:O2.dll O2.cs

Ensuite, il faut compiler le fichier O1.cs, en faisant le lien avec le fichier dll généré précédemment : csc /r:O2.dll O1.cs Fichiers dll Les fichiers portant l’extension .dll sont des fichiers caractéristiques des plates-formes Windows et qui permettent de relier dynamiquement plusieurs fichiers exécutables. C’est la raison pour laquelle, afin de rendre les nouveaux exécutables C# compatibles avec la plate-forme Windows, il faut en faire des fichiers .dll. Dans la plate-forme de développement .Net, ces fichiers .dll permettent de faire le lien entre n’importe quelles classes développées dans n’importe lequel des langages de programmation supportés par .Net (et ils sont nombreux puisqu’on en dénombre vingt-deux).

La situation en Python est telle qu’illustrée dans les deux fichiers qui suivent. Il est nécessaire d’indiquer explicitement dans les premières lignes du fichier O1 où trouver les classes dont il aura besoin.

L’orienté objet

78

Fichier O1.py from O2 import * # rapelle les classes du fichier O2 class O1: __lienO2=O2() def jeTravaillePourO1(self): __lienO2.jeTravaillePourO2(5) Fichier O2.py class O2: def jeTravaillePourO2(self,x): print x

En C++ aussi, il est nécessaire d’inclure dans le fichier O1.cpp, l’instruction d’inclusion du fichier O2.cpp, comme dans le code qui suit : #include "O2.cpp" /* inclusion de la classe dont la nouvelle classe dépend */ class O1 { private: O2* lienO2; public: void jeTravaillePourO1() { lienO2->jeTravaillePourO2(); } };

Nous retrouvons le même type d’inclusion dans la version PHP 5 du même code. Association de classes

Association de classes


lienO2 = new O2(); } public function jeTravaillePourO1() { $this->lienO2->jeTravaillePourO2(); } } $unO1 = new O1(); $unO1->jeTravaillePourO1(); ?>

Collaboration entre classes CHAPITRE 5

79

Avec dans le même répertoire Web, le fichier O2.php. \n"); } } ?>

Dans tout langage, la liaison dynamique entre les classes exige des « liants syntaxiques » supplémentaires et ne se réalise plus implicitement, à l’instar de Java, comme simple résultat de la dénomination des classes et des fichiers.

De l’association unidirectionnelle à l’association bidirectionnelle Une question assez légitime peut être posée, quand on s’aperçoit que la liaison dynamique à la compilation se fait, soit en ordonnant les compilations, comme en C#, à savoir d’abord en compilant le premier fichier dont dépend le second, ensuite le second, soit, comme en Python, PHP 5 et C++, par l’inclusion du premier dans le second. Que se passe-t-il quand les deux classes dépendent mutuellement l’une de l’autre ? Dans le petit diagramme UML suivant, vous pouvez voir la différence entre une association de type directionnelle et une association bidirectionnelle. Ces dernières associations sont très fréquentes dans la conception OO. Prenez, par exemple, l’association entre un employé et un employeur, un joueur de foot et son capitaine, la proie et le prédateur, un ordinateur et son imprimante, etc. L’association entre deux classes est bidirectionnelle quand des messages peuvent être envoyés dans les deux sens. Figure 5-2

Différence entre une association de deux classes de type directionnelle et de type bidirectionnelle.

En Java, cette situation ne change absolument rien à la pratique décrite plus haut. La compilation de l’une des deux classes entraînera automatiquement, dans sa suite, la compilation de l’autre. En C#, comme l’ordre de compilation est déterminé par les liens de dépendances entre les classes, la situation est plus délicate, et la solution la plus immédiate, parmi d’autres, consistera à compiler les fichiers, tous ensemble, et non plus de manière séparée.

80

L’orienté objet

En C++, c’est l’écriture du code qu’il faudra soigner afin de pallier cette situation. Il faudra séparer la déclaration des classes de la description de leur corps. Cette description devra être différée par rapport à la seule déclaration des classes, comme le code ci-dessous en est l’illustration. Fichier O2.cpp #include "iostream.h" class O1; /* juste la déclaration de la classe et rien d’autre */ class O2 { private: O1* lienO1; public: void jeTravaillePourO2(); /* la méthode sera définie plus tard */ };

Fichier O1.cpp #include "iostream.h" #include "O2.cpp" class O2; /* juste la déclaration de la classe et rien d’autre */ class O1 { private: O2* lienO2; public: void jeTravaillePourO1(); /* la méthode sera définie plus tard */ }; /* puis enfin, la description du contenu des méthodes */ void O1::jeTravaillePourO1() { lienO2->jeTravaillePourO2(); } void O2::jeTravaillePourO2() { lienO1->jeTravaillePourO1(); } int main() { cout << "ca marche" << endl; return 0; }

Enfin, comme Python et PHP 5 sont tout deux des langages de script, c’est-à-dire s’exécutant directement, sans phase préalable de compilation, au fur et à mesure que les instructions sont rencontrées, on se rend compte de l’impossilité produite par cette référence (ici importation) circulaire. Lorsque l’exécution de la première classe s’interrompra pour importer la deuxième, et que cette deuxième ne pourra pas non plus s’exécuter faute de la première, on se trouvera coincé dans une situation sans issue. La solution la plus simple est de contourner le problème, de ne réaliser l’inclusion que dans la première classe et prévoir une méthode dans la deuxième pour relier celle-ci à la prémière. Dans l’exemple PHP 5 ci-après, c’est la solution qui est proposée. Notez dans cet exemple que, malgré l’absence de typage dans PHP 5, il est cependant possible, lorsque les arguments de méthode concernent des classes, de le spécifier dans la déclaration. Lors de l’appel, un mauvais passage d’arguments donnera une erreur fatale.

Collaboration entre classes CHAPITRE 5

81

Premier fichier PHP contenant la classe O1 : Association de classes

Association de classes


lienO2 = $unO2; } public function jeTravaillePourO1() { print("je travaille pour O1
\n"); $this->lienO2->jeTravaillePourO2(); // envoi de message vers la classe O2 } public function jeTravailleAussiPourO1() { print("je travaille aussi pour O1
\n"); } } $unO2 = new O2(); $unO1 = new O1($unO2); $unO2->setO1($unO1); $unO1->jeTravaillePourO1(); $unO2->jeTravaillePourO2(); ?>

Deuxième fichier PHP O2-2.php contenant la classe O2 : \n"); $this->lienO1->jetravailleAussiPourO1(); // envoi de message vers la classe O1 } public function setO1(O1 $unO1) { // la méthode réalise l’association avec la première classe $this->lienO1 = $unO1; } } ?>

82

L’orienté objet

Auto-association Une dernière chose : les classes peuvent bien évidemment s’adresser à elles-mêmes, en devenant les destinataires de leur propre message, comme montré dans le diagramme ci-après. Figure 5-3

La classe en interaction avec elle-même.

Lors de l’exécution d’une de ses méthodes, un objet peut demander à une autre de ses méthodes de s’exécuter. Il s’agit du mécanisme classique d’appel imbriqué de méthodes, comme indiqué dans le petit code suivant, dans lequel le corps de la méthode faireQuelqueChose() intègre un appel à exécution de la méthode faireAutreChose(). faireQuelqueChose(int a) { … faireAutreChose() ; } Appel imbriqué de méthodes On imbrique des appels de méthodes l’un dans l’autre quand l’approche procédurale se rappelle à notre bon souvenir. Force est de constater que l’OO ne se départ pas vraiment du procédural. L’intérieur de toutes les méthodes est, de fait, programmé en mode procédural comme une succession d’instructions classiques, assignation, boucle, condition… L’OO vous incite principalement à penser différemment la manière de répartir le travail entre les méthodes et la façon dont les méthodes s’associeront aux données qu’elles manipulent, mais ces manipulations restent entièrement de type procédural. Ces imbrications entre macrofonctions sont la base de la programmation procédurale, ici nous les retrouvons à une échelle réduite, car les fonctions auront préalablement été redistribuées entre les classes.

Plus généralement, tout objet d’une classe donnée peut contenir dans le corps d’une de ses méthodes un appel de méthode à exécuter sur un autre objet, mais toujours de la même classe, comme dans le code qui suit : class O1{ O1 unAutreO1 ; faireQuelqueChose(){ unAutreO1.faireAutreChose(); } }

Un joueur de football peut faire une passe à un autre joueur. Le prédateur peut partir à la recherche d’un autre prédateur.

Collaboration entre classes CHAPITRE 5

83

Les diagrammes qui suivent montrent la différence entre les deux cas, différence qui se marque dans le destinataire du message, dans le premier cas, l’objet lui-même, dans le second cas, un autre objet, mais de la même classe. Figure 5-4

Envoi de message à l’objet.

Figure 5-5

Envoi de message à un autre objet, de la même classe.

Alors qu’il s’agira, contrairement au cas précédent, d’un transfert de message entre deux objets différents, du point de vue des classes, il s’agira d’une interaction entre une classe et elle-même. Cela se produira dans notre petit exemple de l’écosystème, si les prédateurs ou les proies veulent communiquer, entre prédateurs et entre proies.

Package et namespace Comme nous l’avons vu dans le chapitre 2, au même titre que vous organisez vos fichiers dans une structure hiérarchisée de répertoires, vous organiserez vos classes dans une structure isomorphe de paquetage (package en Java et Python, namespace en C++, C# et PHP 5). Il s’agit là, uniquement, d’un mécanisme de nommage

84

L’orienté objet

de classes, comme les répertoires le sont pour les fichiers, et qui vous permet, tout à la fois, de regrouper vos classes partageant un même domaine sémantique, et de donner un nom identique à deux classes placées dans des packages différents. En Java, le système de nommage des classes doit s’accompagner de l’emplacement des fichiers dans les répertoires correspondants. Supposez par exemple que la classe O2 nécessaire à l’exécution de la classe O1 soit dans un paquetage O2, comme indiqué dans le code qui suit. Tant le code source de la classe O2 que son exécutable devront se trouver dans le répertoire O2. La classe O1, elle, se trouvera juste un niveau au-dessus. Fichier O2.java /* Ce fichier ainsi que le fichier .class devront être placés dans le répertoire O2 */ package O2; public class O2 { public void jeTravaillePourO2() { System.out.println("Je travaille pour O2"); } }

Fichier O1.java /* Ce fichier ainsi que le fichier .class devront être placés dans le répertoire juste au-dessus d’O2 */ import O2.*; /* Pour rappeler les classes du répertoire O2 */ public class O1 { public void jeTravaillePourO1(){} public static void main(String[] args) { O2 unO2 = new O2(); /* Il ne s’agit là que d’un système de nommage des classes En lieu et place de l’import, on pourrait renommer la classe O2 par O2.O2.*/ unO2.jeTravaillePourO2(); } } En C#, en revanche, tout comme en C++ et PHP 5, le namespace n’est qu’un système de nommage hiérarchisé de classes, sans nécessaire correspondance avec l’emplacement des fichiers dans les répertoires. Nommage des fichiers et nommage des classes deviennent donc indépendants. Ainsi, les deux fichiers C# qui suivent peuvent parfaitement se retrouver dans un même répertoire, tant dans leur version source que compilée, malgré la présence de namespace dans l’un et de using dans l’autre. Fichier O2.cs /* Le namespace et la classe doivent différer dans leur nom */ namespace O22 { public class O2 { public void jeTravaillePourO2() { System.Console.WriteLine("Je travaille pour O2"); } } }

Collaboration entre classes CHAPITRE 5

85

Fichier O1.cs using O22; public class O1 { public void jeTravaillePourO1(){} public static void Main() { O2 unO2 = new O2();/* Il ne s’agit là que d’un système de nommage des classes En lieu et place du using, on pourrait renommer la classe O2 par O22.O2 unO2.jeTravaillePourO2(); } }

En débutant un programme Java par l’instruction import …. et en C# par using …, vous signalez que, tant durant la compilation que l’exécution du programme, les classes qui y sont référées, si elles sont absentes du répertoire courant, sont à rechercher dans les paquetages mentionnés dans ces deux instructions. import en Java et using en C# Vos classes étant regroupées en paquetages imbriqués, il est indispensable, lors de leur utilisation, soit de spécifier leur nom complet : « Paquetage.classe » (d’abord le nom du paquetage puis le nom de la classe), soit d’indiquer, au début du code, le paquetage qui doit être utilisé afin de retrouver les classes exploitées dans le fichier. Cela se fait par l’addition, au début des codes, de l’instruction import en Java et using en C#.

Finalement en Python, un mécanisme de paquetage est également possible, comme en Java, en totale correspondance avec les répertoires. Supposons le fichier O2.py contenant la classe O2 et placée dans le répertoire O2. En matière de classe, il s’agira donc du paquetage O2. Pour que la classe O1 puisse disposer des classes contenues dans le paquetage, il suffit d’inclure la commande from O2.O2 import * en début de fichier. Il faudra également, truc et ficelle, inclure un fichier vide et dénommé __init__.py dans le répertoire O2 en question. Fichier O2.py # placé dans le répertoire O2 class O2: def jeTravaillePourO2(self,x): print x }

Fichier O1.py from O2.O2 import * class O1: __lienO2=O2() def jeTravaillePourO1(self): __lienO2.jeTravaillePourO2(5) print "ca marche"

Finalement, dans tous les cas, la représentation UML de cette situation (la classe O1 associée à la classe O2 se trouvant dans le paquetage O22) est illustrée par la figure 5-6.

86

L’orienté objet

Figure 5-6

La classe 01 envoie un message à la classe 02 placée dans un paquetage 022.

Exercices Exercice 5.1 Revenez à l’analyse orientée objet du premier exercice du chapitre 1, consistant en une recherche des classes décrivant votre activité favorite. Approfondissez la nature des relations existant entre les classes et prenez soin de différencier des relations d’auto-association, des associations directionnelles ou bidirectionnelles.

Exercice 5.2 Toujours dans la description OO que vous faites de cette activité, réfléchissez à une nouvelle organisation des classes en assemblage. Quelles classes installeriez-vous dans un même assemblage, et quelle structure imbriquée d’assemblage pourriez-vous réaliser ?

Exercice 5.3 Écrivez le code d’une classe s’envoyant un message à elle-même, d’abord lorsque ce message n’implique qu’un seul objet, ensuite lorsque ce message en implique deux.

Exercice 5.4 Écrivez le code d’une classe A qui, lors de l’exécution de sa méthode jeTravaillePourA(), envoie le message jeTravaillePourB() à une classe B. Séparez les deux classes dans deux fichiers distincts et, quel que soit le langage que vous utilisez, réalisez l’étape de compilation.

Exercice 5.5 Sachant que la classe A est installée dans l’assemblage as1, lui-même installé dans l’assemblage as, quel est le nom complet à donner à votre classe ?

6 Méthodes ou messages ? Ce chapitre aborde de manière plus technique les mécanismes d’envoi de message. Les passages d’argument par valeur ou par référent, qu’il s’agisse de variables de type prédéfini ou de variables objet, sont discutés dans le détail et différenciés dans les cinq langages. La différence entre un message et une méthode est précisée. La notion d’interface et le fait que les messages puissent circuler à travers Internet sont, à ce stade, simplement évoqués.

Sommaire : Passage d’arguments dans les méthodes — Passage par valeur et par référent — Passage d’objets — Méthodes et messages — Introduction aux interfaces et aux objets distribués

Candidus — J’aimerais maintenant savoir ce qui se cache derrière les boutons de commande de nos objets. Je sais bien qu’ils actionnent leurs différentes fonctions mais tu appelles ça des messages. Pourquoi ce nouveau terme, d’ailleurs ? Doctus — Parce qu’il s’agit bien de messages. Même dans le cas des langages procéduraux qui ne connaissent que le seul domaine global d’un programme, tu peux voir les appels de fonctions comme des messages envoyés à un objet unique que constitue le programme lui-même. Cand. — Alors nos objets prennent des initiatives ? Ce sont des objets communicants ! Doc. — C’est exact, créer un objet consiste en tout premier lieu à définir son vocabulaire, ce qu’il peut « comprendre », à savoir l’ensemble des messages qu’il peut traiter. On appelle ça son interface. Bébé pourra jouer avec certains boutons et les objets eux-mêmes joueront les uns avec les autres de la même façon. Cand. — Si j’ai bien observé, certains boutons messages doivent être actionnés à l’aide d’accessoires. Il me semble y reconnaître les paramètres de nos fonctions procédurales. Que se passe-t-il exactement quand un message est envoyé à un objet ? Doc. — Tout d’abord, un message écrit par une main humaine sur du papier contient des informations qui ne sont que la copie d’une partie de ce qui se trouve dans le cerveau de son auteur. Cand. — C’est malin ! J’aurais pu trouver ça tout seul… Doc. — …je continue ! Un message peut aussi contenir le moyen d’accéder à d’autres informations que celles qu’il contient…

88

L’orienté objet

Cand. — Une clé par exemple ? Doc. — Exactement. La dénomination adoptée pour cette clé est référent, qui peut n’être qu’une adresse. Cand. — Je vois où tu veux en venir ! L’objet appelant a donc le choix entre donner une copie de ses informations et donner le moyen d’y accéder. Est-ce bien ça ? Doc. — C’est bien ça, la différence étant que l’accès à une source d’informations permet d’en changer la valeur, tandis qu’une simple copie ne le permet pas. Cand. — Et quelle est la distinction entre message et méthode ? Doc. — On appelle méthode ce qu’un objet exécute lorsqu’il reçoit le message associé. Les envois de message, eux, correspondent aux appels de fonction. Cand. — Je pense que je vais retrouver mes marques en passant à l’OO. Mes fonctions, leurs arguments et leur valeur de retour éventuelle... Il ne s’agit en fait que d’un pas supplémentaire vers la distribution des tâches. Doc. — Attention ! Le choix de passer une copie ou une référence n’est pas disponible de manière identique dans tous les langages. Un argument de message peut être lui-même constitué par un objet. Je te suggère de réfléchir à ce que cela implique quant aux possibilités de concevoir des systèmes beaucoup plus ouverts à la créativité que ce que nous permettent les langages procéduraux.

Passage d’arguments prédéfinis dans les messages Pour envoyer un bon message, procédez avec méthode. En effet, les objets se parlent par envois de message, lorsqu’un objet passe la main à un autre, afin qu’une méthode s’exécute sur ce dernier. Lors de son exécution, comme en programmation classique, la méthode peut recevoir des arguments. Ces arguments seront utilisés dans son corps d’instruction. Ces arguments, tout comme lorsqu’on déclare une fonction mathématique f(x), permettent d’affiner ou de calibrer le comportement de la méthode, en fonction de la valeur de l’argument passé. Considérons à nouveau la déclaration de la méthode jeTravaillePourO2(int x) de la classe O2, mais qui, cette fois, prévoit de recevoir un argument de type entier : « x ». Cette méthode peut être activée par un message, comme dans le petit exemple suivant : class O1 { O2 lienO2 ; void jeTravaillePourO1() { lienO2.jeTravaillePourO2(5) ; } }

Rien de particulier n’est à signaler. Ajoutons maintenant, que la méthode jeTravaillePourO2(int x) modifie l’argument qu’elle reçoit, comme dans le petit code Java ci-après. Tâchez, sans regarder le résultat, de prévoir ce qui sera produit à l’écran.

En Java class O2 { void jeTravaillePourO2(int x) { x++; /* incrément de l’argument */ System.out.println("la valeur de la variable x est: " + x); } }

Méthodes ou messages ? CHAPITRE 6

89

public class O1 { O2 lienO2; void jeTravaillePourO1() { int b = 6; lienO2 = new O2(); lienO2.jeTravaillePourO2(b); System.out.println("la valeur de la variable b est: " + b); } public static void main(String[] args) { O1 unO1 = new O1(); unO1.jeTravaillePourO1(); } }

Résultat la valeur de la variable x est : 7 la valeur de la variable b est : 6

Qu’advient-il de la variable locale b, créée dans la méthode jeTravaillePourO1(), et passée comme argument du message jeTravaillePourO2(b)? Sa nouvelle valeur sera-t-elle 7 ? Non, car en général, un passage d’argument s’effectue de manière préférentielle « par valeur ». On entend par là la création d’une variable temporaire x, recopiée de l’originale, qui recevra, le temps de l’exécution de la méthode, la même valeur que la valeur transmise : 6, et disparaîtra à la fin de cette exécution. La variable de départ b est laissée complètement inchangée, seule la copie est affectée. L’exécution de la méthode s’accompagne, en fait, d’une petite mémoire pile (dernier entré premier sorti), dont le temps de vie est celui de cette exécution, pas une seconde de plus. Alors que c’est l’unique type de passage permis par Java, d’autres langages ont enrichi leur offre. Lisez avec attention le code C# suivant et tentez, là encore, de prédire son résultat.

En C# using System; class O2 { public void jeTravaillePourO2(int x++; Console.WriteLine("la valeur de } public void jeTravaillePourO2(ref x++; Console.WriteLine("la valeur de } } public class O1 { O2 lienO2;

x) { la variable x est: " + x); int x) /* observez bien l’addition du mot-clé ref */ { la variable x est: " + x);

void jeTravaillePourO1() { int b = 6; lienO2 = new O2(); lienO2.jeTravaillePourO2(b); Console.WriteLine("la valeur de la variable b est: " + b); lienO2.jeTravaillePourO2(ref b); /* observez bien l’addition du mot-clé ref */ Console.WriteLine("la valeur de la variable b est: " + b);

L’orienté objet

90

} public static void Main() { O1 unO1 = new O1(); unO1.jeTravaillePourO1(); } }

Résultat la la la la

valeur valeur valeur valeur

de de de de

la la la la

variable variable variable variable

x b x b

est est est est

: : : :

7 6 7 7

Nous avons, dans le code C#, déclaré deux fois la méthode jeTravaillePourO2(int x), la première fois, comme en Java, la seconde fois en spécifiant que nous voulions effectuer le passage d’arguments par référent. Nous utilisons ici le mécanisme de surcharge, discuté dans le chapitre 2, qui permet l’utilisation de deux méthodes différentes, bien que nommées de la même manière. Dans le second cas, ce n’est plus la valeur de la variable que nous passons, mais bien une copie de son référent qui, tout comme le référent d’un objet, contient l’adresse de la variable. En modifiant cette variable, on modifiera cette fois la valeur contenue à cette adresse, en conséquence, la variable de départ elle-même, et non plus une copie de celle-ci.

En C++ C++ vous permet, à l’aide d’une écriture un peu plus déroutante, d’y parvenir également. Afin de comprendre le code présenté ci-après, il faut savoir que, lorsqu’une variable est déclarée comme pointeur : int *x, on autorise l’accès direct à son adresse, adresse contenue dans x. On peut, de surcroît, modifier cette adresse, et faire pointer le pointeur vers un autre espace mémoire. Il suffit d’écrire par exemple x++. C’est un jeu évidemment très dangereux dont la pratique entame la réputation du C++ en matière de sécurité. La valeur pointée par le pointeur, quant à elle, est obtenue en écrivant *x. De même, il est toujours possible d’obtenir l’adresse d’une quelconque variable y, en écrivant, simplement, &y. Mais il ne sera jamais possible d’écrire une instruction comme &y++, qui permettrait de modifier cette adresse. Ce qu’on appelle un référent en C++, pour le différencier d’un pointeur, et indiqué par la présence de &, référera toujours une seule et même adresse. #include "iostream.h" class O2 { public: void jeTravaillePourO2(int x){ x++; cout << "la valeur de la variable x est: " << x << endl; } /* void jeTravaillePourO2(int &x) elle ne peut fonctionner en même temps que la première version ➥ car son appel serait alors ambigu { x++; cout << "la valeur de la variable x est: " << x << endl; }*/ void jeTravaillePourO2(int *x) /* on peut, par cette nouvelle signature, surcharger la première ➥ version */ {

Méthodes ou messages ? CHAPITRE 6

91

++*x; /* Si vous écrivez *x++, vous serez surpris du résultat, car l'incrément se fera sur l'adresse et non plus la valeur */ cout << "la valeur de la variable x est: " << *x << endl; } }; class O1 { O2 *lienO2; public: void jeTravaillePourO1() { int b = 6; lienO2 = new O2(); lienO2->jeTravaillePourO2(b); /* appelle de manière semblable la première version ou la deuxième ➥ version, d’où l’impossibilité d’une déclaration commune */ cout << "la valeur de la variable b est: " << b << endl; lienO2->jeTravaillePourO2(&b); /* n'appelle que la troisième version */ cout << "la valeur de la variable b est: " << b << endl; } }; int main() { O1 unO1; unO1.jeTravaillePourO1(); return 0; }

Résultat la la la la

valeur valeur valeur valeur

de de de de

la la la la

variable variable variable variable

x b x b

est est est est

: : : :

7 6 7 7

Dans la classe O2, la première version de la méthode jeTravaillePourO2() fonctionne comme en Java, et le passage d’argument se fait par valeur. Deux options sont alors proposées pour effectuer le passage d’argument par référent. La première, celle qui est usuellement recommandée, effectue un réel passage par référent (en utilisant la notation &), car il s’agit bien de l’adresse qui est passée. Mais, comme vous pouvez le voir dans le code, vous ne pouvez utiliser cette seconde version en même temps que la première (celle pour qui le passage d’argument se fait par valeur) car, lors de l’appel, il est impossible de différencier laquelle des deux est à exécuter. La seconde version (en fait la troisième définition de la méthode) utilise, elle, un pointeur pour recevoir l’adresse de la variable. Vous la rencontrerez moins souvent. Ces écritures sont assez laborieuses et sont à l’origine de nombreux maux de têtes et mal-être existentiels dans notre communauté informatique. Les psychanalystes et autres psychiatres, depuis des années, se battaient pour en interdire l’usage. Java et Python les ont entendus. Ils l’ont fait.

En Python En Python, le code qui suit vous permet de comprendre aisément que le seul passage d’argument autorisé est par valeur. On constatera encore l’absence de typage, puisqu’il n’est pas nécessaire de spécifier le type des arguments lors de la définition des méthodes. Au moment de l’appel de la méthode, le type dépendra automatiquement de la valeur transmise. Dernière observation en passant : remarquez combien ce code est plus court

92

L’orienté objet

et plus simple à écrire que son équivalent en Java, surtout du côté du main, 12 lignes pour Python contre 19 en Java. Qui dit mieux ? La brièveté et la simplicité d’écriture de Python (comme pour pas mal de langages de script, à l’instar de PERL ou de PHP, par exemple) sont des arguments que l’on avance souvent en sa faveur. En fait, ces langages reprennent à leur compte le genre d’arguments que Java a dû avancer pour s’imposer devant C++. Ils ont bien retenu la leçon.

En Python class O2: def jeTravaillePourO2(self,x): x+=1 print "la valeur de la variable x est: %s" % (x) class O1: def jeTravaillePourO1(self): b=6 lienO2=O2() lienO2.jeTravaillePourO2(b) print "la valeur de la variable b est: %s" % (b) unO1=O1() unO1.jeTravaillePourO1()

Résultat la valeur de la variable x est : 7 la valeur de la variable b est : 6

En PHP 5 En PHP 5, comme en C++, les deux passages, par valeur et par référent, sont acceptés selon que l’on ajoute le & oui ou non, lors de la déclaration de l’argument. Le code ci-après est la version PHP 5 des passages de code précédents et seule l’addition du &, comme indiqué à même le code, modifie le résultat. Passage d’arguments

Passage d’arguments


\n"); } }

Méthodes ou messages ? CHAPITRE 6

93

class O1 { public function jeTravaillePourO1(){ $b=6; $lienO2 = new O2(); $lienO2->jeTravaillePourO2($b); print ("la valeur de la variable b est $b
\n"); } } $unO1 = new O1(); $unO1->jeTravaillePourO1(); ?> Passage par valeur ou par référent En ce qui concerne le passage d’arguments de type prédéfini, le passage par valeur aura pour effet de passer une copie de la variable et laissera inchangée la variable de départ, alors que le passage par référent passera la variable originale, sur laquelle la méthode pourra effectuer ses manipulations.

C++ et Bjarne Stroustrup Première difficulté : écrire son nom sans se tromper, pour ne pas parler de la prononciation. Il en va souvent ainsi des Danois, même s’ils vivent depuis longtemps aux États-Unis, comme c’est le cas du créateur du C++. Stroustrup est actuellement professeur à la Texas A&M University et maintient de nombreuses collaborations avec son ancien lieu de travail : le laboratoire d’AT&T Bell (dans le New Jersey). C++ a été pendant longtemps le langage de programmation OO le plus populaire et le plus pratiqué (de nombreux logiciels Microsoft et bien d’autres tels que Netscape ont été programmés en C++, il est à la base de technologies COM et CORBA, décrites au chapitre 16, Stroustrup recence sur son site web des milliers de fameuses applications informatiques programmées dans ce langage), même s’il est en passe d’être détrôné aujourd’hui par Java et peut-être demain par C#. La syntaxe du C++ est le reflet de son histoire et de son évolution, l’union des langages C et Smalltalk ou Simula. Notre auteur ne voulait rien, ou si peu, abandonner du C, tout en greffant une couche OO sur ce même langage. Il souhaitait garder la compatibilité « descendante » avec le C. Or, d’une certaine manière, les objectifs du C et de la programmation OO sont, comme nous espérons vous en avoir convaincu, quelque peu contradictoires : C tend à coller au mieux à l’architecture et au fonctionnement des processeurs actuels, alors que l’OO tend à coller au mieux aux structures mentales et au fonctionnement cognitif des programmeurs. Dur de faire optimal et simple à la fois, car ce qu’on gagne d’un côté, on se trouve contraint et forcé de le perdre ailleurs. OO tire la couverture de la programmation vers le programmeur, C vers le processeur, situation quelque peu schyzophrénique, comme le deviennent de nombreux programmeurs dans ce langage. La parade orale favorite de Stroustrup, face à la critique classique et sempiternelle adressée au C++ de langage hybride, est de retourner ce même argument en sa faveur, en n’en faisant, non plus un langage hybride, mais multi-paradigmatique, s’accommodant de plusieurs styles de programmation : procédural, numérique ou OO. Il met en avant les désavantages de la programmation objet quand le souci premier est la performance, économie mémoire et temps calcul, extrêmement critique pour le développement des applications dites embarquées. Il est indéniable que C++ est le langage le plus riche au travers des possibilités qu’il offre aux programmeurs. On voit clairement un petit noyau de Java ou de C# poindre dans C++, et certainement pas l’inverse.

94

L’orienté objet

Une façon succincte de différencier Java et C++ (attendons un peu pour placer C# dans cet exercice comparatif, car il se situe quelque part au milieu) serait de dire que la difficulté en Java ne réside pas tant dans la syntaxe de base du langage que dans la quantité énorme de librairies qu’il faut maîtriser pour des applications système, et que Java met à disposition dans l’environnement JDK. En revanche, la difficulté en C++ reste à ce jour principalement cantonnée dans la maîtrise de la syntaxe de base. Notez qu’une évolution actuelle du C++ est de privilégier davantage le développement des bibliothèques standards que l’évolution de la syntaxe du langage. Parmi ces nouvelles librairies, on retrouve, en commun avec Java, C#, et Python des utilitaires pour le multithreading, la persistance, la gestion automatique de la mémoire, etc. Pour l’instant, le programmeur C+ + est, soit tenu de programmer l’utilitaire désiré, soit de se reposer entièrement sur les outils mis à sa disposition par le système d’exploitation (mais qui crée une forte dépendance avec la plate-forme sur laquelle tourne le programme) ou sur des développeurs occasionnels, soucieux d’enrichir les fonctionnalités du C++. Enfin, en plus de la couche OO additionnelle, Stroustrup accorde une immense importance au mécanisme de généricité, absent de Java (mais intégré à partir de la version 1.5) et C# (mais intégré à partir de la version 2) et que nous abordons très brièvement dans le chapitre 21 lors de la programmation des listes liées, des graphes et des collections en général. Jusqu’alors, ces deux langages compensaient en partie l’absence de générécité par l’existence de la superclasse objet. La généricité permet de programmer à un très haut niveau d’abstraction, et de récupérer ces programmes pour de multiples objets, quel que soit leur type, sans se préoccuper de problème de surcharge de méthode ou de « casting » inapproprié. Le « casting » est en effet une pratique intéressante de la programma-tion car, alors que Stroustrup la considère comme « tordue » et à éviter (n’oublions pas la force du typage statique en C++), il est omniprésent dans des langages comme Java et C#, du fait du typage dynamique et de la pratique du polymorphisme. Son apect nuisible (il peut entraîner des erreurs à la phase d’exécution du code si le type dynamique de l’objet passé n’est pas celui prévu à la compilation) a incité les développeurs des langages Java et C# (et suivant en cela les critiques et les recommandations de Stroustrup) à en restreindre l’utilisation dans la nouvelle version du language. Vous l’aurez compris, Stroustrup privilégie la phase de compilaton et les assurances qui en découle pour la qualité et l’efficacité des codes. Il ne doit pas trop se retrouver dans l’engouement actuel pour les langages de script tels Python et PHP, qui s’en sont totalement émancipés. Parmi nos manuels de programmation favoris en C++, indiquons (une liste très subjective), en commençant par les ouvrages du maître : – The C++ Programming Language, Addison-Wesley. Plusieurs versions sont disponibles, mais vous aurez compris qu’une seule suffit, la plus récente. Sachez également que C++ est un standard de facto depuis 1998, et que, depuis lors, aucune nouvelle fonctionnalité significative n’a été rajoutée au langage. – Practical C++, Rob MCGREGOR, QUE. – C++ Primer Edition, LIPPMAN et LAJOIE, Addison-Wesley. – More effective C++, Scott MEYERS, Addison-Wesley – un livre pour les pros (qui le conseillent plus), mais illustrant à souhait la richesse et la subtilité de la programmation en C++. – Mieux programmer en C++, Herb SUTTER (trad. Thomas Pétillon), Eyrolles (un excellent livre pour maîtriser des concepts avancés jusqu’aux moindres subtilités du C++). – Le langage C++, Jacques CHARBONNEL, Masson. Enfin, deux excellents ouvrages comparant C++ et Java : – Apprendre Java et C++ en parallèle, Jean-Bernard BOICHAT, Eyrolles. – C++ for Java Programmers, Timothy BUDD, Addison-Wesley.. Nous nous en voudrions enfin de ne pas citer la nouvelle bouture C++ de Microsoft, le Visual C++ .Net, une espèce d’hybride fascinant (surtout pour les problèmes de gestion de la mémoire des objets) entre Java et C++, et certainement un des langages de programmation les plus puissants à ce jour. Microsoft doit en effet beaucoup à C++, au point qu’il aurait pu le renommer $++.

Méthodes ou messages ? CHAPITRE 6

95

Passage d’argument objet dans les messages Supposons maintenant que l’argument transmis à la méthode jeTravaillePourO2(O3 lienO3) soit un argument de type, non plus prédéfini, mais d’une certaine classe, ici O3. Nous avons vu dans le chapitre 4 que cela a pour effet de créer un lien de dépendance entre les classes O2 et O3. Il s’agit là d’une possible manière de déclencher l’exécution de messages en cascade, comme illustré par le code Java présenté ci-après.

En Java class O3 { private int c; public O3(int initC) { c = initC; } public void incrementeC() { c++; } public void afficheC() { System.out.println("l'attribut c est egal a: " + c); } } class O2 { public void jeTravaillePourO2(O3 lienO3) { lienO3.incrementeC(); lienO3.afficheC(); } } public class O1 { private O2 lienO2; private void jeTravaillePourO1() { O3 unO3 = new O3(6); lienO2 = new O2(); lienO2.jeTravaillePourO2(unO3); unO3.afficheC(); } public static void main(String[] args) { O1 unO1 = new O1(); unO1.jeTravaillePourO1(); } }

Résultat l’attribut c est égal à : 7 l’attribut c est égal à : 7

Que se passe-t-il dans ce code ? De nouveau, lors de son exécution, la méthode jeTravaillePourO2() recevra comme argument une copie de la valeur stockée dans le référent unO3. Mais comme cette valeur est, en réalité, l’adresse physique du même objet que celui créé et référé par unO3 dans la méthode jeTravaillePourO1(), un second référent sera créé, qui permettra d’accéder à ce même objet. Au contraire de ce qui se passait dans

96

L’orienté objet

le cas précédent, la méthode jeTravaillePourO2() affectera maintenant réellement l’objet, dont l’adresse lui sera transmise par argument. On affecte, de ce fait, toujours un seul et même objet, et non plus une copie de celui-ci. Alors qu’il s’agit toujours du même passage par valeur, dupliquant l’original dans une zone temporaire, la variable affectée, finalement, ne sera pas qu’une copie de l’entier original dans le cas d’un argument de type prédéfini, mais bien l’objet original dans le cas présent. En fait, ce qui rend possible cette manipulation, c’est le mécanisme d’adressage indirect, qui permet à certaines variables de pointer, non pas directement sur leur valeur, mais vers une variable intermédiaire, pointant, elle, sur cette valeur. Comme indiqué à la figure 6-1, pendant toute la durée de l’exécution de la méthode jeTravaillePourO2(), l’objet unO3 sera référé deux fois, puis une seule fois à la fin de l’exécution de la méthode, puis, plus du tout, à la fin de l’exécution de la méthode jeTravaillePourO1(). L’objet unO3, à l’issue de l’exécution de ces deux méthodes, sera comme « perdu et satellisé » dans la mémoire, à la merci du ramasse-miettes (que nous découvrirons dans le chapitre 9). Comme nous l’avons vu précédemment, la possibilité pour un objet d’être référé un grand nombre de fois est inhérente à la pratique de la programmation orientée objet et sera reconsidérée, lorsque nous nous pencherons avec tristesse sur le passage de vie à trépas des objets. Figure 6-1

Illustration de l’effet du passage du référent « lienO3 » adressant l’objet unO3, dans la méthode jeTravaillePourO2() agissant, elle, sur l’objet O2.

En C## En C#, la pratique et le résultat sont à première vue très semblables. Il n’y aura en général plus lieu de préciser dans la méthode que le passage se fait par référent, car, lorsque ce sont des objets qui sont passés comme argument, il n’y a simplement pas moyen de faire autrement, ils le sont par défaut. En effet, l’esprit de la programmation OO favorise ce type de passage. Ce sont bien toujours les objets originaux qui subissent des

Méthodes ou messages ? CHAPITRE 6

97

transformations. Comme dans la vie réelle, on imagine mal qu’à chaque modification de l’état d’un objet, il faille le dupliquer afin que la modification n’affecte que la copie. Combien de copies inutiles seraient ainsi créées. Même si ces copies ne vivent que le temps d’exécution de la méthode, pendant ce temps-là, elles n’en consomment pas moins de la mémoire. Et pour quoi ? Pour rien ! Car c’est bien l’objet original que l’on cherche à transformer, et non pas cette évanescente copie parasite. Cependant, et comme le code ci-dessous l’indique, le mot-clé ref reste encore d’utilisation possible, y compris lors du passage des arguments référents d’objet et son emploi crée une couche additionelle d’indirection dans l’adressage. using System; using System.Collections.Generic; using System.Text; class O3 { private int c; public O3(int initC) { c = initC; } public void incrementeC() { c++; } public void afficheC() { Console.WriteLine("l'attribut c est égal à: " + c); } } class O2 { public void jeTravaillePourO2(O3 lienO3) { lienO3.incrementeC(); lienO3.afficheC(); } public void jeCreeUnObjetO3(ref O3 lienO3) // le résultat sera différent selon que l’on utilise ➥ou pas ref { lienO3 = new O3(7); lienO3.incrementeC(); lienO3.afficheC(); } public static void Main() { O3 unO3 = new O3(6); O2 unO2 = new O2(); unO2.jeTravaillePourO2(unO3);

L’orienté objet

98

unO3.afficheC(); unO2.jeCreeUnObjetO3(ref unO3); unO3.afficheC(); } } Résultat l’attribut l’attribut l’attribut l’attribut

c c c c

est est est est

égal égal égal égal

à à à à

: : : :

7 7 7 ou 8 // dépendant du passage par référent ou non 7

Dans ce code, la méthode jeCreeUnObjetO3 se comportera différement selon que le passage du référent se fasse, à son tour, par référent ou par valeur. Si le passage du référent se fait par référent, et comme l’illustre la figure 6-2, on se retrouve avec un double niveau d’adressage indirect (et c’est le moment idéal pour vous mettre sur la tête et adopter pendant quelques minutes votre position de méditation zen favorite). Si le référent est passé par référent, c’est bien le référent original que vous affectez au nouvel objet à l’intérieur de la méthode et non plus sa copie, avec pour effet que le nouvel objet créé se trouvera référé par le référent de départ, laissant l’objet référé originellement perdu dans la mémoire et à la merci du « garbage collector ». Bien que tout cela soit correct sur le plan grammatical, cet extrait fait plus ressembler ce livre à un manisfeste dadaïste qu’à un livre de programmation. Figure 6-2

Les deux niveaux d’adressage indirect découlant de la double utilisation des référents. D’abord c’est l’adresse de l’objet qui est dédoublée, ensuite c’est l’adresse de cette dernière qui l’est.

En PHP 5 Le code PHP 5 ci-après est une copie parfaite du code C#. Une des grandes innovations du PHP 5 par rapport à ses versions précédentes est, justement, d’avoir rendu le passage d’arguments objet automatiquement par référent (avant il l’était automatiquement par valeur comme en C++) sauf à le spécifier différemment, par l’addition du &. Passage d'arguments

Méthodes ou messages ? CHAPITRE 6

99

Passage d'arguments


c = $initC; } public function incrementeC(){ $this->c++; } public function afficheC() { print ("l'attribut c est egal a $this->c
\n"); } } class O2 { public function jeTravaillePourO2(O3 $lienO3){ $lienO3->incrementeC(); $lienO3->afficheC(); } public function jeCreeUnObjetO3(O3 &$lienO3){/* avec ou sans le & */ $lienO3 = new O3(7); $lienO3->incrementeC(); $lienO3->afficheC(); } } $unO3 = new O3(6); $unO2 = new O2(); $unO2->jeTravaillePourO2($unO3); $unO3->afficheC(); $unO2->jeCreeUnObjetO3($unO3); $unO3->afficheC(); ?>

En C++ C++, à la différence de C# et de Java, vous oblige à traiter les arguments objets, tout comme vous traiteriez n’importe quelle variable. C’est là encore un lourd tribut payé à son funeste géniteur, le C. Aucun traitement de faveur pour les objets ! C’est bien normal pour un langage qui ne se voulait pas objet au départ. Il faudra donc préciser, comme dans le code ci-après, lors de la définition de la méthode, si le passage de l’objet O3 se fait par valeur ou par référent.

100

L’orienté objet

#include "iostream.h" class O3 { private: int c; public: O3(int initC){ c = initC; } void incrementeC() { c++; } void afficheC() { cout <<"l'attribut c est egal a: " << c << endl; } }; class O2 { public: /* void jeTravaillePourO2(O3 lienO3) { lienO3.incrementeC(); lienO3.afficheC(); } */ void jeTravaillePourO2(O3 &lienO3) { lienO3.incrementeC(); lienO3.afficheC(); } }; class O1 { private: O2 *lienO2; public: void jeTravaillePourO1() { O3 unO3(6); lienO2 = new O2(); lienO2->jeTravaillePourO2(unO3); /* appelle de manière semblable la première version ➥ ou la seconde*/ unO3.afficheC(); } }; int main() { O1 unO1; unO1.jeTravaillePourO1(); return 0; }

Dans ce code, la première méthode reçoit l’objet comme valeur et la seconde comme référent. Les deux ne peuvent être utilisées simultanément, car leur appel se passe de la même manière. Il faut donc lever cette ambiguïté, et choisir l’une ou l’autre de ces manières. Selon que l’on utilise la version par valeur (première méthode) ou par référent, le résultat sera différent.

Méthodes ou messages ? CHAPITRE 6

101

Résultat passage par valeur l’attribut c est égal à : 7 l’attribut c est égal à : 6

Résultat passage par référent l’attribut c est égal à : 7 l’attribut c est égal à : 7

On constate que, dans le premier cas, l’objet original passé comme argument est laissé inchangé (seule la copie a été affectée) alors que, dans le second, c’est bien l’objet original qui a été modifié.

En Python Python, à nouveau, comme Java et comme C#, et comme le code ci-dessous l’indique, lorsqu’il s’agit de référents sur les objets, transmet bien l’adresse en argument et donc, indirectement l’objet original qui se trouvera modifié par l’exécution de la méthode. class O3: __c=0 def __init__(self, initC): self.__c=initC def incrementeC(self): self.__c+=1 def afficheC(self): print "l'attribut c est egal à: %s" %(self.__c) class O2: def jeTravaillePourO2(self,lienO3): lienO3.incrementeC() lienO3.afficheC() class O1: __lienO2=0 def jeTravaillePourO1(self): unO3=O3(6) lienO2=O2() lienO2.jeTravaillePourO2(unO3) unO3.afficheC() unO1=O1() unO1.jeTravaillePourO1()

Résultat l’attribut c est égal à : 7 l’attribut c est égal à : 7

Comme le passage des objets se fait, par défaut, par valeur en C++, de nombreux objets seront soumis à des clonages temporaires, qu’il faudra réaliser avec soin. Nous verrons au chapitre 9 que, ce clonage demandant une attention toute particulière, C++ vous invite à utiliser un constructeur particulier, appelé « constructeur par copie », qui entre en action dès qu’un objet est cloné.

102

L’orienté objet

Passage par référent La programmation orientée objet favorise dans sa pratique le passage des arguments objets comme référent plutôt que comme valeur. C’est toujours sur ces mêmes malheureux objets que la majorité des méthodes s’acharnent sans créer de nouveaux cobayes le temps de leurs méfaits. Les langages Java, C#, PHP 5 et Python en ont fait, légitimement, leur mode de fonctionnement par défaut, alors que le C++ s’est limité à généraliser aux objets le passage par valeur propre aux variables de type prédéfini. Une des lourdeurs inhérentes au C++ est qu’il faudra recourir à une pratique non intuitive (due à l’utilisation explicite de pointeurs ou de référents) pour obtenir le comportement, a priori, le plus intuitif.

Une méthode est-elle d’office un message ? Nous avons vu que message il y a quand une méthode intervient dans l’interaction entre deux objets. Les concepts de message et de méthode deviennent-ils dès lors synonymiques ? Pas vraiment et ce pour plusieurs raisons. Le message ramène la méthode à sa seule signature. Pour qu’un objet s’adresse à un autre, il doit uniquement connaître la signature de la méthode, et peut se désintéresser complètement du corps de cette dernière. Ce qu’il doit connaître de la méthode, c’est son mode d’appel, c’est-à-dire : son nom, ses arguments et le type de ce que la méthode retourne, pour autant qu’elle retourne quelque chose.

Même message, plusieurs méthodes Le fait de tenir la signature séparée du corps de la méthode permet aussi de prévoir plusieurs implémentations possibles pour un même message, implémentations qui pourraient, ou évoluer dans le temps, sans que le message lui-même ne s’en trouve affecté, ou, toujours plus fort, qui pourraient différer, selon la nature ultime de l’objet à qui le message est destiné. Dans un film des Monty Python, au départ d’un 100 m pour coureurs qui n’ont pas le sens de l’orientation, le même coup de feu déclenchait le départ des coureurs dans toutes les directions. Au contraire, lors de la même épreuve pour sourds, le coup de feu laissait tous les coureurs de marbre. C’est d’ailleurs de ces comiques que provient le nom d’un des langages de programmation que nous utilisons dans ce livre. On vous laisse deviner lequel… Nous verrons dans les chapitres 12 et 13 que cette variation sur un même thème est permise en OO : elle est nommée « polymorphisme ». Message = signature de méthode disponible Le message se limite uniquement à la signature de la méthode : le type de ce qu’elle retourne, son nom et ses arguments. En aucun cas, l’expéditeur n’a besoin, lors de l’écriture de son appel, de connaître son implémentation ou son corps d’instructions. Cela simplifie la conception et stabilise l’évolution du programme.

Interface : liste de signatures de méthodes disponibles Toutes les signatures de méthodes ne deviendront pas des messages pour autant. Nous expliquerons dans les deux chapitres suivants la pratique de « l’encapsulation », qui n’octroie qu’à un nombre restreint de méthodes l’heureux privilège de pouvoir être appelées de l’extérieur. Pour l’instant, vous pouvez vous borner à lier ce terme à la seule utilisation des objets limonade, bière ou Bacardi. L’idée est de pouvoir extraire de la définition de chaque classe la liste des signatures de méthode qui pourront faire l’objet d’envoi de messages. De manière quelque peu anticipée, nous appellerons cette liste l’interface de la classe, car il s’agit bien de la partie visible de la classe, seule disponible pour des utilisateurs extérieurs. Dans la figure 6-3, vous observerez l’extraction, à partir de la définition des classes, des seules méthodes qui pourront faire l’objet de messages.

Méthodes ou messages ? CHAPITRE 6

103

Figure 6-3

Extraction de l’interface de la classe O1, ne reprenant que les signatures des méthodes disponibles pour les autres classes.

Nous préciserons aussi, plus avant dans cet ouvrage, au chapitre 15, le lien entre la classe O1 et son interface, dénommée ici InterfaceO1. Pour l’instant, le seul point à retenir est que chaque objet se caractérisera par une liste de messages disponibles, son interface, que d’autres pourront déclencher sur lui. Dorénavant, ce sera l’interface, plus que la classe directement, qui reprendra les services que l’objet sera en mesure de rendre à tout autre. Les objets sont d’une pudeur extrême et ne montre que leur interface aux autres objets. Pourquoi extraire de la classe cette seule partie visible ? Car il n’est pas nécessaire pour un premier objet, utilisant les méthodes du second, d’avoir accès à toutes ces méthodes, surtout si celles-ci risquent d’évoluer au cours du temps. Certaines relèvent du fonctionnement interne et intime de l’objet, et ne peuvent être actionnées que par l’objet lui-même.

Des méthodes strictement intimes Quand le conducteur démarre sa voiture, il change de vitesse puis appuie sur la pédale d’accélération. Il se moque éperdument de savoir que, lorsqu’il appuie sur cette pédale, il accroît la dimension de l’entrée du mélange gazeux dans le moteur. Il laisse le soin à la pédale elle-même de communiquer avec le moteur, de manière à prolonger le seul service que le conducteur exige de sa voiture : accélérer. Quand nous sauvegardons un fichier, nous laissons le soin au traitement de texte de stocker de manière fiable tout ce qui est écrit sur le disque dur. Il devra repérer un espace libre sur le disque, éventuellement fractionner le fichier en un ensemble de morceaux qu’il devra se préoccuper de relier entre eux, et associer, également sur le disque, le nom du fichier à l’adresse où celui-ci se trouve. Pour ce faire, le traitement de texte, lui-même, utilisera les services du pilote du disque dur intégrés dans le système d’exploitation. En fait, un des rôles majeurs de tout système d’exploitation informatique est de fournir à l’utilisateur ou à toute application qui le requiert un ensemble d’interfaces, « amicales » (traduction littérale du fameux « user-friendly » américain, dans une conception de l’amitié très éloignée de celle que Montaigne vouait à La Boétie), pour réaliser très intuitivement un ensemble de services, dont l’implémentation complexe, invisible à vos yeux et à ceux de l’application, est entièrement laissée au soin du système d’exploitation lui-même.

104

L’orienté objet

Interface La liste des messages disponibles dans une classe sera appelée l’interface de cette classe.

La mondialisation des messages Message sur Internet Un message se limite-t-il à circuler dans la mémoire vive de l’ordinateur, comme nous l’avons vu dans les chapitres précédents, ou peut-il franchir les murs, les frontières, les océans, les planètes et les univers… Oui, il peut franchir tout cela, et bien plus encore, pour autant qu’il trouve là-bas un objet à qui s’adresser, et qui a prévu, là-bas, de par sa classe, de pouvoir répondre à ce message. Il existe une manière qui s’est extraordinairement répandue aujourd’hui pour relier des objets informatiques entre eux… On vous la donne en mille… Eh oui ! Internet. Deux objets pourront se parler à travers Internet, non pas pour s’envoyer par e-mail des plaisanteries salaces ou des spams qui ne le sont pas moins, mais pour se charger mutuellement de certains services, occupation bien plus noble, s’il en est… Pour qu’un premier objet parle à un second, il lui sera maintenant important de connaître, non seulement son nom, mais également son adresse Internet, de manière à retrouver l’ordinateur sur lequel cet objet s’activera. Il lui faudra bien évidemment posséder l’interface des services rendus par ce distant interlocuteur. Tout aussi important, il s’agira également de définir une stratégie d’activation de l’objet destinataire. Par exemple, faudra-t-il exécuter l’application qui active l’objet, avant que celui-ci ne soit réquisitionné pour exécuter son message, ou l’objet sera-t-il automatiquement activé, dès que l’ordinateur qui peut l’exécuter recevra le message ? Quelques complications apparaîtront, par rapport au simple envoi de message dans un seul et même ordinateur. Cependant, l’ambition des concepteurs des mécanismes d’objets distribués (Java-RMI, CORBA ou services web) est de rendre l’aspect Internet le plus transparent possible, c’est-à-dire, qu’à quelques détails près, vite assimilés, comme l’adresse des objets et les stratégies d’activation, la réalisation d’applications distribuées soit en tout point semblable à celle d’applications locales.

L’informatique distribuée En fait, tout le mécanisme de communication entre objets par envois de messages a permis de repenser la conception des applications informatiques distribuées. La distribution d’applications informatiques à travers un réseau reste le fait qu’un programme s’exécutant sur un ordinateur puisse, à un certain moment, déléguer une partie de sa tâche à un autre programme, s’exécutant sur un autre ordinateur. Cela se faisait déjà. Simplement, tout a été repensé et reformulé à la sauce OO. Ce ne sont plus des procédures qui s’appellent à distance, mais des objets qui se parlent à distance, par envoi de messages. Le chapitre 16 sera entièrement dédié aux objets distribués. Les objets distribués Les technologies d’objets distribués tentent d’étendre à tout Internet la portée des envois de message entre objets, et ce de la manière la plus simple et transparente qui soit.

Méthodes ou messages ? CHAPITRE 6

105

Exercices Exercice 6.1 Qu’afficheront à l’exécution les deux programmes suivants ? Notez l’obligation pour la méthode main, statique, de ne pouvoir intégrer dans son code que des attributs ou des méthodes également déclarés statiques. Code : Chapitre6.java public class Chapitre6 { static int i; static public void test(int i) { i++; System.out.println ("i = " + i); } public static void main(String[] args) { i = 5; test(i); System.out.println("i = " + i); } } Code : Chapitre6.csc using System; public class Chapitre6 { static int i; static public void test(ref int i) { i++; Console.WriteLine ("i = " + i); } static public void test(int i) { i++; Console.WriteLine ("i = " + i); } public static void Main() { i = 5; test(i); Console.WriteLine("i = " + i); test(ref i); Console.WriteLine("i = " + i); } }

Exercice 6.2 Qu’affichera à l’exécution le code C++ suivant ? #include "stdafx.h" #include "iostream.h" void test(int i) { i++; cout <<"i = "<
106

L’orienté objet

void test2(int &i) { i++; cout <<"i = "<i = i; } void getI() { cout << "i = " << i << endl; } void incrementeI(int i) { this->i += i; // this est le référent de l'objet lui-même } }; void test(int i) { i++; cout <<"i = "<getI(); Test2(*unAutreTest, i); unAutreTest->getI(); return 0; }

107

108

L’orienté objet

Exercice 6.5 Qu’affichera à l’exécution le code Java présenté ci-après ? class TestI { int i; public TestI(int i) { this.i = i; } public void getI() { System.out.println ("i = " + i); } public void incrementeI(int i) { this.i += i; // this est le référent de l'objet lui-même } } public class Chapitre6 { static TestI unTest = new TestI(5); static void Test(TestI unTest) { unTest = new TestI(6); unTest.incrementeI(5); } public static void main(String[] args) { Test(unTest); unTest.getI(); } }

Exercice 6.6 Pourquoi C# ne permet le passage par « référent » que pour des arguments de type prédéfinis ? En quoi C++ ne choisit pas la facilité en utilisant par défaut le passage d’arguments par valeur pour les objets ?

7 L’encapsulation des attributs Ce chapitre a pour objet d’introduire la pratique d’encapsulation que, dans un premier temps, nous limiterons aux seuls attributs. Cette encapsulation pour les attributs est justifiée par la préservation de l’intégrité des objets, la lecture des attributs détachée du stockage et la stabilisation des codes.

Sommaire : Private ou public — Attributs private — Encapsulation — L’intégrité des objets — Gestion d’exception — Stabilisation des codes

Candidus — Certains termes de l’OO, tels « encapsulation » et « cloisonnement », me font penser à hermétisme et réglementation. Nous n’allons pas embrigader notre pauvre bébé tout de même ! Les nouveaux langages me semblaient pourtant offrir plus de souplesse ! Pourquoi donc ce « touche pas à ma classe ? » Doctus — Je ne vois aucune contradiction entre souplesse et élégance. L’encapsulation est le moyen de ranger proprement le contenu de chaque objet. Il faut y voir un souci de répartition des tâches. Cand. — Proprement ? Doc. — Oui, chaque objet doit traiter l’information d’une manière qui lui soit propre. Cand. — Et concrètement, j’y gagnerai quoi ? Doc. — En tout premier lieu, les accesseurs (setters et getters) doivent faire partie de l’interface des objets. Cette interface est rigoureusement spécifiée. Il en résulte que les éléments internes, variables et méthodes, pourront évoluer sans la moindre conséquence pour les objets environnants. Ce qui nous sera bien utile lorsque le fabricant décidera de changer quelque chose à l’intérieur de ses jouets. Et tu constateras qu’on n’y perd pas en liberté au bout du compte. Cette encapsulation n’est pas autre chose qu’un emballage. Il permet d’éviter les fuites… Cand. — … ou les mains baladeuses. Au lieu de me servir dans les affaires de quelqu’un, mieux vaut donc lui demander poliment. Il saura toujours mieux que quiconque où il les a rangées. Doc. — Il saura également faire mieux que toi le nécessaire pour s’assurer que tu peux en disposer en toute sécurité. Par exemple, il te fera attendre en cas de besoin. Cand. — Ces objets font encore mieux que simplement communiquer, ils coopèrent, en fait !

110

L’orienté objet

La charte du bon programmeur OO : Arthur J. Riel Souvent dans notre ouvrage, nous faisons référence à une supposée charte de la bonne programmation OO. Cette charte, en fait, n’existe que dans notre imagination. Ce n’est autre qu’une compilation d’un ensemble de bonnes pratiques OO, acquises naturellement après tant d’années passées devant l’écran, et disséminées çà et là dans la grande quantité d’ouvrages que nous avons lus, avant d’oser nous lancer tête baissée dans le nôtre. Néanmoins, il est un écrit qui pourrait presque prétendre à ce titre : nous l’avons rencontré sous la plume de Arthur J. Riel, sous le titre Object-Oriented Design Heuristics (AddisonWesley). Plus de 60 recommandations de bonnes pratiques OO y sont présentées, illustrées et défendues. En voici quelquesunes, piochées au hasard, et que vous rencontrerez pour certaines plusieurs fois dans ce livre : – Minimisez le nombre de messages dans le protocole d’une classe. – N’encombrez pas la partie publique d’une classe avec des choses que les utilisateurs de cette classe ne sont pas aptes à utiliser ou dont ils ne voient pas l’intérêt. – Une classe capture une et une seule abstraction. – Ne créez pas de classes « God » dans votre application. Soyez très sceptiques devant toute classe s’appelant en partie « Pilote », « Manager », « Système » ou « Sous-Système ». – Méfiez-vous des classes contenant beaucoup de méthodes d’accès. Cela impliquerait que les données et l’utilisation que l’on en fait ne sont pas tenues dans une seule et même classe. – Minimisez le nombre de classes avec lesquelles une autre classe collabore. – Minimisez le nombre de messages qui peuvent être envoyés entre une classe et celles qui collaborent avec cette dernière. – L’héritage ne devrait être utilisé que pour modéliser une relation de spécialisation. – Les superclasses ne doivent rien savoir de leurs sous-classes. – N’utilisez pas le mot-clé« protected ». – L’héritage devrait être très profond ; plus il l’est, mieux c’est. – Toutes les superclasses devraient être abstraites. – Factorisez les attributs, les méthodes, aussi haut que possible dans la hiérarchie d’héritage. Certaines de ces recommandations sont parfois discutables, et toute bonne pratique souffre toujours d’un point de vue très subjectif de la chose. Toutefois, l’effort est plus que louable et, indéniablement, cette tentative d’énoncer toutes ces exhortations les unes à la suite des autres dans un seul livre aura très positivement impressionné la communauté informatique. Celleci y a répondu largement, en faisant de certaines d’entre elles des préceptes presque aussi incontournables pour les programmeurs que ne le furent les dix commandements d’un certain Moïse pour le peuple hébreu.

Accès aux attributs d’un objet Accès externe aux attributs Lorsque, dans notre écosystème du chapitre 3, l’objet proie boit l’eau, et dès le moment où la proie possède parmi ses attributs un possible accès à l’objet eau (par la présence d’un référent), pourquoi ne pourrait-elle pas directement s’occuper de la diminution de la quantité d’eau, sans ce détour obligé par une méthode de la classe Eau qui s’en charge elle-même ? En tous les cas, il serait plus facile d’écrire directement : class Proie { Eau eau void bois() { eau.quantite = eau.quantite – 1000; // plutôt que eau.diminueQuantite(1000) } }

L’encapsulation des attributs CHAPITRE 7

111

que de passer par la méthode de l’eau diminueQuantite(), rajoutée à cet effet dans la classe Eau, et qui se limite à refaire exactement la même chose, c’est-à-dire diminuer la quantité d’eau. De même, pourquoi le feu-de-signalisation, quand il passe au vert, ne pourrait-il pas directement changer la vitesse de la voiture, à l’aide d’une instruction telle que laVoitureDevant.vitesse = 50, sans devoir passer, là encore, par une méthode de la classe Voiture ? En fait, pratiquement tous les langages de programmation OO, dans la lignée du C++, le permettent, à tort comme nous le verrons, pour autant que l’on déclare explicitement les attributs comme public. Et nous voici en présence d’un nouveau mot-clé, capital de la programmation OO, qui caractérise l’accès aux attributs et aux méthodes de la classe par toute autre classe. Ce mot-clé ne devrait idéalement prendre que deux valeurs : public et private (laissons pour l’instant les deux mots en anglais, vu qu’ils le sont dans les langages de programmation, en exprimant nos regrets les plus sincères auprès de la French Academy). Attribut private Un attribut ou une méthode sera private, si l’on souhaite restreindre son accès à la seule classe dans laquelle il est déclaré. Il sera public si son accès est possible par, ou dans, toute autre classe.

Cachez ces attributs que je ne saurais voir Si vous nous avez suivi jusqu’ici, permettez-nous de vous poser la question suivante : comment, jusqu’à présent et de façon implicite (nous n’avons pour l’instant encore jamais fait allusion au mode d’accès), avonsnous considéré le mode d’accès des attributs d’une classe : private ou public ? Ouf ! vous nous avez fait peur… Mais oui, private, bien entendu ! Il est inconcevable que vous ayez pu répondre autre chose. Nous avons dit et redit dans les chapitres précédents que les seuls accès possibles aux attributs d’une classe, y compris leur simple lecture, ne pouvaient se faire que par l’entremise des méthodes de cette classe. En déclarant les attributs comme private, toute tentative d’accès direct, du genre : o2.unAttribut = 50 (quand dans l’objet o1, la valeur de l’attribut unAttribut de l’objet o2 se voit directement changée), sera verbalisée par le compilateur. Ce mot-clé, private ou public, permet au compilateur de nous seconder (toujours ce même côté cerbère dans les langages qui en font un usage bien entendu), en faisant d’une mauvaise pratique orientée objet une erreur de syntaxe (et de compilation). Dans le petit code suivant, on a rajouté le mode d’accès à la déclaration des attributs et des méthodes, rendant maintenant complète la déclaration de notre classe. class Feu-de-signalisation { private int couleur ; /* attribut à l’accès privé */ private Voiture voitureDevant ; /* autre attribut de type référent à l’accès privé */ public Feu-de-Signalisation (int couleurInit, Voiture voitureInit) { /* le constructeur sera presque ➥ toujours public évidemment, puisqu’on crée un objet de l’extérieur de la classe de cet objet */ couleur = couleurInit ; voitureDevant = voitureInit ; } public void change() /* une autre méthode accessible de l’extérieur */ { couleur = couleur + 1 ; if (couleur == 4) couleur = 1 ; if (couleur == 1) voitureDevant.changeVitesse(50) ; } }

112

L’orienté objet

Encapsulation des attributs Sachez que certains langages OO, et non des moindres – car ce sont de vénérables langages du troisième âge (40 ans en informatique), comme Smalltalk –, rendent impossible, pour les attributs, tout autre accès que private. Tous les attributs seront private par défaut, circulez, y a rien à voir ! Dans les langages plus modernes et moins scrupuleux, dès le moment où, respectant ainsi la charte de la bonne programmation OO, vous déclarez explicitement ces attributs private, leur simple lecture ou modification se fera, à l’aide de méthodes d’accès, comme dans les cinq codes en cinq langages qui suivent. Dans ces codes, la classe FeuDeSignalisation est déclarée avec le mode d’accès des attributs adéquats. Ensuite, un objet issu de cette classe est créé et son attribut couleur reçoit la valeur 1. En Java class FeuDeSignalisation { private int couleur; /* l’attribut privé */ public FeuDeSignalisation(int couleur) /* le constructeur presque d’office public */ { if ((couleur > 0) && (couleur <= 3)) this.couleur = couleur; } public int getCouleur() /* la méthode qui renvoie la valeur de la couleur */ { return couleur; } public void setCouleur(int nouvelleCouleur) /* une méthode qui modifie la valeur de la couleur */ { if ((nouvelleCouleur > 0) && (nouvelleCouleur <= 3)) couleur = nouvelleCouleur; } } public class Principale { public static void main(String[] args) { FeuDeSignalisation unFeu = new FeuDeSignalisation(2); System.out.println(unFeu.getCouleur()); /* on affiche la valeur de la couleur */ unFeu.setCouleur(1); /* on modifie cette valeur */ /* unFeu.couleur = 1 est une instruction interdite */ } }

En C++ #include "stdafx.h" #include "iostream.h" class FeuDeSignalisation { private : /* on factorise le private et le public */ int couleur; public: FeuDeSignalisation(int couleur) { if ((couleur > 0) && (couleur <= 3)) this->couleur = couleur; } int getCouleur() { return couleur;

L’encapsulation des attributs CHAPITRE 7

113

} void setCouleur(int nouvelleCouleur) { if ((nouvelleCouleur > 0) && (nouvelleCouleur <= 3)) couleur = nouvelleCouleur; } }; int main() { FeuDeSignalisation unFeu(2); cout << unFeu.getCouleur() << endl; unFeu.setCouleur(1); return 0; }

La seule différence sensible à relever avec Java est qu’il n’est pas nécessaire de répéter le mot-clé public ou private, quand, plusieurs attibuts ou méthodes à la suite, partagent un même mode d’accès. En C# using System; class FeuDeSignalisation { private int couleur; public FeuDeSignalisation(int couleur) { if ((couleur > 0) && (couleur <= 3)) this.couleur = couleur; } public int accesCouleur /* méthode d’accès très originale */ { get { return couleur; } set { if ((nouvelleCouleur > 0) && (nouvelleCouleur <= 3)) couleur = value; } } } public class Principale { public static void Main() { FeuDeSignalisation unFeu = new FeuDeSignalisation(2); Console.WriteLine(unFeu.accesCouleur); unFeu.accesCouleur = 1; } }

En C#, les modes d’accès, set et get, sont regroupés dans une seule méthode. value indique la valeur à transmettre dans l’attribut. L’appel de la méthode se fait tout comme un accès direct à un attribut quelconque. Comme il s’agit, en effet, d’une forme « indirecte » d’accès à l’attribut, on conçoit mieux l’existence de cette syntaxe.

L’orienté objet

114

En PHP 5 Encapsulationn

Encapsulation


0) && ($couleur <=3)) $this->couleur = $couleur; } public function getCouleur() { return $this->couleur; } public function setCouleur($nouvelleCouleur) { if (($nouvelleCouleur > 0) && ($nouvelleCouleur <=3)) $this->couleur = $nouvelleCouleur; } } $unFeu = new FeuDeSignalisation(2); print($unFeu->getCouleur()); $unFeu->setCouleur(1); $unFeu->couleur = 3; /* Le programme se plante ici et ne fait plus rien*/ print($unFeu->getCouleur()); ?>

Rien de bien spécial à dire. Bien évidemment, en l’absence de compilation, c’est lors de l’éxécution qu’une tentative d’accès à quoi que ce soit de privé dans la classe déclenchera une erreur fatale. En Python class FeuDeSignalisation: __couleur = 0 def __init__(self,couleur): if couleur>0 and couleur <=3: self.__couleur=couleur def getCouleur(self): return self.__couleur def setCouleur(self,nouvelleCouleur): if nouvelleCouleur>0 and nouvelleCouleur<=3: self.__couleur=nouvelleCouleur

L’encapsulation des attributs CHAPITRE 7

115

unFeu=FeuDeSignalisation(2) print unFeu.getCouleur() unFeu.setCouleur(1) #changement de notre attribut privé unFeu.__couleur = 2 #cela n'affecte en rien l'attribut privé #un nouvel attribut est simplement créé print unFeu.getCouleur() #valeur de notre attribut privé print unFeu.__couleur #valeur du nouvel attribut

Résultat 2 1 2

En Python, comme le code l’illustre, la mise en œuvre des attributs privés est encore différente, dû à l’absence d’étape de compilation préalable. Pas de mot-clé private, un attribut privé est simplement signalé par la présence de deux underscores pour précéder son nom. Lorsque la déclaration de celui-ci est rencontrée à l’exécution, son nom est automatiquement changé de manière invisible (ces changements s’opérant également là où il apparaît dans les méthodes), ce qui fait que toute tentative d’accès par la suite ne concerne plus ce même attribut. L’attribut en devient inaccessible en dehors des méthodes de la classe. Mais pourquoi ces mille détours avant de lire ou de modifier un attribut ? Pourquoi les classes ne peuvent-elles exhiber leurs attributs en public ? Il y a plusieurs justifications à l’obligation, morale nous l’avons vu (car il y a possibilité de contourner cette obligation), de déclarer les attributs comme private. Nous retrouverons certaines de ces justifications, lorsque nous discuterons du mode d’accès des méthodes, qu’il faut également privilégier comme private. Bien sûr, tout ne peut être privé dans ce club très sélect que sont les classes, car on n’y verrait plus grand monde. Pas tout, mais beaucoup de choses néanmoins. Encapsulation L’encapsulation est ce mécanisme syntaxique qui consiste à déclarer comme private une large partie des caractéristiques de la classe, tous les attributs et de nombreuses méthodes.

Encapsulation : pourquoi faire ? Pour préserver l’intégrité des objets Et tout d’abord pourquoi sommes-nous instamment priés de déclarer les attributs comme private ? Une première raison étend la responsabilité des classes, non seulement au typage de leurs objets, mais également à la préservation de l’intégrité de ces derniers. En général, les objets d’une classe, décrits et caractérisés par la valeur de leurs attributs, ne peuvent admettre que ces attributs prennent toute et n’importe quelle valeur. La couleur du feu ne peut prendre que les valeurs 1, 2 et 3. La quantité d’eau ne peut devenir négative, de même que l’énergie des animaux. Pourtant, rien dans la déclaration même des attributs ne permet ces restrictions. Vous allez dire que tout programmeur est suffisamment malin et prévoyant pour deviner ce que l’on fera de ces attributs (ces attributs et non les siens). Le programmeur de la classe elle-même, sans doute, mais pourquoi les programmeurs de toutes les autres classes, appelées à interagir avec la première, devraient-ils également se préoccuper de cette intégrité ?

116

L’orienté objet

Rendons à chaque programmeur la classe qui lui appartient. Mieux vaut prévenir que guérir… Un coup de paille lors de la compilation d’un programme est préférable à un coup de poutre à son exécution (ou quelque chose du genre…). Laissons ainsi, à chaque classe, le soin de s’assurer qu’aucun de ses objets ne subira de changements d’état non admis. L’unique manière de procéder consiste à rendre les attributs inaccessibles, sinon par l’entremise de méthodes publiques, c’est-à-dire accessibles, elles, et qui s’assureront que les nouvelles valeurs prises restent dans celles admises. Charité bien ordonnée commence par soi-même (c’est le dernier dicton, promis !). Vous comprendrez très facilement comment les méthodes peuvent s’en charger, en lisant les deux petits codes qui suivent. class Feu-de-signalisation { private int couleur; private Voiture voitureDevant; public void changeCouleur(int nouvelleCouleur) { if (nouvelleCouleur >= 1) && (nouvelleCouleur <=3) /* intégrité assurée */ couleur = nouvelleCouleur ; } } class Voiture { private int vitesse ; public int changeVitesse(int nouvelleVitesse) { if (nouvelleVitesse >= 0) && (nouvelleVitesse <=130) /* intégrité assurée */ vitesse = nouvelleVitesse ; return vitesse ; } }

En quelque sorte, les méthodes de la classe filtrent l’usage que l’on fait des attributs de la classe. En entrée, elles ne toléreront que certaines valeurs. En sortie, elles présenteront les attributs, d’une manière qui convient aux autres classes, à celles qui veulent connaître leur valeur. C’est ce que Betrand Meyer tente d’installer d’une manière moins forcée dans sa version des langages OO, par l’introduction des notions d’invariance, par le fait qu’un objet puisse, avant d’éxécuter une méthode, vérifier qu’un ensemble de pré-conditions soit satisfait et, qu’à l’issue de cette exécution, c’est un ensemble de post-conditions qui le soit. Intégrité des objets Une première raison justifiant l’encapsulation des attributs dans la classe est d’obliger cette dernière, par l’intermédiaire de ses méthodes, à se charger de préserver l’intégrité de tous ses objets.

La gestion d’exception Dans nos cinq langages OO, il est également prévu que toute tentative, lors de l’exécution du programme, visant à violer l’intégrité d’un objet, en lui passant des valeurs d’attributs inadmissibles, puisse faire l’objet d’un mécanisme de gestion d’exception. Ce mécanisme permet, soit à la classe elle-même, soit à son interlocutrice, de prévoir et de prendre en compte la réponse à donner à cette tentative avortée : on interrompt le programme, la classe interlocutrice essaie une autre valeur, on continue comme si de rien n’était, mais, cette fois-ci, sans avoir à effectuer le changement. La gestion d’exception est un mécanisme de programmation assez sophistiqué, destiné à la réalisation de code plus robuste et qui permet d’anticiper et de gérer les problèmes

L’encapsulation des attributs CHAPITRE 7

117

pouvant survenir lors de l’exécution d’un code dans un contexte sur lequel le programmeur n’a pas tout contrôle. Il pourrait faire l’objet d’un chapitre à lui tout seul. Toute source de problèmes pouvant survenir à l’exécution n’est pas évitable, tel un réseau ou un disque dur inaccessible, un processeur inapte au multithreading, un accès incorrect à une base de données, un entier devant servir de dividente égal à zéro et beaucoup d’autres. En général, les instructions susceptibles de poser de tels problèmes sont placées dans un bloc try-catch. Lorsque le problème se pose effectivement, le programmeur est censé l’avoir anticipé et avoir prévu dans la partie catch du bloc une manière de récupérer la situation, un filet de sûreté, afin de reprendre le code à ce stade. Sans cela, le code s’interrompt en déclenchant juste l’exception. En présence du try-catch, le code continue et exécute le remède que le programmeur a prévu en réponse à ce problème. Un ensemble d’exceptions déjà répertoriées (comme le fameux NullPointerException en Java ») existent dans les librairies associées aux différents langages de programmation et ne demandent alors qu’à être simplement « rattrapées ». En héritant de la classe Exception (comme dans le code ci-après), le programmeur peut créer ses propres classes d’exception, en accord avec la logique de son code et de manière à bénéficier de ce mécanisme de gestion d’exceptions prêt-à-l’emploi. Dans le code Java qui suit, nous nous limitons à en montrer un exemple à titre pédagogique, dans lequel le programmeur du FeuDeSignalisation prévoit à l’avance ce qui devra se produire si un quelconque utilisateur du code tente de changer la couleur du feu en lui passant une valeur non autorisée. Exemple Java d’exception class FeuDeSignalisation { private int couleur; public void changeCouleur(int nouvelleCouleur) throws MauvaiseCouleurException { if ((nouvelleCouleur >= 1) && (nouvelleCouleur <=3)) /* intégrité assurée */ couleur = nouvelleCouleur ; else throw new MauvaiseCouleurException(nouvelleCouleur); /* C’est à cet endroit précis du code qu’on génère l’exception pour des couleurs non autorisées */ } } /* Puis on définit la classe, sous-classe d’exception qui indiquera ce qu’il y a lieu de faire */ class MauvaiseCouleurException extends Exception { public MauvaiseCouleurException(int couleur) { System.out.println("La couleur " + couleur + " que vous avez rentree n'est pas permise"); } } public class TestException { public static void main(String[] args) { FeuDeSignalisation unFeu = new FeuDeSignalisation(); try { // Toute exception doit être intégrée dans un bloc “ try – catch ” unFeu.changeCouleur(5); } catch (MauvaiseCouleurException e) {System.out.println("L'exception s'est declenchee");} } }

118

L’orienté objet

Résultats La couleur 5 que vous avez rentree n’est pas permise L’exception s’est declenchee La gestion d’exception Toujours dans la perspective de sécuriser au maximum l’exécution des codes, tous les langages OO que nous présentons intègrent dans leur syntaxe un mécanisme de gestion d’exception dont la pratique est très voisine. Seul change le recours obligatoire ou non à cette gestion, Java étant le plus contraignant en la matière. En Java, la non-prise en compte de l’exception sera signalée et interdite par le compilateur. Une exception est levée quand quelque chose d’imprévu se passe dans le programme. Il est possible alors « d’attraper (try-catch) » cette exception et de prendre une mesure correctrice qui permette de continuer le programme malgré cet événement inat-tendu. Paradoxalement, toute la gestion d’exception consiste à rendre l’inattendu plus attendu qu’il n’y paraît, et de se préparer au maximum à toutes les éventualités problématiques ainsi qu’à la manière de les affronter. À l’instar de Java, il apparaît donc assez cohérent de forcer le programmeur à en faire usage.

Pour cloisonner leur traitement Une deuxième justification à l’encapsulation des attributs, et partagée avec l’encapsulation des méthodes, comme nous le verrons dans le chapitre suivant, est de renforcer la stabilité du logiciel à travers le temps et ses multiples et possibles évolutions. Nous avons vu que la classe permet une décomposition naturelle du logiciel, en autant de modules à répartir entre plusieurs programmeurs. Afin que les travaux de chacun des programmeurs ne doivent faire l’objet de révision et d’adaptation, à chaque changement par l’un d’entre eux d’une partie de son code, il est extrêmement important de rendre les codes les plus indépendants possible entre eux. Il faut limiter l’impact dans le reste du code d’un quelconque changement dans une petite partie de ce dernier. Autorisant tous les modules fonctionnels à interagir avec l’ensemble des données du problème, la programmation procédurale ne favorise en rien cette stabilité. En effet, toute transformation dans le typage ou le stockage des données affectera tous ces modules. Comme, de surcroît, ces modules s’imbriquent entre eux, l’impact se propagera, tant en largeur qu’en profondeur. En programmation OO, en revanche, toute modification d’une partie private de la classe n’aura aucun impact sur le reste du programme. Il est bien connu par les développeurs de logiciel que la maintenance du code constitue une dépense aussi importante, sinon plus importante, que l’obtention d’une première version. De manière à diminuer cette dépense, il est capital qu’un travail d’anticipation, concrétisé par l’encapsulation, entraîne les programmeurs à séparer, dans le développement de leur classe, ce qui restera stable dans le temps (en le déclarant comme public) de ce qui est susceptible, encore, de possibles modifications (en le déclarant comme private).

Pour pouvoir faire évoluer leur traitement en douceur Les attributs et leur typage sont de façon typique une partie de code susceptible de nombreuses évolutions dans le temps. Déjà, la manière même de sauvegarder l’état de l’objet sur le disque dur, dont nous traiterons au chapitre 19, risque d’être revue à travers le temps : sauvegarde en tant qu’objet, sauvegarde séparée des attributs dans un fichier ASCII, sauvegarde en tant qu’enregistrement d’une base de données relationnelle, sauvegarde dans une base de données orientée objet. Il devient alors capital, afin de neutraliser l’impact d’un tel changement, de déclarer comme private tout ce qui concerne le stockage des attributs. En effet, seul le type de la lecture de l’attribut, et nullement la manière de le stocker ou le coder, devrait concerner toute autre classe désirant y avoir accès.

L’encapsulation des attributs CHAPITRE 7

119

Considérons la petite situation suivante, qui n’ira pas sans rappeler un certain « bogue » devenu tristement célèbre. À chaque objet voiture est rajouté un attribut codant la date de fabrication que, dans un premier temps, nous décidons, idiotement d’accord (mais c’est uniquement pour l’exemple !), de coder en tant qu’entier écrit sur 8 chiffres, par exemple 20120415 pour le 15 avril 2012 (quatre chiffres pour l’année, c’est cher mais plus malin), et de déclarer cet attribut comme public (plus si malin que ça !). La classe Voiture sera codée de la manière suivante : class Voiture { public int dateFabrication ; //…… autres attributs …… // …… autres méthodes…… }

Considérons également une autre classe, modélisant les possibles acheteurs de véhicule qui, dans les différentes méthodes qui les caractérisent telles que : calculPrix(), négociePrix(), comparePrixAvecArgus(), achete(), font souvent référence à la date de fabrication de la voiture. Par exemple, la méthode négociePrix() pourrait se définir comme suit, en tolérant un accès direct à la date de la voiture : class Acheteur { private Voiture voitureInteressante ; public int négociePrix() { int prixPropose = 0 ; if (voitureInteressante.dateFabrication < 19970101) /* accès possible à l’attribut date */ prixPropose = voitureInteressante.getPrixDeBase() – 10000; } }

Supposons maintenant que le programmeur de la classe Voiture se rende compte, après quelques mois, de l’incongruité qu’il y a à coder la date de cette manière et décide, plus logiquement, de la coder comme un String, c’est-à-dire une chaîne de caractères, par exemple : « 15/04/2012 ». Automatiquement, l’instruction conditionnelle if (voitureInteressante.dateFabrication < 0101997) devient complètement absurde et provoque l’ire du compilateur, car une chaîne de caractères ne peut se comparer à un entier. Le pauvre programmeur de la classe Acheteur en sera réduit à entièrement récrire le code de sa classe (ne le plaignons pas, plus d’un programmeur s’étant enrichi de la sorte), vu qu’il y a de fortes chances que la date de fabrication des voitures soit souvent reprise dans ce code. Quelle solution aurait-elle été plus sécurisée, en garantissant plus de résistance aux changements (nous voulons des programmeurs progressistes mais des classes conservatrices) ? Il aurait fallu que le programmeur de la classe Voiture anticipe que l’attribut dateFabrication puisse subir de nombreux changements dans le temps, et décide qu’il devienne adéquat, dès lors, de séparer son stockage de sa lecture. Dorénavant, quelle que soit la manière dont cet attribut sera typé et stocké, manière déclarée private, il sera toujours lu, donc présenté aux autres classes, comme un String. Prévoir que, dans dix mille ans d’ici, une date sera toujours conçue comme une chaîne de caractères n’est pas faire preuve de si grand don de prescience. La bonne version du code de la classe Voiture devient : class Voiture { private int dateFabrication ; //…. autres attributs ….

120

L’orienté objet

public String getDateFabrication() { String date = null; // ... instructions qui transforme l’entier //date en un string ……; return date ; } // …… autres méthodes …. }

Cette nouvelle écriture de la classe conduira à accroître la stabilité de l’ensemble du logiciel car, si le stockage ou le typage de l’attribut dateFabrication change, il faudra simplement adapter le corps d’instructions de la méthode de la classe Voiture qui renvoie l’attribut. Aucune autre classe ne se trouvera plus affectée et, de ce fait, l’impact d’un tel changement restera confiné à la classe elle-même.

La classe : enceinte de confinement En plus d’un type, d’un fichier, d’un garant de l’intégrité des objets, la classe se doit d’être, également, une enceinte de confinement. La conception du logiciel demande un travail d’anticipation, destiné à ne laisser public que ce qui est appelé à se transformer le moins, au fil du temps et des versions du logiciel. Il est évident que cette pratique ne prend vraiment toute sa raison d’être qu’avec le grossissement des projets informatiques et la multiplication des programmeurs. Plus la taille d’un programme devient importante, plus il est crucial de pouvoir facilement le décomposer et de distribuer les modules entre plusieurs développeurs, qui seront incités, dans leur pratique, à rechercher l’équilibre parfait entre les modifications incessantes de leur code et le peu d’impact que celles-ci provoquent sur les développements de leurs collègues. C’est aussi la raison pourquoi ce même mécanisme est souvent difficile à faire avaler aux étudiants qui, pour l’essentiel de leurs travaux de programmation, réaliseront, seul ou à très peu, un minuscule programme de mille lignes, sur lequel ils maintiendront l’entièreté du contrôle et qu’ils se dépêcheront d’oublier une fois l’évaluation obtenue. Situations parfaitement antagonistes à celles qui réclament l’encapsulation. Plus d’un étudiant a été surpris à déclarer les attributs comme public... Ah ! les traîtres… Stabilisation des développements Il y a une seconde raison de déclarer les attributs comme private, commune aux méthodes : c’est d’éviter que tout changement dans le typage ou le stockage de ceux-ci ne se répercute sur les autres classes.

Exercices Exercice 7.1 Réalisez un petit code qui stocke un attribut date comme un entier, et autorise sa lecture par les autres classes uniquement comme un String.

Exercice 7.2 Si une classe contient 10 attributs, combien de méthodes d’accès à ses attributs vous paraissent-elles nécessaires ?

L’encapsulation des attributs CHAPITRE 7

121

Exercice 7.3 Réalisez une classe de type compte en banque, en y intégrant deux méthodes, l’une déposant de l’argent, l’autre en retirant, et dont vous vous assurerez que l’attribut solde ne puisse jamais être négatif.

Exercice 7.4 Dans la même classe que celle de l’exercice précédent, écrivez une méthode d’accès au solde, qui retourne ce dernier comme un entier, alors qu’il est stocké comme un réel.

8 Les classes et leur jardin secret Ce chapitre poursuit l’exposé de la pratique de l’encapsulation en l’étendant aux méthodes. Il sépare l’interface d’une classe de son implémentation. Il justifie cette encapsulation par la stabilisation des développements qu’elle améliore. Il discute les différents niveaux d’encapsulation rendus possibles dans les langages de programmation. Il termine par une petite allusion aux systèmes complexes dont se rapproche tant la pratique de l’OO et qui contribue à en permettre la maîtrise.

Sommaire : Méthode publique ou privée — Interface et implémentation — Améliorer la stabilité des développements — Niveaux intermédiaires d’encapsulation : amitié, héritage, paquetage, classes imbriquées — L’effet papillon dans les systèmes complexes Doctus — Imagine que nous souhaitions modifier un mécanisme interne à un objet. Nous en avons toute liberté pour peu qu’on ait pris la précaution de limiter son usage, à celui privé de notre boîte noire ! Candidus — Oui, mais la modification des méthodes publiques qui font partie de l’interface perturbe les relations avec les autres objets du programme ! Doc. — Ce n’est pas dit… Tu peux fournir un accès public à une fonctionnalité tout en évitant d’en dire trop. Il s’agit juste de dire à un objet ce qu’on attend de lui sans pour autant lui dire comment il doit s’y prendre ! C’est lui qui doit savoir comment implémenter la chose, avec ses propres méthodes privées. Cand. — Et c’est comme ça que je pourrai les bidouiller sans craindre d’entendre crier : « Ça marche plus, je parie que t’as encore fais une modif au message faismoica_302_v1r3() ! ». Doc. — L’héritage lui-même doit être réglementé, les parents doivent pouvoir décider de ce qu’ils gardent pour eux. Cand. — Je pourrais donc appliquer ce même principe de cloisonnement face aux utilisateurs de mes classes ! Ils n’hériteront que des méthodes que j’aurai choisi de mettre à leur disposition. Doc. — On peut également constituer des relations de groupe. Des classes d’objets travaillant en équipe pourront utiliser un vocabulaire commun tout en restant inaccessibles au public. Cand. — Ni privé ni public, un jargon de spécialistes en quelque sorte ! Doc. — Ou comme des amis qui parlent de ce qu’ils ont en commun alors que l’entourage n’est pas du tout concerné. Encore plus fort : dans une voiture, le volant, l’accélérateur et le frein peuvent n’exister que pour « l’objet » conducteur. Cand. — Hmmm… Ton conducteur me fait penser à un chauffeur esclave de sa voiture qui attend ses ordres pour pouvoir s’amuser sur ses pédales.

124

L’orienté objet

Doc. — On peut effectivement faire dans le genre poupées russes : des objets complètement imbriqués les uns dans les autres. Cand. — Des classes d’objets privées alors ! Doc. — Autre direction maintenant. Après la fermeture de nos boîtes noires pour interdire l’accès à leurs rouages internes, il faudra également prévoir de les interconnecter pour construire notre système. Il nous restera à doser raisonnablement la complexité des branchements du réseau de communications entre tous ces objets. Il s’agira alors d’éviter les cascades d’événements inextricables !

Encapsulation des méthodes Idéalement, même la simple lecture des attributs ne devrait que très rarement constituer le contenu de méthode publique. La raison en est simple. Les autres classes ont-elles jamais besoin de simplement lire les attributs d’une classe donnée ? Exceptionnellement. Le plus souvent, elles modifient ces attributs ou les utilisent à travers une méthode, afin qu’à partir de la valeur de ceux-ci, une nouvelle activité se déclenche, quitte à se propager de classes en classes. Simplement les lire, et rien d’autre, n’apparaîtra que très rarement utile. Cela nous amène naturellement à une nouvelle règle de bonne conduite OO, à rajouter à la charte du bon artisan OO : Méthode private En plus des attributs, une bonne partie des méthodes d’une classe doit être déclarée comme private.

Interface et implémentation On différencie les méthodes private des méthodes public en déclarant, comme nous l’avions anticipé dans un chapitre précédent, que les premières sont responsables de l’implémentation de la classe, alors que les secondes le sont de l’interface de la classe. Comme indiqué à la figure 8-1, la partie interface d’une classe doit rester réduite par rapport à son implémentation. Plus cette partie sera réduite, plus la possibilité d’un changement dans celle-ci est réduite, et moins conséquent devient le possible impact des changements dans cette classe. Gardez à l’esprit que l’interface ne reprend de toutes les méthodes de la classe, que les seuls possibles messages, c’est-à-dire les signatures des méthodes publiques. Ce sont ces seules signatures qui ne peuvent évoluer dans le temps, car même le corps des méthodes identifiées par ces signatures peut être modifié, sans conséquence sur les autres classes. De son côté, la partie private est un large espace maintenu de modifications possibles, tout comme un chantier en cours. Figure 8-1

La séparation dans une classe entre une large partie implémentation et une plus petite partie interface.

Les classes et leur jardin secret CHAPITRE 8

125

Toujours un souci de stabilité Cette séparation force tout programmeur d’une classe à réfléchir de façon anticipée, afin de tenir clairement détachées les méthodes qu’ils prédestinent aux autres classes de celles qui font partie du jardin secret de la classe qu’il programme. Tout changement dans une classe qui se produit dans les méthodes d’implémentation n’affectera d’aucune manière le codage de toutes les classes interagissant avec celle-là. Les méthodes private de la classe agissent dans la mesure où elles sont appelées dans les méthodes public de cette classe. Ce qui se révèle ne pas être possible pour les premières, c’est qu’elles soient appelées de l’extérieur de la classe. Ainsi dans les deux petits codes qui suivent, en Java (en C++, C# et PHP c’est parfaitement équivalent) et en Python, la méthode privée pasSetCouleur(), qui se déclenche si la couleur passée n’est pas autorisée, ne pourra être appelée que de l’intérieur de la classe. En Java class FeuDeSignalisation { private int couleur; public FeuDeSignalisation(int couleur) { if ((couleur >= 1) && (couleur <=3)) { this.couleur = couleur ; } } public int getCouleur() { return couleur; } private void pasSetCouleur(int nouvelleCouleur) { System.out.println ("pas bonne couleur, la: " + nouvelleCouleur); } public void setCouleur(int nouvelleCouleur) { if ((nouvelleCouleur >= 1) && (nouvelleCouleur <=3)) couleur = nouvelleCouleur ; else pasSetCouleur(nouvelleCouleur); // appel de la methode privée } } public class TestPrive { public static void main(String[] args) { FeuDeSignalisation unFeu = new FeuDeSignalisation(2); System.out.println(unFeu.getCouleur()); unFeu.setCouleur(5); System.out.println(unFeu.getCouleur()); /* unFeu.pasSetCouleur(5); ici, on ne peut appeler cette méthode privée */ } }

126

L’orienté objet

Resultats 2 pas bonne couleur, la : 5 2

En Python class FeuDeSignalisation: __couleur = 0 def __init__(self,couleur): if couleur>0 and couleur <=3: self.__couleur=couleur def __pasSetCouleur(self,nouvelleCouleur): #methode privée par #le double underscore print "pas bonne couleur, la: %s" %(nouvelleCouleur) def getCouleur(self): return self.__couleur def setCouleur(self,nouvelleCouleur): if nouvelleCouleur>0 and nouvelleCouleur<=3: self.__couleur=nouvelleCouleur else: self.__pasSetCouleur(nouvelleCouleur) #appel de la méthode privée

unFeu=FeuDeSignalisation(2) print unFeu.getCouleur() unFeu.setCouleur(5) print unFeu.getCouleur() #unFeu.__pasSetCouleur(5) ici, on ne peut appeler cette méthode privée

Notez qu’une telle pratique, que l’on cherche à encourager par la programmation OO, est déjà monnaie courante dans bien d’autres secteurs de l’industrie. Avez-vous l’impression que l’interface des voitures, des téléphones, des frigos, des machines à laver, se soit considérablement modifiée depuis des années ? Mais conduisez aux États-Unis, et vous subirez de plein fouet les inconvénients d’un changement d’interface de la classe Voiture sur la classe Conducteur (sans parler également des contraventions pour excès de vitesse, mais cela c’est une autre histoire). En revanche, l’implémentation des moteurs ou des téléphones a subi des changements substantiels. La technologie des moteurs automobiles s’est largement améliorée, les moteurs, autrefois à injection indirecte, sont aujourd’hui à injection directe, alors que votre mode de conduite ne s’en est, en rien, ressenti. La raison, en termes OO, tient au fait que tout ce qui concerne l’allumage du mélange de carburant, l’explosion… de l’objet voiture reste du domaine privé et donc inaccessible à l’objet conducteur, même s’il utilise son briquet. Les téléphones sans fil et ceux avec fil reposent sur des protocoles de communication foncièrement différents, sans que votre manière de téléphoner ne s’en trouve affectée. Dans le premier chapitre, relatant ce que vous observiez par la fenêtre, vous vous êtes limités à ne citer que la voiture, sans détailler sa structure car, là encore, l’interface que vous utilisez ne requiert pas une connaissance

Les classes et leur jardin secret CHAPITRE 8

127

structurelle du véhicule. S’il est possible que, dans la déclaration de la classe Voiture, on retrouve des attributs agrégés de type moteur ou roue, il y a peu de chances que l’interface de Voiture y fasse une allusion explicite dans les signatures des méthodes. Cette séparation private/public ne se fait pour le programmeur qu’au prix d’un travail d’anticipation concernant les fonctionnalités de ces classes qu’il juge stables dans son code pour de nombreuses années (et qu’il peut rendre publiques et accessibles) et celles qu’il soupçonne d’être susceptibles de changer (et qu’il vaut mieux garder private). Avez-vous constaté que lorsque vous changez l’imprimante de votre ordinateur, vous n’avez pas à recompiler toutes les applications – et elles sont nombreuses – dans lesquelles vous avez la possibilité d’imprimer un document ? C’est toujours la même idée : tous les objets imprimantes, quelle que soit la manière physique (leur implémentation) dont ils le font (laser, matriciel, jet d’encre), sont capables d’imprimer un document, et donc de s’interfacer adéquatement à la fonction print de ces applications. Vous ne trouverez jamais une fonctionalité de manipulation de laser dans les menus à votre disposition dans le traitement de texte que vous utilisez.

Signature d’une classe : son interface Dans le schéma d’interaction entre classes, qui est la base de la programmation OO, il est, en vérité, plus correct de parler d’interaction entre interfaces qu’entre classes. Seules les interfaces apparaissent comme disponibles aux autres classes. De là, la pratique courante, que nous approfondirons plus avant dans le chapitre 15, qui consiste à extraire de chacune des classes la seule partie visible par les autres, celle qu’elle met à disposition des autres : son interface. À nouveau, la syntaxe de certains langages vous permet de forcer le trait (toujours cette assistance, dans les bons langages OO, de la syntaxe, à vous encourager à une bonne pratique de l’OO), par l’existence d’une structure syntaxique d’interface, qui sera héritée par la classe implémentant cette interface.

Interaction avec l’interface plutôt qu’avec la classe Lorsqu’une classe interagit avec une autre, il est plus correct de dire qu’elle interagit avec l’interface de cette dernière. Une bonne pratique de l’OO vous incite, par ailleurs, à rendre tout cela plus clair, par l’utilisation explicite des interfaces comme médiateurs entre les classes.

Les niveaux intermédiaires d’encapsulation Nous n’avons vu que deux modes d’accès possibles pour les propriétés d’une classe : public, pour les rendre accessibles à toutes les autres et qu’il convient d’utiliser avec prudence, et private, pour les rendre inaccessibles aux autres, et que l’on peut consommer sans modération. Certains langages de programmation introduisent des raffinements additionnels pour ce mode d’accès, en en tolérant des niveaux intermédiaires. Par exemple, une classe pourrait décider de se rendre entièrement accessible à quelques autres classes, privilégiées, qu’elle déclarera comme faisant partie de ses « amies ». Qu’une classe déclare une autre comme étant son amie (utilisation du mot-clé friend), et ce qui est private dans la première deviendra public pour la seconde. Elle tolérera un début d’atteinte à sa vie privée. C++ est un de ces langages qui, au contraire de Java et de C#, permettent ce raffinement additionnel dans la mise en œuvre de l’encapsulation. Ainsi, dans le petit code C++ qui suit, l’objet O2 peut utiliser une méthode déclarée private dans la classe O1, car cette dernière a accepté d’ouvrir son cœur à la classe O2. La classe O2 est déclarée comme friend de la classe O1.

128

L’orienté objet

#include "stdafx.h" #include "iostream.h" class O1 { private: int a; void jeTravailleSecretementPourO1() /* méthode déclarée private */ { cout <<"la valeur de a est: " << a << endl; } public: O1(int initA):a(initA) {} friend class O2; /* la classe O2 est déclarée comme amie de la classe O1, ce qui lui donne ➥ un droit de regard privilégié sur O1 */ }; class O2 { public: O2() { O1 unO1(5); unO1.jeTravailleSecretementPourO1(); /* O2 peut utiliser cette méthode pourtant « private » ➥ dans O1 */ } }; int main(int argc, char* argv[]) { O2 unO2; return 0; }

Résultat la valeur de a est : 5

Dans les langages permettant aux classes de se faire quelques amies, comme dans la réalité hélas, l’amitié n’est ni symétrique ni transitive (non, les amis de vos amis ne seront plus automatiquement vos amis).

Une classe dans une autre Une autre possibilité de rendre accessible les attributs et les méthodes déclarés private dans une classe à une autre classe se présente lorsque cette seconde classe est créée à l’intérieur de la première. Ce système de classes imbriquées l’une dans l’autre n’est pas des plus simples à mettre en œuvre, et ne devrait être exploité que très rarement, vu les autres modes, plus intuitifs, qui vous sont proposés pour associer deux classes. Les deux codes qui suivent, le premier en Java et l’autre en C#, vous montrent comment, en effet, une classe peut être déclarée à l’intérieur d’une autre. La classe englobée aura un accès privilégié à tout ce qui constitue la classe englobante. À nouveau, n’utilisez ce stratagème que si vous voulez, le plus étroitement qui soit, solidariser le fonctionnement et le développement des deux classes. En Java class O4 { public O4() { O3.DansO3 unTest = new O3.DansO3(); // il est possible d'utiliser directement la classe englobée } }

Les classes et leur jardin secret CHAPITRE 8

129

public class O3 /* définition de la classe englobante */ { static private int a; public O3(int b) { a = b; } public static void jeTravaillePourO3() { a = 5; DansO3 unDansO3 = new DansO3(); } static class DansO3 /* définition imbriquée d'une nouvelle classe englobée par la première */{ private int b; public DansO3(){ b = a; /* malgré qu'il soit privé dans la classe englobante, a est accessible par la classe ➥ englobée */ System.out.println("la valeur de b est : " + b); } } public static void main(String[] args){ jeTravaillePourO3(); O4 unO4 = new O4(); } }

Résultat la valeur de b est : 5 la valeur de b est : 5

L’équivalent en C# using System; class O4 { public O4(){ O3.DansO3 unTest = new O3.DansO3(); } } public class O3{ static private int a; public O3(int b){ a = b; } public static void jeTravaillePourO3() { a = 5; DansO3 unDansO3 = new DansO3(); } public class DansO3{ private int b; public DansO3(){ b = a; Console.WriteLine("la valeur de b est : " + b); }

130

L’orienté objet

} public static void Main(){ jeTravaillePourO3(); O4 unO4 = new O4(); } }

Utilisation des paquetages Une autre possibilité encore, que nous retrouverons dans un prochain chapitre détaillant le mécanisme d’héritage, consiste à ne permettre qu’aux seuls enfants de la classe (ses héritiers) un accès aux attributs et méthodes protected du parent. Si j’ai droit à l’héritage, pourquoi n’aurais-je pas le droit d’exploiter toutes les caractéristiques dont j’hérite. Attendez de le savoir petit vénal ! Enfin, une ultime possibilité permet de libérer l’accès, uniquement aux classes faisant partie d’un même paquetage, en général, quand les fichiers ne contiennent qu’une classe, aux fichiers faisant partie d’un même répertoire. Par exemple, en Java, quand vous n’indiquez ni private ni public, comme mode d’accès pour les propriétés de la classe, le mode par défaut est celui limité aux seuls paquetages. Dans le code Java, ci-après, la classe O1, ne rend disponible sa méthode, jeTravailleSecretementPourO1(), précédée d’aucun mot-clé d’accès, qu’uniquement aux classes présentes dans le même paquetage ou « package » (OO1 dans le code). package OO1; /* déclaration qui intègre la classe dans le package OO1. Le nom de la classe, ➥ dorénavant, sera précédé d’OO1 */ public class O1 { private int a; void jeTravailleSecretementPourO1() /* sans rien indiquer, la méthode ne sera accessible qu’à ➥ partir du même package OO1 */{ System.out.println("la valeur de a est: " + a); } public O1(int initA) { a = initA; } }

Les paquetages existent également en C# et C++, où ils sont nommés namespace, « espace de nommage », ce qui leur correspond, en effet, plus fidèlement. L’équivalent en C# du code Java précédent est : using System; namespace OO1 { /* déclaration du namespace */ public class O1{ private int a; void jeTravailleSecretementPourO1() /* sans rien indiquer ou en utilisant un mot-clé additionnel ➥ « internal », la méthode ne sera accessible qu’à partir du même namespace OO1 */{ Console.WriteLine("la valeur de a est: " + a); } public O1(int initA){ a = initA; } } }

Les classes et leur jardin secret CHAPITRE 8

131

En C#, et en .Net en général, il faut néanmoins faire la différence entre les concepts de namespace et d’assembly (les .dll de Microsoft). On installe les fichiers classes dans l’assembly lors de l’opération de compilation. Par exemple, ci-dessous, les versions exécutables de trois fichiers classes sont installées dans l’assembly exempleAssembly.dll. csc /t :library /out :exempleAssembly.dll Classe1.cs Classe2.cs Classe3.cs

Plutôt qu’aux namespace, les niveaux d’accès et d’encapsulation seront plutôt relatifs à la découpe des classes et des fichiers correspondants en « assembly ». Il y a donc tout intérêt à nommer les namespace et les assembly de la même manière, afin de faciliter la compréhension et la gestion de l’ensemble des classes. Le namespace de ces trois classes serait donc ici : exempleAssembly. Ces niveaux d’accessibilité intermédiaire ont le défaut d’accroître la portée d’un changement effectué dans une petite partie du code. Ils sacrifient, de ce fait, l’effort anticipatif qui consiste à jouer de cet accès avec la plus grande prévoyance aux futurs changements plus étendus, faisant suite à une modification quelconque du code. À vous de choisir. Mais, là encore, la charte du bon programmeur OO, plébiscitée par ce livre, vous encourage à n’utiliser que les deux seuls accès, private et public, et avec une grande parcimonie quant au second. Désolidariser les modules Alors que les deux niveaux extrêmes de l’encapsulation – « private » : fermé à tous et « public » : ouvert à tous – sont communs à tous les langages de programmation OO, ceux-ci se différencient beaucoup par le nombre et la nature des niveaux intermédiaires. De manière générale, cette pratique de l’encapsulation permet, tout à la fois, une meilleure modularisation et une plus grande stabilisation des codes, en désolidarisant autant que faire se peut les réalisations des différents modules.

Afin d’éviter l’effet papillon La physique d’aujourd’hui s’intéresse de près à la modélisation et la compréhension de systèmes complexes composés de multiples agents simples en interaction. Stuart Kauffman est un de ces chercheurs qui se sont appliqués à comprendre le fonctionnement de tels réseaux, et surtout, l’impact sur ce fonctionnement de la façon dont les agents sont interconnectés. De ces nombreuses études effectuées sur des systèmes et réseaux aussi variés que les réseaux de neurones, réseaux génétiques, immunitaires, verre de spin et autres, il résulte que ces systèmes ont les comportements les plus riches quand les agents ne sont pas insuffisamment connectés entre eux et quand ils ne le sont pas trop. Stuart A. Kauffman et Albert-Lazlo Barabasi Stuart Kauffman est un de ces chercheurs qui auront marqué (et continuent à le faire) très durablement les sciences de la complexité. Biologiste de formation, et longtemps un des piliers du célèbre Institut Santa Fe, il a depuis fondé sa propre compagnie, Bios Group, dans laquelle il met ses compétences en matière de systèmes complexes au service des problèmes économiques et managériaux. Aujourd’hui, il est retourné au monde académique en acceptant une charge professorale à l’université de Calgary. Un système complexe est un système non décomposable, constitué de multiples agents, généralement au comportement individuel simple, mais interconnectés entre eux. La biologie foisonne de pareils systèmes : écosystèmes, réseaux de neurones, réseaux immunitaires, réseaux génétiques, réseaux cellulaires, etc. Ce que Kauffman s’est surtout efforcé de montrer, par des simulations informatiques aussi originales que convaincantes, c’est qu’il existe dans ces réseaux une manière pour les agents de s’interconnecter entre eux, qui rendent leur comportement, ni totalement figé, ni totalement chaotique, mais quelque part entre les deux, au bord du chaos. C’est dans cet entre-deux, dans ce régime intermédiaire, que les systèmes exhibent les comportements dynamiques les plus riches d’intérêt, en termes d’adaptabilité et de stockage d’information.

132

L’orienté objet

Auteur de nombreux ouvrages importants, un de ces derniers s’intitule simplement Chez soi dans l’Univers – La recherche des lois de l’auto-organisation. Dans cet ouvrage, il montre que les systèmes complexes tendent spontanément et gratuitement à se structurer de manière à produire des comportements complexes émergents. Cette complexité résulte du fonctionnement collectif des unités en interaction et se produit très simplement et très naturellement. En affirmant cela, Kauffman cherche à contrer une opinion très répandue en biologie, qui consiste à attribuer toute la complexité des systèmes à la seule évolution darwinienne. Dans cette vision, des systèmes de plus en plus complexes apparaissent car survivant au fil de l’évolution la sélection darwinienne. Les simulations de Kauffman montrent que les systèmes biologiques sont capables très spontanément de comportements complexes, sans pour cela subir de pression sélectionniste. Ce qui nous intéresse le plus dans ses travaux, c’est la nécessité pour les agents constituant ces systèmes de maintenir entre eux des interactions de portée réduite. Ils le font pour une raison très proche de celle qui justifie la structure d’interaction entre objets dans les développements OO. Comme dans les écosystèmes, comme dans le cerveau, comme dans les réseaux génétiques, les agents doivent interagir avec un minimum de leurs collègues. Et ce de manière à véhiculer l’information le plus subtilement possible, ni trop ostensiblement, tout le monde influençant tout le monde, ni trop timidement, personne n’influençant personne. Dans sa suite, Albert-Lazlo Barabasi, professeur de physique à l’université américaine de Notre-Dame, a décelé dans une très large variété de réseaux informatiques, biologiques, sociaux et de transport (réseaux qu’ils a analysé à la loupe), que ceux-ci ne présentent pas une topologie aléatoire et une structure de connectivité uniforme. En revanche, il y a décelé un petit nombre de nœuds possédant un très grand nombre de connexions. Leur nombre reste très largement inférieur à celui des nœuds faiblement connectés mais se trouve être beaucoup plus important que dans un cas purement aléatoire. Ce sont ces nœuds singuliers mais stratégiques, comme autant de carrefours de ce réseau, que l’on désigne par l’appellation de « connecteur ». Leur position centrale en matière de connectivité ainsi que leur nombre plus important que par simple tirage aléatoire les rendent responsables de nombreuses propriétés et fonctions caractérisant les réseaux qui les hébergent. Les diagrammes de classe des grands codes OO tendent à vérifier cette topologie non aléatoire en présence de quelques classes centrales fortement connectées et un très grand nombre de classes qui le sont beaucoup moins.

Par exemple, parmi les trois réseaux de la figure 8-2, c’est le troisième qui présenterait le comportement le plus riche d’intérêt. Dans un réseau insuffisamment connecté, l’isolation des agents ne permet pas à des comportements émergents, c’est-à-dire innovants par rapport au seul comportement des agents, de se produire. Toute modification de l’agent reste cantonnée à lui-même, et ne produit aucun impact sur les autres. Le système est rigide, gelé, complètement décomposable, non plus uniquement lors de sa conception, ce qui est souhaitable mais, aussi, lors de son fonctionnement, ce qui l’est beaucoup moins. En revanche, dans un réseau largement interconnecté, c’est-à-dire quand un agent se trouve en moyenne connecté à plus de 3 ou 4 autres agents, le comportement devient complètement chaotique. La moindre modification sur un agent se propage sur tous les autres, au risque de produire des comportements toujours instables et imprédictibles. L’impact qui va sans cesse s’amplifiant, bien que résultant d’une petite perturbation locale, a été métaphoriquement dénommé par les physiciens « d’effet papillon », quand le battement d’aile d’un papillon à Paris est responsable de l’arrivée d’un ouragan en Floride. Une conséquence première serait d’intensifier la chasse aux papillons dans les rues de Paris. Mais une pratique plus facile à mettre en œuvre, c’est de limiter la connectivité entre agents à 1 ou 2 voisins, ce qui permet l’émergence de nouveaux et intéressants comportements, et surtout une certaine stabilité et robustesse de ces comportements face à des perturbations locales. Il en va de la physique des systèmes complexes comme de la programmation orientée objet, ce qui est plutôt heureux. Dans les deux cas, on simplifie un problème en le décomposant en un ensemble d’agents simples et en interaction, dont l’effet, collectif, produit les comportements souhaités. Comme pour un réseau de neurones, un ensemble d’agents stupides, mais en relation, peut produire, in fine, un comportement collectif et temporel complexe. Ensuite, s’il est inévitable de faire interagir les classes pour obtenir un comportement émergent

Les classes et leur jardin secret CHAPITRE 8

133

Figure 8-2

Trois structures d’interconnexion entre les éléments d’un réseau. Dans les deux premiers réseaux, les éléments sont trop ou insuffisamment connectés. Le troisième réseau présente un schéma d’interconnexion idéal.

complexe, il reste à maintenir cette interaction à un faible niveau, de manière à stabiliser l’ensemble du système face à des changements indésirables. Il y a deux manières d’affaiblir cette interaction. La première est de ne faire interagir la majorité des classes qu’avec un minimum d’autres classes. Cela permettra de limiter la portée de l’impact d’une modification dans le code d’une classe. Par exemple, toujours dans les réseaux de neurones, chaque neurone ne se trouve connecté qu’à 1/10000000e de tous les autres. Ainsi, les espèces animales n’entretiennent des relations qu’avec très peu d’autres espèces (et les informaticiens, pour leur part, ont tendance à se reproduire entre eux…). La seconde est de ne permettre, dans chacune des classes, qu’à peu de méthodes de se transformer en messages. Il faut que l’interface soit une partie très congrue de la classe. Là encore, la portée d’un changement dans une classe s’en trouvera largement minimisée. Certains chercheurs ont étudié la manière dont les classes étaient connectées dans les librairies Java. Ils ont remarqué un petit nombre de classes clés extrêmement connectées aux autres et un grand nombre de classes faiblement connectées. Cette structure de connectivité s’avère commune à tous les réseaux de nature humaine tels, par exemple, les réseaux d’affinité sociale : quelques connecteurs essentiels connaissent tout le monde et sont connus de tous, tandis qu’un grand nombre d’êtres humains sont beaucoup plus faiblement connectés. C’est la même structure de connectivité que l’on retrouve lorsqu’on étudie la manière dont les ordinateurs sont connectés sur Internet et dont les sites web sont connectés (par les hyperliens) sur le Web. Cette topologie allie les avantages de la robustesse (il faut éliminer les connecteurs moins nombreux pour entamer le réseau), de la vitesse de communication (ce réseau est qualifié de petit monde car les nœuds restent très proches les uns des autres) à l’économie de conception (il y a très peu de liens entre les nœuds).

134

L’orienté objet

Exercices Exercice 8.1 Décrivez les différents modes d’encapsulation existant dans les langages OO et ordonnez-les, des plus sévères au moins sévères.

Exercice 8.2 Voici quelques méthodes constitutives de la classe Voiture ; séparez les méthodes faisant partie de l’interface de la classe de celles faisant partie de son implémentation : tourne, accélère, allumeBougie, sortPiston, coinceRoue, changeVitesse, injecteEssenceDansCylindre.

Exercice 8.3 Comment une méthode déclarée private dans une classe sera-t-elle indirectement déclenchée par une autre classe ?

Exercice 8.4 En quoi l’existence d’assemblage de classes peut compenser l’absence des relations d’amitié ?

Exercice 8.5 Pourquoi l’interface est tout ce qu’une classe B doit connaître d’une classe A, si elle désire communiquer avec cette dernière ?

Exercice 8.6 À votre avis, pourquoi l’amitié en C++ n’est-elle pas transitive ?

9 Vie et mort des objets Ce chapitre a pour objectif de présenter les différentes manières d’effacer les objets de la mémoire pendant qu’un programme s’exécute. Nous verrons comment les langages utilisés dans ce livre traitent de ce problème : de la version libérale du C++, confiant la responsabilité au seul programmeur, aux versions plus « marxistes » de Java, Python et PHP 5, laissant un système de régulation centralisé extérieur, appelé ramasse-miettes, s’en occuper, en passant par la troisième voie chère à Tony Blair et proposée par le C#.

Sommaire : Gestion de la mémoire RAM — Dépenses de mémoire inhérentes à l’OO — Mémoire pile et mémoire tas — Le « delete » du C++ — Le ramasse-miettes de Java, C#, PHP 5 et Python

Candidus — Comment les objets s’arrangent-ils avec la mémoire ? Doctus — Un objet est constitué d’un ensemble de données et de méthodes pour les manipuler. Lorsqu’il entre en scène un processus de chargement réserve la place nécessaire au stockage de ces deux ingrédients. Cand. — Tu veux dire que les segments de code doivent également entrer dans les préoccupations du programmeur ! Tu parles d’un progrès ! Doc. — Bien que le code ne soit chargé qu’à un seul exemplaire, un objet est tout de même plus encombrant qu’une donnée primitive. Mais tu oublies une chose, il disposera des mécanismes nécessaires pour traiter la question. Sa suppression de la mémoire fait partie de son cycle de vie. Cand. — Veux-tu dire que ça se fait tout seul ? Doc. — En C++, non, mais en Java, C#, PHP 5 et Python, tu disposes de l’allocation et de la libération automatiques de mémoire. Cand. — Je me contente donc d’appliquer les principes de localisation des données là où elles seront utilisées plutôt que de les allouer globalement au début du programme ? Doc. — C’est bien ce que proposent ces quatre langages. Le mécanisme du ramasse-miettes, encore appelé Garbage Collector, prendra le soin de déterminer les circonstances où les données temporaires ont fini de servir et s’arrangera pour n’intervenir qu’en cas de réel besoin.

136

L’orienté objet

Cand. — Cela semble miraculeux… Comment fait ce ramasse-miettes pour savoir à coup sûr qu’une donnée ne sera plus utilisée ? Doc. — Il n’y a aucune magie derrière tout ça ! Ces langages disposent d’un mécanisme pour détecter les occasions où le programme coupe les liens avec ses données temporaires. L’idée principale repose sur le fait que le seul moyen de créer un objet consiste à utiliser les zones mémoire contrôlées par la machine virtuelle et que cette même machine peut détecter que cet objet est devenu inutilisable et parfait pour la « casse ».

Question de mémoire Un rappel sur la mémoire RAM Object wanted : dead or alive ! Ce chapitre a la sinistre mais non impossible mission de vous expliquer le cycle de vie des objets, comment ils vécurent et comment ils sont morts. Vous en voulez encore ? Alors écoutez l’histoire de … Mais d’abord, quelques rappels élémentaires sur le fonctionnement d’un ordinateur lorsqu’il exécute un programme seront bienvenus en guise d’introduction. Un programme, pour qu’il s’exécute, nécessite, avant tout, de l’espace mémoire, pour pouvoir y stocker les données qu’il manipule et les instructions responsables de ces manipulations. Lors de l’exécution d’un programme OO, il faudra pouvoir stocker, et les objets et les méthodes. La mémoire dite RAM, ou vive ou encore centrale, sert à cela. C’est une mémoire rapidement accessible, volatile et chère, au contraire du disque dur qui lui, le pauvre, est lent à la détente, mais permanent et à bon marché. De plus, elle doit se partager entre les multiples programmes qui peuvent s’exécuter en même temps, chacun ayant droit à sa part du gâteau. Les différents programmes auront une zone mémoire propre qui leur sera réservée, comme un casier dans un vestiaire, et qu’ils utiliseront exclusivement durant leur exécution. Aujourd’hui, on dit que les applications sont bien cloisonnées entre elles, ce qui permet d’éviter que l’une s’aventure dans un territoire réservé à l’autre, car, à l’instar des guerres de gangs à Los Angeles, cela peut faire beaucoup de dégâts. Bien sûr, lorsque le programme s’interrompt, qu’il soit normalement ou anormalement terminé, toute la mémoire se vide, et c’est alors l’hécatombe du côté des objets. Et c’est bien pour cela qu’il faudra, si ce que ceux-ci sont devenus vous importe encore, vous préoccuper de sauver leur état, d’une manière ou d’une autre, sur le disque dur (nous aborderons la sauvegarde des objets sur le disque dur au chapitre 19). Pour qu’un programme tourne vite, il est idéal que toutes les données et instructions qu’il manipule puissent être stockées dans la RAM, sinon le programme rame… Ce que l’on ne peut installer dans la RAM pourra, en dernier recours, être stocké sur le disque dur (on parle alors de mémoire virtuelle), provoquant en cela un effondrement des performances, vu que celui-ci prend pour l’extraction des données un million de fois plus de temps que la mémoire RAM. Cette mémoire-là est donc extrêmement précieuse mais, vu sa sophistication et son prix, non extensible à l’infini. Par ailleurs, comme vous l’aurez constaté dans la pratique, en installant la nouvelle version de votre logiciel favori, plus on en a, plus on en use, pour ne pas dire abuse. La gourmandise (ou plutôt l’avidité des applications) s’adapte à la disponibilité des ressources. La mémoire RAM brûle les poches des développeurs d’application. De fait, une des préoccupations des programmeurs d’antan était d’économiser les ressources de l’ordinateur lors du développement des applications, le temps calcul et la mémoire. Aujourd’hui, l’existence même de pratique informatique comme l’OO permet de s’affranchir quelque peu de ce souci d’optimisation, pour le remplacer graduellement par un souci de simplicité, clarté, adaptabilité et facilité de maintenance. Ce que l’on gagne d’un côté, on le perd ailleurs. En effet, la pratique de l’OO ne regarde pas trop à la dépense, et ce, à plusieurs titres.

Vie et mort des objets CHAPITRE 9

137

L’OO coûte cher en mémoire L’objet, déjà en lui-même, est généralement plus coûteux en mémoire que les simples variables int, char ou double de type prédéfini. Il pousse à la dépense. De plus, rappelez-vous le « new », qui vous permet d’allouer de la mémoire pendant le déroulement de l’exécution du programme, et ce n’importe où. Alors pourquoi s’en priver ? À la différence d’autres langages, tout l’espace mémoire utilisé pendant l’exécution du programme n’est pas déterminé à l’avance, ni optimisé par l’étape de compilation. Par ailleurs, certains langages OO, et non des moindres comme C++, sont des grands consommateurs d’objets temporaires utilisés, ou dans le passage d’argument ou comme variable locale (nous reviendrons sur ce point précis dans la suite). Bien que la pratique de l’OO soit une grande consommatrice de mémoire RAM, et que celle-ci va s’accroissant dans les ordinateurs suivant la fameuse loi de Moore (qui, comme un 11e commandement à force d’être citée, dit que tout en informatique fait l’objet d’un doublement de capacité tous les 16 mois), elle reste une ressource extrêmement précieuse, et toute pratique visant à économiser cette ressource pendant l’exécution du programme est plus qu’appréciable. Économiser de la mémoire La mémoire RAM est une denrée rare et chère, qu’il est important de gérer au mieux pendant l’exécution du programme, au risque de déborder sur le disque dur, avec, pour conséquence, un effondrement des performances.

Qui se ressemble s’assemble : le principe de localité Un autre point capital dans la gestion de la mémoire est qu’il est important que les instructions et les données qui seront lues et exécutées à la suite se trouvent localisées dans une même zone mémoire. La raison en est l’existence aujourd’hui dans les ordinateurs d’un système de mémoire hiérarchisé (telle la mémoire cache), où des blocs de données et d’instructions sont extraits d’un premier niveau lent, pour être installé dans un second niveau plus rapide. Cela permet, lors de l’exécution du programme, d’extraire, le plus souvent possible, les données nécessaires à cette exécution hors du premier niveau. Suite aux ratés, quand ce qui est requis pour la poursuite de l’exécution ne se trouve plus dans le niveau rapide, il sera nécessaire d’extirper à nouveau un bloc de données du niveau lent, en ralentissant considérablement l’exécution. Si lors du transfert de la mémoire lente vers la mémoire rapide, on ramène un peu plus que le strict nécessaire, et au vu du principe de localité, alors la probabilité d’un raté sera diminuée d’autant, car il y a de fortes chances que le surplus du transfert réponde aux prochaines requêtes. Comme les objets, au fur et à mesure de leur création, peuvent s’installer n’importe où dans la mémoire, et que l’essentiel de l’exécution consiste à passer d’un objet à l’autre, on conçoit que, là encore, la pratique OO soit presque antinomique avec toutes les démarches d’économie et d’accélération des performances. On verra qu’afin de diminuer les effets néfastes d’une telle répartition des objets, des systèmes automatiques cherchent à compacter au mieux la zone mémoire occupée par ces objets, et à les maintenir le plus possible dans la mémoire cache.

Les objets intérimaires Si, au fur et à mesure de son exécution, le programme rajoute de nouveaux objets dans la mémoire, il serait commode, dans le même temps, de se débarrasser de ceux devenus inutiles et encombrants. Mais quand un objet devient-il inutile ? Tout d’abord, quand le rôle qu’il doit jouer est par essence temporaire. Par exemple, quand il permet à des structures de données de se transformer en passant par lui, mais n’est plus requis une fois les structures finales obtenues. En Java, existe la classe Integer qui permet, entre autres, de créer des

138

L’orienté objet

objets entiers à partir de String (chaîne de caractères), et de les manipuler, pour, une fois ces manipulations terminées, les stocker dans une simple variable de type int. Dès que ce nouveau stockage est achevé, il serait intéressant de pouvoir facilement se débarrasser de l’objet Integer, qui a juste servi de « passerelle » entre le String et l’int. Le petit code qui suit transforme l’argument String reçu lors de l’exécution du programme, par la ligne de commande indiquée ci-après, en un véritable entier correspondant. Il vous permettra également de comprendre pourquoi la méthode main de Java doit inclure obligatoirement un vecteur de String comme argument. Il s’agit en effet d’arguments qu’il est possible d’indiquer lors de l’exécution du programme (par exemple, le nom d’un fichier d’input…). Le passage de ces arguments se fait lors de l’instruction d’exécution du programme. Dans l’exemple, l’argument 5 est transmis comme le premier élément du vecteur de String et est ensuite transformé en l’entier « 5 ». Ligne de commande : java ObjetInterimaire 5 public class ObjetInterimaire { public static void main(String args[]) { Integer unEntierInterimaire = new Integer(args[0]); /* On récupère le premier String passé en argument par args[0] */ int a = unEntierInterimaire.intValue(); //transformation /* la méthode intValue() appliquée sur l'objet Integer permet d'en * récupérer la valeur entière, à ce stade-ci, l’objet * unEntierInterimaire n’est plus utile et pourrait être supprimé */ System.out.println(args[0] + " s'est transforme en " + a); } }

Résultat 5 s'est transforme en 5

Mémoire pile Mais ce qui est vrai des objets l’est et l’a toujours été de n’importe quelle variable informatique, qui n’aurait de rôle à jouer que pendant un court laps de temps, et à l’occasion d’une fonctionnalité bien précise. Considérez le petit programme suivant, qui serait écrit de manière très semblable dans pratiquement tous les langages informatiques, dans lequel une méthode s’occupe de calculer, à partir de trois arguments reçus, les deux bases et la hauteur, la surface d’un trapèze. Nous avons déjà abordé ce type de mécanisme dans le chapitre 6. Comme indiqué dans la figure 9-1, durant l’exécution de cette méthode, cinq variables intermédiaires vont se créer et disparaîtront aussitôt l’exécution terminée. D’abord, lors de l’appel de la méthode, trois variables nouvelles seront nécessaires pour stocker les trois dimensions du trapèze. Si ces variables existent déjà à l’extérieur de la méthode, elles seront purement et simplement dupliquées, pour être installées dans ces variables intermédiaires. Ensuite, pendant l’exécution de la méthode, une quatrième variable intermédiaire, surface, est créée, qui permettra de stocker le résultat jusqu’à la fin de cette exécution. Si la surface est calculable, une cinquième et dernière variable intermédiaire : somBases, permettra de stocker temporairement une valeur intermédiaire, dont l’usage, un peu forcé ici, permet en général une meilleure lisibilité du programme, et une algorithmique plus sûre, car décomposée en une succession d’étapes plus simples.

Vie et mort des objets CHAPITRE 9

139

Figure 9-1

Illustration de l’existence de cinq variables temporaires utiles au calcul de la surface d’un trapèze.

Il vous paraîtra évident qu’une fois la méthode achevée, toutes ces variables doivent disparaître de la mémoire pour laisser la place à d’autres, et sans qu’on ne les y invite. C’est ce qu’elles feront dans pratiquement tous les langages, et ce le plus simplement du monde. Ces variables sont stockées, comme indiqué dans la figure, dans une mémoire dite mémoire « pile » (et qui ne s’use que si l’on s’en sert). Le principe de fonctionnement de cette mémoire est dit « LIFO » (dernier dedans premier dehors, essayez en anglais et vous comprendrez pourquoi ce fonctionnement n’a pas été dénommé « DDPD »). Dans tout code, un bloc d’instructions, encadré par les accolades, délimite également la portée des variables. Dans une informatique séquentielle traditionnelle (nous verrons une autre solution à cela lorsque nous discuterons du « multithreading » dans le chapitre 17), un bloc d’instructions ne sera jamais interrompu. Quand un bloc se termine, les variables du dessus de la pile disparaissent tout naturellement (car elles ne sont utilisables qu’à l’intérieur de ce bloc), alors que, lorsqu’un bloc s’entame, les nouvelles variables s’installent au-dessus de la pile. Aucune recherche sophistiquée n’est nécessaire pour retrouver les variables à supprimer. Ce seront toujours les dernières à s’être installées sur la pile. De même, de cette façon, aucun gaspillage n’est possible, et aucune zone de mémoire inoccupée peut se trouver, comme un petit village gaulois, perdu au milieu de zones occupées. Gestion par mémoire pile Ce système de gestion de la mémoire est donc extrêmement ingénieux, car il est fondamentalement économe, gère de façon adéquate le temps de vie des variables intermédiaires par leur participation dans des fonctionnalités précises, garde rassemblées les variables qui agissent de concert, et synchronise le mécanisme d’empilement et de dépilement des variables avec l’emboîtement des méthodes.

Ce système de gestion de mémoire est très efficace pour des objets essentiellement intérimaires. Il l’est tant et si bien que C++ et C# l’ont préservé pour la gestion de la mémoire occupée par certains objets (les trois autres, quant à eux, l’ont interdit pour les objets). Idéalement, dans les deux premiers langages, vous utiliserez ce mode de gestion pour des objets dont vous connaissez à l’avance le rôle intermittent qu’ils sont appelés à jouer. Par exemple, en C++, lorsque vous créez un objet o1 de la classe O1, au moyen de la simple instruction :

140

L’orienté objet

O1 o1, n’importe où dans le code, sans l’utilisation du new et de pointeur, vous installez d’office l’objet o1 dans la pile. Cet objet disparaîtra dès que se fermera l’accolade dont l’ouverture précède juste sa création.

De même, si vous passez un objet comme argument, automatiquement un nouvel objet sera créé, copie de celui que vous désirez passer. Une différence clé avec Java, Python et PHP 5 est qu’étant donné qu’il s’agit de la copie du référent et non pas de l’objet, la méthode, dans ces trois langages, agira bien sur l’objet original et non pas sur une copie toute fraîche, mais destinée à disparaître une fois la méthode terminée, comme en C++ et C#. Comme illustré par les codes qui suivent, il est donc possible en C# et C++ de bénéficier du même mode de gestion de mémoire pile des variables non-objets, et ce pour les objets. En C++ #include "stdafx.h" #include "iostream.h" class O1 { public: O1() /* constructeur */ { cout << "un nouvel objet O1 est cree" << endl; } O1(const O1 &uneCopieO1) /* constructeur par copie */{ cout << "un nouvel objet O1 est cree par copie" << endl; } ~O1() /* destructeur */{ cout <<"aaahhhh ... un objet O1 se meurt ..." << endl; } void jeTravaillePourO1() {} }; void usageO1(O1 unO1){ unO1.jeTravaillePourO1(); } int main(int argc, char* argv[]){ O1 unO1; /* je crée un objet O1 */ usageO1(unO1); /* la méthode reçoit une copie de cet objet */ return 0; }

Résultat un nouvel objet O1 est un nouvel objet O1 est aaahhhh... un objet O1 aaahhhh... un objet O1

créé créé par copie se meurt... se meurt...

Il faut, pour comprendre ce code, découvrir l’existence de deux nouvelles méthodes particulières, appelées le constructeur par copie et le destructeur. La première est appelée, automatiquement, dès qu’un objet se trouve dupliqué, notamment lors du passage d’argument. Elle permet, comme nous le comprendrons mieux dans les chapitres 10 et 14, de transformer une copie de surface en une copie profonde. Ici, ce constructeur se borne à signaler qu’on fait appel à lui. Le destructeur, quant à lui, est une méthode appelée, automatiquement, dès la destruction d’un objet. Cette méthode ne peut recevoir d’argument car le programmeur n’est pas à l’origine de son appel. Là aussi, nous

Vie et mort des objets CHAPITRE 9

141

comprendrons mieux l’importance de son rôle par la suite et dès le prochain chapitre. Elle est appelée juste avant la destruction de l’objet et permet de libérer certaines ressources référées par celui-ci avant de le faire disparaître. Ici, de même, ce destructeur se borne à signaler son appel. On voit que deux objets sont créés et détruits dans l’exécution de ce code, sans qu’il soit nécessaire de les détruire par une instruction explicite. Le second est créé lors du passage comme argument du premier. Il en est une copie. Toute cette mémoire est gérée par un système de pile, et les objets disparaissent dès la fermeture des accolades, le premier à la fin de la procédure usageO1(), le second à la fin du programme. En C# using System; public struct O1 /* ATTENTION ! On utilise une structure plutôt qu’une classe */{ private int a; public void jeTravaillePourO1() { a = 5; // modifie l’attribut } public void donneA(){ Console.WriteLine("la valeur de a est: " + a); } } public class TestMemoirePile { public static void Test(O1 unO1){ O1 unAutreO1 = new O1(); unAutreO1.jeTravaillePourO1(); unO1.jeTravaillePourO1(); } // la copie de unO1 et l’objet unAutreO1 disparaissent ici. public static void Main(){ O1 unO1 = new O1(); unO1.donneA(); Test(unO1); unO1.donneA(); /* On retrouve la valeur de l’attribut a de l’objet de départ, malgré le passage comme argument dans la méthode Test() */ } }

Résultat la valeur de a est : 0 la valeur de a est : 0

Dans le code C# qui précède, nous utilisons une structure en lieu et place de classe. Cela nous permet de traiter les objets issus de ces structures exactement comme n’importe quelle variable de type prédéfini. Notez qu’aucun destructeur ne peut être déclaré dans une structure d’où son absence dans notre code ici. Ainsi, dans le code, trois objets instance de la structure O1 sont créés. L’un des trois est créé lors du passage par argument, et on constate que la modification de son seul attribut a n’affecte pas l’objet original dont il n’est qu’une simple copie. Tant la copie passée par argument que les deux autres objets créés comme variable locale de la méthode Test(O1) et de la méthode Main() disparaîtront également dès la fermeture des accolades.

142

L’orienté objet

Structure en C# En C#, les objets issus d’une structure sont traités directement par valeur, dans la mémoire pile, et sans référent intermédiaire. Ils le sont comme n’importe quelle variable de type prédéfini. Les structures sont utilisées en priorité pour des objets que l’on veut et que l’on sait temporaires. Un constructeur par défaut y est prévu et donc on ne peut le surcharger en en définissant explicitement un autre. Plus important encore, les structures ne peuvent hériter entre elles, bien qu’elles héritent toutes de la classe Objet. En revanche, elles peuvent implémenter des interfaces. Tout cela s’explique aisément lorsqu’on sait que les structures sont exploitées par valeur et non par référent, et que les mécanismes d’héritage et de polymorphisme sont plus faciles à réaliser pour des objets uniquement adressés par leur référents. Il est plus facile de s’échanger les référents que les objets dans leur entièreté.

C# et Anders Hejlsberg Se retrouver au côté de Bill Gates en février 2002 à San Francisco, pour annoncer la mise sur le marché d’un nouveau logiciel Microsoft, Visual Studio .Net, présenté comme révolutionnaire car colonne vertébrale de toute une stratégie à venir et dénommée .Net, n’est pas donné au premier quidam venu. Anders Hejlsberg, de fait, n’en est pas un. Concepteur principal du langage C#, innovation capitale dans l’environnement de programmation Microsoft, Hejlsberg n’en est pas à son premier coup de maître. Danois d’origine, pays décidément fournisseur de grandes sirènes de la programmation, avant de rejoindre Microsoft en 1996, il passe quelques années chez Borland comme créateur du Turbo Pascal et en tant que leader de l’équipe à l’origine de Delphi (un autre langage OO bien connu). Chez Microsoft, il prend une part active au développement du Visual J++, habillage Microsoft de Java. On connaît les déboires que connu ce langage, hybride très habile de la syntaxe de Java avec les librairies de Microsoft, et qui irrita Sun au point de mettre la justice sur le coup. Mieux valait pour Microsoft renommer la main basse effectuée sur Java, afin de donner le jour à C# (prononcé C Sharp). Le « J » s’est, comme par magie, transformé en « C ». Évidemment, le nom prête à plaisanteries et elles ne manquent pas. Ce langage s’est déjà vu traité de Visual J- - ou de C bémol. C’est sans doute sa ressemblance avec Java qui le rend le plus vulnérable à ces agressions. Et nous savons la profonde et indéfectible amitié qui lie Bill Gates à Scott McNeal, dirigeant de Sun. Dans la bouche de ce dernier .Net devient .Not, .Not yet ou .Nut. Rien d’étonnant à qui déclare aussi que « dans cette guerre sans merci, nous récupèrerons chaque développeur et ne le laisserons pas s’abandonner du côté sombre ». Plusieurs fois dans notre ouvrage, nous constatons, parfois avec agacement, ces similitudes dont se défend pourtant notre auteur (stratégie et marketing obligent). En effet, dans les premiers écrits consacrés à ce nouveau langage, il était très difficile de trouver une simple mention à Java. Le langage était perçu comme une évolution naturelle de C++, mais l’absence d’allusion à Java ramenait celui-ci à un statut de véritable « chaînon manquant ». S’il est vrai que C# s’est moins éloigné de C++ que Java ne l’a fait (on y retrouve davantage de types de données communs à C++, des objets stockables en mémoire pile, une prédominance du typage statique, et on peut même y intégrer, à ses risques et périls, des pointeurs C++), il n’en reste pas moins vrai que son plus proche voisin demeure Java et non pas C++. Et pour cause, le langage récupère la cohérence OO héritée de Smalltalk, mais, tout comme Java, base cet habillage OO sur une syntaxe C. On y retrouve le ramasse-miettes et une interprétation du code plutôt qu’une compilation, interprétation qui se transforme, cependant, en une véritable compilation dès la première exécution du code (les performances sont alors améliorées). Ce niveau intermédiaire permet une communication facilitée entre différents langages de programmation. Est-il meilleur que Java ? Voilà le type même de question à laquelle il est impossible de répondre, et cela l’est également pour tous les langages que nous traitons dans ce livre. L’Italie est-elle meilleure que la France ? La paëlla est-elle meilleure que le couscous ? C# a pour lui d’apparaître cinq ans après Java et de pouvoir puiser çà et là ce qui se fait de mieux dans les étals de plusieurs langages. Sans doute a-t-il passé un peu plus de temps au rayon Sun et, postérieur à Java, il a pu éviter certaines maladresses de celui-ci dont se plaignent les programmeurs et que Java ne peut supprimer par souci de compatibilité avec l’existant. C# intègre, par exemple, des aspects de VB, comme les mécanismes d’accès aux attributs. Son jeune âge lui permet aussi d’incorporer des éléments technologiques plus modernes. Il en va ainsi de la totale prise en compte d’XML, langage universel de description de contenu de documents publiés sur le Web. Il est possible, à partir du code, de générer très facilement une description de son contenu en XML. Hejlsberg le décrit comme le premier langage facilitant véritablement la programmation à base de composants, bien qu’il reste généralement très évasif sur ce qu’il entend par là, et en quoi ni Java ni d’autres ne pourraient servir à la programmation de ces mêmes composants.

Vie et mort des objets CHAPITRE 9

143

Il est clair que, quitte à s’embarquer sur la nouvelle plate-forme Microsoft de développement Internet, le langage C# apparaît comme un outil de développement de choix (il est d’ailleurs recommandé par Microsoft). Nous verrons dans le chapitre 16 sa prise en compte très simple et très naturelle des services web, version XML des objets distribués communiquant à travers le Web. De fait, Hejlsberg présente toujours son langage comme partie intégrante de .Net, en le plébiscitant comme un des éléments clés de cette énorme boîte à outils de développement Internet. .Net permet, en effet, un développement facilité et transparent d’applications distribuées, par l’entremise des services Web générés automatiquement à partir des codes sources. .Net, dont la vocation première est de faciliter la conception d’applications distriuées sur le Web par l’entremise d’ASP.Net, lorsque ces applications se parlent via un browser, ou par service web quand les objets communiquent directement par envoi de message d’une application à l’autre, a enrichi la description sémantique de ces interactions par l’utilisation abondante et largement automatisée du langage XML. Cette intégration a permis à Microsoft de prendre quelques longueurs d’avance par rapport à son concurrent direct, Sun. Malgré son « interprétabilité » (commune à Java), C# ne tourne que sur Windows. Microsoft parle d’universalité de cette plateforme mais dans un sens nouveau. La version interprétable du langage (CLR – Common Langage Runtime) est partageable avec de nombreux autres langages supportés par .Net (vingt-deux à ce jour), comme C++, VB .Net, Jscript, Cobol, Eiffel (d’où la participation de Meyer), Perl, Python, Smalltalk et d’autres à venir. Cela permet à un code C# (en théorie, nous ne l’avons personnellement pas tester) d’hériter éventuellement d’une classe préalablement développée en VB .Net, et d’envoyer un message à une classe développée en Eiffel. Cela est possible, car tous ces langages se conforment à une CLS (Common Langage Specification – d’où, de fait, la nouvelle version de VB) qui permet de passer de l’un à l’autre. Là où Java est monolangue mais multi- plates-formes, .Net est multi-langues mais mono-plate-forme. Une tentative actuelle de standardisation de C# est en cours. À l’heure où nous écrivons ces lignes, tous les efforts des créateurs de C# sont concentrés sur la troisième version du langage (la deuxième a surtout intégré la généricité traitée dans le chapitre 21), qui tentera d’améliorer la correspondance entre le monde des bases de données relationnelles et l’orienté objet, problème que nous abordons au chapitre 19. Visual C++.Net C++ étant largement étudié dans cet ouvrage, nous nous en voudrions de ne pas dire un petit mot sur Visual C++.Net, la nouvelle mouture de ce langage, une version assez radicalement remaniée grâce à son intégration à .Net et à cause de la nécessité de se rendre compatible aux vingt-et-un autres langages supportés par la plate-forme. Ce nouveau langage est étonnament puissant car, par exemple, il combine les systèmes de gestion mémoire par ramasse-miettes (on parle alors de _gc_class plutôt que de class, et toute la sophistication consiste à mêler ces deux types de class et la gestion mémoire différente qui s’y rapporte) et par instruction directe du programmeur. De même que C++ ne présente pas la même offre en librairies que Java (Multithread, GUI, bases de données), ce nouvel arrivant intègre idéalement toutes les librairies .Net, ce qui le rend aussi riche en services et librairies que Java. (Ces librairies sont de fait communes à tous les langages de la plate-forme. Vous pouvez en voir quelques-unes – Ado.Net, les services web ou les GUI – à l’œuvre dans certains chapitres de ce livre.) Il est difficile, vu son jeune âge, de vous donner une liste définitive de manuels de programmation C#. Comme introduction très rapide et très économique au langage, on peut citer : – Le Langage C#, Christine EBEHARDT, Campus Press – Introduction à C#, Pierre-Yves SAUMONT et Antoine MIRECOURT, Osman Eyrolles MultiMedia. – Une description rapide mais approfondie (parfaite pour les programmeurs Java) se trouve dans : C# Essentials, Ben ALBARHI, Peter DRAYTON et Brad MERRIL, O’Reilly. – DEITTEL et DEITELL ne sont évidemment pas en reste et ont récemment publié C# how to program dans la collection Prentice Hall ainsi qu’une version plus avancée ; C# for Experienced Programmers. – Le très bon Programming C# de Jesse LIBERTY chez O’Reilly et C# And Object Orientation de John HUNT chez Springer.

144

L’orienté objet

Et enfin pour C# dans le contexte .Net : – Microsoft .Net for Programmers, Fergal GRIMES, Manning – Beginning Asp.Net using C#, Chris ULLMAN, Chris GOODE, Juan T. LLIBRE et Ollie CORNES, Wrox. – Microsoft. NET for Programmers, Fergal GRIMES, Manning.

Disparaître de la mémoire comme de la vie réelle Ce mode de gestion de la mémoire pile est intimement lié à une organisation procédurale de la programmation, où le programme est décomposé en procédures ou en blocs imbriqués, lesquels nécessiteront, uniquement pendant leur déroulement, un ensemble de variables, qui seront éliminées à la fin. Nous avons vu que la programmation OO se détache de cette vision, en privilégiant les objets aux fonctions. Il est, en conséquence, tout aussi important de détacher le temps de vie des objets de leur participation à certaines fonctions précises. L’esprit de l’OO est qu’un objet devient encombrant si, dans le scénario même que reproduit le programme, l’objet réel, que son modèle informatique « interprète », disparaît tout autant de la réalité. Dans le petit écosystème vu précédemment, la proie disparaît quand elle se fait manger par le prédateur, l’eau disparaît quand sa quantité devient nulle. Représentez-vous tous ces jeux informatiques, dans lesquels des balles apparaissent et disparaissent, des avions explosent, des héros meurent, des footballeurs quittent le terrain. À chaque fois, l’objet représenté disparaît, tant et si bien que son élimination de la mémoire est même souhaitée, pour permettre à un nouvel objet d’exister et prendre sa place. Il est bien plus difficile d’organiser cette gestion de la mémoire par un mécanisme de pile car, une fois l’objet créé, son temps de vie peut transcender plusieurs blocs fonctionnels, pour ne finalement disparaître, éventuellement de manière conditionnelle, dans l’un d’entre eux (et pas du tout automatiquement dans le bloc où il fut créé). L’OO permet aux objets de vivre bien plus longtemps (et ils vous en remercient) et, surtout, rend leur élimination indépendante des fonctions qui les manipulent, mais plus dépendante du scénario qui se déroule, aussi inattendu soit-il. La vie des objets indépendante de ce qu’ils font L’orienté objet, se détachant d’une vision procédurale de la programmation, tend à rendre indépendante la gestion mémoire occupée par les objets de leur participation dans l’une ou l’autre opération. Cette nouvelle gestion mémoire résultera d’un suivi des différentes transformations subies par l’objet et sera, soit laissée à la responsabilité du programmeur, soit automatisée.

Mémoire tas Tous les langages OO permettent donc un mode de création et de destruction d’objets autrement plus flexible que la mémoire pile. En C++ et C#, ce nouveau mode est en complément de la mémoire pile. En Java, PHP 5 et Python, ce nouveau mode est le seul possible pour les objets, la mémoire pile restant, en revanche, la seule possibilité pour toutes les autres variables de type prédéfini ou primitif. Dans ce mode plus flexible et en C++, C, PHP 5 et Java, tous marqués à vie par leur précurseur, le C, on peut créer les objets n’importe où dans le programme par le truchement du new (en Python, new n’est plus nécessaire). Ils seront créés n’importe quand et pourront être installés partout où cela est possible dans la mémoire, d’où la nécessité d’un référent qui connaisse leur adresse et permette de les retrouver et les utiliser. Mais comment fera-t-on disparaître un objet ? Simplement, quand la « petite histoire » que raconte le programme l’exige ? De nouveau, il est nécessaire de différencier deux politiques : celle très libérale du C++, qui laisse au seul programmeur le soin de décider de la vie et de la mort des objets, et le mode étatisé des quatre autres langages, qui s’en occupe pour vous, en arrière-plan.

Vie et mort des objets CHAPITRE 9

145

C++ : le programmeur est le seul maître à bord En C++, vous pouvez, n’importe où dans un programme, supprimer un objet qui a été créé par new, en appliquant sur son référent l’instruction delete. Vous devenez les seuls maîtres à bord, et, à ce titre, capable du meilleur comme du pire. Pour le meilleur, on vous fait confiance, malheureusement, pour le pire, on vous conserve cette confiance. Ainsi, voici deux scénarios catastrophes, toute proportion gardée bien entendu, que les programmeurs C++ reconnaîtront aisément, même s’ils s’en défendent.

Un premier petit scénario catastrophe en C++ #include "stdafx.h" #include "iostream.h" class O1{ private: int a; public: O1() /* constructeur */{ a = 5; cout << "un nouvel objet O1 est cree" << endl; } O1(const O1 &uneCopieO1) /* constructeur par copie */{ cout << "un nouvel objet O1 est cree par copie" << endl; } ~O1() /* destructeur */{ cout <<"aaahhhh ... un objet O1 se meurt ..." << endl; } void jeTravaillePourO1() { cout << "a vaut: "<< a << endl; } }; void jeTueObjet(O1 *unO1){ delete unO1; // on efface l’objet O1 } void jeCreeObjet(){ O1 *unO1 = new O1(); jeTueObjet(unO1); unO1->jeTravaillePourO1(); //l’objet a disparu bien que son utilisation reste parfaitement ➥possible. } int main(int argc, char* argv[]){ jeCreeObjet(); return 0; }

146

L’orienté objet

Résultats un nouvel objet O1 est créé aaahhhh... un objet O1 se meurt... a vaut : – 572662307

Un même objet unO1 est référencé deux fois. Dans la procédure jeCreeObjet() (on parlera de procédure ou de fonction car elle est définie en dehors de toute classe), on crée d’abord l’objet unO1, et puis on le passe en argument de la méthode jeTueObjet(), qui s’empresse de l’effacer. Mais alors qu’il est éliminé par la méthode jeTueObjet(), il est encore référencé dans la méthode jeCreeObjet() par le référent unO1. Comme l’objet référé par ce référent a disparu, ce dernier se mettra à référer n’importe quoi dans la mémoire, avec toutes les mauvaises surprises dont les programmeurs du C++ sont friands. Vous voyez, par exemple, qu’au lieu d’afficher la valeur 5, ce à quoi on aimerait s’attendre, c’est une valeur complètement imprévue qui apparaît. Rien dans la compilation du programme n’a pu prévenir ce dysfonctionnement. Évidemment, dans ce petit code, l’endroit où est créé l’objet est tellement proche de l’endroit où celui-ci est détruit qu’une telle situation vous semble parfaitement improbable. Détrompez-vous ! Dans un code plus grand et bien plus réparti entre les programmeurs, un de ces derniers, dans l’écriture de sa méthode, aura tout loisir de détruire un objet encore utile à un tas d’autres programmeurs. Les référents fous ou pointeurs fous sont légion en C++, et aucune voiture d’ambulanciers ne se charge de les récupérer dans la mémoire. Un autre scénario tout aussi dramatique est celui qui consiste à effacer plusieurs fois un même objet par la répétition de l’instruction delete sur un même référent.

La mémoire a des fuites Rappelez-vous le petit laïus moralisateur au début du chapitre, vous incitant à ne pas gaspiller la mémoire des ordinateurs, sauf à la jeter dans un sac poubelle prévu à cet effet. Il est très fréquent, dans les langages où vous êtes responsables de la gestion mémoire, de laisser traîner des objets devenus inaccessibles, donc parfaitement encombrants. On parle alors de « fuite de mémoire ». Ce scénario porte moins à conséquence que le précédent. C’est même le scénario parfaitement inverse car, maintenant, alors que les objets existent encore, ils sont devenus hors de portée. Ils ralentissent le code et font chuter les performances, mais n’occasionnent rien de totalement imprévisible.

Second petit scénario catastrophe en C++ #include "stdafx.h" #include "iostream.h" class O2{ public: O2(){ cout << "un nouvel objet O2 est cree" << endl; } ~O2() /* destructeur */{ cout <<"aaahhhh ... un objet O2 se meurt ..." << endl; } }; class O1{ private: O2 *monO2; /* on agrège un objet O2 dans O1 */ public:

Vie et mort des objets CHAPITRE 9

147

O1() /* constructeur */{ cout << "un nouvel objet O1 est cree" << endl; monO2 = new O2(); /* on crée ici l’objet O2 */ } ~O1() /* destructeur */{ cout <<"aaahhhh ... un objet O1 se meurt ..." << endl; } }; int main(int argc, char* argv[]){ O1 *unO1 = new O1(); unO1 = new O1(); /* on ré-utilise le même référent */ delete unO1; return 0; }

Résultats un nouvel objet O1 est un nouvel objet O2 est un nouvel objet O1 est un nouvel objet O2 est aaahhhh... un objet O1

créé créé créé créé se meurt...

La figure 9-2 montre ce qui se passe dans la mémoire des objets lorsque le programme procède à la destruction du dernier objet O1 référé. Trois objets continueront à encombrer la mémoire inutilement, mémoire gaspillée et irrécupérable. La première raison en est l’utilisation du même référent unO1 pour les deux objets créés. Alors que deux objets O1 sont créés, seul le second sera accessible, car le même référent que celui utilisé pour le premier objet est exploité à nouveau. De ce fait, comme l’attribut monO2 du premier objet O1 pointe vers un second objet, les deux objets occuperont inutilement la mémoire. Lorsque grâce au delete, vous effacez le second O1, en fait vous n’effacez que cet objet et le référent vers son objet O2. Mais si vous omettez d’effacer Figure 9-2

Le déroulement en mémoire des codes C++ et Java commentés dans le texte. Le référent « unO1 » pointe d’abord sur un premier objet O1 (chaque objet O1 pointe à son tour sur un objet O2) pour ensuite se mettre à pointer sur un autre objet O1 avant de passer à null. En C++, seul le troisième objet dans la mémoire sera effacé. En Java, C# et Python grâce au comptage des référents et au ramassemiettes, tous les objets finiront par être effacés de la mémoire.

148

L’orienté objet

l’objet O2 également (ce que vous pouvez faire par un mécanisme qui sera détaillé dans le prochain chapitre, et qui consiste à redéfinir le destructeur), celui-ci, à son tour, occupera inutilement la mémoire. En substance, un langage comme C++, qui vous espère adulte et baptisé en matière de gestion de mémoire, a tendance à quelque peu surestimer ses programmeurs. Et ceux-ci se retrouvent, soit avec des référents fous, qui se mettent à référer de manière imprévisible tout et n’importe quoi dans la mémoire, soit avec des objets perdus, comme des satellites égarés dans l’espace, sans aucun espoir de récupération.

En Java, C#, Python et PHP 5 : la chasse au gaspi D’autres langages, comme Java, C#, Python et PHP 5, se montrent moins confiants quant à vos talents de programmeurs, et préfèrent prévenir que guérir. Ils partent de l’idée toute simple qu’un objet n’est plus utile dès lors qu’il ne peut plus être référencé. Un objet devenu inaccessible ne demande qu’à vous restituer la place qu’il occupait. L’idéal serait donc de réussir à vous débarrasser, à votre insu (mais tout à votre bénéfice), pendant l’exécution du programme, des objets devenus encombrants. Une manière simple est de rajouter, comme attribut caché de chaque objet, un compteur de référents, et de supprimer tout objet dès que ce compteur devient nul. En effet, un objet débarrassé de tout référent est inaccessible, donc inutilisable, et donc bon à jeter. Si vous vous repenchez sur les deux petits codes présentés précédemment, vous verrez que ce seul mécanisme vous aurait évité, d’abord, le référent fou (puisque vous ne pouvez vous-même effacer un objet, un référent fou devient impossible), ensuite la perte de mémoire. En effet, le premier objet O1 disparaît car il n’est plus référencé par le référent unO1. Avec lui, disparaît également le référent vers l’objet O2, entraînant donc ce dernier dans sa perte. Par exemple, le petit code Java suivant, équivalent, dans l’esprit, au code C++ précédent, vous montre que tous les objets inaccessibles seront en effet détruits. La méthode finalize() joue, en substance, le même rôle que le destructeur en C++, et s’exécute lors de la destruction de l’objet. Nous reviendrons sur son rôle dans les prochains chapitres.

En Java class O2{ public O2() /* constructeur */{ System.out.println("un nouvel objet O2 est cree"); } protected void finalize () /* le destructeur */{ System.out.println("aaahhhh ... un objet O2 se meurt ..."); } } class O1{ private O2 monO2; /* on agrège un objet O2 dans O1 */ public O1() { /* constructeur */ System.out.println("un nouvel objet O1 est cree"); monO2 = new O2(); /* on crée ici l’objet O2 */ } protected void finalize() { /* destructeur */ System.out.println("aaahhhh ... un objet O1 se meurt ..."); } }

Vie et mort des objets CHAPITRE 9

public class TestFuiteMemoire{ public static void main(String[] args){ O1 unO1 = new O1(); unO1 = new O1(); /* on ré-utilise le même référent */ unO1 = null; System.gc(); /* appel explicite du garbage-collector */ } }

Résultats un nouvel objet O1 est un nouvel objet O2 est un nouvel objet O1 est un nouvel objet O2 est aaahhhh... un objet O1 aaahhhh... un objet O2 aaahhhh... un objet O1 aaahhhh... un objet O2

créé créé créé créé se meurt... se meurt... se meurt... se meurt...

En C# using System; class O2{ public O2() /* constructeur */{ Console.WriteLine("un nouvel objet O2 est cree"); } ~ O2() /* le destructeur */{ Console.WriteLine("aaahhhh ... un objet O2 se meurt ..."); } } class O1{ private O2 monO2; /* on agrège un objet O2 dans O1 */ public O1() { /* constructeur */ Console.WriteLine("un nouvel objet O1 est cree"); monO2 = new O2(); /* on crée ici l’objet O2 */ } ~ O1() { /* destructeur */ Console.WriteLine("aaahhhh ... un objet O1 se meurt ..."); } } public class TestFuiteMemoire{ public static void Main(){ O1 unO1 = new O1(); unO1 = new O1(); /* on réutilise le même référent */ unO1 = null; GC.Collect(); /* appel explicite du garbage-collector */ } }

149

L’orienté objet

150

Le code C#, parfaitement équivalent au code Java, est présenté de manière à indiquer les quelques différences d’écriture avec Java, notamment dans la syntaxe du destructeur, qui rappelle plutôt celle du C++.

En PHP 5 Rien de bien original dans la version PHP 5 du même code qui produira, là encore, le même résultat. Gestion mémoire des objets

Gestion mémoire des objets


\n"); } public function __destruct () { // déclaration du destructeur print ("aaahhhh .... un objet O2 se meurt ...
\n"); } } class O1 { private $monO2; public function __construct() { print ("un nouvel objet O1 est cree
\n"); $this->monO2 = new O2(); } public function __destruct () { print ("aaahhhh .... un objet O1 se meurt ...
\n"); } } $unO1 = new O1(); $unO1 = new O1(); $unO1 = NULL; ?>

Le ramasse-miettes (ou garbage collector) On peut voir qu’au contraire du C++, dans les trois autres langages, tous les objets encombrants sont effacés : • le premier objet O1, car on réutilise son référent pour la création d’un autre objet ; • le second car on a mis son référent à null.

Vie et mort des objets CHAPITRE 9

151

Cette dernière instruction est utile lorsque l’on cherche à se débarrasser d’un objet devenu encombrant : il suffit d’assigner la valeur null à son référent. À la différence de C++, les autres langages n’autorisent pas une suppression d’objet par une simple instruction. La dernière instruction du programme ne se rencontre en général pas, car il y est explicitement fait appel à l’effaceur d’objets : le garbage collector. En général, cet appel se fait à une fréquence soutenue et calibrée par défaut par la machine virtuelle de Java, C#, Python ou PHP 5, dès que la mémoire RAM commence à être sérieusement occupée. Ce calibrage suffit dans la plupart des cas, mais vous avez néanmoins la possibilité d’interférer directement avec ce mécanisme, comme indiqué dans les codes. Le mécanisme responsable de la découverte et de l’élimination des objets perdus s’appelle, en effet, le garbage collector, traduisible par « camion-poubelle » ou « ramasse-miettes ». Le ramasse-miettes passe en revue tous les objets de la mémoire avec pour mission d’effacer ceux qui possèdent un compteur de référents nul. En général, il s’exécute en parallèle (c’est-à-dire sur un thread à part) avec votre programme. (Nous découvrirons le multithreading dans le chapitre 17.) Souvent, celui-ci se déclenchera naturellement, lorsqu’on commence à remplir la mémoire de manière conséquente. Il est quelquefois paramétrable, selon que vous le souhaitiez hyperactif et donc très concentré sur les économies à réaliser dans la mémoire, ou plus laxiste. Il est clair qu’un compromis subtil est à rechercher ici, car souvent les économies mémoire permettent une accélération du programme. Mais si le prix à payer pour récupérer cette précieuse mémoire est un ralentissement encore plus important du programme, occasionné par le fonctionnement simultané du ramasse-miettes et de votre programme, vous en percevez aisément le ridicule (qui ne tue ni les êtres humains ni les objets malheureusement).

Des objets qui se mordent la queue Un seul problème subsiste et, hélas, non des moindres : ce compteur de référents peut être non nul pour certains objets, bien que ces objets en question soient non accessibles. Il s’agit de tous les objets impliqués dans des structures relationnelles présentant des cycles, comme dans la figure 9-3. Figure 9-3

Tous les objets sont référés au moins une fois, mais le cycle entier d’objets est inaccessible.

152

L’orienté objet

Cette situation est plus que fréquente en OO, car il suffit par exemple que deux objets, comme notre proie et prédateur, se réfèrent mutuellement pour que cela se produise. La détection de ces cycles nécessite une très laborieuse exploration de la mémoire, qui a finalement l’effet pervers, si elle se déroule simultanément à l’exécution du programme, de ralentir celui-ci. Les langages qui ont opté pour la manière automatique de récupération de mémoire ont donc inventé des systèmes ingénieux, dont la description dépasserait le cadre de cet ouvrage, pour parer au mieux à ce problème. Ils sont sûrs de ce qu’ils font, en ceci qu’aucun objet utile ne peut disparaître, et qu’aucun référent ne puisse être atteint de soudaine folie. Cependant, ils acceptent de ne pas être parfaits et exhaustifs, en abandonnant dans la mémoire quelques objets qui sont devenus inutiles. Par exemple, ils choisissent de ne pas systématiquement passer toute la mémoire en revue, à la recherche des objets perdus, mais uniquement de se concentrer sur les objets les plus récemment créés. Les objets dont la création récente résulte d’une utilisation temporaire pour une fonctionnalité précise seront d’excellents candidats à une élimination rapide. Une dernière opération à réaliser, une fois la mémoire récupérée, est de ré-organiser celle-ci de façon à très facilement repérer les zones occupées et inoccupées. Cette opération aura pour effet de déplacer les objets du programme afin de les compacter dans la mémoire. Ce recompactage des objets permet d’exploiter au mieux les systèmes de mémoire hiérarchique, tels que la mémoire cache. En effet, la chance que les objets agissant de concert se trouvent localisés dans une zone voisine s’accroît, en les installant les uns à côté des autres. Il faudra, à votre insu (mais c’est autant de gagné bien sûr), changer les adresses contenues dans les référents. Ce ramasse-miettes existant en Java, C#, Python, PHP 5 et originellement dans LISP, est donc un procédé d’une grande sophistication, tentant au mieux de vous éviter les terribles méprises ou gaspillages inhérents au C++, tout en « prenant conscience » de son coût en temps calcul, et des moyens de diminuer celui-ci. La claque, quoi ! Nous finissons le chapitre en présentant la même version des codes Java, C# et PHP 5 mais cette fois-ci en Python, afin d’expliquer davantage le rôle du mot-clé self. Les résultats des deux versions du code sont indiqués en dessous de celui-ci.

En Python import gc # imbrication dans le code des fonctionnalités du ramasse-miettes class O2: def __init__(self): print "un nouvel objet O2 est cree" def __del__(self): print "aaahhhh ... un objet O2 se meurt ..." class O1: __monO2 = None def __init__(self): print "un nouvel objet O1 est cree" # self.__monO2 = O2() # première version du code # __monO2 = O2() # deuxième version du code def __del__(self): print "aaahhhh ... un objet O1 se meurt ..." unO1 = O1() unO1 = O1() unO1 = None gc.collect()

Vie et mort des objets CHAPITRE 9

153

Résultat de la première version du code, avec le self, comme dans les exemples Java et C# un nouvel objet O1 est cree un nouvel objet O2 est cree un nouvel objet O1 est cree un nouvel objet O2 est cree aaahhhh ... un objet O1 se meurt aaahhhh ... un objet O2 se meurt aaahhhh ... un objet O1 se meurt aaahhhh ... un objet O2 se meurt

... ... ... ...

Résultat de la deuxième version du code, sans le self un nouvel objet O1 est cree un nouvel objet O2 est cree aaahhhh ... un objet O2 se meurt un nouvel objet O1 est cree un nouvel objet O2 est cree aaahhhh ... un objet O2 se meurt aaahhhh ... un objet O1 se meurt aaahhhh ... un objet O1 se meurt

...

... ... ...

La première version du code est identique à celle des codes Java et C#. Cependant, dès que l’on supprime le self, le référent __monO2 ne réfère plus l’attribut de la classe, mais un nouvel objet, variable locale du constructeur et qui se borne à disparaître dès que le constructeur a fini de s’exécuter. C’est la présence du self (en tout point semblable à la présence du $this-> en PHP 5) qui permet de différencier l’appel explicite aux attributs de l’objet de la création et l’utilisation de variables locales aux méthodes. Le paramètre self est donc indispensable, comme dans la plupart des codes Python qui précèdent, dès que l’on utilise les attributs propres à l’objet. L’omettre entraîne la création et la manipulation de variables locales aux méthodes. Afin de clarifier davantage encore la façon subtile dont Python différencie, dans ses classes variables locales, attributs de classe et attributs d’objet, le petit code suivant devrait vous être très utile.

En Python class O1: a=0 b=0 c=0 def test(self): a=1 #cela reste une variable locale car elle n’est liée à rien lors de son affectation O1.b=2 #cela reste un attribut de classe car il est référé comme tel self.c=3 #cela devient, grâce à la présence de self, un attribut d’objet print a,O1.b,O1.c unO1 = O1() unO1.test() print O1.a,unO1.a print O1.b,unO1.b print O1.c,unO1.c

154

L’orienté objet

Résultats 1 0 2 0

2 0 0 2 3 #ici, on fait bien la différence entre « c » attribut de classe et « c » attribut d’objet

Le garbage collector ou ramasse-miettes Il s’agit de ce mécanisme puissant, existant dans Java, C#, Python et PHP 5, qui permet de libérer le programmeur du souci de la suppression explicite des objets encombrants. Il s’effectue au prix d’une exploration continue de la mémoire, simultanée à l’exécution du programme, à la recherche des compteurs de référents nuls (un compteur de référents existe pour chaque objet) et des structures relationnelles cycliques. Une manière classique de l’accélérer est de limiter son exploration aux objets les plus récemment créés. Ce ramasse-miettes est manipulable de l’intérieur du programme et peut être, de fait, appelé ou simplement désactivé (dans les trois langages, ce sont des méthodes envoyées sur « gc » qui s’en occupent). Les partisans du C++ mettent en avant le coût énorme en temps de calcul et en performance occasionné par le fonctionnement du « ramasse-miettes ». Mais à nouveau, lorsqu’il est question de comparer le C++ aux autres langages (notez que des librairies existent qui permettent de rajouter un mécanime de garbage collector au C++), la chose s’avère délicate car les forces et les faiblesses ne portent en rien sur les mêmes aspects. C++ est un langage puissant et rapide, mais uniquement pour ceux qui ont choisi d’en maîtriser toute la puissance et la vitesse. Mettez une Ferrari dans les mains d’un conducteur qui n’a d’autres besoins et petits plaisirs que des sièges confortables, une voiture large et silencieuse ainsi qu’une complète sécurité, vous n’en ferez pas un homme heureux. Mettez un appareil photo aussi sophistiqué qu’un Hasselblad dans les mains d’un amateur qui n’a d’autres priorités que de faire rapidement et sur-le-champ des photos assez spontanées de ses vacances et les photos seront toutes ratées.

Exercices Exercice 9.1 Expliquez pourquoi la mémoire RAM est une ressource précieuse, et pourquoi il est nécessaire de tenter au mieux de rassembler les objets dans cette mémoire.

Exercice 9.2 Dans le petit code C++ suivant, combien d’objets résideront-ils de façon inaccessible en mémoire, jusqu’à la fin du programme ? #include "stdafx.h" class O1 { }; class O2 { private: O1 *unO1; public: O2() { unO1 = new O1(); } };

Vie et mort des objets CHAPITRE 9

155

class O3 { private: O2 *unO2; public: O3(){ unO2 = new O2(); } }; int main(int argc, char* argv[]) { O3 *unO3 = new O3(); delete unO3; return 0; }

Exercice 9.3 Dans un code équivalent en Java, tel que le code écrit ci-après, combien d’objets résideront encore en mémoire après l’appel au ramasse-miettes ? class O1 { } class O2 { private O1 unO1; public O2(){ unO1 = new O1(); } } class O3 { private O2 unO2; public O3(){ unO2 = new O2(); } } public class Principale { public static void main(String[] args) { O3 unO3 = new O3(); unO3 = null; System.gc(); } }

Exercice 9.4 Indiquez ce que produirait le petit code C++ suivant, et expliquez pourquoi ce problème ne pourrait survenir en Java et en C# : #include "stdafx.h" #include "iostream.h" class O1 { private: int a;

156

L’orienté objet

public: O1() { a = 10; } void donneA() { cout << a << endl; } }; class O2 { public: O2(){ } void jeTravaillePourO2(O1 *unO1) { delete unO1; } void jeTravailleAussiPourO2(O1 *unO1) { unO1->donneA(); } }; int main() { O1* unO1 = new O1(); O2* unO2 = new O2(); unO2->jeTravaillePourO2(unO1); unO2->jeTravailleAussiPourO2(unO1); return 0; }

Exercice 9.5 Expliquez pourquoi le code Java suivant présente des difficultés pour le ramasse-miettes : class O1 { private O3 unO3; public O1(){ unO3 = new O3(this); } } class O2 { private O1 unO1; public O2(O1 unO1) { this.unO1 = unO1; } } class O3 { private O2 unO2; public O3(O1 unO1) { unO2 = new O2(unO1); } }

Vie et mort des objets CHAPITRE 9

class O4 { private O1 unO1; public O4() { unO1 = new O1(); } } public class Principale2 { public static void main(String[] args) { O4 unO4 = new O4(); unO4 = null; System.gc(); } }

Exercice 9.6 Quel nombre affichera ce programme C++ à l’issue de son exécution ? #include "stdafx.h" #include "iostream.h" class O1 { private: static int morts; public: static int getMorts(){ return morts; } public: ~O1(){ morts++; } }; int O1::morts = 0; void essai(O1 unO1) { O1 unAutreO1; } int main(int argc, char* argv[]) { O1 unO1; for (int i=0; i<5; i++) { essai(unO1); } cout << O1::getMorts() << endl; return 0; }

157

158

L’orienté objet

Exercice 9.7 Expliquez le fonctionnement du ramasse-miettes, les difficultés qu’il rencontre et les manières de l’accélérer.

Exercice 9.8 Expliquez comment et pourquoi dans la gestion mémoire des objets, C# tâche d’être un compromis parfait entre Java et C++.

10 UML 2 Ce chapitre est centré sur quelques diagrammes UML2, les plus importants pour la conception et le développement de programmes OO, tels le diagramme de classe et le diagramme de séquence. Par leur proximité avec le code (malgré leur expression graphique) et leur côté visuel (vu leur expression graphique), ils constituent un excellent support pédagogique à la pratique de l’OO. Ils sont devenus aujourd’hui incontournables pour la réalisation et la communication d’applications informatiques complexes.

Sommaire : Diagramme de classe — Diagramme de séquence — Les bienfaits d’UML

Doctus. — Il est temps d’aborder un aspect fondamental de la programmation objet. Elle ne constitue pas seulement une nouvelle manière d’organiser le travail de développement logiciel. Le découpage modulaire est en premier lieu le résultat d’un processus d’analyse et de conception… Candidus. — … je te vois venir. Tu vas royalement m’annoncer que l’OO est l’architecture idéale pour réaliser un paradis virtuel et qu’un plan, décrivant les informations et les processus mis en jeu, peut être transformé en programme exécutable grâce aux objets logiciels… Doc. — C’est bien ce que vise la méthode UML en tout cas ! Elle nous permet de présenter les choses sous forme de « blocs-diagrammes » codifiés aussi représentatifs qu’expressifs. Je dirais qu’une représentation UML constitue le meilleur cahier des charges qu’on puisse désirer. Cand. — Et un bon dessin vaut mieux que tous les discours, surtout mais pas seulement lors d’une discussion entre informaticiens et non informaticiens ! Doc. — Et si les outils UML sont bien exploités, la simple lecture du plan de bataille permet de relever les incohérences d’un schéma incomplet ou ambigu. Les dépendances entre classes figurent clairement dans les diagrammes. La structure des programmes sera donc mise en évidence au-delà de la conception. L’implémentation de nos objets sera visible sur le plan de leurs interdépendances. Cand. — Il ne reste pas grand-chose au programmeur avec tout ça ! Doc. — UML favorise pour l’instant la vision globale d’un projet. L’implémentation est contrôlée par des processus encore assez complexes pour mériter les efforts d’un programmeur. La gestion mémoire par exemple… Jusqu’à demain peut-être, où programmer reviendra juste à dessiner quelques diagrammes.

160

L’orienté objet

Les trois amigos : Grady Booch, James Rumbaugh, Ivar Jacobson Il est de coutume d’appeler les trois créateurs d’UML les « trois amigos » dans la communauté informatique. En fait, ces trois auteurs étaient déjà bien connus pour leur apport respectif dans les outils méthodologiques de l’OO, avant qu’ils ne décident de réunir leur force et d’homogénéiser leurs efforts, d’où la présence du terme « unified » dans UML. Grady Booch depuis 1981, travaillait à la mise au point de formalismes graphiques pour représenter le développement de programmes OO. Il est actuellement le directeur scientifique de la branche « Rational » chez IBM. Sa méthode OOD (Object Oriented Design) fut conçue à la demande du ministère de la Défense des États- Unis. Il avait mis au point des diagrammes de classe et d’objet, des diagrammes d’état-transition, et d’autres diagrammes de processus, pour mieux visualiser les programmes pendant leur exécution. Ces diagrammes devaient accompagner et améliorer l’organisation et la structure de programmes écrits en ADA et C++. Pour l’anecdote, les classes de Booch étaient dessinées comme des espèces de patatoïdes que l’on retrouve si, dans Rational Rose, vous choisissez de dessiner vos diagrammes « à la Booch ». James Rumbaugh, alors à la General Electric, fut l’auteur d’un formalisme graphique, précédant et en cela anticipant UML, appelé OMT (Object Modelling Technique). C’est d’OMT qu’UML s’est indéniablement le plus inspiré. Cette inspiration fut puisée dans les langages à objet mais également dans la modélisation conceptuelle appliquée à l’analyse et au stockage des données. La plupart des diagrammes importants faisant partie d’UML se retrouvaient déjà dans OMT, comme le diagramme de classe et autres diagrammes dynamiques et fonctionnels. C’est de fait Rumbaugh qui, chez IBM aujourd’hui, est le plus impliqué dans la maintenance et l’évolution de son bébé. Il accorde aussi énormément d’importance, au-delà de l’utilisation de ces diagrammes, au suivi d’une méthodologie décomposée en plusieurs phases, de l’analyse à l’implémentation, phases qui, au lieu d’être complètement séquentielles, se recouvrent en partie. Un développement logiciel devrait plutôt se dérouler comme une succession de petites itérations, de courte durée, toutes intégrant de l’analyse et du développement, mais le poids de l’analyse et du développement s’inversant graduellement vers la fin. On retrouve ces directives méthodologiques dans RUP (Rational Unified Process) et dans la programmation agile. Ivar Jacobson, auteur de la méthode OOSE, Object Oriented Software Engineering, est surtout connu pour avoir recentré l’analyse et le développement informatique sur les besoins humains et, en conséquence, pour l’apport dans UML de la notion de cas d’utilisation et du diagramme correspondant. Il s’est toujours intéressé à la nécessité première d’une bonne représentation du cahier des charges de l’application, ainsi que d’une bonne stratégie de développement, également basée sur une succession de phases courtes, dans lesquelles l’analyse et le développement varient en importance durant la progression du projet. C’est essentiellement à lui que l’on doit RUP, la méthodologie de développement logiciel, qui accompagne souvent l’apprentissage d’UML. Il était bon et très stratégique que ces trois chercheurs et auteurs se rejoignent dans la société Rational (rachetée depuis par IBM, deux des amigos y sont toujours) pour homogénéiser leur notation, car elles circulaient déjà beaucoup dans la communauté, et souffraient de la multiplication de symboles graphiques pour signifier une même chose. Booch le premier à intégrer Rational y attira Rumbaugh puis Jacobson. C’est une des premières raisons à avoir fait le succès et la possible standardisation d’UML : forcer les développeurs à traduire leurs notations favorites dans une notation commune, plutôt que de s’acharner à utiliser de multiples notations concurrentes. Fin 1997, UML est devenu une norme OMG (Object Management Group). L’OMG est un organisme à but non lucratif, fondé en 1989 sous l’impulsion de grandes sociétés (HP, Sun, Unisys, American Airlines, Philips...). Aujourd’hui, l’OMG fédère plus de 1000 acteurs du monde informatique, y compris Microsoft.et tous les dinosaures informatiques Son rôle est de promouvoir des standards qui garantissent l’interopérabilité entre applications orientées objet, développées dans des environnements informatiques hétérogènes (Windows, Linux, Java, C++). Comme nous le verrons souvent dans le chapitre, UML est une des voies vers cette interopérabilité. Corba (dont nous parlerons dans le chapitre 16 et qui est le deuxième standard de l’OMG) en étant une autre, pour faciliter la communication entre les objets à travers le Web. Ces derniers temps, le dernier cheval de bataille de l’OMG est le MDA (Model Driven Architecture), qui force davantage encore l’utilisation d’UML afin de désolidariser au maximum les développements informatiques des plates-formes sur lesquelles ils se réalisent. Voir UML, en montant encore d’un niveau d’abstraction, comme un possible langage de programmation à part entière s’inscrit totalement dans cette nouvelle croisade de l’OMG.

UML 2 CHAPITRE 10

161

De mauvaises langues diront que la multiplicité des diagrammes UML, dès lors que certains de ces diagrammes apparaissent vraiment comme étant redondants par rapport à d’autres, provient de l’impossibilité de parvenir à une unification totale des notations, chacun des trois amigos persistant à défendre sa manière de représenter telle ou telle chose. Ils s’en défendent, en répondant que chaque diagramme apporte un nouveau point de vue sur le système, mais, en général, à l’usage, les utilisateurs sont amenés à favoriser deux ou trois de ces diagrammes, dont les plus utilisés : celui de classe et celui de séquence. Voici quelques ouvrages de référence : – Une petite entrée en matière très digeste et très économe : UML, Martin FOWLER (Campus Press) – traduction anglaise de UML Distilled, Second Edition, Addison-Wesley, le livre UML le plus vendu et le plus lu. La nouvelle et troisième édition est un des premiers ouvrages à rendre compte d’UML 2. Pour la petite histoire, Martin Fowler est également très actif pour populariser et promouvoir les méthodologies de développement telle la « programmation agile ». La lecture qui découle de ce livre l’est tout autant. – Cet ouvrage est un des premiers à rendre compte des modifications apportées par UML 2 : UML 2 ToolKit, Hans-Erik ERIKSON, Magnus PENKER, Brian LYONS et David FADO, chez Wiley – Très didactique car truffé de petits exemples bien pensés : UML par la pratique – Études de cas et exercices corrigés, Pascal ROQUES, Eyrolles, qu’il faut choisir, comme toujours, dans sa dernière édition. – Plus approfondi et plus théorique : Modélisation objet avec UML, Pierre-Alain MULLER et Nathalie GAERTNER, Eyrolles. – Très lié à la manipulation de Rational Rose : Modélisation UML avec Rational Rose 2000, Terry QUATRIANI, Eyrolles. – Très puissant, une référence absolue, car il intègre à la fois UML et les design patterns : UML et les Design Patterns de Craig LARMAN chez Campus Press. – Deux ouvrages introductifs très convaincants tous deux chez Addison-Wesley : – Developing Software with UML, Bernd OESTERIECH – Using UML, Perdita STEVENS – Enfin, une fois n’est pas coutume, un site web très bien fait et très fourni : uml.free.fr

Diagrammes UML 2 Nous avons, sans vous aviser de la chose, fait explicitement usage de deux types très particulier de diagramme dans les chapitres qui précèdent. Ces deux diagrammes, appelés diagramme de classe et diagramme de séquence, font partie des treize diagrammes répertoriés et, surtout, standardisés par le langage graphique de modélisation objet dénommé : UML (Unified Modelling Langage). UML 2 (la deuxième version a été acceptée et standardisée fin 2003) est en effet, depuis quelques années, le standard pour la représentation graphique de

UML 2 UML 2 permet, au moyen de ses 13 diagrammes, de représenter le cahier des charges du projet, les classes et la manière dont elles s’agencent entre elles. Afin d’accompagner le projet tout au long de sa vie, il permet, également, de scruter le programme quand celui-ci s’exécute, soit en suivant les envois de messages, soit en suivant à la trace un objet particulier et ses changements d’état (nous découvrirons mieux le diagramme d’état-transition dans le chapitre 22). Il permet, finalement, d’organiser les fichiers qui constituent le projet, ainsi que de penser leur stockage et leur exécution dans les processeurs. Il y a donc un diagramme pour chaque phase du projet. Certains pensent que le graphisme UML est à ce point puissant qu’il peut servir à la modélisation de n’importe quelle situation complexe (par exemple, le fonctionnement d’un moteur automobile ou des institutions européennes…), que celle-ci se prête ou non, par la suite, à un développement informatique. C’est notamment le point de vue de G. Booch et d’auteurs tels que Martin et Odell de voir dans UML un langage de modélisation qui transcende les seules applications informatiques. Nous restons sceptiques quant à l’exploitation d’UML en dehors du monde informatique et nous nous limiterons dans ce chapitre à l’appréhender comme un moyen de faciliter la conception, le développement et la communication d’un tel projet.

162

L’orienté objet

la succession des phases, de l’analyse à l’installation sur site, que comprend un projet informatique. Les diagrammes UML ont pour mission d’accompagner le développement de ce projet, en permettant aux personnes impliquées une autre perception, plus globale, plus intuitive, plus malléable, et plus facilement communicable, de ce qu’ils sont en train d’accomplir. UML est le moyen graphique de garantir que « ce qui se conçoit et se programme bien s’énonce clairement ».

Représentation graphique standardisée L’avantage d’une représentation graphique standardisée est que tous les développeurs l’abordent, l’appréhendent et la comprennent d’une seule et même manière. Une flèche terminée par une pointe particulière, et reliant deux rectangles, signifiera la même chose, précise au niveau du code, pour tous les programmeurs, mais sera également compréhensible, en partie, pour les personnes, qui, bien qu’impliquées dans le projet, ne mettront pas directement la main à la pâte logicielle. Ils comprendront suffisamment le sens de cette flèche, et les rectangles qu’elle relie, pour que le code final qui réalisera précisément et définitivement ce que la flèche signifie ne trahisse en rien cette compréhension. Surtout, cela permettra un langage commun, situé quelque part entre deux idiomes, le code final, trop précis et trop peu lisible par tous, et la compréhension intuitive qu’en manifesteront toutes les personnes impliquées, trop imprécise et trop peu formalisée. Question lisibilité, on conviendra aisément qu’il est beaucoup plus facile de comprendre les interdépendances entre 10 classes lorsque celles-ci sont représentées comme autant de rectangles sur un tableau, un écran ou une page, que de feuilleter un long et pénible listing contenant la définition de ces mêmes classes. Les diagrammes UML sont formels car ils visent à une sémantique très précise de tous les symboles dont ils sont composés. Cette sémantique fait d’ailleurs l’objet d’un métamodèle UML (« méta- », car écrit lui-même dans les notations UML – essentiellement le diagramme de classe). UML est au code informatique final et exécutable un peu ce que pourrait être la partition musicale à son interprétation ou le projet d’architecture à la maison qui s’ensuivra. Ce projet d’architecture est réalisé à échelle, ou à l’aide de notations que les architectes savent très rigoureuses. Or, ce projet reste compréhensible à vos yeux, même si vous seriez bien en peine de construire votre maison. Les notations utilisées dans le plan d’architecture sont à ce point précises que la maison finale ne devrait en rien trahir ce même plan sur lequel vous vous êtes mis d’accord, dans un monde idéalisé bien sûr (un monde dans lequel, malheureusement, on ne construit pas de maisons…). En effet, au vu de leur précision sémantique, les diagrammes restent extrêmement contraignants une fois la construction entamée. Très peu de libertés seront laissées au programmeur, une fois ces diagrammes affichés. Ensuite, à l’instar des musiciens et des architectes, qui comprendront leur partition et leur plan d’une seule et même façon, tous les informaticiens comprendront les diagrammes UML d’une seule et même façon, juste avec quelques libertés résiduelles d’interprétation, pour optimiser çà et là une partie du code, non spécifiée dans les diagrammes. Eu égard à la programmation, UML apparaît, devant la diversité des langages de programmation OO, comme une espèce d’espéranto graphique de ces langages, reprenant tous les mécanismes de base de l’OO, même si leur traduction dans ces langages diffère. Cela permet aux personnes impliquées dans le développement logiciel de restreindre de plus en plus leur apport à la seule conceptualisation et analyse, en se libérant des contraintes syntaxiques propres aux langages et en différant de plus en plus les détails techniques liés à l’implémentation. Grâce à l’ULM (oouhps… pardon !), l’UML, les objets s’envolent. Concrètement, un programmeur C++ et un programmeur Java, C#, Python ou PHP, pourront travailler, pour l’essentiel, dans un langage graphique commun, et automatiser, autant que faire se peut, la traduction des diagrammes dans les différents langages.

UML 2 CHAPITRE 10

163

Là où un langage choisit d’implémenter l’héritage par :public, l’autre par « : », le troisième par extends, le quatrième par inherits et le cinquième par de simples parenthèses – et encore on en oublie –, UML se borne à le traduire par une flèche pointant de la sous-classe vers la superclasse (voir l’héritage chapitre 11). Quoi de plus clair et de plus compréhensible.

Du tableau noir à l’ordinateur Ce côté formel et très proche des langages de programmation OO, proximité, comme nous allons le voir, encore accrue dans UML 2, amène la plupart des utilisateurs UML à se répartir sur un axe selon l’importance qu’ils accordent aux diagrammes lors de la réalisation finale du code et l’exigence ou non d’une parfaite fidélité de ce code à ces diagrammes. À la première extrémité, il y a ceux que nous nommerons les « UMListes du tableau noir », ceux dont le recours à ces diagrammes est moins déterminant et moins contraignant. Ils en reconnaissent l’utilité, surtout lorsque leur programme se complexifie et qu’ils sentent le besoin de petits dessins (comme nous l’avons dit et même pour eux, 10 classes représentées dans un diagramme de classe sur un tableau noir sera toujours plus clair que le code de ces mêmes 10 classes dans un listing) pour s’aider eux-mêmes à résoudre un problème de conception, et surtout pour faciliter la discussion avec leurs collègues programmeurs. Ils y recourent avec parcimonie et la volonté essentielle de se faciliter la vie, sans respecter religieusement tous les détails syntaxiques proposés par UML. Surtout, cela ne les empêche pas, une fois que leur problème est résolu et qu’ils ont pris leurs décisions, de se détourner du tableau noir pour retourner à l’écriture du code. Ils voient surtout les diagrammes UML comme des éléments d’appoint, qu’ils utilisent à des moments précis du développement, lorsque la complexité les dépasse, ou lors d’interactions avec les autres programmeurs. Ils ne sont pas en faveur des environnements de développement logiciel basés sur UML (et de plus en plus fréquents pourtant) car ils leur semblent plus contraignants qu’autre chose. Le code reste leur entière création, leur propriété, leurs soucis, et ils restent très circonspects devant toute production automatisée. À l’autre extrémité, nous trouvons les « programmeurs UML », ceux qui choisissent d’abord de réaliser ces diagrammes à l’aide d’environnements logiciels tels Together, Rose, Omondo ou Enterprise Architect, environnements plus contraignants, car ils exigent dès le départ une certaine cohérence entre les diagrammes. Par la même occasion, ils se reposent aussi beaucoup sur la génération automatique de code afin d’accompagner dans une large partie leur écriture des programmes. Pour ces derniers, UML doit évoluer vers un langage de programmation à part entière, le but étant de pouvoir programmer un jour en UML tout comme en Java, Python, C# ou PHP, c’est-à-dire de faire d’UML un vrai environnement informatique de conception de programmes exécutables, sans l’intermédiaire, comme c’est le cas aujourd’hui, des codes générés au départ de ces diagrammes. La plupart des environnements de développement permettent cette génération de code dans la plupart des langages OO les plus courants. Mais les concepteurs d’UML ainsi que l’OMG, son organisme de standardisation, veulent aller plus loin dans l’opérationnalisation d’UML. La deuxième version d’UML est la preuve indéniable de cette évolution, comme nous le verrons, par exemple, lorsque nous détaillerons le diagramme de séquence. Celui-ci s’est nettement enrichi dans sa deuxième version afin d’intégrer un maximum de mécanismes procéduraux (test, boucles, …). L’OMG promeut une pratique du développement logiciel dite « MDA » (Model Driven Architecture), c’est-à-dire axée essentiellement sur la modélisation et non plus sur l’écriture de code, laissant cette écriture s’automatiser de plus en plus, grâce à des outils informatiques qui, à partir des diagrammes du modèle, l’adaptent en fonction des plates-formes sur lesquelles le code doit s’exécuter. Plus d’informaticiens mais des modélisateurs, voici ce dont ils rêvent pour l’avenir de la profession.

164

L’orienté objet

Nous nous bornons ici à présenter cet axe et ces deux extrêmes. De nombreux praticiens adopteront une attitude intermédiaire. C’est à l’informaticien de voir et à l’avenir de nous dire vers quoi UML évoluera et surtout qu’en feront et quelle importance lui donneront ses utilisateurs et ses partisans. UML 2 entre le coup de pouce au tableau noir et un vrai langage de programmation Alors que la plupart des utilisateurs d’UML le voient aujourd’hui comme un complément et une assistance graphique à l’écriture de code, ses créateurs et ses avocats espèrent le voir évoluer vers un véritable langage de programmation, capable d’universaliser et de chapeauter tous ceux qui existent aujourd’hui. En substance, UML pourrait devenir, en lieu et place des langages de programmation, ce que ceux-ci devinrent en remplacement à l’assembleur. Les codes seraient purement et simplement produits par une nouvelle génération de compilateur et les diagrammes s’exécuteraient sans programmation additionnelle. L’informatique n’a de cesse de se caractériser par cette succession de montées en abstraction : portes électroniques, circuits booléens, instructions élémentaires, assembleur, langages de programmation, langage de modélisation (UML).

Programmer par cycles courts en superposant les diagrammes UML n’est pas une méthodologie, c’est juste un langage. En prenant l’exemple d’une langue étrangère, UML serait plutôt le dictionnaire auquel il manque encore l’Assimil pour mettre la langue en contexte (et qui vous indique vraiment comment trouver votre chemin et demander où sont les toilettes). Rien n’est proposé sur la manière la plus pratique d’utiliser ses diagrammes : lesquels et à quelle étape du développement ? Toutefois, les créateurs, et tout ceux qui font la promotion d’UML en général, accompagnent celle-ci d’une offre en matière méthodologique : le RUP (Rational Unified Process) ou la programmation dite extrême ou agile. Quelques éléments communs à ces nouvelles propositions méthodologiques sont les suivants. Il est capital de travailler par cycle court, cycle reprenant la succession des phases classiques des projets informatiques : cahier de charge, analyse, modélisation, développement, déploiement, et débouchant systématiquement sur un code exécutable tous les mois ou les deux mois au maximum. Le client pour lequel vous développez doit pouvoir rester dans le coup et suivre les progrès. Pour ce faire, rien n’est plus éclairant quant à l’évolution du projet, qu’un programme dont vous lui faites la démonstration. Pas de blabla, de promesses en l’air, mais du concret que diable ! Un programme qui tourne et exécute des choses supplémentaires tous les mois. Le retour du client ainsi que les possibles adaptations de sa demande en seront véritablement facilitées. Les informaticiens se sont rendus compte depuis longtemps que leurs clients n’ont pas toujours les idées très claires sur ce qu’ils veulent réellement au départ du projet et sur les possibilités mirobolantes qui leur seront révélées au fur et à mesure de l’avancement de ce projet. L’utilisation des diagrammes doit se superposer dans le temps. S’il est évident que ceux qui ont pour objet le cahier des charges du projet seront plus sollicités au début et que ceux qui décrivent l’architecture des fichiers et la manière dont ils s’exécutent sur les machines plutôt à la fin, tous les diagrammes doivent néanmoins pouvoir être repris à tout moment du projet. C’est la raison de cette successsion de cycles qui, chacun, voient toutes les phases classiques d’analyse et de développement se dérouler. Le développement doit montrer une grande flexibilité et capacité d’adaptation (d’où le terme « agile ») et il faut pouvoir revenir sur des décisions, mêmes prises au tout début de la conception si des problèmes imprévus se produisent en fin de parcours (par exemple des problèmes de temps d’exécution). Il est clair que travailler par cycle court aide à cette adaptation, car cela permet tant au client qu’aux développeurs de repérer des problèmes à tout moment, y compris très tôt dans la réalisation.

UML 2 CHAPITRE 10

165

Diagrammes de classe et diagrammes de séquence Les deux seuls diagrammes que nous avons utilisés jusqu’à présent sont les diagrammes de classe et les diagrammes de séquence. Pourquoi les avoir utilisés avant d’officiellement vous les présenter ? Parce que nous osons penser qu’ils peuvent parfaitement accompagner l’explication des mécanismes OO, sans nécessiter une explicitation détachée. L’OO les explique au même titre qu’ils servent à expliquer l’OO. Ils permettent une visualisation alternative des mécanismes OO, mais qui aide à la compréhension formelle que l’on doit en avoir. Le pari que nous avons pris dans les chapitres précédents est que vous ayez compris leur importance et leur signification, sans qu’il n’ait été nécessaire, dans un premier temps, de vous les détailler symbole par symbole. En fait, en plus des différents avantages déjà évoqués, ce pourrait être parmi les meilleurs outils didacticiels pour expliquer l’OO. Une raison simple à cela est que, nonobstant que le langage graphique dans lequel ils s’expriment semble apparemment bien détaché du langage de programmation final, ils restent parfaitement fidèles à leur traduction dans ce langage de programmation. Certains environnements logiciels de conception UML, comme Together ou Omondo (le plug-in UML d’Eclipse), parmi les plus puissants à ce jour, ont fait de cette traduction automatique leur cheval de bataille et leur valeur ajoutée. Comme nous l’avons dit précédemment, ils évoluent incontestablement dans le bon sens, car de plus en plus d’outils se développent, favorisant l’utilisation de ces diagrammes et assurant, « derrière », la génération automatique de code. Diagramme de classe et diagramme de séquence peuvent se traduire, automatiquement, dans tous les langages de programmation objet, jusqu’à ce qu’un jour cette traduction ne s’avère plus nécessaire, les diagrammes UML se suffisant à eux-mêmes. À titre de petit exercice, nous allons, dans la suite, nous livrer à une présentation des symboles graphiques les plus usités, propres au diagramme de classe et de séquence, et vous montrer, dans le même temps, les traductions automatiques qui peuvent être faites de ces diagrammes dans les langages C++, C#, Java, Python et PHP 5. Par l’addition d’une fonction main (en C++) ou d’une méthode main (en Java et C#), nous ferons de ces codes des exécutables qui, lors de l’exécution, produiront à l’écran quelques phrases qui témoignent de leur bon fonctionnement.

Diagramme de classe Une classe Commençons par le diagramme de classe. Une classe se décrit par ses trois compartiments : nom, attributs et méthodes. Figure 10-1

Une classe dans le diagramme de classe UML.

O1 -unAttribut:int -unAutreAttribut:int <> +01 +jeTravaillePour01:void +uneMethodeStatique:void +uneAutreMethode:int

166

L’orienté objet

En Java : UML1.java class O1 { private int unAttribut; // private est indiqué par un signe – dans le diagramme private int unAutreAttribut; public O1() { // public est indiqué par un signe + } /* Il serait également possible de générer automatiquement les méthodes d’accès aux attributs privés, tels : public void setUnArribut(int unAttribut) et public int getUnAttribut() {return unArribut ;} */ public void jeTravaillePourO1() { System.out.println ("Je suis au service de toutes les classes"); } public static void uneMethodeStatique() { // la méthode statique est soulignée dans le diagramme } public int uneAutreMethode(int a) { return a; } } public class UML1 { public static void main(String[] args) { O1 unObjet = new O1(); unObjet.jeTravaillePourO1(); } }

Résultat Je suis au service de toutes les classes

En C# : UML1.cs using System; class O1 { private int unAttribut; private int unAutreAttribut; public O1() { } /* Il serait également possible de générer les méthodes d’accès qui en C# se définissent comme : public int UnAttribut { set { unAttribut = value ; } get { return unAttribut ; } */ public void jeTravaillePourO1() { Console.WriteLine ("Je suis au service de toutes les classes "); }

UML 2 CHAPITRE 10

public static void uneMethodeStatique() { } public int uneAutreMethode(int a){ return a; } } public class UML1 { public static void Main() { O1 unObjet = new O1(); unObjet.jeTravaillePourO1(); } }

Résultat Je suis au service de toutes les classes

En C++ : UML1.cpp #include "stdafx.h" #include "iostream.h" class O1 { private: int unAttribut; int unAutreAttribut; public: O1() { } void jeTravaillePourO1() { cout <<" Je suis au service de toutes les classes " << endl; } void static uneMethodeStatique(){ } int uneAutreMethode(int a){ return a; } }; int main(int argc, char* argv[]){ O1* unObjetTas = new O1(); /* un objet construit dans le tas */ O1 unObjetPile; /* un objet construit sur la pile */ unObjetTas->jeTravaillePourO1(); unObjetPile.jeTravaillePourO1(); return 0; }

Résultat Je suis au service de toutes les classes Je suis au service de toutes les classes

167

L’orienté objet

168

En Python : UML1.py class O1: __unAttribut=0 __unAutreAttribut=0 def __init__(self): pass def jeTravaillePourO1(self): print ("je suis au service de toutes les classes") def uneMethodeStatique(): pass uneMethodeStatique=staticmethod(uneMethodeStatique) def uneAutreMethode(self,a): return a unObjet = O1() unObjet.jeTravaillePourO1()

En PHP 5 : UML1.php Traduction classe UML

Traduction classe UML


\n"); } public static function uneMethodeStatique() {} public function uneAutreMethode(int $a) { return $a; } } $unO1 = new O1(); $unO1->jeTravaillePourO1(); ?>

UML 2 CHAPITRE 10

169

Similitudes et différences entre les langages À quelques détails de syntaxe près, que le lecteur pourra assez facilement épingler, les codes Java et C# sont équivalents. Il en va un peu différemment des codes Python et PHP 5 qui, comme nous l’avons déjà vu, ne typent ni les attributs ni les méthodes (Python exige de les initialiser ce qui permet en effet de les typer indirectement). En Python, une méthode ne peut se trouver sans instruction d’où la présence de pass. Nous avons déjà vu également dans les chapitres précédents la syntaxe particulière de l’encapsulation « private » et des constructeurs. C++, quant à lui, exige de spécifier, lors de la création de l’objet, si cette création se fait sur la mémoire pile ou sur la mémoire tas. De manière générale, C++ offrant bien plus de degrés de liberté que les deux autres, les codes écrits dans ce langage seront toujours moins immédiats à saisir. Les deux possibilités sont illustrées dans le code. Tous les autres langages ne laissent que la mémoire tas pour les objets (C# autorise la pile pour les objets issus des « structures »). Toujours en C++, si c’est la mémoire tas que nous souhaitons utiliser, il faut que le référent sur l’objet soit explicitement déclaré comme une variable de type adresse, qu’on appelle un pointeur en C++. C’est bien parce qu’il n’y a aucune autre possibilité en Java, Python ou PHP 5 pour la création d’un objet qu’il n’est plus nécessaire de préciser que cette variable est effectivement de type pointeur. C# permet également les deux modes de gestion, mais réserve le tas aux seules classes, et la pile aux structures. En C++, la syntaxe de l’envoi de message sur les deux objets est différente, selon que l’objet est sur la pile ou sur le tas. En fait, l’instruction : unObjetTas->jeTravaillePourO1() n’est qu’une réécriture de l’instruction unObjetTas*.jeTravaillePourO1().

Association entre classes Figure 10-2

Une association dirigée dans le diagramme de classe UML.

Considérons, dans un second temps, une première association « dirigée » entre la classe O1 et la classe O2. Cette relation d’association est celle que nous avons déjà rencontrée entre la proie et le prédateur, la proie et l’eau, etc. La présence de cette association, ici, dans le sens d’O1 vers O2 (en l’absence de la flèche, l’association serait considérée bi-directionnelle), exige que, dans le code de la classe O1, un message soit envoyé vers O2, comme montré dans les cinq codes écrits ci-après, dans les cinq langages.

En Java : UML 2.java class O1 { private int unAttribut; private int unAutreAttribut ; private O2 lienO2; // réalise l’association avec la classe O2

170

L’orienté objet

public O1(O2 lienO2) /* Le constructeur prévoit de recevoir un référent vers un objet de classe 02 */{ this.lienO2 = lienO2; } public void jeTravaillePourO1() { lienO2.jeTravaillePourO2() ; /* Voici l’envoi de message */ } public int uneAutreMethode(int a){ return a; } } class O2 { private int unAttribut ; private double unAutreAttibut ; public O2() {} public void jeTravaillePourO2() { System.out.println("Je suis au service de toutes les classes ") ; } } public class UML 2{ public static void main(String[] args){ O2 unObjet2 = new O2(); O1 unObjet1 = new O1(unObjet2) ; /* on passe dans le constructeur de l’objet O1 le référent de l’objet O2 */ unObjet1.jeTravaillePourO1(); /* un premier message envoyé par le main à l’objet de classe O1 en déclenchera un autre vers un ➥ objet de classe O2 */ } }

Résultat Je suis au service de toutes les classes.

En C# : UML 2.cs using System; class O1 { private int unAttribut; private int unAutreAttribut; private O2 lienO2; public O1(O2 lienO2) { this.lienO2 = lienO2; } public void jeTravaillePourO1() { lienO2.jeTravaillePourO2(); /* l’envoi de message */ } public int uneAutreMethode(int a) { return a; }

UML 2 CHAPITRE 10

} class O2 { private int unAttribut; private double unAutreAttibut; public O2() {} public void jeTravaillePourO2() { Console.WriteLine("Je suis au service de toutes les classes"); } } public class UML 2c{ public static void Main() { O2 unObjet2 = new O2(); O1 unObjet1 = new O1(unObjet2); /* On passe ici le référent de l’objet O2 lors de la ➥ construction de l’objet O1 */ unObjet1.jeTravaillePourO1(); } }

Résultat Je suis au service de toutes les classes.

En C++ : UML 2.cpp #include "stdafx.h" #include "iostream.h" class O2 { private: int unAttribut2; double unAutreAttribut2; public: void jeTravaillePourO2() { cout << "Je suis au service de toutes les classes" << endl; } }; class O1 { private: int unAttribut; int unAutreAttribut; O2* lienO2; /* il s’agit d’un pointeur vers un objet de type O2 */ public: O1(O2* lienO2) { this->lienO2 = lienO2; /* passage du référent */ } void jeTravaillePourO1() { lienO2 -> jeTravaillePourO2(); /* l’envoi de message */ } int uneAutreMethode(int a) { return a; } };

171

172

L’orienté objet

int main(int argc, char* argv[]){ O2* unObjet2Tas = new O2(); /* un objet construit dans le tas */ O2 unObjet2Pile; /* un objet construit sur la pile */ O1* unObjet1Tas = new O1(unObjet2Tas); /* passage du référent */ O1* unObjet11Tas = new O1(&unObjet2Pile); /* passage du référent de l’objet pile */ O1 unObjet1Pile(unObjet2Tas); O1 unObjet11Pile(&unObjet2Pile); unObjet1Tas->jeTravaillePourO1(); unObjet11Tas->jeTravaillePourO1(); unObjet1Pile.jeTravaillePourO1(); unObjet11Pile.jeTravaillePourO1(); return 0; }

Résultat Je Je Je Je

suis suis suis suis

au au au au

service service service service

de de de de

toutes toutes toutes toutes

les les les les

classes. classes. classes. classes.

En Python : UML 2.py class O1: __unAttribut=0 __unAutreAttribut=0 __lienO2=None // il faut initialiser le référent à None def __init__(self, lienO2): self.__lienO2=lienO2 def jeTravaillePourO1(self): self.__lienO2.jeTravaillePourO2() def uneAutreMethode(self,a): return a class O2: __unAttribut=0 __unAutreAttribut=0 def __init__(self): pass def jeTravaillePourO2(self): print "Je suis au service de toutes les classes" unObjet2=O2() unObjet1=O1(unObjet2) unObjet1.jeTravaillePourO1()

UML 2 CHAPITRE 10

173

En PHP 5 : UML2.php Traduction classe UML

Traduction classe UML


lienO2 = $lienO2; } public function jeTravaillePourO1() { $this->lienO2->jeTravaillePourO2(); } public static function uneMethodeStatique() {} public function uneAutreMethode(int $a) { return $a; } } class O2 { private $unAttribut; private $unAutreAttribut; public function __construct() {} public function jeTravaillePourO2() { print("je suis au service de toutes les classes
\n"); } } $unO2 = new O2(); $unO1 = new O1($unO2); $unO1->jeTravaillePourO1(); ?>

Similitudes et différences entre les langages De nouveau, Java et C# sont quasi équivalents. Les codes Python et PHP 5 ne devraient pas, eux non plus, poser de grands problèmes de compréhension. Une fois encore, l’illustration des deux modes de gestion mémoire en C++ rend le code plus compliqué. Différentes possibilités sont indiquées, selon que l’on utilise des objets créés en mémoire pile ou en mémoire tas. Le caractère « & » signifie que l’on va chercher l’adresse

174

L’orienté objet

de la variable plutôt que sa valeur. Lorsqu’il s’agit d’un objet en mémoire pile, l’explicitation de son adresse se fait à l’aide de ce caractère. En revanche, pour un objet en mémoire tas, le référent est directement l’adresse. Le résultat du code C++, malgré les différents modes de gestion de mémoire, exécutera quatre fois le même message et imprimera à l’écran quatre fois la même phrase. Nous avons voulu simplement distinguer quatre possibilités, selon que l’objet O1 et l’objet O2 se trouvent dans la mémoire pile ou dans la mémoire tas.

Pas d’association sans message Il n’y aura jamais lieu de dessiner une telle association dirigée entre deux classes si, nulle part dans le code de la première, n’apparaît un message à destination de la seconde. C’est cela dont permet, également, de s’assurer certains environnements de développements UML, comme Rational Rose. Dans le diagramme de classe et de séquence apparaissant, ci-après, dans Rational Rose, on peut voir que les messages proposés dans le diagramme de séquence sont, en effet, les seules méthodes déclarées dans le diagramme de classe.

Figure 10-3

Le petit menu apparaissant dans le diagramme de séquence en dessous de la flèche de l’envoi de message reprend les seules méthodes déclarées dans la classe O2.

UML 2 CHAPITRE 10

175

Si on spécifie un nouveau message, une méthode correspondante viendra automatiquement se rajouter dans la classe qui reçoit le message. Un autre avantage certain de l’utilisation d’un logiciel de développement UML est qu’au-delà de l’assistance graphique, une mise en cohérence automatisée est assurée entres les différents diagrammes. Rational Rose fut le premier environnement logiciel à assurer cette cohérence entre les diagrammes UML et à montrer, ainsi, les avantages d’un logiciel de développement sur la simple utilisation d’un papier et d’un crayon ou du tableau noir. Cette mise en correspondance entre les différents diagrammes est possible grâce au recours au métamodèle. Association entre classes Il y a association entre deux classes, dirigée ou non, lorsqu’une des deux classes sert de type à un attribut de l’autre, et que dans le code de cette dernière apparaît un envoi de message vers la première. Sans cet envoi de message, point n’est besoin d’association. Plus simplement encore, on peut se représenter l’association comme un « tube à message » fonctionnant dans un sens ou dans les deux.

Rôles et cardinalité Figure 10-4.

Illustration des rôles et de la cardinalité des associations.

Comme la figure 10-4 le montre, d’autres informations peuvent figurer sur un diagramme de classe UML, afin de caractériser plus finement l’association entre les classes. Dans cette figure, les relations sont toutes bi-directionnelles. Sachant les difficultés que cela pose pour certains langages, nous nous limiterons à la traduction en Java et C++ (en C#, c’est tout à fait identique). Les « rôles » sont les noms donnés, aux deux pôles de l’association, à la classe qui recevra le message par la classe qui lui envoie. Ainsi, le code Java des classes O1 et O2 qui est généré à partir de ce diagramme UML pourrait être le suivant , le nom des rôles se substituant au nom des attributs référents . class O1{ O2 lienO2 ; } class O2{ O1 lienO1 ; }

176

L’orienté objet

Une information de type « cardinalité » peut également se retrouver aux deux pôles de l’association, signifiant le nombre d’instances de la première classe en interaction avec le nombre d’instances de la seconde. Cette cardinalité peut rester imprécise, comme dans la figure 10-4, ou plus précise, par exemple « 1…3 », si l’on considère qu’il n’y aura que de un à trois objets entrant dans une interaction avec un autre. Dans la figure 10-4, chaque objet O2 sera associé à un certain nombre, non défini ici, d’objets O3 (on verra qu’il est possible d’utiliser un diagramme d’objet de manière à préciser la nature des objets repris par cette cardinalité). Le code Java associé transformera son référent O3 en un tableau de référents : class O2 { O3[] lesLiensO3 ; // ou ArrayList lesLiensO3 ; }

Il est possible, depuis les dernières version de Java et de C#, d’utiliser une liste extensible (une ArrayList) typée par la classe des objets que cette liste contiendra. L’utilisation d’un tableau en général exige de connaître le nombre d’éléments que celui-ci contiendra. En C++, comme le pointeur d’un tableau ne pointe toujours que sur le premier élément du tableau, dans les deux cas de figure, une relation 1-1 ou une relation 1-n, le code sera équivalent (d’où une synchronisation plus délicate entre le diagramme de classe et le code dans le cas de l’utilisation du C++) : class O2 { O3* lesLiensO3 ; }

Afin de préciser davantage les problèmes posés par la cardinalité, examinons le petit diagramme UML de la figure 10-5, dans lequel figure une relation, non dirigée cette fois, de type 1 – n. Figure 10-5.

Un petit exemple de cardinalité qui peut s’écrire indifféremment « 1-n » ou « 1..* ».

Utilisons dans la traduction de ce diagramme de classe, une ArrayList qui nécessite d’importer dans le code Java le package java.util. Il suffit d’actionner la méthode add pour pouvoir y ajouter autant d’objets que l’on veut. Nous nous préoccupons dans le code qui suit (et qui également pourrait faire l’objet d’une génération automatique) d’assurer une certaine cohérence dans la relation 1 – n ; c’est-à-dire que chaque fois qu’un objet est ajouté du côté du « n » , on se débrouille pour que celui du côté du « 1 » soit bien celui auquel on vient d’ajouter cet objet, et pas un autre. Si cela vous paraît un peu biscornu, on vous comprend, mais lisez bien le petit code qui suit et vous devriez mieux comprendre. import java.util.*; class Musicien { private ArrayListmesInstruments = new ArrayList(); // déclaration de la liste typée public Musicien() {}

UML 2 CHAPITRE 10

177

public void addInstrument(Instrument unInstrument) { mesInstruments.add(unInstrument); // ajout d’un élément à la liste unInstrument.setMusicien(this); // assure la cohérence de la relation 1-n // this réfère l’objet lui-même } } class Instrument { private Musicien monMusicien; public Instrument() {} public void setMusicien(Musicien monMusicien) { this.monMusicien = monMusicien; } }

Le code Python qui suit est encore plus détaillé afin de montrer comment la relation 1-n est assurée et comment la coder. L’autre raison de la présence de ce code est de rendre un petit hommage à la simplicité avec laquelle Python permet au programmeur d’utiliser deux types de données extrêmement puissants (et sur lesquelles nous reviendrons) qui sont les « listes » et les « dictionnaires ». C’est une des forces de ce langage. Une liste est une séquence ordonnée et modifiable d’éléments extrêmement facile à manipuler et à gérer. Un dictionnaire est une collection quelconque d’objets, cette fois-ci non ordonnée, les objets étant indicés par des valeurs arbitraires appelées clés, et, là aussi, fort efficace à l’emploi. Il s’agit certainement du type intégré de données le moins contraingnant et le plus apprécié des programmeurs Python. class Musicien: __mesInstruments = [] #déclaration de la liste, difficile de faire plus simple __nom = None def __init__(self,nom): self.__nom = nom def addInstrument(self,unInstrument): self.__mesInstruments.append(unInstrument) # ajout d’un élément à la liste unInstrument.setMusicien(self) def printInstrument(self): for x in self.__mesInstruments: #instruction très élégante également print x def __str__(self): return self.__nom #définit ce qui apparaît quand on appele le référent de l’objet class Instrument: __monMusicien = None __type = None def __init__(self,type): self.__type = type def setMusicien(self,monMusicien): self.__monMusicien = monMusicien

178

L’orienté objet

def printMusicien(self): print self.__monMusicien def __str__(self): return self.__type guitare = Instrument("guitare") django = Musicien("django") django.addInstrument(guitare) django.printInstrument() guitare.printMusicien()

Résultat guitare django

Il est possible de préciser ou de désambiguïser cette cardinalité en recourant à un troisième diagramme UML, très rarement utilisé, sinon à cela, : le diagramme d’objet, qui s’apparente à une version instanciée du diagramme de classe. Rappelons que lors de l’exécution du programme, ce sont les objets qui occupent la mémoire pour s’occuper également des traitements. Néanmoins, comme tout ce qu’ils font, y compris les envois de message, est repris dans leur classe, le diagramme de classe suffit le plus souvent à décrire leur comportement. Dans ce diagramme d’objet, en lieu et place des classes, chaque objet apparaîtra, ainsi que le ou les objets avec lesquels il se trouve en interaction. Par exemple, le diagramme d’objet représenté par la figure 10-6 permet de préciser le diagramme de classe dans le cas d’un musicien, dont le référent est Django, ne possédant que deux instruments, dont les référents sont guitare et violon. Étant donnée la présence du n dans la cardinalité, il serait tout à fait imaginable que d’autres musiciens possèdent 1 ou 10 000 instruments. Figure 10-6.

guitare : Instrument

Diagramme d’objet précisant un diagramme de classe.

Django : Musicien

violon : Instrument

Finalement, un diagramme de classe peut marquer la différence entre plusieurs référents pointant pourtant vers une même classe, comme illustré par la figure 10-7 en présence du code Java correspondant. Figure 10-7.

Un diagramme de classe avec deux liens d’association.

UML 2 CHAPITRE 10

179

class Musicien { Instrument unPremierInstrument ; Instrument unDeuxièmeInstrument ; }

La multiplicité des référents plutôt qu’une même association avec une cardinalité multiple (dans le cas présent, une relation 1-2 pourrait sembler faire l’affaire) tient au rôle différent que sont appelés à jouer les deux référents. Ainsi, le premier instrument pourrait être un instrument solo que le musicien utilise pour l’essentiel et le deuxième, un instrument d’accompagnement, nettement moins sollicité. Les messages qui leur seront envoyés seront suffisamment différents pour qu’il en devienne nécessaire d’en faire deux attributs séparés.

Dépendance entre classes Nous avons expliqué dans le chapitre 4 qu’il suffit, pour que le lien d’association s’affaiblisse en un lien de dépendance (dans le diagramme de classe, le trait d’association se transforme alors en un trait en pointillés), que la méthode jeTravaillePourO1(O2 lienO2) reçoive en argument un objet de type O2. Une autre version que nous avons vue d’un lien de dépendance est indiquée en Java dans le code qui suit : class O1 { private int unAttribut; private int unAutreAttribut ; public void jeTravaillePourO1() { O2 lienO2 = new O2() ; /* création d’un objet local O2 */ lienO2.jeTravaillePourO2() ; /* Voici l’envoi de message */ } // l’objet lienO2 disparaît public int uneAutreMethode(int a){ return a; } }

Dans cette seconde version d’un lien de dépendance, l’objet O2, qui exécutera le message et justifie la dépendance, n’aura, comme dans le cas précédent, d’existence que pendant l’exécution de la méthode où cet objet est créé. En dehors de cette méthode, l’objet O2, ainsi que le lien entre les deux classes, s’effacent. Dans le cas d’un passage par argument, seul le lien s’efface car l’objet O2 continue à exister. En UML, un lien de dépendance entre deux éléments signifie, simplement, que toute modification dans l’un risque d’entraîner une modification de celui qui en dépend. Comme pour une association, un envoi de messages entre les deux classes pourra avoir lieu. Cependant, ce lien ne dure que le temps de l’exécution de la méthode, et ne s’apparente plus à une propriété structurelle de la classe O1. Ce lien de dépendance entre classes est, en conséquence, moins fréquemment rencontré que le lien, permanent et plus effectif, d’association. Comme illustré dans la figure 10-8, on retrouve ce même lien de dépendance (une flèche) dans un quatrième diagramme UML, le diagramme de composants, entre les fichiers dans lesquels sont stockés des éléments logiciels dépendants. On comprend mieux encore dans le cas des fichiers la nature du lien de dépendance pointillée. En effet, lorsque l’on modifie un fichier, il est fréquent qu’il faille modifier tous ceux qui en dépendent. Par exemple, si l’on modifie le contenu d’une base de données, il faudra également vérifier que tous les programmes qui s’interfacent avec cette dernière soient toujours valides.

180

L’orienté objet

Figure 10-8.

Diagramme de composants reprenant simplement les fichiers et les liens de dépendance entre ceux-ci. Le fichier 1 dépend du fichier 2 et ce dernier dépend du fichier 3.

Composition Transformons maintenant le lien d’association entre les deux classes en un lien de composition (dit encore d’agrégation forte ou d’agrégation par valeur), et observons-en les conséquences sur le code. Le lien de composition entre deux classes entraîne les instances correspondantes à s’imbriquer l’une dans l’autre dans la mémoire, pile ou tas. La disparition du composite entraînera systématiquement la disparition des composants, et la cardinalité ne pourra prendre que la valeur « 1 » du côté du composite. Le lien d’agrégation faible, dit simplement d’agrégation, ne se comporte pas, une fois traduit en code, de manière différente au lien d’association. Dès lors, parler d’agrégation plutôt que d’association revient simplement à particulariser la sémantique de cette relation et à augmenter la fidélité à la réalité qu’elle dépeint. Si les objets sont physiquement imbriqués les uns dans les autres, comme les atomes dans une molécule, on choisira le lien de composition. Sinon, et tant que subsiste malgré tout une situation d’appartenance entre deux objets, on parlera d’agrégation. Ainsi, on peut dire d’un enfant qu’il est agrégé dans une famille, mais qu’il est simplement associé à son père. Il aura été, pendant une première période de son existence, délicate à estimer très précisément (car les progrès de la science font qu’elle diminue chaque jour un peu plus), un objet « composant » de sa mère (jusqu’au jour où l’enfant naîtra, et ne le sera plus du tout ). On ne sera pas trop surpris d’apprendre qu’une et une seule mère peut porter l’enfant. Les « mères porteuses », c’est autre chose. Ce lien de composition est celui, dans l’écosystème, qui relie la vision à la proie et au prédateur. En effet, les deux animaux sont bien dotés d’une vision, qui les suivra dans la vie comme dans la mort. Cette vision fait partie intégrante d’eux mêmes. Pour un adepte de la bricole informatique, les composants hardware, comme le processeur, le disque dur ou la RAM, sont agrégés dans l’ordinateur. Pour tous les autres, et ils sont nombreux, ce sont des composants de l’ordinateur qui l’accompagneront d’office à la poubelle. Lors de l’agrégation, la classe « agrégeante » peut indiquer une multiplicité supérieure à 1, alors que lors d’une composition, la classe « composite » ne peut indiquer qu’une multiplicité inférieure ou égale à 1, pour la simple raison qu’un objet ne peut s’imbriquer physiquement dans deux objets à la fois. Le lien de composition peut d’ailleurs se visualiser dans un diagramme d’objet comme un rectangle dans un autre. Éliminons le premier rectangle et celui qui se trouve à l’intérieur disparaît automatiquement. Il n’est pas possible pour le rectangle intérieur d’être présent dans deux rectangles à la fois. Transformons le diagramme UML précédent en sa nouvelle version, dans laquelle la première association se transforme en composition et la deuxième en agrégation, et voyons l’effet résultant dans les différents langages de programmation. De manière à différencier les mécanismes de vie et de mort des objets découlant de ces différents types de relation, dans l’exemple qui suit, la classe O1 sera reliée à la classe

UML 2 CHAPITRE 10

181

O3 par un lien d’agrégation, et à la classe O2 par un lien de composition (le losange est vide pour l’agrégation et plein pour la composition). Attention à l’emplacement du losange du côté de la classe contenante et non contenue. Figure 10-9.

Dans ce diagramme de classe UML, la classe O2 est reliée à la classe O1 par un lien de composition, et la classe O3 est, quant à elle, reliée à la classe O1 par un lien d’agrégation. Un objet O2 sera physiquement intégré dans un objet O1.

En Java Nous allons, d’abord en Java, proposer deux versions de code réalisant cette relation de composition. Dans le premier code (UML3.java), l’objet O2 est un composant de O1, pour la simple raison qu’il n’a d’existence qu’à l’intérieur de O1. En effet, il est construit dans le constructeur d’O1, avec, pour conséquence, que le seul référent à cet objet O2 est un attribut de O1. De manière à illustrer cette extrême dépendance entre l’objet O1 et l’objet O2, nous avons recourt à une méthode particulière appelée finalize(). Nous avons déjà rencontré cette méthode, appelée le « destructeur », dans le chapitre précédent. Elle permet, juste avant l’élimination d’un objet, de s’assurer que les ressources utilisables, uniquement à partir de cet objet, seront libérées également. Elle ne peut recevoir d’argument et est d’office sans « retour », vu son mode d’appel. En pratique, il pourrait s’agir d’une connexion à un fichier (la méthode s’occupera donc de libérer l’accès à ce fichier), ou d’une connexion réseau que, là encore, la méthode pourrait interrompre, après avoir éliminé le seul objet connecté au réseau. Ici, nous l’utiliserons, juste pour qu’elle nous indique à quel moment l’objet est éliminé. Le code qui suit crée un ensemble d’objets à répétition, de manière que le ramasse-miettes soit appelé automatiquement, et récupère un certain nombre de ces objets devenus inutiles. À la différence des codes présentés au chapitre précédent, il n’y a pas, ici, d’appel explicite au ramasse-miettes. Nous préférons l’utiliser comme il l’est dans la plupart des cas, c’est-à-dire, décidant seul de la nécessité de son intervention, quand la mémoire commence à se saturer. Nous forçons simplement son intervention par l’utilisation de la boucle. Le résultat de la simulation des deux codes Java est indiqué juste en dessous de ces codes. Nous n’en montrons qu’une partie, vu la présence de la boucle allant jusqu’à 10 000.

182

L’orienté objet

UML3.java class O1 { private private private private

int unAttribut; int unAutreAttribut; O2 lienO2; O3 lienO3;

public O1(O3 lienO3) { lienO2 = new O2(); /* un lien de composition est créé */ this.lienO3 = lienO3; /* un lien d’agrégation est créé */ } public void jeTravaillePourO1() { lienO2.jeTravaillePourO2(); /* un message vers O2 */ lienO3.jeTravaillePourO3(); /* un message vers O3 */ } public int uneAutreMethode(int a) { return a; } protected void finalize() /* appel de cette méthode quand l’objet est effacé de la mémoire */{ System.out.println("aaahhhh... un Objet O1 se meurt ..."); } } class O2 { private int unAttribut; private double unAutreAttibut; public O2() {} public void jeTravaillePourO2() { System.out.println("Je suis une instance d'O2 " + "au service de toutes les classes"); } protected void finalize(){ System.out.println("aaahhhh... un Objet O2 se meurt ...."); } } class O3 { public void jeTravaillePourO3() { System.out.println("Je suis une instance d'O3 " + "au service de toutes les classes"); } protected void finalize(){ System.out.println("aaahhhh... un Objet O3 se meurt ...."); } } public class UML3 { public static void main(String[] args) { O3[] lesObjets3 = new O3[10000]; for (int i=0; i<10000; i++){ lesObjets3[i] = new O3();

UML 2 CHAPITRE 10

183

O1 unObjet1 = new O1(lesObjets3[i]); // On passe ici le référent de l'objet O3 à l'objet O1 unObjet1.jeTravaillePourO1(); unObjet1 = null; /* Par cette instruction, on cherche à se débarasser de l’objet unObjet1, ➥ mais elle n’est pas nécessaire */ } } }

Résultats aaahhhh... un objet O1 se aaahhhh... un objet O1 se aaahhhh... un objet O2 se aaahhhh... un objet O1 se aaahhhh... un objet O1 se aaahhhh... un objet O2 se Je suis une instance d’O2 Je suis une instance d’O3 Je suis une instance d’O2 Je suis une instance d’O3 Je suis une instance d’O2 aaahhhh... un objet O1 se aaahhhh... un objet O1 se aaahhhh... un objet O2 se aaahhhh... un objet O1 se

meurt... meurt... meurt... meurt... meurt... meurt... au service au service au service au service au service meurt... meurt... meurt... meurt...

de de de de de

toutes toutes toutes toutes toutes

les les les les les

classes classes classes classes classes

Ce n’est, en effet, qu’une petite partie du résultat affiché. Nous constatons, dans le résultat de la simulation, qu’en assignant les référents des objets O1 à null, le ramasse-miettes fait son boulot, comme prévu, en se débarrassant au fur et à mesure, et à notre insu, des objets O1 devenus encombrants. Notez que cette affectation à null n’est pas requise pour faire disparaître l’objet, étant donné que le même référent unObjetO1 est partagé par tous les objets O1, et que chacun de ces objets ne sera donc référé que le temps d’une itération de la boucle. À la fin de cette itération, il sera livré en pâture au ramasse-miettes. Mais ce qui nous importe le plus ici, c’est l’élimination des objets O2, alors que nulle part dans l’écriture du code nous ne l’avons explicitement ordonné. L’objet O1, en disparaissant, entraîne dans sa disparition le seul référent à l’objet O2, ce qui permet au ramassemiettes d’y jeter également son dévolu. Nous constatons, en revanche, que les objets O3, juste agrégés qu’ils sont, les veinards, subsisteront, quant à eux, jusqu’à l’arrêt pur et simple du programme. UML3bis.java class O1 { private private private private

int unAttribut; int unAutreAttribut; O2 lienO2; O3 lienO3;

public O1(O3 lienO3) { lienO2 = new O2(); /* une relation de composition */ this.lienO3 = lienO3; /* une relation d’agrégation */ }

184

L’orienté objet

public void jeTravaillePourO1() { lienO2.jeTravaillePourO2(); /* un message vers O2 */ lienO3.jeTravaillePourO3(); /* un messaege vers O3 */ } public int uneAutreMethode(int a){ return a; } protected void finalize(){ System.out.println("aaahhhh... un Objet O1 se meurt ...."); /* appel de cette méthode quand l’objet est effacé de la mémoire */ } private class O2 { /* la classe O2 est maintenant déclarée à l’intérieur de O1 */ private int unAttribut; private double unAutreAttibut; public O2() {} public void jeTravaillePourO2() { System.out.println("Je suis une instance d’O2 " + "au service de toutes les classes"); } protected void finalize() { System.out.println("aaahhhh... un Objet O2 se meurt ...."); } } } class O3 { public void jeTravaillePourO3() { System.out.println("Je suis une instance d'O3 " + "au service de toutes les classes"); } protected void finalize(){ System.out.println("aaahhhh... un Objet O3 se meurt ...."); } } public class UML3bis{ public static void main(String[] args){ O3[] lesObjets3 = new O3[10000]; for (int i=0; i<10000; i++) { lesObjets3[i] = new O3(); O1 unObjet1 = new O1(lesObjets3[i]); // On passe ici le référent de l'objet O3 à l'objet O1 unObjet1.jeTravaillePourO1(); unObjet1 = null; /* Par cette instruction, on cherche à se débarrasser de l’objet ➥unObjet1, mais elle n’est pas nécessaire */ } } }

Résultat Le résultat est en tout point semblable au code précédent, une succession et une alternance de : aaahhhh... un objet O1 se meurt...

UML 2 CHAPITRE 10

185

aaahhhh... un objet O2 se meurt... Je suis une instance d’O3 au service de toutes les classes Je suis une instance d’O2 au service de toutes les classes

Le contenu de ce code présente une manière encore plus radicale de rendre totalement dépendant les objets O2 des objets O1. Dans le code UML3bis.java, la classe O2 est déclarée et créée à l’intérieur de la classe O1. La classe O2 devient interne à O1. Ce mécanisme, d’utilisation assez rare en Java et C#, car assez subtil, permet de fortement solidariser les classes O1 et O2. Suite à cette écriture, seule la classe O1 pourra disposer de la classe O2. Le lien de composition entre les objets se renforce en s’étendant au niveau des classes. Dans toutes ses méthodes, la classe O2 pourra utiliser tout ce qui caractérise la classe O1 et vice versa.

En C# Le premier fichier C# est assez semblable au fichier Java, à quelques détails de syntaxe près que nous laisserons de côté pour l’instant. Le plus important est, sans doute, le remplacement de la méthode finalize() par un destructeur écrit « à la C++ ». À l’instar du constructeur, le destructeur porte le même nom que la classe en le faisant précéder du caractère « ~ ». Il ne peut recevoir d’argument et est d’office dans « retour ». Le résultat affiché est le même que celui obtenu par le programme Java. Le second fichier Java pourrait également être repris en C#, en le laissant pratiquement inchangé. Une solution bien plus intéressante est présentée par le fichier UML3bis.cs, dans lequel à la classe O2 on substitue une « structure » O2. En C#, la structure, bien que se décrivant également à l’aide d’attributs et de méthodes, est différente de la classe à plus d’un titre. Parmi ces différences essentielles, nous avons vu que les structures ne peuvent hériter entre elles. Cependant, la seule différence qui nous intéresse ici plus particulièrement est le mode de stockage auxquels les objets sont astreints. Alors que les objets instances de la classe O1 et de la classe O3 seront installés dans la mémoire tas, et livrés à la gestion par ramasse-miettes couplée à cette mémoire, les objets instance d’une structure seront, quant à eux, stockés dans la mémoire pile, et livrés, pour leur part, au mode de gestion de mémoire de substitution associé à la pile. Une manière, assez directe donc, de faire d’un objet O2 un objet composite d’un objet O1 est de déclarer O2 comme une structure, et de simplement faire de l’objet O2 un attribut de O1. Il sera de ce fait automatiquement attaché au seul objet O1 et disparaîtra avec celui-ci. Vous constatez, par rapport au code précédent, qu’il n’y a pas lieu de construire celui-ci dans le constructeur d’O1. Il se construit automatiquement avec une instance d’O1.

UML3.cs using System; class O1 { private int unAttribut; private int unAutreAttribut; private O2 lienO2; private O3 lienO3; public O1(O3 lienO3) { lienO2 = new O2(); this.lienO3 = lienO3; } public void jeTravaillePourO1() { lienO2.jeTravaillePourO2(); lienO3.jeTravaillePourO3(); }

186

L’orienté objet

public int uneAutreMethode(int a){ return a; } ~O1() /* le destructeur en C# semblable à la manière de le définir en C++ sans argument et sans ➥ retour*/ { Console.WriteLine("aaahhhh... un Objet O1 se meurt ...."); } } class O2 { private int unAttribut; private double unAutreAttibut; public O2() {} public void jeTravaillePourO2() { Console.WriteLine("Je suis une instance d'O2 " + "au service de toutes les classes"); } ~O2(){ Console.WriteLine("aaahhhh... un Objet O2 se meurt ...."); } } class O3 { public void jeTravaillePourO3() { Console.WriteLine("Je suis une instance d'O3 " + "au service de toutes les classes"); } ~O3(){ Console.WriteLine("aaahhhh... un Objet O3 se meurt ...."); } } public class UML3 { public static void Main(String[] args){ O3[] lesO3 = new O3[100000]; for (int i=0; i<100000; i++){ lesO3[i] = new O3(); O1 unObjet1 = new O1(lesO3[i]); /* On passe ici le référent de l'objet3 à l'objet1 */ unObjet1.jeTravaillePourO1(); unObjet1 = null; /* pas forcément nécessaire */ } } }

UML3bis.cs using System; class O1 { private int unAttribut; private int unAutreAttribut; private O2 lienO2; private O3 lienO3; public O1(O3 lienO3) { this.lienO3 = lienO3; }

UML 2 CHAPITRE 10

187

public void jeTravaillePourO1() { lienO2.jeTravaillePourO2(); lienO3.jeTravaillePourO3(); } public int uneAutreMethode(int a) { return a; } ~O1() { /* le destructeur en C# semblable à la manière de le définir en C++ sans argument et ➥sans retour */ Console.WriteLine("aaahhhh... un Objet O1 se meurt ...."); } } struct O2 { /* Nous faisons de O2 une structure plutôt qu’une classe */ private int unAttribut; private double unAutreAttibut; public void jeTravaillePourO2() { Console.WriteLine("Je suis une instance d'O2 " + "au service de toutes les classes"); } } class O3 { public void jeTravaillePourO3() { Console.WriteLine("Je suis une instance d’O3 " + "au service de toutes les classes"); } ~O3() { Console.WriteLine("aaahhhh... un Objet O3 se meurt ...."); } } public class UML3 { public static void Main(String[] args) { O3[] lesO3 = new O3[100000]; for (int i=0; i<100000; i++) { lesO3[i] = new O3(); O1 unObjet1 = new O1(lesO3[i]); /* On passe ici le référent de l'objet3 à l'objet1 */ unObjet1.jeTravaillePourO1(); unObjet1 = null; /* pas forcément nécessaire */ } } }

En C++ En C++, au contraire du Java et du C#, rien n’est prévu pour se débarrasser automatiquement des objets qui squattent la mémoire tas. Aucun ramasse-miettes ne viendra seconder un programmeur défectueux. Vous êtes seul maître à bord et, à ce titre, vous ne pourrez quitter le navire qu’une fois que tous les objets l’auront quitté. Pour cela, une instruction vous est proposée : delete, qui permet, effectivement, d’éliminer les objets installés dans la mémoire tas. Dans la mémoire pile, le mécanisme d’effacement est automatique, et se fait par désempilement systématique de ce qui y a été le plus récemment empilé.

188

L’orienté objet

Afin de suivre à la trace la disparition des objets, le « destructeur » est utilisé qui, comme en C#, porte le même nom que la classe, avec juste un petit « ~ » qui précède son nom. Nous verrons dans la suite que le rôle qui lui est imparti est beaucoup plus important et sensible que celui en Java, plus marginalisé, du finalize(). La raison en est, une fois encore, l’absence du ramasse-miettes en C++. Dans le code UML3.cpp ci-après, l’objet de la classe O2 est un composant de la classe O1, alors que l’objet de la classe O3 lui est simplement associé. On constate que la différence principale réside dans le mode de déclaration de ces attributs. L’objet O3 l’est, via l’utilisation explicite d’un pointeur, l’objet O2, non. Dans la mémoire, l’objet O2 sera comme installé à l’intérieur de l’objet O1, alors que l’objet O3 sera, comme c’est l’usage en Java et C#, juste référé (ou pointé) par une variable adresse stockée dans l’objet O1. Comme le montre le résultat de l’exécution du code UML3.cpp, lors de la destruction d’O1, l’objet O2 sera également détruit, puisque son seul champ d’action est l’objet O1. De nouveau, les quatre apparitions des mêmes phrases sont liées aux alternatives mémoire tas et pile que nous expérimentons. Les objets « tas » disparaissent suite à l’utilisation de l’instruction delete, alors que les objets « pile » disparaissent à la fin du main.

UML3.cpp #include "stdafx.h" #include "iostream.h" class O2 { private: int unAttribut2; double unAutreAttribut2; public: void jeTravaillePourO2() { cout << "Je suis une instante d'O2" << " au service de toutes les classes" << endl; } ~O2(){ cout <<"aaahhhh... un Objet O2 se meurt ...." << endl; } }; class O3 { public: void jeTravaillePourO3() { cout << "Je suis une instance d'O3" << " au service de toutes les classes" << endl; } ~O3() { cout <<"aaahhhh... un Objet O3 se meurt ...." << endl; } }; class O1 { private: int unAttribut; int unAutreAttribut; O3* lienO3; O2 lienO2;

UML 2 CHAPITRE 10

public: O1(O3* lienO3) { this->lienO3 = lienO3; } void jeTravaillePourO1() { lienO2.jeTravaillePourO2(); lienO3 -> jeTravaillePourO3(); } int uneAutreMethode(int a) { return a; } ~O1(){ cout <<"aaahhhh... un Objet O1 se meurt ...." << endl; } }; int main(int argc, char* argv[]) { O3* unObjet3Tas = new O3(); O3 unObjet3Pile; O1* unObjet1Tas = new O1(unObjet3Tas); O1* unObjet11Tas = new O1(&unObjet3Pile); O1 unObjet1Pile(unObjet3Tas); O1 unObjet11Pile(&unObjet3Pile); unObjet1Tas->jeTravaillePourO1(); unObjet11Tas->jeTravaillePourO1(); unObjet1Pile.jeTravaillePourO1(); unObjet11Pile.jeTravaillePourO1(); delete unObjet1Tas; /* effacement du premier objet sur le tas */ delete unObjet11Tas; /* effacement du deuxième objet sur le tas */ return 0; } /* tous les objets piles disparaissent */

Résultat Je suis une instance d’O2 Je suis une instance d’O3 Je suis une instance d’O2 Je suis une instance d’O3 Je suis une instance d’O2 Je suis une instance d’O3 Je suis une instance d’O2 Je suis une instance d’O3 aaahhhh... un objet O1 se aaahhhh... un objet O2 se aaahhhh... un objet O1 se aaahhhh... un objet O2 se aaahhhh... un objet O1 se

au service au service au service au service au service au service au service au service meurt... meurt... meurt... meurt... meurt...

de de de de de de de de

toutes toutes toutes toutes toutes toutes toutes toutes

les les les les les les les les

classes classes classes classes classes classes classes classes

189

190

L’orienté objet

aaahhhh... aaahhhh... aaahhhh... aaahhhh...

un un un un

objet objet objet objet

O2 O1 O2 O3

se se se se

meurt... meurt... meurt... meurt...

Y a-t-il moyen de réaliser, comme en Java et en C#, un lien de composition entre deux objets, mais installés dans la mémoire tas cette fois ? Cela est tout à fait possible, comme dans le code UML3bis.cpp montré ci- après, mais l’utilisation du destructeur devient alors capitale. Cette élimination ne s’effectuant plus automatiquement, comme en Java et C# grâce au ramasse-miettes, il faudra, lors de la destruction d’un objet O1, récrire le destructeur de la classe, de manière qu’il s’occupe également de la liquidation de son protégé. Le code du destructeur de la classe O1 se doit maintenant d’inclure l’instruction delete lienO2 , autrement de nombreux objets O2 risquent de flotter sans ancrage et en totale perdition dans la mémoire. Cette vigilance accrue de la part du programmeur est une des raisons essentielles de la présence du ramasse-miettes dans les autres langages. L’autre raison étant la présence, de nouveau si la vigilance se relâche, de référents fous, obtenus, à l’inverse du cas précédent, par l’usage, cette fois-ci un peu précipité, de l’instruction delete().

UML3bis.cpp UML3bis.cpp #include "stdafx.h" #include "iostream.h" class O2 { private: int unAttribut2; double unAutreAttribut2; public: void jeTravaillePourO2(){ cout << "Je suis une instante d'O2" << " au service de toutes les classes" << endl; } ~O2(){ cout <<"aaahhhh... un Objet O2 se meurt ...." << endl; } }; class O3 { public: void jeTravaillePourO3() { cout << "Je suis une instante d'O3" << " au service de toutes les classes" << endl; } ~O3() { cout <<"aaahhhh... un Objet O3 se meurt ...." << endl; } }; class O1 { private: int unAttribut; int unAutreAttribut;

UML 2 CHAPITRE 10

191

O3* lienO3; O2* lienO2; public: O1(O3* lienO3) { lienO2 = new O2(); this->lienO3 = lienO3; } void jeTravaillePourO1() { lienO2->jeTravaillePourO2(); lienO3->jeTravaillePourO3(); } int uneAutreMethode(int a) { return a; } ~O1(){ cout <<"aaahhhh... un Objet O1 se meurt ...." << endl; delete lienO2; /* Il faut s'occuper également de l'élimination de l'objet O2 */ } }; int main(int argc, char* argv[]){ O3* unObjet3Tas = new O3(); O3 unObjet3Pile; O1* unObjet1Tas = new O1(unObjet3Tas); O1* unObjet11Tas = new O1(&unObjet3Pile); O1 unObjet1Pile(unObjet3Tas); O1 unObjet11Pile(&unObjet3Pile); unObjet1Tas->jeTravaillePourO1(); unObjet11Tas->jeTravaillePourO1(); unObjet1Pile.jeTravaillePourO1(); unObjet11Pile.jeTravaillePourO1(); delete unObjet1Tas; delete unObjet11Tas; return 0; }

Grâce à la redéfinition explicite du destructeur dans la classe O1, et c’est le rôle premier que celui-ci est appelé à jouer en C++, un vrai lien de composition peut exister entre deux classes, même si leurs instances sont stockées dynamiquement dans la mémoire RAM.

En Python En Python, rien de particulier, c’est encore le ramasse-miettes qui fait la pluie et le beau temps, et les deux codes que nous présentons sont en tout point semblables aux deux codes Java et C#, le premier avec l’objet créé dans le constructeur de la classe composite, le deuxième par le mécanisme de classes imbriquées. Profitez de cet exemple pour vérifier la souplesse et la facilité d’utilisation des dictionnaires :

192

L’orienté objet

UML3.py class O1: __unAttribut=0 __unAutreAttribut=0 __lienO2=None __lienO3=None def __init__ (self, lienO3): self.__lienO2=O2() self.__lienO3=lienO3 def jeTravaillePourO1(self): self.__lienO2.jeTravaillePourO2() self.__lienO3.jeTravaillePourO3() def uneAutreMethode(a): return a def __del__(self): print "aaahhhh... un Objet 01 se meurt ..." class O2: __unAttribut=0 __unAutreAttribut=0 def __init__(self): pass def jeTravaillePourO2(self): print "je suis une instance d'02 " + "au service de toutes les classes" def __del__(self): print "aaahhhh... un Objet 02 se meurt....." class O3: def jeTravaillePourO3(self): print "je suis une instance d'03 " + "au service de toutes les classes" def __del__(self): print "aaahhhh... un Objet 03 se meurt....." lesObjetsO3={} i=0 while i<10: lesObjetsO3[i]=O3() unObjet1=O1(lesObjetsO3[i]) unObjet1.jeTravaillePourO1() unObjet1=None i+=1

UML 2 CHAPITRE 10

UML3bis.py class O1: __unAttribut=0 vunAutreAttribut=0 lienO2=None lienO3=None def __init__ (self, lienO3): self.lienO2=self.O2() self.lienO3=lienO3 def jeTravaillePourO1(self): self.lienO2.jeTravaillePourO2() self.lienO3.jeTravaillePourO3() def uneAutreMethode(a): return a def __del__(self): print "aaahhhh... un Objet 01 se meurt ..." class O2: #la classe O2 est maintenant imbriquée dans la classe O1 __unAttribut=0 __unAutreAttribut=0 def __init__(self): pass def jeTravaillePourO2(self): print "je suis une instance d'02 " + "au service de toutes les classes" def __del__(self): print "aaahhhh... un Objet 02 se meurt....." class O3: def jeTravaillePourO3(self): print "je suis une instance d'03 " + "au service de toutes les classes" def __del__(self): print "aaahhhh... un Objet 03 se meurt....." lesObjetsO3={} i=0 while i<10: lesObjetsO3[i]=O3() unObjet1=O1(lesObjetsO3[i]) unObjet1.jeTravaillePourO1() unObjet1=None i+=1

En PHP 5 Relation de composition

Relation de composition



193

L’orienté objet

194


lienO2 = new O2(); $this->lienO3 = $lienO3; } public function jeTravaillePourO1() { $this->lienO2->jeTravaillePourO2(); $this->lienO3->jeTravaillePourO3(); } public function uneAutreMethode(int $a) { return $a; } public function __destruct() { print ("aaahhhh.... un objet O1 se meurt ....
\n"); } } class O2 { private $unAttribut; private $unAutreAttribut; public function __construct() {} public function jeTravaillePourO2() { print("je suis O2 au service de toutes les classes
\n"); } public function __destruct() { print ("aaahhhh.... un objet O2 se meurt ....
\n"); } } class O3 { public function jeTravaillePourO3() { print("je suis O3 au service de toutes les classes
\n"); } public function __destruct() { print ("aaahhhh.... un objet O3 se meurt ....
\n"); } }

UML 2 CHAPITRE 10

195

while ($i<10) { $lesObjetsO3[$i]=new O3(); $unObjet1=new O1($lesObjetsO3[$i]); $unObjet1->jeTravaillePourO1(); $unObjet1 = NULL; $i+=1; } ?>

Rien de bien particulier. Nous ne présentons en PHP 5 que la première version de la compostion car les classes internes ne semblent pas vouloir être supportées par le langage à l’heure où nous écrivons ces lignes. Ici également, le « ramasse-miettes » remplit son rôle en se débarassant des objets inutiles et, cela, dès le moment où ils le sont devenus. Le ramasse-miettes agit à tout moment et non pas de façon intermittente comme en Java et .Net. Ainsi, à chaque nouvel affectation du référent, les objets précédemment référés disparaîtront de la mémoire. Composition Bien que le lien d’agrégation et de composition servent à reproduire, tous deux, une relation de type « un tout et ses parties », le lien de composition rend, de surcroît, l’existence des objets tributaires de l’existence de ceux qui les contiennent. L’implantation de cette relation dans les langages de programmation dépendra de la manière très différente dont les langages de programmation gèrent l’occupation mémoire pendant l’exécution d’un programme. Nous retrouvons le besoin pour les programmeurs C++ de redoubler d’attention par l’utilisation du delete. Pour les programmeurs des autres langages, ils devront s’assurer que le seul référent de l’objet contenu est possédé par l’objet contenant.

Classe d’association Une dernière possibilité offerte par le diagramme de classe est la notion de classe d’association, illustrée par la figure 10-10 dans le cas de musiciens et de leurs instruments. Figure 10-10.

Petit exemple d’une classe d’association : la classe Performance qui relie un et un seul musicien à son instrument pendant la performance.

Musiciens

Instruments 1..*

1..*

Performance

class Performance { Musicien unMusicien ; Instrument unInstrument ; }

196

L’orienté objet

Dans la figure 10-10, la classe Performance est une classe d’association qui fait le lien entre un et un seul musicien et un et un seul instrument, et cela bien qu’un musicien puisse être associé à plusieurs instruments et réciproquement. Elle se rattache par un trait pointillé à la liaison entre les deux classes qu’elle associe. Il y aura un objet performance bien particulier pour toute association entre un objet musicien et un objet instrument. Chaque objet de la classe d’association possédera un référent vers un objet particulier de chacune des classes associées. D’autres classes d’association typiques sont l’emprunt d’un livre par un lecteur dans une bibliothèque, la réservation d’un billet de spectacle par un client donné ou l’emploi d’une personne dans une société.

Les paquetages Il est également possible de représenter les paquetages et leurs liens de dépendance ou d’imbrication, comme le montre la figure 10-11. Un paquetage sera dépendant d’un autre lorsqu’une classe ou un fichier contenu dans le premier s’avère dépendant d’une classe ou d’un fichier contenu dans le deuxième. Figure 10-11.

Le paquetage E est dépendant du paquetage B. Le paquetage C est dépendant des paquetages B et D. Les paquetages B, C et E se trouvent à l’intérieur du paquetage A.

D

A C B

E

Les bienfaits d’UML De manière à illustrer l’apport précieux des diagrammes de classe, deux exemples sont présentés ci-après. Le premier reprend le petit écosystème du chapitre 3 (dont le code sera esquissé par la suite). Le second, non accompagné d’un code, présente ce que pourrait donner une première ébauche d’analyse, ayant comme but final la réalisation d’un logiciel de match de football. Nous représentons les classes le plus simplement possible, sans y indiquer leurs attributs et méthodes. Ce qui nous intéresse le plus ici, davantage que les classes ellesmêmes, c’est la nature de leurs relations. Ces diagrammes de classe devraient vous apparaître assez compréhensibles, même si nous ne les détaillons pas ici. C’est de fait, dans ses grandes lignes, le pari d’UML.

Un premier diagramme de classe de l’écosystème Voir figure 10.12.

Des joueurs de football qui font leurs classes Voir figure 10.13.

UML 2 CHAPITRE 10 Figure 10-12

Premier diagramme de classe de l’écosystème du chapitre 3.

Figure 10-13

Petit diagramme de classe d’une éventuelle simulation d’un match de football.

197

198

L’orienté objet

Les avantages des diagrammes de classe Plusieurs points importants doivent être soulignés en concluant cette présentation centrée sur le diagramme le plus utilisé des treize diagrammes UML : le diagramme de classe. D’abord, nous sommes loin d’en avoir fini avec l’utilisation de ce dernier. Nous y reviendrons, pas plus tard que dans le prochain chapitre, car nous avons, pour l’instant, mis sous le boisseau un type de relation entre les classes, fondamental en OO : la relation d’héritage. Or, nous voyons bien que, tant dans l’écosystème que lors du match de football, nous pourrions simplifier la conception du modèle, en factorisant dans une classe Animal tout ce qu’il y a de commun entre la proie et le prédateur, et en spécialisant en défenseur et attaquant les joueurs de football. Le diagramme de classe permet de représenter ces liens d’héritage et de spécialisation d’une manière qui sera décrite plus avant. Ensuite, l’isomorphisme entre le diagramme de classe et les codes logiciels qui le traduisent est tel que plusieurs environnements de développement UML, à l’instar de ceux utilisés principalement dans ce livre : TogetherJ et Omondo, permettent une parfaite synchronisation (dénommée en anglais « reverse engineering ») entre ce diagramme et le squelette de code. On parle de squelette de code, car l’intérieur des méthodes n’est nullement spécifié à ce stade. C’est d’ailleurs souvent ce code généré (les principaux langages OO sont concernés), qui permet à un diagramme de classe, créé dans un de ces environnements, d’être récupéré dans un autre. Cette synchronisation peut être extrêmement précieuse, quand on cherche à homogénéiser les codes développés par des développeurs divers, ainsi qu’à documenter ces développements de manière uniforme. La génération automatique de code, qui tenait préalablement du gadget, est une évolution désirée dans la communauté informatique (comme nous l’avons dit, c’est clairement le chemin tracé par l’OMG avec le MDA), qui préfère consacrer l’essentiel de ses efforts à la conception et l’analyse des logiciels en UML, tout en laissant, chaque jour davantage, aux environnements de développement UML, le soin de générer le code qui concrétise cette conception. Par ailleurs, et le modèle embryonnaire du match de football est là pour en témoigner, ce diagramme est une aide extrêmement précieuse à la conception du logiciel et à l’interaction entre les développeurs. Il ne faut pas être un informaticien de génie pour le réaliser et le comprendre. Tout amateur de football (même hooligan) en comprendra aisément la structure. Cela explique que l’existence de ces diagrammes facilite grandement l’interaction entre des personnes impliquées dans un projet, personnes qui peuvent intervenir à différents niveaux de la conception : des décideurs aux programmeurs, et dont le goût pour la programmation peut largement varier. Ils sont une preuve éclatante que l’orienté objet permet au monde qui nous environne d’être la principale source d’inspiration pour le portrait logiciel que l’on cherche à en tirer. L’OO rapproche la programmation du monde réel et, chemin faisant, l’éloigne des instructions élémentaires des processeurs. Le diagramme de classe ouvre la voie à une programmation complètement automatisée, à partir de la seule élicitation des acteurs du problème et des interactions qu’ils entretiennent entre eux. Il est aussi l’ultime étape de cette montée en abstraction qui n’a de cesse de caractériser les développements logiciels. Finalement, ces diagrammes permettent une appréhension globale du développement, qui est impossible quand seul le code est disponible. Par exemple, dans le chapitre 21 dédié aux graphes informatiques, on verra apparaître la structure récursive d’une des solutions, avec la présence très nette d’une fermeture dans le diagramme. Différentes solutions architecturales et algorithmiques peuvent être rapidement évaluées, et surtout comparées, grâce aux diagrammes de classe. Ces derniers sont devenus incontournables dans des projets informatiques de plus en plus lourds et complexes ; utilisés dès le début, ils les accompagnent du long, et peuvent être discutés à différents stades de leur développement, documentés et uniformisés. De par sa décomposition naturelle en classe, l’OO offre à l’informatique une manière de simplifier ses développements. Les diagrammes de classe accompagnent cette offre, la rendant plus attrayante encore, par son détachement accru de l’écriture logicielle et du fonctionnement intime du processeur.

UML 2 CHAPITRE 10

199

Un diagramme de classe simple à faire, mais qui décrit une réalité complexe à exécuter Alors que l’analyse par UML favorise une approche éclatée, classe par classe, au pire une classe se devant de connaître l’interface de quelques autres, l’exécution qui en résulte peut elle, au contraire, impliquer bien plus d’objets, et de manière assez tortueuse. Rien de grave à cela, puisque vous avez laissé la main au seul processeur. Toute la partie compliquée de création, de localisation des objets et de transmission des messages, allant jusqu’au droit de vie et de mort pour chaque objet, lui incombe. Prenez par exemple le marquage d’un but. Le joueur se limite à taper dans la balle. La balle se limite à se déplacer. Les filets se limitent à constater qu’une balle les « pénètre ». S’ils sont pénétrés, ils le signalent à l’arbitre. L’arbitre, alors, envoie le message incrémenteScore() au score. Le score déclenche les cris de joie ou de désespoir des supporters, etc. Nulle part, l’effet très indirect du coup de pied dans le ballon sur les cris des supporters n’a été réellement anticipé, pensé et décortiqué. Cet effet résulte d’une avalanche de messages, circulant de manière conditionnelle (si … alors) et de lien en lien. Ces liens, au cas par cas, sont les seuls à avoir fait l’objet d’une vraie réflexion. Le joueur ne s’occupe que du ballon même si l’effet d’un de ses coups de pied pourrait être les cris des supporters. Le programmeur n’a pas pas programmé cet effet. C’est l’aboutissement d’une succession de messages, chaque programmeur n’ayant pensé et conçu qu’une très petite partie de ceux-ci. On décompose le problème, on le pense acteur par acteur, même si le jeu d’interaction d’acteurs qui s’ensuit se révèle complexe, au point parfois de vous surprendre. C’est l’image du feu d’artifice que nous avons dans le chapitre 4 empruntée à Bertrand Meyer. La chronologie des messages et des effets de ces derniers n’est jamais attaquée de front. On pense les envois de message et ce qui conditionne ceux-ci, de manière logique, au cas par cas. S’il y a une succession de messages, c’est que l’ensemble des conditions se trouve vérifiée, mais ce déroulement n’aura jamais fait l’objet d’une étude exhaustive préalable. C’est un peu comme des musiciens d’orchestre qui répéteraient, deux par deux, de manière à apprendre à jouer ensemble. Quand ils se trouvent, tous, dans la fosse d’orchestre, pour la première fois, la musique qu’ils produisent individuellement s’harmonise, prend corps. Il ne faut pas programmer les classes comme un tout, en se préoccupant de ce qu’elles peuvent faire pour nous, mais plutôt de les programmer en pensant à ce que celles-ci pourront faire d’elles-mêmes et entre elles. C’est la clé de la pratique OO, sans véritable équivalent dans la pratique procédurale.

Procéder de manière modulaire et incrémentale La décomposition en modules indépendants permet, non seulement de simplifier le travail d’analyse, mais de faciliter également la progression du projet dans le temps, par rajout progressif de nouveaux modules. Ces modules, pour autant que leur interface et leur implémentation soient clairement tenues séparées, résisteront assez bien à cette incrémentation progressive, et seront réutilisables dans des contextes très différents. Vous pourrez réutiliser la balle au handball, au volley-ball, et même au rugby, si la signature de la méthode responsable du déplacement de la balle a été clairement détachée de l’implémentation de ce déplacement. Cette pratique incrémentale et modulaire, ponctuée de programmes de complexité croissante, est l’essence même des nouvelles méthodologies de développement qui souvent accompagnent la promotion de l’OO.

Diagramme de séquence Les diagrammes de séquence, que nous avons déjà approchés dans les chapitres 4 et 5, peuvent accompagner le développement d’un projet, à un stade plus avancé que les diagrammes de classe. En effet, ces derniers permettent de visualiser le programme lors de son exécution. Quand celui-ci s’exécute en effet, ce sont les objets

200

L’orienté objet

Figure 10-14.

Un petit diagramme de séquence.

qui s’agitent, en se sollicitant mutuellement par l’envoi de messages, et ce sont précisément ces envois de message qui constituent l’essentiel de ces diagrammes. Tous les programmes présentés plus haut, et quel que soit le langage de programmation utilisé, peuvent se représenter par le diagramme repris du chapitre 4, quand l’objet o1 issu de la classe O1, lors de l’exécution de sa méthode jeTravaillePourO1(), envoie le message jeTravaillePourO2() à l’objet o2 issu de la classe O2. Le temps s’écoule de haut en bas, la succession des messages aussi. La présence des rectangles ainsi que des numéros de messages (1, 1.1…), indiquent la succession et l’emboîtement des appels de méthodes correspondants. Nous reviendrons sur cet emboîtement par la suite, car il peut être fortement relaxé, quand les programmes disposent pour s’exécuter de plusieurs processeurs ou de plusieurs « threads » en parallèle (le multithreading sera présenté au chapitre 17). Dans un cadre d’exécution de programme fonctionnant uniquement de manière séquentielle, on comprend bien la raison de l’emboîtement des rectangles. Il faut bien que la méthode jeTravaillePourO2 termine son exécution afin que la méthode jeTravaillePourO1 puisse reprendre la sienne, d’où le premier rectangle englobant le deuxième et l’addition successive de « . » dans la numérotation des messages. Ainsi, les flèches qui décochent les messages auront des terminaisons différentes, selon que ces messages sont synchrones (l’expéditeur est bloqué en attendant que le destinataire en ait fini avec sa méthode) ou asynchrones (l’expéditeur et le destinatire peuvent travailler en parallèle). Par défaut, les messages sont considérés comme synchrones, s’exécutant sur un processeur unique et sans multithreading. Dans pareil cas, lorsqu’une des instructions d’un corps d’instructions consiste en l’envoi d’un message vers un autre objet, le flot d’instructions du premier objet, expéditeur du message, s’interrompt, le temps que le flot d’instructions du deuxième objet, destinataire du message, déclenché par l’envoi de message, se termine. Le petit bonhomme dans le diagramme représente le point de départ de la séquence de message. Pour un programme dont on représenterait l’entièreté du diagramme de séquence, le petit bonhomme représenterait le main. Cependant, un diagramme de séquence peut démarrer au départ de n’importe quel appel de méthode. Très tôt, certains environnements de développement UML, les précurseurs (comme TogetherJ) ont tenté, au prix de contorsions importantes, et acceptant quelques écarts par rapport à la norme UML, de synchroniser, aussi parfaitement que possible, l’écriture du logiciel et le diagramme de séquence qui l’accompagne. À titre d’exemple, et sans le commenter davantage, un code et le diagramme de séquence généré automatiquement par TogetherJ à partir de celui-ci sont montrés ci-après. L’observation conjointe du code et du diagramme

UML 2 CHAPITRE 10

201

Figure 10-15

Un diagramme de séquence plus compliqué, généré automatiquement à partir du code Java.

résultant devrait permettre d’apprécier l’effort important fourni historiquement par certains développeurs, pour synchroniser, davantage encore, la symbolique des diagrammes UML et l’écriture du logiciel. public class O1 { private int attribute1; private O2 lienO2; private O3 lienO3; public void jeTravaillePourO1(int a) { if (a > 0){ lienO2.jeTravaillePourO2(); } else{ lienO3.jeTravaillePourO3(a); } } } class O2 { private O3 lienO3; public void jeTravaillePourO2() { lienO3.jeTravaillePourO3(6); } }

202

L’orienté objet

class O3 { private O2 lienO2; public void jeTravaillePourO3(int a) { if (a > 0){ a--; jeTravaillePourO3(a); } else { lienO2.jeTravaillePourO2(); } } }

En cela, et vu la nouvelle version d’UML et les nombreuses additions affectant le diagramme de séquence, on peut dire de Together qu’il était en avance sur son temps. En effet, dans UML 2, le diagramme de séquence s’est considérablement enrichi, de façon à synchroniser davantage encore la représentation des diagrammes et le code correspondant. Il ne s’est pas, malheureusement pour eux, enrichi à la manière de Together, mais a conservé le type d’addition que ce dernier avait déjà anticipé dans sa version à lui des diagrammes. Cela va bien sûr dans le sens d’un rhabillage d’UML vers une nouvelle forme de langage de programmation. Voici un exemple de petit code Java. On supposera que les classes O2, O3 et O4 existent par ailleurs avec les méthodes appropriées. La figure 10-16 présente la nouvelle mouture du diagramme de séquence UML 2 correspondant, généré cette fois par la nouvelle version de Together ou par Omondo (le logiciel UML qui se greffe sur l’environnement de développement Java d’Eclipse). class O1 { private O2 o2; private O3 o3; private O4 o4; public void jeTravaillePourO1() { int i = 0; int j = 0; while (i < 100) { if (j > 20) { o2.jeTravaillePourO2(); } else { o3.jeTravaillePourO3(); } i++; } if (j < 50) { o4.jeTravaillePourO4(); } } }

On voit apparaître dans le diagramme de nouveaux éléments graphiques qui permettent un meilleur collage aux mécanismes classiques de la programmation procédurale, qui continuent à constituer le corps des méthodes. Ainsi, les trois grands rectangles intitulés loop, alt et opt correspondent à des « frames », des régions du

UML 2 CHAPITRE 10

203

Figure 10-16

Nouveau diagramme de séquence dans UML 2.

diagramme de séquence divisées en un ou plusieurs fragments (comme dans le cas du frame alt divisé en deux fragments d’un if – else). Une fois labellées par le petit texte en haut à gauche du frame, ces parties du diagramme de séquence peuvent se retrouver n’importe où dans un autre diagramme de séquence. Cela permet, par exemple, de découper le diagramme de séquence en parties distinctes et de déplacer ces parties d’un diagramme à l’autre. Les développeurs ayant réalisé des diagrammes de séquence comprenant des centaines d’objet comprendront très aisément l’intérêt de la chose. Finalement, comme représenté dans le diagramme de séquence suivant, un objet peut être responsable tant de la création que de la disparition d’un autre. Un code C++ correspondant à ce diagramme de classe s’écrirait comme suit : class O1 { public void jeConstruis(){ O2 *unO2 = new O2() ; } public void jeDetruis(){ delete unO2 ; } }

204

L’orienté objet

Figure 10-17.

Un diagramme de séquence qui montre comment l’objet O1 peut avoir droit de vie et de mort sur l’objet O2, d’abord il le crée puis il le détruit.

L’instruction delete étant absente de Java et C#, un effet aussi net et efficace serait plus difficile à obtenir dans ces langages, et il faudrait passer par les bons et loyaux services du ramasse-miettes, en forçant son intervention par un appel explicite. Le diagramme de séquence étant très proche du flot d’exécution d’un programme, on conçoit que l’évolution d’UML vers une forme de langage de programmation ait obligé à ajouter de nouveaux éléments graphiques qui rendent compte de la partie procédurale de ce flot. Cette évolution est très controversée, car elle alourdit considérablement ces mêmes diagrammes, ce qui rend l’utilisation de logiciels de développement UML inévitable. Certains développeurs, qui se sentent plus à l’aise avec une suite logique et écrite d’instructions procédurales, ne verront pas l’intérêt d’un tel enrichissement. La réalisation de ces diagrammes de séquence à la main et pour autant que l’on cherche à respecter fidèlement leur symbolique graphique (surtout l’emboîtement des cadres) tient du parcours du combattant. On comprend dès lors les réticences exprimées par les « UMListes du tableau noir », accrues davantage encore par la deuxième version d’UML. Si l’utilisation d’UML leur fait perdre du temps ou leur complique la vie, retour à l’expéditeur !! Il faudra revoir la copie. Vive les bons vieux langages de programmation. Il n’en reste pas moins que le souci d’universalisation d’UML au-delà de tous les langages de programmation continue à se vérifier, car tous ces langages reprennent ce type de mécanismes procéduraux (test, boucle,…) même si tous le font encore à leur guise et à partir d’une syntaxe légèrement modifiée. UML 2 permet, à nouveau, de transcender les différences, gommer la cosmétique, en se limitant à l’essentiel, les fonctionalités pures. Il permet d’intégrer dans cet esperanto OO, au départ uniquement dédié aux mécanismes OO, les mécanismes de la programmation procédurale. Plus rien ne veut ou ne peut lui échapper. L’entièreté de ce que vous avez fait en Python ou en Java, pourra faire l’objet d’une traduction simultanée en C++ ou en C#. Personnellement, je crois que cela vous permettrait une grosse économie de travail. On est preneur.

UML 2 CHAPITRE 10

205

Exercices Dans plusieurs des énoncés qui suivent, des relations d’héritage doivent être mises en œuvre. Nous vous conseillons donc de vous attaquer à ces énoncés-là après avoir lu le chapitre suivant.

Exercice 10.1 Tentez de dessiner les diagrammes de classe correspondant à la modélisation informatique des énoncés suivants : 1. Vous organisez un convoi humanitaire de véhicules. Les véhicules sont de trois sortes : camion, camionnette, voiture. Chacun des véhicules se caractérise par sa capacité tant à stocker des vivres qu’à transporter des passagers. Vous désirez prévoir à l’avance la consommation du convoi (dépendant, de manière différente pour chaque sorte de véhicule, de leur puissance et de leur charge). 2. Vous décidez de faire des travaux dans votre maison et vous vous interrogez quant aux dépenses à prévoir pour le paiement des salaires des ouvriers qui travailleront sur ce chantier. Vous savez que vous aurez affaire à plusieurs types d’ouvriers se différenciant par la façon dont ils veulent être payés. Certains sont déclarés, d’autres non. Certains veulent être payés à l’heure, d’autres par jour et d’autres encore par semaine. Finalement, certains veulent être payés cash, d’autres par chèque et d’autres encore par virement bancaire. 3. Vous réalisez un programme s’occupant de la gestion d’une petite « Cdthèque » de CD-Rom : éducatif, programme informatique et jeux, que vous souhaitez mettre à la disposition de vos amis pour une période de temps limité (maximum 10 jours). Le prix et la période maximale d’emprunt s’établissent différemment selon la nature des CD (on se base sur un échelon journalier pour les CD jeux, hebdomadaire pour les programmes et mensuel pour les CD éducatifs). Vos amis possèdent 4 types de matériel informatique : Mac, PC (avec Windows XP), PC (avec Windows 95), PC (avec Linux). Les CD-Rom et les informations qu’ils contiennent ne sont lisibles ou exécutables que sur certains de ces systèmes. Lorsqu’un de vos amis désire vous emprunter un ou plusieurs de vos CD, et ce pour une période désirée, votre programme doit être capable de : a. vérifier si ce CD est compatible avec son système informatique ; b. vérifier s’il est encore disponible ; c. lui indiquer combien cela lui coûtera ; d. expliquer à votre ami la procédure d’installation du CD, différente selon son système informatique (mais identique pour tous les CD) ; e. et de lui réclamer les CD qu’ils posséderaient encore et dont le temps d’emprunt est depuis dépassé. 4. Vous réalisez un programme s’occupant de la gestion d’un bureau de réservation pour spectacle. Votre programme vend des réservations pour une représentation (un certain jour à une certaine heure) d’un spectacle (caractérisé par son titre et son auteur). Un client, identifié par ses nom, adresse et numéro de téléphone, peut effectuer plusieurs réservations. Selon que le client est un abonné du bureau ou pas, il bénéficie d’une ristourne, d’une priorité sur les réservations et de facilités de paiement. Chaque réservation peut être de deux types, soit une réservation individuelle, soit une réservation de groupe. Dans les deux cas, des tickets sont délivrés au client : soit un ticket, soit autant de tickets que de personnes du groupe. À chaque ticket correspond une place pour la représentation.

206

L’orienté objet

5. Un organisme bancaire est propriétaire d’un certain nombre d’agences. Chaque agence possède un nombre important de clients et de comptes bancaires détenus par ces clients. Plusieurs clients peuvent avoir procuration sur un même compte et un même client peut posséder plusieurs comptes. L’organisme bancaire est également responsable d’un grand nombre de distributeurs que les clients peuvent utiliser pour tirer de l’argent ou consulter leurs comptes. À chaque compte sont associées une ou plusieurs cartes bancaires qui peuvent être de type différent, « carte bleue », « visa », « amex » et qui, selon leur type, permettent des modalités de crédit ou de remboursement qui peuvent varier. Seuls certains types de carte peuvent être utilisés dans un distributeur. Finalement, les comptes sont de deux sortes selon qu’ils peuvent être associés à une carte bancaire ou qu’ils ne le peuvent pas. 6. Vous devez réaliser la simulation d’un réseau ferroviaire sur lequel circulent différents types de train : des omnibus qui vont lentement et s’arrêtent à toutes les gares, des trains de marchandises qui vont moyennement vite et s’arrêtent dans une gare sur deux, et des trains à grande vitesse qui vont vite et ne s’arrêtent nulle part entre la gare de départ et la gare d’arrivée. Les trains de marchandises et à grande vitesse doivent ralentir à la vue de certains obstacles comme un feu de signalisation de couleur orange, un passage à niveau, un aiguillage ou une gare. Tous les trains doivent s’arrêter dès qu’un feu de signalisation passe au rouge, dès qu’un pylône est tombé sur la voie ou qu’un pont sur lequel le train doit passer est en pièces. Seuls les trains de marchandises et les omnibus s’arrêtent dans les gares. On simulera également le transport de la marchandise. 7. Vous réalisez la simulation d’un petit exercice de manœuvre militaire. Dans votre simulation, doivent apparaître les différents militaires avec leurs grades respectifs : général, colonel, capitaine, lieutenant et simple soldat. Tous les militaires sont capables de charger, de décharger leur arme, de déserter, etc., mais, selon leur grade, la manière de s’exécuter diffère. Chaque militaire doit s’en remettre à son responsable hiérarchique immédiat pour recevoir ses ordres de mission. Chaque militaire appartient à un régiment particulier. Les ordres de manœuvre sont donnés au régiment (par l’entremise du plus haut gradé) et ce par un QG central. Dans cette manœuvre, chaque régiment se voit désigner un emplacement initial et a comme objectif de conquérir un lieu particulier. En général, les ordres de manœuvre envoyés par le QG à chaque régiment peuvent être de trois types : « conquérir », « conquérir le lieu et y organiser un immense thé dansant », « conquérir le lieu et revenir chez soi avec un souvenir ». Chacun de ces types de manœuvre se particularise par une durée d’exécution, un budget à dépenser (et la manière de le dépenser), un nombre de militaires donné, ainsi qu’un ensemble de pratiques militaires particulières. 8. Vous devez réaliser la simulation d’un réseau ferroviaire sur lequel circulent différents types de train : des omnibus qui vont lentement et s’arrêtent à toutes les gares, des trains de marchandises qui vont moyennement vite et s’arrêtent dans une gare sur deux, et des trains à grande vitesse qui vont vite et ne s’arrêtent nulle part entre la gare de départ et la gare d’arrivée. Les trains de marchandises et à grande vitesse doivent ralentir à la vue de certains obstacles comme un feu de signalisation de couleur orange, un passage à niveau, un aiguillage ou une gare. Tous les trains doivent s’arrêter dès qu’un feu de signalisation passe au rouge, dès qu’un pylône est tombé sur la voie ou qu’un pont sur lequel le train doit passer est en pièces. Seuls les trains de marchandises et les omnibus s’arrêtent dans les gares. On simulera également le transport de la marchandise. Réalisez le diagramme de classe UML de cette application, en adaptant autant que faire se peut les principes de la programmation orientée objet.

UML 2 CHAPITRE 10

207

Exercice 10.2 Réalisez en C++ le squelette du programme qui accompagne ces diagrammes de classe et de séquence.

Exercice 10.3 Réalisez le squelette de code Java que l’on pourrait générer automatiquement à partir du diagramme de classe suivant :

208

L’orienté objet

Exercice 10.4 Réalisez le squelette de code C# que l’on pourrait générer automatiquement à partir de ces deux diagrammes UML :

UML 2 CHAPITRE 10

209

Exercice 10.5 Réalisez le squelette de code C++ que l’on pourrait générer automatiquement à partir de ces deux diagrammes UML :

Exercice 10.6 Réalisez le diagramme de classe UML correspondant au code Java écrit ci-après (les deux colonnes se suivent : import java.util.*; class A { private B unB; public A() {} public void faireA() {} } class B extends C implements D { private Vector v = new Vector(); public B() { for (int i=0; i<100; i++) v.addElement(new A()); }

210

L’orienté objet

public void faireD(E unE) { unE.faireE(); } } class E { public void faireE() {} } interface D { public void faireD(E unE); } class C extends A { private A unA; }

11 Héritage Dans la poursuite de la modularisation mais verticale cette fois, ce chapitre introduit la pratique de l’héritage, comme, vers le haut, une factorisation dans la superclasse des attributs et des méthodes communs aux sous-classes, et, vers le bas, la spécialisation de ces sous-classes par l’addition des méthodes et des attributs qui leur sont propres.

Sommaire : Héritage simple — Principe de substitution — Héritage ou composition ? — Emploi de protected — Héritage multiple en C++ — Héritage public, private, protected, virtual

Doctus — Parmi plusieurs instances d’objets d’une même classe, l’ensemble des attributs permet de différencier les individus. Chacun d’eux pourra donc avoir une couleur ou une taille qui lui est propre. Candidus — Mais que faire si nous voulons spécialiser un objet existant en lui ajoutant de nouveaux attributs ? Doc. — Pour ça, l’OO nous propose le mécanisme d’héritage. Il nous permet de fabriquer ces objets à partir de ceux que nous avons sous la main tout en leur ajoutant ces nouveaux attributs. Cand. — Intéressant… Ça donne un moyen économique pour fabriquer de nouvelles classes. Doc. — C’est exact mais ils restent des représentants à part entière de toutes leurs superclasses : mêmes attributs, mêmes signatures de méthode. Même avec des attributs et méthodes supplémentaires, ils peuvent parfaitement ne jouer qu’un des rôles de base comme s’ils n’étaient pas spécialisés. Certaines superclasses peuvent juste servir d’intermédiaires pour réaliser plusieurs sous-classes. Cand. — … et ces intermédiaires nous dispensent de dupliquer le tout dans chacune des classes concernées, c’est bien ça ? Doc. — Exactement ! Cand. — Est-ce que nos sous-classes héritent vraiment tout de leurs parents et grands-parents ? Doc. — Heureusement, non ! Que fais-tu donc du principe d’encapsulation ? Même pour le mécanisme d’héritage, les méthodes private ne concerneront que l’implémentation intime de chaque parent, les sous-classes n’y ayant pas accès. Cand. — C’est vrai que, si quelqu’un se sert d’une de mes classes pour en faire hériter une des siennes, il ne serait pas avisé d’utiliser mes méthodes privées !

212

L’orienté objet

Doc. — En revanche, elles héritent des méthodes protected, c’est-à-dire les méthodes d’implémentation que tu veux mettre à disposition des sous-classes, tout en les rendant inaccessibles à toutes les autres. Cand. — L’héritage est donc une porte ouverte qu’il peut être bon de refermer sur certains mécanismes intimes des parents. Doc. — Une autre combinaison de classes, le multihéritage, constitue apparemment une économie par rapport à la composition. Il permet d’éviter les échanges de message nécessaires à la collaboration d’un composant. Toutefois, pour trouver une méthode de superclasse, cela devient plus complexe ; plusieurs chemins doivent être explorés,et il se peut même que nous devions choisir parmi plusieurs solutions possibles.

Comment regrouper les classes dans des superclasses Reprenons l’exemple de notre petit écosystème du chapitre 3, dont une vue de la simulation est présentée ci- après. Le premier constat qui s’impose, c’est que nous avons démultiplié le nombre de proies, prédateurs, plantes et points d’eau. Nous n’allons pas nous priver de l’un des avantages premiers de la classe, qui est de donner naissance à une multitude d’objets sans se préoccuper, pour chacun, de re-préciser ce qu’il fait et de quoi il est fait. Figure 11.1

Vue de la simulation finale du programme Java de l’écosystème.

Comme le diagramme de classe UML du chapitre précédent le montre parfaitement, jusqu’ici nous avons codé ce modèle à l’aide de 5 classes (oublions la vision pour l’instant). D’abord, les deux classes d’animaux : Proie et Prédateur, ensuite les deux classes de ressources naturelles : Plante et Eau, puis finalement la classe Jungle. Cette dernière agrège toutes les autres et lance la méthode principale qui, de manière itérée, fait évoluer les ressources et se déplacer les animaux. Vous aurez sans doute été sensible à ce petit dérapage sémantique, effectué sous contrôle bien sûr, et qui nous amène à réunir, sous le même concept d’animaux, la proie et le prédateur, ainsi que sous le même concept de ressource, l’eau et la plante. Nous allons, en effet, joindre le geste logiciel à la parole, et introduire deux nouvelles classes Faune et Ressource, dont la raison d’être sera de regrouper ce qu’il y a de commun à la proie et au prédateur pour la première, et

Héritage CHAPITRE 11

213

de commun aux points d’eau et aux plantes pour la seconde. Voyons, dans un premier temps, les attributs communs à la proie et au prédateur, que nous installerons dorénavant dans la faune. Chacun se trouve situé en un point précis (x,y), chacun se déplace avec une vitesse projetée sur chaque axe (vitx, vity), chacun possède une énergie qui décroît suite aux efforts, et s’accroît grâce aux ressources, chacun est associé aux ressources disponibles dans la jungle, avec lesquelles ils interagissent. Toutes ces propriétés se retrouveront dès à présent « plus haut dans les classes », c’est-à-dire dans la faune. Que reste-t-il, qui particularise et différencie encore la proie du prédateur ? Le prédateur interagit, en plus, avec les seules proies et vice versa. Les proies pouvant mourir, elles possèdent un attribut supplémentaire indiquant leur état de vie. Toutes les plantes et tous les points d’eau sont caractérisés par les deux mêmes attributs : leur quantité et un compteur temporel qui sert à rythmer leur évolution naturelle (la plante qui pousse et l’eau qui s’évapore). Ici, la factorisation est encore plus radicale que dans le cas des animaux, car il ne reste, au bout du compte, aucun attribut qui différencie les plantes de l’eau. Dans quelle mesure ne sont-ils pas alors simplement des objets différents d’une même classe Ressource ? Car si la seule différence qui subsiste entre les objets est la valeur de certains de leurs attributs, il n’y a plus lieu de découper les classes en sous-classes. Il n’existe pas de sous-classes de voiture rouge ou bleue, car ce sont simplement deux objets différents de la même classe voiture. Dans pareil cas, il suffit de s’en tenir, bien sûr, à la seule diversification des objets, qui sert justement à cela : encoder par chacun des valeurs d’attributs différentes. N’utilisez jamais l’héritage de manière abusive, pour ce qu’il n’est pas dans les langages OO, c’est-à-dire la différenciation des objets appartenant à une même classe par la seule valeur des attributs. Ne faites pas systématiquement de sous-classes pour les jeunes hommes et les hommes âgés, si seul leur âge les différencie, ou de sous-classes pour les voitures rapides ou lentes si, là encore, seule leur vitesse maximale les différencie et rien d’autre. Pour l’instant, nous nous sommes limités aux seuls attributs, et nous verrons bien vite que les méthodes jouent un rôle encore plus important, lors de cette factorisation dans une superclasse des caractéristiques communes aux sousclasses. À elles seules, elles justifieront, pour les ressources, la présence de ces deux niveaux hiérarchiques.

Héritage des attributs Concentrons-nous, d’abord, sur l’héritage des attributs. Le diagramme UML ci-après (voir figure 11-2) illustre, à l’aide d’un nouveau symbole graphique, que nous avions délibérément laissé en suspens dans le chapitre précédent, le mécanisme d’héritage par les classes Predateur et Proie de la classe Faune, et par les classes Plante et Eau de la classe Ressource. Comme cela est visible sur le diagramme, de par la présence de la flèche d’héritage (seule la pointe la différencie de celle symbolisant le lien d’association dirigée), tous les attributs et les méthodes caractérisant la superclasse deviennent, automatiquement, attributs et méthodes de la sous-classe, sans qu’il n’y ait besoin de le préciser davantage. C’est la direction de la flèche qui spécifie lesquelles sont les superclasses et lesquelles sont les sous-classes, nullement leur position dans le diagramme de classe même s’il est de coutume d’installer les sous-classes en dessous des superclasses. Première conséquence de cette application de l’héritage Il ne peut y avoir dans la sous-classe, par rapport à sa superclasse, que des caractéristiques additionnelles ou des précisions. Ce que l’héritage permet d’abord, c’est de rajouter dans la sous-classe de nouveaux attributs et de nouvelles méthodes, les seuls à préciser dans la déclaration des sous-classes.

214

L’orienté objet

Figure 11-2

Diagramme de classe plus complet de l’écosystème où on voit apparaître deux superclasses : Faune et Ressource, et la manière dont les sous-classes héritent de celles-ci. Observez bien le sens et le dessin de la flèche symbolisant l’héritage. Les deux sont capitaux.

Ce que la relation d’héritage cherche à reproduire dans l’écriture logicielle, c’est l’existence, dans notre manière d’appréhender le monde, de concepts plus spécifiques et génériques. Nous le faisons tout naturellement pour des raisons d’économie déjà entrevue dans le premier chapitre, et nous retrouvons cette pratique « taxonomique » dans de nombreuses disciplines intellectuelles humaines : politique, économique, sociologique, biologique, zoologique… La possibilité de hiérarchiser notre conceptualisation du monde en différents niveaux d’abstraction nous permet une utilisation plus flexible de l’un ou l’autre de ces niveaux, selon le contexte d’utilisation. Alors que l’un de nous tape ce texte sur son portable posé sur la table de la salle à manger, l’informaticien du labo lui téléphone pour lui demander s’il souhaite une batterie de rechange pour un IBM ThinkPad. Sa compagne lui demande de retirer son ordinateur afin de pouvoir mettre la table et son enfant de cinq ans lui demande de pouvoir taper sur les touches du clavier. Quatre dénominations pour un même objet, quatre termes le désignant à différents niveaux d’abstraction, selon quatre contextes d’utilisation différents, quatre interlocuteurs et quatre besoins distincts. Chacun le désigne à sa manière, afin de communiquer son souhait le plus économiquement et le plus effectivement qui soit. Le choix du bon niveau d’abstraction se justifie par un souci d’optimisation de la communication, par le souhait « de dire le plus avec le moins ». Nul besoin de savoir qu’il s’agit d’un IBM ThinkPad pour réaliser que, tout IBM qu’il est, il encombre la table

Héritage CHAPITRE 11

215

de la salle à manger. Un concept est plus abstrait qu’un autre si, dans sa fonction descriptive, il englobe cet autre, s’il est plus général, plus passe-partout, plus adaptable. Comme la figure 11-3 l’illustre, il en est ainsi de « machine », plus abstrait que « ordinateur », plus abstrait que « portable », plus abstrait que « IBM ThinkPad ».

Figure 11-3

Le même ordinateur portable vu selon différents niveaux d’abstraction.

On vous montre une voiture et on vous demande de nous dire de quoi il s’agit. Il y a fort à parier que vous nous répondiez une « voiture » plutôt qu’une « Renault Kangoo » ou un « moyen de transport », ce qu’elle serait également. Un chien sera sans doute, un « chien », éventuellement un « caniche » mais certainement pas un « caniche nain ». Vous nous verrez taper sur un « portable » et non pas sur un « IBM ThinkPad ». Clairement, un des niveaux d’abstraction se voit privilégié par rapport aux autres. La raison en est simple, c’est le niveau le plus usité lors de toute communication d’information concernant, en effet, l’objet référé au cours de cette communication. C’est le niveau de base, celui qui caractérise le mieux l’objet, qui capture le plus d’informations sur les rôles et les fonctions qu’on lui attribue.

Pourquoi l’addition de propriétés ? Pourquoi une classe plus spécifique ne peut-elle que rajouter des propriétés par rapport à sa superclasse ? Pour la bonne et simple raison qu’elle se doit de rester également cette superclasse, et qu’elle le restera, tant qu’elle partagera avec cette superclasse les mêmes propriétés. D’où la seule pratique valide qui consiste à trouver

216

L’orienté objet

dans la classe héritant un ajout d’informations par rapport à la classe héritée. Rien de ce qu’est ou de ce que fait une superclasse ne peut ne pas être ou ne pas être fait par toutes ses sous-classes. La sous-classe peut en faire plus, pour se démarquer, mais jamais moins. Ce mode de fonctionnement est évidemment transposable aux objets, instances des classes et sous-classes correspondantes, et donne lieu à un principe clé de la pratique orientée objet, principe qui est dit de « substitution ». Principe de substitution : un héritier peut représenter la famille Partout où un objet, instance d’une superclasse apparaît, on peut, sans que cela pose le moindre problème, lui substituer un objet quelconque, instance d’une sous-classe. Tout message compris par un objet d’une superclasse le sera obligatoirement par tous les objets issus des sous-classes. L’inverse est évidemment faux. Toute Ferrari peut se comporter comme une voiture, tout portable comme un ordinateur et tout livre d’informatique OO comme un livre. Vous pouvez le jeter par la fenêtre comme tout livre en effet. C’est pour cette simple raison qu’il sera toujours possible de typer statiquement un objet par une superclasse bien que, lors de sa création et de son utilisation, il sera plus précisément instance d’une sous-classe de celle-ci, par exemple « SuperClasse unObjet = new SousClasse() » (le compilateur ne bronche pas, même si l’objet typé superclasse sera finalement créé comme instance de la sous-classe ; c’est aussi la raison pour laquelle vous devez écrire deux fois le nom de la classe dans l’instruction de création d’objet) ou encore : SuperClasse unObjet = new SuperClasse() ; SousClasse unObjetSpecifique = new SousClasse() ; unObjet = unObjetSpecifique ; // l’inverse serait refusé par le compilateur Tout message autorisé par le compilateur lorsqu’il est censé s’exécuter sur un objet d’une superclasse peut tout autant s’exécuter sur un objet de toutes ses sous-classes. La Ferrari peut prendre des passagers ou simplement démarrer. L’inverse n’est pas vrai. Demandez à une voiture quelconque (une Mazda, par exemple) d’atteindre 300 km/h dans les 5 secondes et à un livre quelconque (la Bible, par exemple) de vous révéler les subtiles secrets de l’héritage en OO.

L’héritage : du cognitif aux taxonomies Nous allons, au cours de notre développement, raffiner et illustrer ces différents fondements de la pratique de l’orienté objet. Mais, d’ici là, gardons à l’esprit que l’héritage a pour première vocation de reproduire ce mode Figure 11-4

Interprétation ensembliste de l’héritage.

Héritage CHAPITRE 11

217

cognitif extrêmement puissant de conceptualisation hiérarchisée du monde, du plus général au plus spécifique. En conséquence, il n’y aura jamais lieu de le mettre en œuvre en OO autrement qu’entre des classes, qui, dans la conceptualisation que nous en avons, entrent dans ce rapport taxonomique. Si la source première d’inspiration de ce mécanisme puissant, permettant une organisation et un encodage économique de la connaissance, reste notre fonctionnement cognitif, il y a lieu dans une pratique, détachée maintenant des sciences cognitives, de tendre vers une formalisation plus rigoureuse et fiable. L’intelligence artificielle, d’où est née en partie, par les travaux de Minsky, ce mécanisme informatisé d’héritage, a toujours, au départ d’une inspiration issue de notre fonctionnement cognitif, aspiré à une formalisation de ces mécanismes, pour les transposer dans une pratique d’ingénieur robuste et normative. Il en va ainsi de toutes les logiques classiques et moins classiques, des réseaux de neurones, de la théorie des probabilités subjectives, de la logique floue, autant d’outils ingénieristes qui trouvent leur origine dans les sciences cognitives. Marvin Minsky Marvin Minsky est un des pères fondateurs de l’intelligence artificielle. Il est depuis 50 ans une des grandes figures de cette discipline au célèbre MIT. En 1974, dans un article intitulé « A Framework for Representing Knowledge », il traite de la structuration de nos connaissances et de l’utilisation de celle-ci durant les processus cognitifs de résolution de problème, de compréhension du langage et de la perception visuelle. Dans cet article, il décrit des structures computationnelles particulières appelées « Frame », agrégat d’attributs, qui peuvent s’instancier lorsque les valeurs de ces attributs sont connues, et qui s’organisent dans notre cognition de manière hiérarchique, des plus génériques aux plus spécifiques. Ce fut une source d’inspiration certaine pour les premiers langages et développements orientés objet. Les auteurs dont Minsky s’inspira pour sa conception des Frames ne sont pas des informaticiens. Ses influences majeures sont les schémas du psychologue Jean Piaget, les paradigmes de l’épistémologue Thomas Khün ou les noumènes du philosophe Kant. Comme quoi, il faut de tout pour faire un bon et solide modèle informatique. Avant cela, pour avoir conçu les premiers réseaux de neurones et avoir clairement perçu leurs forces et leurs faiblesses, il fut, pendant 30 ans, le fossoyeur de l’approche neuronale en intelligence artificielle, redevenue très populaire depuis. On l’a décrit comme la sorcière du conte de Blanche-Neige, offrant la pomme empoisonnée à toute la communauté neuronale, très importante pendant les années 1950 et quasi léthargique jusqu’au début des années 1980. Minsky est un fervent défenseur de l’approche symbolique en intelligence artificielle. Il ne nie pas le parallélisme inhérent à notre fonctionnement cérébral, mais il préfère appréhender celui-ci comme un ensemble d’agents spécialisés et travaillant de concert (comme autant d’objets s’envoyant des messages) pour la réalisation des tâches cognitives (cette vision a été popularisée dans son ouvrage The Society of Mind). Il déteste le tournant pris dans son propre laboratoire par des roboticiens outranciers, véritables apprentis sorciers, qui pensent qu’il suffit de mettre un robot embryonnaire, doté d’immenses facultés d’apprentissage, dans un environnement réel, pour le voir se transformer, avec le temps, en une créature intelligente. Il préfère l’approche moins empirique, qui consiste à davantage et mieux encore s’inspirer de nos processus mentaux, pour les reproduire le plus fidèlement possible dans des entités artificielles. Cependant, il ne pense pas que faire de la cognition humaine un système purement logique soit également la voie la plus prometteuse, vu l’immense flexibilité qui sous tend le raisonnement humain. En résumé, il pense que la seule voie prometteuse est la sienne et ses critiques dévastatrices à l’encontre de tout ce qui s’en éloigne sont devenues légendaires. Ses intérêts sont multiples : les articulations mécaniques, l’éducation scolaire des jeunes enfants (avec un fidèle parmi les fidèles, Seymour Papert, ils ont conçu la petite tortue du « LOGO »), la possibilité d’extraterrestres et leurs éventuelles capacités intellectuelles, la musique, les mécanismes cognitifs sous-jacents à l’humour et aux blagues, les émotions (sujet de son tout dernier livre). Il participa à l’élaboration du scénario du chef-d’œuvre de Kubrick (tiré de l’œuvre d’Arthur C. Clarck), 2001 : l’Odyssée de l’espace, surtout de la partie consacrée au célèbre et inquiétant HAL. Pour la petite histoire, le nom de cet ordinateur ne serait pas constitué des trois lettres qui précédent respectivement les lettres IBM, mais, plus simplement, viendrait de Heuristic Algorithm (en référence aux travaux de Minsky à l’époque). Tout était formidablement anticipé dans ce livre et film : la reconnaissance vocale, le jeu d’échecs automatisé, la lecture sur les lèvres, tout … Sauf qu’aucun des ordinateurs de bord n’affiche de fenêtre sur son écran et que nul souris n’est manipulée par les protagonistes.

218

L’orienté objet

Interprétation ensembliste de l’héritage Ainsi, une possible interprétation, plus normative, de la pratique de l’héritage, passe tout simplement par la théorie des ensembles. Ce détour pourra vous être quelquefois utile, quand vous ressentirez un doute sur le pourquoi et le comment de la mise en œuvre de cette pratique dans un contexte donné. Comme cela est visible à la figure 11-3, la superclasse, comme « ensemble », regroupe en tant qu’instance tous les éléments, y compris tous ceux que regroupe l’ensemble sous-classe. C’est ce qui permet d’affirmer que tout objet d’une sousclasse est également objet de sa superclasse, et que l’on peut passer d’un type sous-classe à un type superclasse sans que cela ne cause de difficulté. L’inverse n’est évidemment plus vrai, car les sous-classes étant plus spécifiques, elles se permettent des choses qui ne sont pas du ressort des superclasses. Essayez en effet de faire rouler toutes les voitures comme des Ferrari, de faire aboyer tous les animaux comme des chiens, de pratiquer tous les sports comme vous pratiquez le ski, de trimballer tous les ordinateurs comme vous trimballez un portable, et vous serez vite convaincus de l’impossibilité de substituer à la sous-classe sa superclasse. « Casting explicite » versus « casting implicite » Comme nous le verrons dans le chapitre suivant, le « casting » permet à une variable typée d’une certaine façon lors de sa création d’adopter un autre type le temps d’une manœuvre. Conscient des risques que nous prenons en recourant à cette pratique, le compilateur l’accepte si nous recourons à un « casting explicite ». C’est le cas en programmation classique quand on veut, par exemple, assigner une variable réelle à une variable entière. Dans le cas du principe de substitution, les informaticiens parlent souvent d’un « casting implicite », signifiant par là, qu’il n’y a pas lieu de contrer l’opposition du compilateur quand nous faisons passer un objet d’une sous-classe pour un objet d’une superclasse. Le compilateur (gardien du temple de la bonne syntaxe et de la bonne pratique) ne bronchera pas, car cette démarche est tout à fait naturelle et acceptée d’office. En revanche, faire passer un objet d’une superclasse pour celui d’une sous-classe requiert la présence d’un « casting explicite », par lequel vous prenez l’entière responsabilité de ce comportement risqué. Le compilateur vous met en garde (vous êtes en effet sur le point de commettre une bêtise) mais, ensuite, vous en faites ce que vous voulez en votre âme et conscience. Par exemple, si vous transférez un réel dans un entier (la superclasse dans la sous-classe), c’est en effet une bêtise, car vous perdez la partie décimale. En général, vous en êtes pleinement conscients et assumez la responsabilité de cette perte d’information. Les puristes de l’informatique détestent la pratique du casting (explicite évidemment) qui est toujous évitable par un typage plus fin et une écriture de code plus soignée.

Qui peut le plus peut le moins Revenons aux ensembles. Cette manière de concevoir de l’héritage (la plus solide) a permis de contrer un collègue qui affirmait qu’en se basant sur le seul rajout d’attributs, il était possible de voir la classe des nombres réels comme une sous-classe de celle des nombres entiers, ou les rectangles comme une sous-classe des carrés. En effet, un réel peut être simplement vu comme un entier auquel on ajouterait l’attribut valeur décimale et un rectangle comme un carré auquel on ajoute un côté supplémentaire. Or, il n’est point besoin de pouvoir suivre la démonstration du théorème de Fermat pour savoir que les nombres entiers sont un sous-ensemble des nombres réels et les carrés un sous-ensemble des rectangles, et qu’en prolongeant cette vision, la bonne, les entiers et les carrés deviennent respectivement, non plus la superclasse, mais la sous-classe des réels et des rectangles. Et c’est ce qu’ils sont en effet, en informatique tout comme en mathématique. En vérité, la notion de classe et de sous-classe se base essentiellement sur la nature opératoire des objets qui en découle. Tout ce que vous faites avec un réel en informatique, vous pouvez le faire avec un entier. L’inverse est faux. Par exemple, quand vous dimensionnez une fenêtre sur un écran, les programmes qui le font s’attendent à recevoir un entier, et ne titillez pas le compilateur en transmettant un réel à la place. En revanche, la situation inverse laissera le compilateur aussi froid que les circuits qui le font fonctionner.

Héritage CHAPITRE 11

219

Pour se sortir du dilemme des attributs, il serait correct de dire que, tous comme les réels, les entiers ont : l’attribut entier plus l’attribut décimal, mais ils sont beaucoup plus spécifiques que les réels, en ceci que la valeur de leur attribut décimal est forcément nulle. On pourrait croire que, au vu de cette spécificité accrue, la sous-classe en fera toujours plus que la superclasse. Cependant, ce qui fait la spécificité de la sous-classe est, dans le même temps, ce qui risque d’être le moins sollicité par les autres classes. Il n’est pas rare que la sousclasse passe plus de temps à jouer le rôle de sa superclasse qu’à se laisser aller à exprimer vraiment ce qu’elle a au plus profond des tripes, comme nous le verrons dans la suite. Finalement, à observer les diagrammes de classe UML, vous constaterez que les rectangles des sous-classes sont très souvent plus petits que ceux des superclasses, car ils ne se différencient que par quelques ajouts.

Héritage ou composition ? Un objet de type sous-classe est d’autant plus un objet de type superclasse que son stockage en mémoire se compose, d’abord, d’un objet de type superclasse, puis d’un espace mémoire réservé aux attributs qui lui sont propres, comme indiqué dans la figure ci-après. Ce mode de stockage ressemble à s’y méprendre au mode de stockage d’un objet, qui serait en partie composé d’un autre objet en son sein. Cela revient à dire qu’eu égard au stockage des objets en mémoire, rien ne différencie vraiment un lien de composition entre deux classes d’un lien d’héritage entre ces deux même classes. Nous verrons qu’il n’en va plus de même lors de l’appel des méthodes. Dans le cas de la composition, il y a bien envoi de messages de la classe qui offre le logement à celle qui est logée. Dans le cas de l’héritage, on ne parlera plus d’envoi de message, car il s’agit bien d’une méthode propre à la classe elle-même, quitte à être héritée d’une autre. N’oublions pas que l’héritage crée une vraie fusion entre les deux classes, alors que la composition maintient un rapport de clientélisme. Certains programmeurs tendent à favoriser tant que faire se peut la composition au détriment de l’héritage. Nous défendons ici la position classiquement admise : faites parler les concepts de la réalité que vous cherchez à reproduire et écoutezles. C’est la réalité que vous cherchez à dépeindre qui doit avoir le dernier mot. Si deux entités qui vous intéressent entrent dans une relation taxonomique (comme la Ferrarri et la voiture), recourez à l’héritage, dans Figure 11-5

Un objet de la sous-classe est stocké en mémoire, d’abord comme un objet de la superclasse, puis en rajoutant les attributs qui lui sont propres.

220

L’orienté objet

tous les autres cas (comme le moteur et la voiture) choisissez la composition, sans oublier bien sûr les autres possibilités que sont l’agrégation ou l’association (comme la voiture et son propriétaire). Juste pour en rajouter une petite couche, et vous convaincre que si le monde était simple, on ne posséderait sans doute plus les facultés nécessaires à le conceptualiser, la réalité elle-même, du moins ce que l’on en conçoit, n’est pas toujours aussi tranchée entre la composition et l’héritage. Vous êtes une espèce d’animal, ne le prenez pas mal, car vous aurez beaucoup de mal à contenir l’animal qui est en vous, n’est-ce pas ? Le jour où vous vous demanderez s’il y a quoi que ce soit à lire dans ce livre que nous nous efforçons d’écrire, au moins vous nous aurez fait le plaisir d’illustrer, une nouvelle fois, l’ambiguïté qu’il peut y avoir entre composition et héritage.

Économiser en rajoutant des classes ? La pratique d’héritage en OO a été introduite pour favoriser l’économie de représentation et une simplification accrue. Or, dans le seul exemple vu jusqu’à présent, le programme a été plus alourdi par l’addition de deux nouvelles classes qu’autre chose. En matière d’économie, c’est assez discutable. Sept classes, c’est plus que cinq, et, par ailleurs, cette démarche de factorisation n’est pas forcément des plus élémentaires. Il est clair que cet effort ne portera ses fruits que si le programme s’enrichit de l’addition de nouvelles ressources et de nouveaux animaux. Nous pourrions rajouter une multitude d’autres espèces de proies et de prédateurs, différentes des précédentes. Nous pourrions penser également à un ensemble de nouvelles ressources. Après un certain nombre d’additions, nous aurons largement rentabilisé ce préalable effort de factorisation, par une épargne en écriture et l’évitement de possibles erreurs, causées par l’addition de codes redondants. Dans notre cognition également, l’utilisation de ces super-concepts se renforce avec la multiplication des concepts qui en sont dérivés. L’héritage favorise la réutilisation de code existant, de surcroît s’il a été écrit par un informaticien plus aguerri que vous, ce qui mâche considérablement la besogne, en permettant de ne vous concentrer que sur votre seul apport. Héritez d’une des classes collections déjà pré-codées en Java pour y encoder vos voitures, vos petits amis et petites amies, les examens de fin d’année… et vous héritez gratuitement d’une série de fonctionnalités bien utiles, comme, la possibilité d’ordonner très simplement les éléments de cette collection (algorithme qui aura été écrit par un pro). Java recourt largement à l’héritage, afin que vous puissiez récupérer dans l’écriture de vos classes un ensemble de librairies pré-codées, utiles à la réalisation d’interface graphique, de gestion d’événements, de programmation concurrentielle, etc. La place de l’héritage L’héritage trouve parfaitement sa place dans une démarche logicielle dont le souci principal devient la clarté d’écriture, la réutilisation de l’existant, la fidélité au réel, et une maintenance de code qui n’est pas mise à mal par des évolutions continues, dues notamment à l’addition de nouvelles classes.

Héritage des méthodes Passons maintenant aux méthodes. À l’instar de l’héritage des attributs, les méthodes s’héritent également, et toute sous-classe peut ajouter de nouvelles méthodes lors de sa déclaration. Comme schématisé par le diagramme UML de la figure 11-6, lorsque la classe O1 désire communiquer avec la classe filleO2, elle a la possibilité, soit de lui envoyer les messages qui sont propres à cette classe, soit de lui envoyer tous ceux hérités de la classe O2. Dans la famille classe, je demande la fille… Quand la proie ou le prédateur consomme l’eau ou

Héritage CHAPITRE 11

221

la plante, elles envoient à l’eau ou la plante le même message diminueQuantite(). Ce message n’est ni déclaré dans la classe Eau ni dans la classe Plante, mais elles en héritent toutes deux de leur superclasse Ressource. Pour les proies et les prédateurs aussi, de nouvelles méthodes communes aux deux, comme tournerLaTete(), repereUnObjet(), peuvent être déclarées plus haut dans la classe Faune. Cela permet à toute classe nécessitant d’interagir avec les proies ou les prédateurs de le faire directement avec la faune « qu’il y a en eux », sans se préoccuper de savoir exactement de quelle faune il s’agit. Messages et nivaux d’abstraction L’héritage permet à une classe, communiquant avec une série d’autres classes, de leur parler à différents niveaux d’abstraction, sans qu’il soit toujours besoin de connaître leur nature ultime (on retrouvera ce point essentiel dans l’explication du polymorphisme), ce qui facilite l’écriture, la gestion et la stabilisation du code.

Vous dites que vous avez eu un accident de voiture. Cela suffit pour vous plaindre, sans avoir besoin de connaître la marque de la voiture, sauf si vous êtes garagiste ou assureur. Aussi, dans le diagramme UML de l’écosystème, on s’aperçoit que la classe Faune interagit avec la classe Ressource car, quelle que soit la ressource, l’objet Faune pourra envoyer à l’objet ressource un message, qui ne nécessitera pas de connaître la nature de la ressource en question. Voici également un petit bout de code extrait de la déclaration de la classe Faune public boolean repereObjetJungle(ObjetJungle unObjet) { repere = false; if (maVision.voisJe(unObjet.getMaZone())) { vitX = (int)((unObjet.getMaZone().x + (unObjet.getMaZone().width/2) - getPosX()) * energie); vitY = (int)((unObjet.getMaZone().y + (unObjet.getMaZone().height/2) - getPosY()) * energie); maVision.suisObjet(unObjet.getMaZone()); repere = true; } return repere; }

Le seul point d’intérêt que présente ce bout de code est l’apparition d’une nouvelle classe qui, dans un premier temps, a été omise, pour alléger le diagramme de classe, et passée comme argument de la méthode repereObjetJungle (). Il s’agit de la super-superclasse ObjetJungle. Cette méthode, apparaissant dans la classe Faune, a pour fonction unique de vérifier si la vision dont la faune est composée rencontre un objet quelconque de la jungle : une autre faune ou une ressource, et, si c’est le cas, de forcer l’objet vision à suivre cet objet repéré. Ici, ce repérage concerne indifféremment n’importe quel objet de la jungle. Bien évidemment, la suite des événements dépendra de la nature intime de l’objet : proie, prédateur, plante ou eau. Mais la fonctionnalité repérage, elle, peut se désintéresser de cette nature intime. On s’aperçoit, dès lors, de l’intérêt qu’il y a à rajouter une nouvelle superclasse au-dessus de Faune et Ressource, comme illustré ci-après. Ce dernier

222

L’orienté objet

diagramme constitue de fait la dernière et définitive version de notre programme. La classe ObjetJungle ne contient que les coordonnées de la position de tous les objets, qu’ils se déplacent ou pas (récupérable par la méthode getMaZone()). La seule partie des faunes et des ressources qui compte pour la vision, c’est leur position, c’est-à-dire l’ObjetJungle dont ils sont constitués. Ces seules coordonnées sont tout ce qui importe à la vision pour repérer un objet. Autrement dit, la vision ne nécessite d’interagir qu’avec la partie ObjetJungle de tous les objets de la jungle.

Figure 11-6

Diagramme de classe complet de l’écosystème.

Dans le petit diagramme UML qui suit, figure 11-7, et les codes qui le traduisent respectivement dans les trois langages, la classe O1 peut s’adresser à la classe FilleO2 en tant que FilleO2 ou en tant qu’O2. Elle a, en définitive, la possibilité d’envoyer deux messages à la classe FilleO2 : jeTravaillePourO2() ou jeTravaillePourLaFilleO2(). En plus, la classe O1, dans une autre de ses méthodes, reçoit un argument de type « FilleO2 ». Cela nous permet d’illustrer le principe de substitution, qui dit que l’on pourra appeler cette méthode, en lui passant indifféremment un argument de type « FilleO2 » ou un argument de type « O2 ».

Code Java class O1 { private FilleO2 lienFilleO2; public O1(FilleO2 lienFilleO2) { this.lienFilleO2 = lienFilleO2; } public void jeTravaillePourO1() { lienFilleO2.jeTravaillePourO2(); lienFilleO2.jeTravaillePourLaFilleO2();

Héritage CHAPITRE 11 Figure 11-7

La classe O1 communique avec la classe FilleO2 en lui envoyant des messages qui sont, soit hérités de O2, soit propres à la classe héritière.

} /* dans les résultats montrés, cette méthode est d'abord active, puis mise en commentaire */ public void jeTravailleAussiPourO1(FilleO2 lienFilleO2) { lienFilleO2.jeTravaillePourO2(); lienFilleO2.jeTravaillePourLaFilleO2(); } public void jeTravailleAussiPourO1(O2 lienO2) { lienO2.jeTravaillePourO2(); } } class O2 { public O2() {} public void jeTravaillePourO2() { System.out.println("Je suis un service rendu par la classe O2"); } } class FilleO2 extends O2 { /* C'est la syntaxe de l'héritage en java */ public FilleO2() {} public void jeTravaillePourLaFilleO2() { System.out.println("Je suis un service rendu par la classe FilleO2"); } } public class Heritage1 { public static void main(String[] args) { O2 unObjetO2 = new O2(); FilleO2 uneFilleO2 = new FilleO2(); O1 unObjetO1 = new O1(uneFilleO2); unObjetO1.jeTravaillePourO1(); unObjetO1.jeTravailleAussiPourO1(unObjetO2); unObjetO1.jeTravailleAussiPourO1(uneFilleO2); } }

223

L’orienté objet

224

Résultats Je Je Je Je Je

suis suis suis suis suis

un un un un un

service service service service service

rendu rendu rendu rendu rendu

par par par par par

la la la la la

classe classe classe classe classe

O2 FilleO2 O2 O2 FilleO2

Résultats avec la méthode jeTravailleAussiPourO1(FilleO2 lienFilleO2) mise hors d’action Je Je Je Je

suis suis suis suis

un un un un

service service service service

rendu rendu rendu rendu

par par par par

la la la la

classe classe classe classe

O2 FilleO2 O2 O2

La différence essentielle dans ce code Java réside dans le fait que la première méthode jeTravailleAussiPourO1(), codée pour recevoir un argument de type superclasse O2, sera maintenant appelée avec un argument de type sous-classe, sans que cela ne pose le moindre problème (au vu du casting implicite présenté plus haut). Remarquez également que la surcharge d’une méthode par le simple fait qu’un des arguments soit de la sous-classe d’un argument de la méthode surchargée ne pose aucune difficulté car, à l’exécution, il est facile de choisir la bonne méthode à exécuter selon le type statique de l’argument que l’on passe dans la méthode. S’il s’agit d’un argument de type superclasse, il ne peut s’agir que de la méthode prévue à cet effet. En revanche, s’il s’agit d’un argument de type sous-classe et si elle existe, on choisira d’exécuter la méthode prévue à cet effet (voir le prochain chapitre). Sinon on peut se rabattre sur la méthode censée être exécutée sur un objet de type superclasse et dont la sous-classe hérite de toute manière.

Code C# using System; class O1 { private FilleO2 lienFilleO2; public O1(FilleO2 lienFilleO2) { this.lienFilleO2 = lienFilleO2; } public void jeTravaillePourO1() { lienFilleO2.jeTravaillePourO2(); lienFilleO2.jeTravaillePourLaFilleO2(); } public void jeTravailleAussiPourO1(FilleO2 lienFilleO2) { lienFilleO2.jeTravaillePourO2(); lienFilleO2.jeTravaillePourLaFilleO2(); } public void jeTravailleAussiPourO1(O2 lienO2) { lienO2.jeTravaillePourO2(); } }

Héritage CHAPITRE 11

class O2 { public O2() {} public void jeTravaillePourO2() { Console.WriteLine("Je suis un service rendu par la classe O2"); } } class FilleO2 : O2 { /* C'est la syntaxe de l'héritage en C# plus proche du C++ */ public FilleO2() {} public void jeTravaillePourLaFilleO2() { Console.WriteLine("Je suis un service rendu par la classe FilleO2"); } } public class Heritage1 { public static void Main() { O2 unObjetO2 = new O2(); FilleO2 uneFilleO2 = new FilleO2(); O1 unObjetO1 = new O1(uneFilleO2); unObjetO1.jeTravaillePourO1(); unObjetO1.jeTravailleAussiPourO1(unObjetO2); unObjetO1.jeTravailleAussiPourO1(uneFilleO2); } }

Résultats … les mêmes qu’en Java.

Code C++ #include "stdafx.h" #include "iostream.h" class O2 { public: O2() {} void jeTravaillePourO2() { cout << "Je suis un service rendu par la classe O2" << endl; } }; class FilleO2 : public O2 { /* C'est la syntaxe de l'héritage en C++, notez la présence du ➥" public " que nous justifierons dans la suite */ public: FilleO2() {} void jeTravaillePourLaFilleO2() { cout << "Je suis un service rendu par la classe FilleO2" << endl; } };

225

226

L’orienté objet

class O1 { private: FilleO2* lienFilleO2; public: O1(FilleO2* lienFilleO2) { this->lienFilleO2 = lienFilleO2; } void jeTravaillePourO1() { lienFilleO2->jeTravaillePourO2(); lienFilleO2->jeTravaillePourLaFilleO2(); } void jeTravailleAussiPourO1(FilleO2* lienFilleO2) { lienFilleO2->jeTravaillePourO2(); lienFilleO2->jeTravaillePourLaFilleO2(); } void jeTravailleAussiPourO1(O2* lienO2) { lienO2->jeTravaillePourO2(); } void jeTravailleAussiPourO1(FilleO2 lienFilleO2) { lienFilleO2.jeTravaillePourO2(); lienFilleO2.jeTravaillePourLaFilleO2(); } void jeTravailleAussiPourO1(O2 lienO2) { lienO2.jeTravaillePourO2(); } }; int main(int argc, char* argv[]) { O2* unObjetO2Tas = new O2(); FilleO2* uneFilleO2Tas = new FilleO2(); O1* unObjetO1 = new O1(uneFilleO2Tas); unObjetO1->jeTravaillePourO1(); unObjetO1->jeTravailleAussiPourO1(unObjetO2Tas); unObjetO1->jeTravailleAussiPourO1(uneFilleO2Tas); cout <jeTravaillePourO1(); unAutreObjetO1->jeTravailleAussiPourO1(&unObjetO2Pile); unAutreObjetO1->jeTravailleAussiPourO1(&uneFilleO2Pile); cout <jeTravaillePourO1(); unAutreObjetO1->jeTravailleAussiPourO1(unObjetO2Pile); unAutreObjetO1->jeTravailleAussiPourO1(uneFilleO2Pile); return 0; }

Héritage CHAPITRE 11

227

Résultats Rien de vraiment spécial ne se produit dans ces différents essais, testant l’héritage avec des objets stockés sur la pile ou sur le tas. Le même résultat est obtenu trois fois. En C#, rien de semblable ne peut être produit si l’on choisit de recourir à des objets dont le temps de vie est géré par la mémoire pile, car les « structures » qui le permettent ne peuvent simplement pas hériter entre elles. En conséquence et malgré l’existence des structures en C#, le seul recours pour des jeux d’héritage comme ceux-ci est, tout comme en Java, de se limiter aux seuls référents et à la mémoire tas. Je Je Je Je

suis suis suis suis

un un un un

service service service service

rendu rendu rendu rendu

par par par par

la la la la

classe classe classe classe

O2 FilleO2 O2 (écrit deux fois) FilleO2

la la la la

classe classe classe classe

O2 FilleO2 O2 (écrit deux fois) FilleO2

Essais avec des objets piles Je Je Je Je

suis suis suis suis

un un un un

service service service service

rendu rendu rendu rendu

par par par par

Derniers essais avec des objets piles Je Je Je Je

suis suis suis suis

un un un un

service service service service

rendu rendu rendu rendu

par par par par

la la la la

classe classe classe classe

O2 FilleO2 O2 (écrit deux fois) FilleO2

Code Python class O1: lienFilleO2=None def __init__(self,lienFille2): self.__lienFilleO2=lienFille2 def jeTravaillePourO1(self): self.__lienFilleO2.jeTravaillePourO2() self.__lienFilleO2.jeTravaillePourLaFilleO2() def jeTravailleAussiPourO1(self,lienO2): if isinstance(lienO2,FilleO2): lienO2.jeTravaillePourO2() lienO2.jeTravaillePourLaFilleO2() else: lienO2.jeTravaillePourO2() class O2: def __init__(self): pass def jeTravaillePourO2(self): print "je suis un service rendu par la classe O2"

228

L’orienté objet

class FilleO2(O2): #remarquez la manière dont Python réalise l’héritage def __init__(self): pass def jeTravaillePourLaFilleO2(self): print "Je suis un service rendu par la classe FilleO2" unObjetO2=O2() uneFilleO2=FilleO2() unObjetO1=O1(uneFilleO2) unObjetO1.jeTravaillePourO1() unObjetO1.jeTravailleAussiPourO1(unObjetO2) unObjetO1.jeTravailleAussiPourO1(uneFilleO2)

Comme Python ne type pas les paramètres des méthodes et que la surcharge de méthode est impossible, il est difficile de restituer le même exemple. Cependant, quelque chose de très approchant est présenté dans la version Python. Cela suffit à illustrer le fait que les méthodes peuvent être héritées et qu’en fonction du type dynamique de l’objet (ici testé par le biais de l’instruction isinstance), la méthode choisie sera celle de la superclasse ou de la sous-classe. Code PHP 5 Héritage et substitution

Héritage et substitution


lienFilleO2 = $lienFilleO2; } public function jeTravaillePourO1() { $this->lienFilleO2->jeTravaillePourO2(); $this->lienFilleO2->jeTravaillePourLaFilleO2(); } public function jeTravailleAussiPourO1($lienO2){ if ($lienO2 instanceof FilleO2) { $lienO2->jeTravaillePourO2(); $lienO2->jeTravaillePourLaFilleO2(); } else { $lienO2->jeTravaillePourO2(); } } }

Héritage CHAPITRE 11

229

class O2 { public function __construct() {} public function jeTravaillePourO2() { print("je suis un service rendu par la classe O2
\n"); } } class FilleO2 extends O2 { //heritage PHP = syntaxe Java public function __construct() {} public function jeTravaillePourLaFilleO2() { print("je suis un service rendu par la classe FilleO2
\n"); } } $unObjetO2 = new O2(); $uneFilleO2 = new FilleO2(); $unObjetO1 = new O1($uneFilleO2); $unObjetO1->jeTravaillePourO1(); $unObjetO1->jeTravailleAussiPourO1($unObjetO2); $unObjetO1->jeTravailleAussiPourO1($uneFilleO2); ?>

Dans le code PHP 5, on retrouve une syntaxe de l’héritage proche de Java, par l’entremise du mot-clé extends et tout comme en Python, en l’absence de typage, l’utilisation de l’expression instanceof() aussi empruntée à Java, permet de vérifier quelle version de la méthode doit vraiment s’exécuter.

La recherche des méthodes dans la hiérarchie Les méthodes des superclasses et des sous-classes étant, comme les attributs, stockées ensemble, mais dans la mémoire des méthodes cette fois, lors de l’envoi du message d’O1 vers la FilleO2, la méthode recherchée le sera, d’abord, dans la zone mémoire correspondante au type de l’objet, c’est-à-dire la zone mémoire FilleO2. Si la méthode ne s’y trouve pas, grâce à l’instruction d’héritage (comme indiqué à la figure 11-8), on sait que cette méthode peut se trouver plus haut, quelque part dans une superclasse. La montée en cordée, de superclasse en superclasse, à la découverte de la méthode recherchée, peut-être longue, et tout dépendra de la profondeur de la structure hiérarchique d’héritage réalisée dans l’application logicielle. Toutes les méthodes dans la hiérarchie peuvent s’appliquer sur l’objet, car le compilateur aura bien vérifié que chacune, quel que soit le niveau hiérarchique où elle se trouve, n’interférera qu’avec les attributs et les méthodes qui existent à ce niveau. Ces montées et descentes, pendant l’exécution du programme, à la recherche de la méthode appropriée à exécuter sur l’objet, ont amené certains à parler d’un fonctionnement de type « yoyo ». Sans conteste, les voyages dans la RAM ralentissent considérablement toute l’exécution d’un programme. Or, on a déjà rencontré ces déplacements en examinant l’activation successive d’objets, qui peuvent se trouver stockés n’importe où dans la RAM. On accroît ce phénomène, en le reproduisant du côté des méthodes, dont la quête peut également occasionner ces périples incessants.

230

L’orienté objet

Figure 11-8

Recherche de la méthode de superclasse en superclasse dans la structure hiérarchique de classes.

Rien de bien original à répondre à cette critique fondée si ce n’est, une nouvelle fois, d’accepter la programmation OO pour ce qu’elle est : une approche plus simplifiée, plus intuitive, plus stable, et dont la complexification est mieux maîtrisée, du développement logiciel, et non pour ce qu’elle n’est pas, une volonté d’exploitation à tous crins des possibilités d’optimisation liée au fonctionnement intime des processeurs. Il s’agit bien d’un parti pris OO contre processeur.

Encapsulation protected protected est une troisième manière, en plus de private et public, de caractériser tant les attributs que les méthodes d’une classe. Il s’agit de raffiner le souci d’encapsulation, discuté aux chapitres 7 et 8, en l’adaptant à la pratique d’héritage. Rappelons les deux raisons premières de cette pratique d’encapsulation, qui consiste à tenter de maximiser la partie privée des classes au détriment de leur partie publique.

Concernant les attributs et les seules valeurs qui sont tolérées à leur égard, il faut laisser à leur classe, et à aucune autre, le soin de gérer leur intégrité. Ensuite, pour les attributs comme pour les méthodes, il est logique d’anticiper de possibles changements dans le codage d’une classe, et souhaitable de minimiser au mieux l’impact de ces changements sur les classes qui interagissent avec elle. L’addition de protected vient d’un souci légitime, ayant pour objet le statut des sous-classes par rapport aux autres classes. Dans la société, pour qui a l’esprit de famille, il est indéniable que l’héritier est un être privilégié dans une famille. Mais dans la programation OO, les sous-classes doivent-elles être privilégiées ou logées à la même

Héritage CHAPITRE 11

231

enseigne que toutes les autres classes ? Question légitime car, en effet, un attribut ou une méthode protected rendra son accès possible, en plus de la classe où ils se trouvent déclarés, aux seules sous-classes héritant de celle-ci. Quoi de plus légitime que de permettre aux héritiers d’hériter de leur dû le plus facilement qui soit ? Si l’héritier ne jouit d’aucun privilège par rapport à la première classe venue, quelle espèce d’héritage est-ce donc là ? Protected Un attribut ou une méthode déclaré protected dans une classe devient accessible dans toutes les sous-classes. La charte de la bonne programmation OO déconseille l’utilisation de « protected ». D’ailleurs, Python ne possède pas ce niveau d’encapsulation.

Si ce souci de statut est compréhensible, d’où, de fait, la possibilité qui reste offerte aux programmeurs d’utiliser l’accès protected, l’avidité des héritiers est à ce point déconnectée du motif premier de l’encapsulation que la charte du bon programmeur OO bannit l’utilisation de protected. Python ne permet d’ailleurs pas ce niveau d’encapsulation. En effet, même par rapport à toutes ses sous-classes, la superclasse se doit de préserver son intégrité. De même, tout changement dans une partie de code protected affectera toutes les sous-classes. Pour toutes les classes, séparées dans leur écriture logicielle d’une classe concernée, il vaut mieux renforcer la sécurité et la stabilité, en ne laissant publique qu’une faible partie du code de la classe, publique pour toutes les autres classes, quelle que soit leur proximité sémantique avec la classe concernée. Les héritières resteront toujours privilégiées, en ceci que, malgré un accès plus indirect, via les méthodes, elles posséderont les mêmes attributs que la superclasse, et en ceci, aussi, qu’elles pourront, à loisir, ré-utiliser les méthodes de cette dernière, sans recourir à l’envoi de messages. Ce sont leurs méthodes à elles aussi. Elles pourront les appeler à l’intérieur de leur code, comme s’il s’agissait de méthodes définies par elles. Néanmoins, dans l’organisation logicielle et les tracas causés par sa répartition entre une équipe de programmeurs, rien ne contribue vraiment à distinguer une sous-classe d’une autre. Malgré les réserves exprimées à l’égard de l’encapsulation protected, une utilisation très courante de l’encapsulation protected, par exemple dans les librairies Java, se retrouve dans la définition de méthodes de superclasse que l’on encourage l’utilisateur de ces superclasses à redéfinir dans les sous-classes (nous préciserons cela dans le prochain chapitre). Il faut les redéfinir en bas en faisant explicitement appel à ces méthodes de là-haut (d’où le protected pour n’autoriser cet appel que par les sous-classes). protected incite alors à une redéfinition, en faisant toutefois appel aux méthodes originelles de la superclasse de départ. Nous clarifierons cela dans les prochains chapitres.

Héritage et constructeurs Comme nous l’avons vu précédemment, il est fréquent que la sous-classe ajoute des attributs par rapport à la superclasse. Tout objet, instance de la sous-classe, possède dès lors deux ensembles d’attributs, ceux qui lui sont propres et ceux hérités de là-haut. Se pose alors le problème de la pratique des constructeurs, que nous savons être indispensable, en tous cas vivement conseillée, pour l’initialisation de ces attributs lors de la création de chaque objet. Comment doit se comporter le constructeur de la sous-classe dans le traitement des attributs qui ne lui incombent qu’indirectement, c’est-à-dire par héritage ? Java, C# et C++ se comportent de la même façon, que nous allons décortiquer grâce à trois petits codes Java, qui visent à clarifier cet aspect assez subtil de la programmation objet. Python et PHP 5, que nous verrons à la fin, se particularisent.

232

L’orienté objet

Premier code Java class O1 { protected int unAttributO1; // attribut protected public O1() { this.unAttributO1 = 5; // le constructeur initialise l’attribut } } class FilsO1 extends O1 { private int unAttributFilsO1; public FilsO1() {} /* ici, le constructeur de la superclasse est appelé par défaut ou de manière implicite */ public void donneAttribut() { System.out.println("mes Attributs sont: " + unAttributO1 + " " + unAttributFilsO1); /* l’attribut de O1 est accessible grâce au « protected » } } public class TestConsHerit { public static void main(String[] args) { FilsO1 unFils = new FilsO1(); unFils.donneAttribut(); } }

Résultats mes Attributs sont 5 0

Ce code Java est élémentaire sauf sur un point. Une classe O1 possède un attribut que nous déclarons protected pour pouvoir y accéder dans les sous-classes. Le constructeur de cette classe initialise l’attribut à 5. Une classe FilsO1 est déclarée qui hérite de O1 et possède un attribut supplémentaire. Apparamment, le constructeur de la sous-classe ne fait rien. Or, et toute la subtilité est là, si nous découvrons le résultat du code, nous constatons que le constructeur de la superclasse a pourtant été appelé car l’attribut hérité de la superclasse vaut bien 5. Nous voyons à l’œuvre un mécanisme implicite, commun à Java, C# et C++ et absent des langages de script comme Python et PHP 5 : un constructeur de la superclasse sans argument est toujours appelé par défaut par la sous-classe. Soit il a été défini, comme dans ce code-ci, soit Java en propose un par défaut, qui se limite à initialiser tous les attributs à des valeurs par défaut : 0 ou null. S’il est défini, il se substitue purement et simplement à celui par défaut.

Deuxième code Java class O1 { protected int unAttributO1; // attribut protected public O1(int unAttributO1) { this.unAttributO1 = unAttributO1; }

Héritage CHAPITRE 11

233

} class FilsO1 extends O1 { private int unAttributFilsO1; public FilsO1(int unAttributO1, int unAttributFilsO1) { this.unAttributFilsO1 = unAttributFilsO1; } public void donneAttribut() { System.out.println("mes Attributs sont: " + unAttributO1 + " " + unAttributFilsO1); } } public class TestConsHerit { public static void main(String[] args) { FilsO1 unFils = new FilsO1(5,10); unFils.donneAttribut(); } }

Ce code est assez logique dans sa forme, le constructeur de la superclasse s’occupe d’initialiser son attribut et celui de la sous-classe le sien. Pourtant, le compilateur fait des siennes et gromelle qu’il ne trouve plus aucun constructeur ne recevant aucun argument, et pour cause : le nouveau constructeur de la superclasse O1 a balayé celui-ci afin de le remplacer par un constructeur à un argument, la valeur initiale de l’attribut. Si l’idée de laisser chaque constructeur s’occuper de ses propres attributs est plutôt une bonne idée, il reste à forcer la sous-classe à appeler le constructeur de la superclasse avec la valeur initiale de l’attribut qui le concerne, comme le code Java ci-dessous, qui compile et s’exécute sans problème, l’illustre.

Troisième code Java : le plus logique et le bon class O1 { protected int unAttributO1; // attribut protected public O1(int unAttributO1) { this.unAttributO1 = unAttributO1; } } class FilsO1 extends O1 { private int unAttributFilsO1; public FilsO1(int unAttributO1, int unAttributFilsO1) { super(unAttributO1) // appel explicite du constructeur this.unAttributFilsO1 = unAttributFilsO1; } public void donneAttribut() { System.out.println("mes Attributs sont: " + unAttributO1 + " " + unAttributFilsO1); } }

234

L’orienté objet

public class TestConsHerit { public static void main(String[] args) { FilsO1 unFils = new FilsO1(5,10); unFils.donneAttribut(); } }

Résultats mes Attributs sont : 5 10 Le constructeur de la sous-classe fait appel, par l’entremise de super(), au constructeur de la superclasse. Ici super est simplement un pointeur vers la superclasse, pointeur que nous retrouverons pas plus tard que dans le prochain chapitre. Ici, super() se borne à rappeler le constructeur de la superclasse. Pourquoi, de fait, faire appel au constructeur de la superclasse ? Simplement, dixit le compilateur, parce qu’on n’a pas le choix. Chaque classe s’occupe de l’initialisation de ses propres attributs. Gardez toujours à l’esprit le découpage fort des responsabilités en OO. Rendez à chaque classe ce qui appartient à chaque classe. Chacun à sa classe... et les attributs seront bien gardés. Héritage et constructeur La sous-classe confiera au constructeur de la superclasse (qu’elle appellera par l’entremise de super() en Java et base en C# et parent en PHP 5) le soin d’initialiser les attributs qu’elle hérite de celle-ci. C’est une excellente pratique de programmation OO que de confier explicitement au constructeur de la superclasse le soin de l’initialisation des attributs de cette dernière. D’ailleurs, si vous ne le faites pas, Java, C# et C++ le font par défaut, en appelant implicitement un constructeur sans argument.

Nous ajoutons ici les versions C# et C++, parfaitement équivalentes, à quelques détails de syntaxe près, au troisième petit code Java ci-dessus.

En C# using System; class O1 { protected int unAttributO1; public O1(int unAttributO1) { this.unAttributO1 = unAttributO1; } } class FilsO1:O1 { private int unAttributFilsO1; public FilsO1(int unAttributO1, int unAttributFilsO1):base(unAttributO1) { /* notez la version différente de l’appel au constructeur de la superclasse */ this.unAttributFilsO1 = unAttributFilsO1; }

Héritage CHAPITRE 11

235

public void donneAttribut() { Console.WriteLine("mes Attributs sont: " + unAttributO1 + " " + unAttributFilsO1); } } public class TestConsHerit { public static void Main() { FilsO1 unFils = new FilsO1(5,10); unFils.donneAttribut(); } }

La seule vraie différence est l’appel au constructeur de la superclasse qui se fait par le mot-clé base plutôt que super, et dès la déclaration de la méthode plutôt que dans le corps d’instructions.

En C++ #include "stdafx.h" #include "iostream.h" class O1 { protected: int unAttributO1; public: O1(int unAttributO1) { this->unAttributO1 = unAttributO1; } }; class FilsO1:public O1 { private: int unAttributFilsO1; public: FilsO1(int unAttributO1, int unAttributFilsO1):O1(unAttributO1) { /* appel du constructeur de la superclasse */ this->unAttributFilsO1 = unAttributFilsO1; } void donneAttribut() { cout << "mes Attributs sont: " <donneAttribut(); return 0; }

L’orienté objet

236

La syntaxe de l’appel du constructeur de la superclasse est très proche du C# (dans la déclaration plutôt que dans le corps d’instructions), à ceci près qu’il faut explicitement faire référence au nom de la superclasse. Comme nous le verrons par la suite, le C++ permet le multihéritage, ce qui rend les mots-clés super et base parfaitement ambigus.

En Python class O1: unAttributO1=0 def __init__(self,unAttributO1): self.unAttributO1 = unAttributO1; class FilsO1(O1): unAttributFilsO1=0 def __init__(self,unAttributO1,unAttributFilsO1): O1.__init__(self,unAttributO1) #appel du constructeur de la superclasse self.unAttributFilsO1 = unAttributFilsO1 def donneAttribut(self): print "mes Attributs sont: %s" % self.unAttributO1,self.unAttributFilsO1 unFils = FilsO1(5,10) unFils.donneAttribut();

À la différence des trois autres langages, Python ne fait jamais d’appel implicite au constructeur de la superclasse. Dès lors, tout appel doit s’expliciter. Si ce n’est pas le cas, le code s’exécute malgré tout, mais les attributs de la superclasse ne seront pas initialisés. Par économie d’écriture, et le protected n’existant pas dans Python, les attributs ont été laissés publics. Finalement, il faut, comme pour le C++ avec lequel Python partage l’acceptation du multihéritage, indiquer le nom de la superclasse dont on déclenche le constructeur. En PHP 5 Héritage des constructeurs

Héritage des constructeurs


unAttributO1 = $unAttributO1; } } class FilsO1 extends O1 { private $unAttributFilsO1; /* Il faut obligatoirement appeler le constructeur de la superclasse */

Héritage CHAPITRE 11

237

public function __construct($unAttributO1, $unAttributFilsO1) { parent::__construct($unAttributO1); // attention à la syntaxe avec « parent » $this->unAttributFilsO1 = $unAttributFilsO1; } public function donneAttribut() { print("mes attributs sont: $this->unAttributO1 et $this->unAttributFilsO1
\n"); } } $unFils = new FilsO1(5,10); $unFils->donneAttribut(); ?>

Comme en Python, l’appel du constructeur de la superclasse est obligatoire pour initialiser les attributs de la superclasse. Comme PHP 5 n’admet que l’héritage simple, tout comme Java et C#, la référence ci-dessus se fait cette fois par l’utilisation du mot-clé parent.

Héritage public en C++ C++ n’est pas avare de subtilités et de mécanismes sophistiqués. D’aucuns les décrieront comme tordus et inutiles, alors que d’autres les qualifieront, émerveillés, de méga-puissants et de vitaux. Parmi ceux-ci, un héritage, au lieu d’être public (comme vous pouvez le constater dans le code C++ plus haut), peut alternativement être déclaré comme private ou protected. Comme indiqué dans le diagramme de classe qui suit, la différence entre ces trois héritages réside dans l’accès des attributs et méthodes de la superclasse, lorsqu’une classe associée à la sous-classe désire accéder à ceux-ci. Figure 11-9

Différence en C++ entre les héritages public, protected et private.

238

L’orienté objet

Limitons-nous aux seules méthodes publiques dans la superclasse. Si l’héritage est public, ce qui est très majoritairement le cas, les méthodes publiques héritées de la superclasse deviennent également publiques pour toutes les classes. Nous avons pris l’héritage public comme le fonctionnement par défaut, quand la classe O1 pouvait envoyer à la filleO2 des messages dont le corps se trouvait déclaré, soit directement dans la filleO2, soit hérité de O2. De fait, des héritages autres que public ne sont pas possibles dans les autres langages. Lors d’un héritage privé, une méthode public devient private dans la sous-classe, et la même deviendra protected dans la version protected de l’héritage. Ceux que ce mécanisme séduit le justifieront par un renforcement encore plus marqué de l’encapsulation, car il devient possible de limiter davantage encore l’impact de modifications dans les parties publiques. Mais comme les méthodes public le sont par ailleurs pour les empêcher de trop changer, cette sévérité accrue apparaît quelque peu exagérée. Nous retrouverons souvent dans C++ une offre bien plus abondante de degrés de liberté à sélectionner ou calibrer. En C++, tout ce qui pouvait être imaginé comme trucs et ficelles de programmation l’a été. C’est au programmeur, alors, de procéder pour chacun de ces degrés au bon calibrage. Ce calibrage requérant une compréhension suffisante des conséquences de chacun des choix, compréhension faisant défaut chez de nombreux programmeurs, les langages OO plus jeunes ont fait le choix de se débarrasser d’un grand nombre de ces degrés de liberté (un choix qui semble plutôt leur réussir). Comme nous avons déjà eu l’occasion de le dire et le redire, C++ est à Java ce que sont les supers appareils photo 24 × 36, super réflex et autres aux simples instamatics. Beaucoup de règlages en plus, mais qui n’empêchent pas les instamatics de faire souvent de bien meilleures photos. Trop de règlages nuit. Trop de liberté tue la liberté (cela aurait pu être de Sartre, mais c’est de nous).

Le multihéritage Il n’y a rien de conceptuellement dérangeant à ce qu’une classe puisse hériter de plusieurs superclasses à la fois. Un artiste de cirque est souvent un clown, un trapéziste, un musicien, un jongleur et un dompteur, tout à la fois. Un ordinateur portable est en même temps un ordinateur et un bagage. Il hérite des deux fonctionnalités : on l’allume, le « boote », le « back-up » (désolé pour le français, sorry vraiment…) mais, également, on le passe dans le détecteur de métaux, on l’enlève de sa malette devant les membres du service de sécurité de l’aéroport avant d’enlever ses chaussures, on le glisse dans le compartiment à bagages ou sur le siège arrière d’une voiture. Il a donc deux chances de se faire voler : soit en tant qu’ordinateur, soit en tant que bagage. Un livre d’informatique est un livre et aussi l’objet de mille rancœurs et frustrations pour beaucoup d’étudiants plongés dans ce livre.

Ramifications descendantes et ascendantes Notre conceptualisation du monde s’arrange bien de cette multiplicité qui, de manière plus formelle, élargit la structure de l’héritage : d’arbre (quand l’héritage ne peut se ramifier que de manière descendante), en graphe (quand les ramifications peuvent se faire autant dans le sens descendant – plusieurs sous-classes pour une classe – qu’ascendant – plusieurs superclasses pour une classe, voir la figure qui suit). En principe, toute classe pourrait réunir en son sein des caractéristiques différentes provenant de plusieurs superclasses. Il suffit qu’elle les additionne. Or, les langages Java, C# et PHP 5 interdisent le multihéritage (en partie, ils l’autorisent pour les interfaces comme nous le verrons plus bas), alors que C++ et Python l’autorisent totalement. Tout le problème provient de la nécessité pour les caractéristiques héritées d’être vraiment différentes entre elles.

Héritage CHAPITRE 11

239

Figure 11-10

Différence entre un arbre où la ramification ne peut se faire que de manière descendante et un graphe où celle-ci peut se faire dans les deux sens.

Multihéritage en C++ et Python Nous allons, dans un premier temps, illustrer le multihéritage, en nous limitant au C++ et Python, étant donné son bannissement des autres langages de programmation. Nous poursuivrons uniquement avec ces langages, en découvrant, par une succession de petits exemples, des situations normales et d’autres plus problématiques, ces mêmes situations qui ont incité Java, C# et PHP 5 à préférer s’abstenir. Remarquez, de fait, que, pour réaliser le petit diagramme UML ci-après, nous sommes passés de TogetherJ à Rational Rose, car TogetherJ étant parfaitement synchronisé avec Java, un tel diagramme n’aurait pu être réalisé. Au contraire, la version de Rose utilisée ici est prévue pour s’interfacer avec le C++. Figure 11-11

Exemple de multihéritage : la classe FilleO2 hérite de deux superclasses.

Le code C++ correspondant est indiqué ci-après.

240

L’orienté objet

Code C++ illustrant le multihéritage class O2 { private: int unAttributO2; public: O2() { unAttributO2 = 5; } void jeTravaillePourO2() { cout << "Je suis un service rendu par la classe O2" << endl; } }; class O22 { private: int unAttributO22; public: O22() { unAttributO22 = 10; } void jeTravaillePourO22() { cout << "Je suis un service rendu par la classe O22" << endl; } }; class FilleO2 : public O2, public O22 { /* Hérite des deux classes */ public: FilleO2() {} void jeTravaillePourLaFilleO2() { cout << "Je suis un service rendu par la classe FilleO2" << endl; } }; class O1 { private: FilleO2* lienFilleO2; public: O1(FilleO2* lienFilleO2) { this->lienFilleO2 = lienFilleO2; } void jeTravaillePourO1() { lienFilleO2->jeTravaillePourO2(); /* message en provenance de la classe O2*/ lienFilleO2->jeTravaillePourO22(); /* message en provenance de la classe O22*/ /* notez qu'un tel message aurait été impossible si l'héritage concernant ➥la classe O22 avait été déclaré comme protected ou private */ lienFilleO2->jeTravaillePourLaFilleO2(); } }; int main(int argc, char* argv[]) { FilleO2* uneFilleO2 = new FilleO2(); O1* unObjetO1 = new O1(uneFilleO2); unObjetO1->jeTravaillePourO1(); return 0; }

Héritage CHAPITRE 11

241

Le résultat attendu est Je suis un service rendu par la classe O2 Je suis un service rendu par la classe O22 Je suis un service rendu par la classe FilleO2

Rien de bien compliqué à cela, les caractéristiques de O2 et celles d’O22 deviennent ensemble caractéristiques de FilleO2. Chaque objet FilleO2 sera, tout à la fois, un objet O2 et un objet O22. Il en va de même en Python comme le code suivant, équivalent en tout point au précédent, l’illustre : Code Python illustrant le multihéritage class O2: __unAttributO2=0 def __init__(self): self.__unAttribut=5 def jeTravaillePourO2(self): print "Je suis un service rendu par la classe O2" class O22: __unAttributO22=0 def __init__(self): self.__unAttributO22=10 def jeTravaillePourO22(self): print "Je suis un service rendu par la classe O22" class FilleO2(O2,O22): #héritage des deux classes def __init__(self): pass def jeTravaillePourLaFilleO2(self): print "Je suis un service rendu par la classe FilleO2" class O1: __lienFilleO2=0 def __init__(self, lienFilleO2): self.__lienFilleO2=lienFilleO2 def jeTravaillePourO1(self): self.__lienFilleO2.jeTravaillePourO2() self.__lienFilleO2.jeTravaillePourO22() self.__lienFilleO2.jeTravaillePourLaFilleO2() filleO2=FilleO2() unObjetO1=O1(filleO2) unObjetO1.jeTravaillePourO1()

Des méthodes et attributs portant un même nom dans des superclasses distinctes Passons maintenant à une première situation plus délicate, obtenue en rajoutant le même attribut : unAttributAProbleme, dans les deux superclasses, ainsi que deux méthodes, mais présentant la même signature : uneMéthodeAProbleme(). La mise à jour est effectuée dans le diagramme UML et dans le code qui suit. Nous avons délibérément commis un anathème OO, en déclarant l’attribut à problème protected dans les deux superclasses, c’est-à-dire directement accessibles dans la sous-classe.

242

L’orienté objet

Figure 11-12

Un attribut et une méthode portent le même nom dans deux superclasses distinctes.

Code C++ illustrant un premier problème lié au multihéritage class O2 { private: int unAttributO2; protected: int unAttributAProbleme; public: O2() { unAttributO2 = 5; unAttributAProbleme = 5; } void uneMethodeAProbleme() { cout << "dans O2 attribut a probleme vaut " <
Héritage CHAPITRE 11

243

class FilleO2 : public O2, public O22 { public: FilleO2() {} void jeTravaillePourLaFilleO2() { cout << O22 ::unAttributAProbleme << endl ; /* il faut spécifier lequel des deux attributs*/ O22::uneMethodeAProbleme(); /* il faut spécifier laquelle des deux méthodes est utilisée */ cout << "Je suis un service rendu par la classe FilleO2" << endl; } }; class O1 { private: FilleO2* lienFilleO2; public: O1(FilleO2* lienFilleO2) { this->lienFilleO2 = lienFilleO2; } void jeTravaillePourO1() { lienFilleO2->jeTravaillePourO2(); lienFilleO2->jeTravaillePourO22(); lienFilleO2->jeTravaillePourLaFilleO2(); lienFilleO2->O2::uneMethodeAProbleme(); /* il faut, ici aussi, spécifier laquelle des deux ➥méthodes est utilisée */ } }; int main(int argc, char* argv[]) { FilleO2* uneFilleO2= new FilleO2(); O1* unObjetO1 = new O1(uneFilleO2); unObjetO1->jeTravaillePourO1(); return 0; }

Resultat Je suis un service rendu par la classe O2 Je suis un service rendu par la classe O22 dans O22 attribut à probleme vaut 10 Je suis un service rendu par la classe FilleO2 dans O2 attribut à probleme vaut 5

Un problème survient, car les deux superclasses nomment de la même manière un attribut et une méthode. Lors de l’appel de la méthode et de l’attribut dans la sous-classe, naît une ambiguïté fondamentale, épinglée par le compilateur. De laquelle des deux méthodes et duquel des deux attributs s’agit-il ? Le compilateur ne s’offusquera que si la sous-classe fait un usage explicite de la méthode ou de l’attribut à problème. La seule manière pour éviter que le compilateur ne rechigne est de spécifier, comme vous pouvez le voir dans le code, l’attribut et la méthode en question que l’on souhaite utiliser. Cela se fait très simplement, lors de l’appel, en attachant au nom de l’attribut ou de la méthode celui de la classe, comme vous pourriez le faire avec des fichiers portant un même nom, mais situés dans des répertoires distincts. En effet, il s’agit réellement, à l’échelle des attributs et des méthodes, de préciser le chemin à effectuer pour les retrouver ou bien encore de spécifier leur adresse complète.

244

L’orienté objet

En Python class O2: __unAttributO2=0 unAttributAProbleme = 0 def __init__(self): self.__unAttributO2=5 self.unAttributAProbleme = 5 def uneMethodeAProbleme(self): print "dans O2 attribut à problème vaut %s" %self.unAttributAProbleme def jeTravaillePourO2(self): print "Je suis un service rendu par la classe O2" class O22: __unAttributO22=0 unAttributAProbleme = 0 def __init__(self): self.__unAttributO22=10 self.unAttributAProbleme = 10 def uneMethodeAProbleme(self): print "dans O22 attribut à problème vaut %s" %self.unAttributAProbleme def jeTravaillePourO22(self): print "Je suis un service rendu par la classe O22" class FilleO2(O2,O22): #héritage des deux classes def __init__(self): O2.__init__(self) O22.__init__(self) def jeTravaillePourLaFilleO2(self): O2.uneMethodeAProbleme(self) #manière de choisir la version désirée print "Je suis un service rendu par la classe FilleO2" class O1: __lienFilleO2=0 def __init__(self, lienFilleO2): self.__lienFilleO2=lienFilleO2 def jeTravaillePourO1(self): self.__lienFilleO2.jeTravaillePourO2() self.__lienFilleO2.jeTravaillePourO22() self.__lienFilleO2.jeTravaillePourLaFilleO2() self.__lienFilleO2.uneMethodeAProbleme() filleO2=FilleO2() unObjetO1=O1(filleO2) unObjetO1.jeTravaillePourO1()

Résultats

Je suis Je suis dans O2 Je suis dans O2

un service un service attribut à un service attribut à

rendu par la classe O2 rendu par la classe O22 problème vaut 10 rendu par la classe FilleO2 problème vaut 10

Héritage CHAPITRE 11

245

En Python, il faut aussi rendre moins ambigu l’appel à la méthode, et cela de la manière précisée dans le code. Quant à l’attribut, celui-ci étant d’office un attribut d’instance, Python ne considère qu’une seule et unique occurrence de cet attribut, la dernière, d’où l’apparition des deux « 10 ». S’agissant des méthodes, le problème n’est pas uniquement qu’elles soient signées de la même façon dans les deux superclasses, mais qu’à signature identique ne corresponde pas un corps d’instructions identiques. Alors que cette signature partagée, en présence d’un corps d’instructions différent, est la base du polymorphisme, lorsque les classes impliquées sont à des niveaux hiérarchiques différents, le problème survient, car les deux classes se trouvent au même niveau. Cette dernière considération mène très logiquement à la réponse trouvée à ce problème par Java, C# et PHP 5. Ces trois langages ne permettent pas le multihéritage de classe, mais permettent, en revanche, le multihéritage d’interfaces (que nous approfondirons au chapitre 15). En Java, C# et PHP 5, une sous-classe peut hériter d’une classe et d’autant d’interfaces que l’on veut ou juste du nombre d’interfaces souhaité. Rappelez-vous que l’interface se limite à la liste des signatures de méthode et, de fait, en l’absence de corps, ne conduira jamais aux problèmes d’ambiguïté rencontrés en C++ et en Python. Rien n’interdit plusieurs interfaces, héritées par une même sous-classe, de posséder des signatures de méthodes communes, étant donné que les difficultés apparaissent uniquement en présence de corps d’instructions différents. En ce qui concerne les attributs, les interfaces Java autorisent uniquement des attributs « public », « finaux » et « statique » (c’est-à-dire des constantes de classe), mais qu’il vous reste malgré tout à nommer différemment. Les interfaces C# et PHP 5, quant à elles, n’en autorisent simplement pas. Plusieurs chemins vers une même superclasse La POO favorisant l’éclatement dans le développement logiciel, la possibilité que deux méthodes, présentes dans des classes différentes, portent le même nom (par exemple, implémentant une fonctionnalité commune) n’est pas nulle, d’où la prudence des autres langages. Le seul recours à cela est d’explicitement différencier leur nom ou de spécifier leur chemin d’accès au moment de l’appel. Un autre problème, encore plus subtil, est posé par le nouveau diagramme UML qui suit. Figure 11-13

Plusieurs chemins d’héritage mènent à une même superclasse.

246

L’orienté objet

Code C++ : illustrant un deuxième problème lié au multihéritage class OO { protected: int unAttributAProbleme; public: OO() { unAttributAProbleme = 0; } }; class O2 : virtual public OO { private: int unAttributO2; public: O2() { unAttributO2 = 5; unAttributAProbleme = 5; } void uneMethodeAProbleme() { cout << "dans O2 attibut a probleme vaut " <
Héritage CHAPITRE 11

247

class O1 { private: FilleO2* lienFilleO2; public: O1(FilleO2* lienFilleO2) { this->lienFilleO2 = lienFilleO2; } void jeTravaillePourO1() { lienFilleO2->jeTravaillePourO2(); lienFilleO2->jeTravaillePourO22(); lienFilleO2->jeTravaillePourLaFilleO2(); lienFilleO2->O2::uneMethodeAProbleme(); } }; int main(int argc, char* argv[]) { FilleO2* uneFilleO2 = new FilleO2(); O1* unObjetO1 = new O1(uneFilleO2); unObjetO1->jeTravaillePourO1(); return 0; }

Le problème qui se pose est le suivant. L’héritage se réalise concrètement par une forme dissimulée de composition, puisque l’objet de la sous-classe possède un objet de la superclasse. Que se passe-t-il quand plusieurs superclasses présentent, elles-mêmes, une superclasse commune, comme dans le cas présent ? Logiquement, tout objet de la classe FilleO2 se composera deux fois d’un objet de la classe OO, une première fois, en provenance de la classe O2, une seconde fois de la classe O22. Est-ce vraiment nécessaire ? Si on remonte le graphe, des classes plus spécifiques aux classes plus générales, dès qu’une de ces classes est rencontrée en empruntant des chemins différents, le problème se pose. Quand l’héritage n’est pas déclaré « virtuel », la répétition des instances des superclasses dans la sous-classe est la solution par défaut proposée par C++, comme le montre le résultat de l’exécution du code. Résultat Je suis un service rendu par la classe O2 Je suis un service rendu par la classe O22 dans O22 attibut à problème vaut 10 Je suis un service rendu par la classe FilleO2 dans O2 attibut à problème vaut 5

L’héritage virtuel Mais est-ce vraiment un problème ? Il doit y en avoir un, sinon C++ ne vous aurait pas permis de le contourner, en déclarant l’héritage cette fois « virtuel ». Retournons à notre écosystème, les ressources et la faune héritaient toutes deux d’ObjetJungle. La Proie n’héritait que de Faune, pourtant toute proie est également une ressource pour le prédateur. Nous pourrions faire hériter la proie et de Faune et de Ressource comme dans le diagramme ci-après :

248

L’orienté objet

Figure 11-14

Dans ce diagramme de classe illustrant le multihéritage, la proie hérite à la fois de la faune et de ressource.

Est-il nécessaire de dupliquer l’ObjetJungle, vu que celui-ci ne contient que des informations sur la position des objets ? Dans ce cas-ci, non bien sûr, car cette information sur la position se doit de rester unique. La solution par défaut du C++ devient inappropriée ici. La seule possibilité consiste à déclarer l’héritage « virtual », avec, pour effet, de rendre toujours unique l’objet de la superclasse partagée par les deux sous-classes. En revanche, un héritage non « virtual », cette fois, resterait approprié dans le cas suivant, illustré également par le petit diagramme qui suit : Figure 11-15

Une situation de multihéritage où l’utilisation de l’héritage « virtual » est bénéfique.

Quand on possède une assurance vol pour tous ses ordinateurs et une assurance perte pour tous ses bagages, un ordinateur portable doit pouvoir bénéficier des deux assurances, l’une contre le vol, en tant qu’ordinateur, l’autre contre la perte, en tant que bagage. L’héritage n’est plus virtuel, car il est nécessaire pour information commune aux assurances qu’il soit dupliqué. Quand « virtualiser » l’héritage et quand ne pas le faire ? Comme nous le voyons, le problème n’est pas simple, et l’accroissement de la difficulté n’a fait que renforcer la conviction de Java, de C# et de PHP 5 d’éviter toutes ces possibles sources d’ambiguïté et de confusion. Comme toujours, C++, quant à lui, nous juge bien plus intelligents que nous ne le sommes en réalité, et nous offre tous les bras de levier et degrés de liberté nécessaires à la bonne décision et à l’optimisation du logiciel résultant. En rendant l’héritage virtuel dans le code précédent, il ne peut plus y avoir qu’une seule instance de l’attribut à problème.

Héritage CHAPITRE 11

249

Le résultat sera maintenant le suivant : Résultat avec héritage virtuel Je suis un service rendu par la classe O2 Je suis un service rendu par la classe O22 dans O22 attribut à problème vaut 10 Je suis un service rendu par la classe FilleO2 dans O2 attibut à problème vaut 10

Les « 10 » seraient à remplacer par des « 5 » si on inversait l’ordre de l’héritage. Le problème se pose également avec Python, dès l’apparition de ce losange dans les relations d’héritage, cependant il ne pose pas pour les attributs mais pour la redéfinition des méthodes, comme nous le verrons dans le chapitre suivant.

Exercices Exercice 11.1 Dessinez un diagramme de classes UML intégrant les classes suivantes : appareil électroménager, appareil à cuisiner, appareil à nettoyer, ramasse-miettes (pas le garbage collector, l’autre), lave-vaisselle, micro-ondes, four.

Exercice 11.2 Dessinez un diagramme de classes UML intégrant les classes suivantes : ordinateur, ordinateur fixe, ordinateur portable, PC, Macintosh, Dell portable, MAC portable Titatium G4. Discutez du possible apport du multihéritage.

Exercice 11.3 Écrivez le squelette de code dans les trois langages, Java, C# et C++, correspondant au diagramme de classes suivant.

250

L’orienté objet

Exercice 11.4 Écrivez le squelette de code dans les trois langages, Java, C# et C++, correspondant au diagramme de classes suivant. Lorsque cela est nécessaire, remplacez les classes par des interfaces.

Exercice 11.5 Aucun des trois petits codes suivant ne trouvera grâce aux yeux des compilateurs. Expliquez pourquoi et corrigez-les en conséquence : Fichier Exo1.java class O1 {} class O2 {} public class Exo1 extends O1,O2 { public static void main(String[] args) { } }

Fichier Exo2.cs public class O1 {} public interface O2 { int jeTravaillePourInterface(); } public class Exo2 : O1,O2 { public static void Main() {} }

Fichier Exo3.cpp class O1 { public: void jeTravaillePourLaClasse() {} }; class O2 { public: void jeTravaillePourLaClasse() {}

Héritage CHAPITRE 11

251

}; class FilleO1 : public O1, public O2 { public: void jeTravaillePourFilleO1() { jeTravaillePourLaClasse(); } }; int main(int argc, char* argv[]) { printf("Il y a un probleme\n"); return 0; }

Exercice 11.6 Aucun des trois petits codes suivants ne trouvera grâce aux yeux des compilateurs. Expliquez pourquoi et corrigez-les en conséquence : Exo1.java class O1 {} class O2 extends O1 {} public class Exo1 { public static void main(String[] args) { O1 unO1 = new O1(); O2 unO2 = new O2(); unO2 = unO1; } }

Exo2.cs public class O1 {} public interface O2 {} public class O3 : O1, O2 {} public class Exo2 { public static void Main() { O1 unO1 = new O1(); O3 unO3 = new O3(); O2 unO4 = unO3; O3 unO5 = unO4; } }

Exo3.cpp class O1 { public: void jeTravaillePourLaClasse() {} }; class O2 { };

252

L’orienté objet

class FilleO1 : public O1, public O2 { public: void jeTravaillePourFilleO1() { jeTravaillePourLaClasse(); } }; int main(int argc, char* argv[]) { O2 unO2; FilleO1 unFO1; unFO1= unO2; printf("Il y a un probleme\n"); return 0; }

Exercice 11.7 Expliquez pourquoi, malgré l’existence de l’accès protected dans les trois langages, la charte du bon programmeur OO vous incite à ne pas l’utiliser.

Exercice 11.8 Quelle différence existe-t-il en C++ entre un héritage public et private. Pour quelle raison, selon vous, cette subtilité a disparu de Java et C# ?

Exercice 11.9 Quelle version de cette assertion est-elle exacte ? « Partout où apparaît un objet d’une superclasse, je peux le remplacer par un objet de sa sous-classe »

ou « Partout où apparaît un objet d’une sous-classe, je peux le remplacer par un objet de sa superclasse »

Exercice 11.10 Soit superA une classe et sousA sa sous-classe, comment le « casting » sera-t-il employé : a = (sousA)b

ou a=(superA)b ?

Exercice 11.11 Pourquoi les classes Stream et GUI en Java se prêtent-elles idéalement à la mise en pratique des mécanismes d’héritage ?

12 Redéfinition des méthodes Ce chapitre décrit une des possibilités offertes par l’héritage et qui est à la base du polymorphisme : la redéfinition dans les sous-classes de méthodes, d’abord définies dans la superclasse. La mise en œuvre de cette pratique et le résultat surprenant, différent selon les langages, de ces effets, tant pendant la phase de compilation que lors de celle de l’exécution, seront analysés en profondeur.

Sommaire : Redéfinition des méthodes — Polymorphisme — Les mots-clés super et base et parent — Java, Python et PHP 5 : polymorphique par défaut — C++ : non polymorphique par défaut — C# : pas vraiment de défaut — Un mauvais « casting » Candidus — Peut-on avoir une vision philosophique de l’héritage ? Autrement dit, peut-on en avoir une compréhension telle qu’il soit exploité, naturellement, de façon bien inspirée par le programmeur ? Doctus — Les langages de programmation montrent des différences qui se situent justement sur ce plan « philosophique ». Je te répondrai donc qu’il est même important de se forger une telle vision de l’héritage, du polymorphisme et de leurs conséquences. Cand. — Quelles peuvent être les différences d’inspiration de nos cinq langages ? Doc. — Toujours les mêmes soucis de performance et de fiabilité : le compilateur, dans son rôle de juge de la cohérence, doit pouvoir trouver toutes les pièces du puzzle dans le travail du programmeur. Il doit pouvoir constater qu’elles s’assemblent parfaitement. Et l’OO nous permet sans restriction d’utiliser différents objets pour assurer un même rôle. Sa seule exigence, ce sera que l’on puisse s’assurer qu’ils disposent chacun des méthodes correspondantes. Cand. — Tu penses à une superclasse joker remplacée par une instance effective au moment de l’exécution, n’est-ce pas ? Doc. — Exactement. Et c’est là que le compilateur, dans les langages qui ont choisi d’y recourir, joue un rôle différent suivant nos langages objet. Ils peuvent donner ou non prépondérance à l’étape de compilation pour guider le déroulement de l’exécution. En d’autres termes, en fonction de l’importance qu’ils donnent au type déclaré des objets par rapport au type qu’ils endossent au moment de l’exécution. Cand. — Je conçois effectivement que si un programme s’amuse à construire le puzzle lors de l’exécution, sa performance en prend un coup ! Mais une chose m’intrigue dans le remplacement dynamique des jokers : les informations exigées par le compilateur C++ consistent à demander au programmeur de lever de possibles ambiguïtés. Qu’en est-il pour Java qui, à l’exécution, va faire le même travail sans l’assistance du programmeur ?

254

L’orienté objet

Doc. — Pas de magie, encore une fois. Un mécanisme d’exceptions sera mis en jeu. Le programmeur devra donc prévoir du code pour traiter ces situations, sinon le programme s’interrompra. Le gain n’est effectif que si ces accidents sont rares...

La redéfinition des méthodes Nous avons vu que l’héritage permet à des classes d’être à la fois elles-mêmes, et en même temps un ensemble successif de superclasses. Elles sont elles-mêmes car, en plus des caractéristiques qu’elles héritent de leur parent (avec « s » ou sans « s »), grand-parent et arrière-grand-parent, elles peuvent rajouter des attributs et des méthodes qui leur sont propres. L’héritage permet également un mécanisme supplémentaire, un tant soit peu plus subtil, mais extrêmement puissant : la redéfinition de méthodes déjà existantes chez le ou les parents. Comme indiqué dans le petit diagramme UML ci-après, il s’agit de récupérer la même signature de méthode que celle déclarée chez le père, mais d’en modifier le corps d’instruction. En substance, la classe père et la classe fils partagent une activité, bien qu’elles l’exécutent différemment. Ce qu’elles partagent en réalité, c’est le nom de l’activité, mais pas la pratique à proprement parler. Dans le code Java correspondant à ce diagramme, on constate que le corps d’instructions de la version du fils de cette méthode partagée avec le père fait d’abord appel à la version du père, avant d’y mettre son grain de sel. Le mot-clé super sert simplement de référent vers la superclasse. En son absence, on se serait retrouver en présence d’une dangereuse boucle récursive infinie. Il est, de ce fait, indispensable, afin de préciser la version de la méthode dont il s’agit : celle du fils ou celle du père. C’est un type d’écriture très souvent rencontré, pour des raisons que nous expliquerons plus avant. Figure 12-1

Redéfinition dans la sous-classe « FilsO1 » de la méthode jeFaisPresqueLaMemeChose() définie originellement dans la superclasse « O1 ».

public class O1 { public void jeFaisPresqueLaMemeChose() { } } public class FilsO1 extends O1 { public void jeFaisPresqueLaMemeChose() { super.jeFaisPresqueLaMemeChose(); .................................. } }

Redéfinition des méthodes CHAPITRE 12

255

Tel père tel fils. C’est une réalité que celle des fils cherchant à imiter leur père. Chanteur, acteur, écrivain, sportif, homme d’affaires ou politique, comme papa, il devra s’éloigner de celui-ci pour enfin devenir un artiste hors père. Pourquoi l’application de ce principe de redéfinition des méthodes, participant de l’héritage, est-elle très courante en OO ?

Beaucoup de verbiage mais peu d’actes véritables L’héritage s’est inspiré de nos mécanismes d’organisation cognitifs, pour transposer les avantages qu’ils permettent : simplicité, économie, adaptabilité, flexibilité, réemploi, au développement logiciel. Une autre caractéristique de notre conceptualisation du monde est que nous consacrons moins de concepts à en décrire les propriétés fonctionnelles et actives que les simples propriétés structurelles, comme si l’image que nous nous faisions de la nature était moins riche en fonctionnalité qu’en structure. Le vocabulaire que nous dédions aux modalités actives est moins riche que celui dédié à la perception statique des choses. C’est d’ailleurs une des raisons fondamentales qui expliquent que nous organisions notre conceptualisation de manière taxonomique : nous regroupons toutes les classes qui partagent les mêmes modalités actives. Tous les animaux, les millions d’espèces existantes, vivent, mangent, dorment et meurent. Ils le font sans doute d’une manière qui leur est propre, mais ils le font tous. Les chanteurs d’opéra, de rock, de folk, de jazz, de gospel, ceux à la croix de bois…, chantent tous, font tous des disques, passent à la télé mais, heureusement pour nous, de façon différente et pas en même temps. Il n’est dès lors pas surprenant de retrouver des mêmes noms d’activité, ici de méthodes, pour les classes et leurs sous-classes. Notez que cette mise en commun des noms d’activités à différents niveaux hiérarchiques prend toute sa raison d’être, tant dans la pratique cognitive qu’en programmation, dans des situations où ces activités sont mises en pratiques par une tierce « entité ». Ainsi, dans l’exemple que nous avons vu au premier chapitre, le feu rouge envoie un message unique, « démarre », à tous les véhicules lui faisant face, sans se préoccuper outre mesure de la manière ultime dont ce message sera exécuté par les différents types de véhicule. Que lui importe, en effet, au feu rouge, de savoir comment son message sera perçu par les voitures. Toutes les voitures démarrent, c’est la seule chose qui compte vraiment ! Cette possibilité offerte à une classe d’interagir avec un ensemble d’autres classes, en leur envoyant un même message, compris par toutes mais exécuté de manière différente, explique pour une grande part que l’on retrouve ce message à plusieurs niveaux. Elle est illustrée par le petit diagramme UML qui suit. Dans le diagramme de classe qui suit, un objet de la classe O2 déclenche le même message sur tous les objets issus de la superclasse O1, mais ce même message sera exécuté différemment en fonction de la sous-classe finale dont est issu l’objet recevant ce message. Une classe peut donc interagir avec un ensemble d’autres comme s’il s’agissait d’une seule et même classe. Elle n’a pas nécessairement besoin d’en connaître la nature ultime pour en disposer. En fait tout un large pan du programme lui devient invisible. C’est une nouvelle forme d’encapsulation si chère à l’OO. La « tierce classe » devient complètement aveugle aux spécifications des différentes sous-classes avec lesquelles elle interagira en dernier ressort. Ces spécifications constitueront ainsi pour le programmeur un large espace de liberté et de variabilité. Ce faisant, nous entrons en plein dans la pratique du « polymorphisme », que nous retrouverons encore dans les chapitres qui suivent. Ce message, au niveau de la superclasse, possédera, oui ou non, un corps d’instructions par défaut. Nous verrons que, dans un type particulier de classe (appelée classe « abstraite » ), le message pourra se borner à n’exister qu’en tant que seule signature. Une sous-classe, au moins, deviendra indispensable afin d’en permettre une première réalisation.

256

L’orienté objet

La base du polymorphisme L’héritage offre la possibilité pour une classe de s’adresser à une autre, en sollicitant de sa part un service qu’elle est capable d’exécuter de mille manières différentes, selon que ce service, nommé toujours d’une seule et même façon, se trouve redéfini dans autant de sous-classes, toutes héritant du destinataire du message. C’est la base du polymorphisme. Cela permet à notre première classe de traiter toutes ses classes interlocutrices comme une seule, et de ne modifier en rien son comportement si on ajoute une de celles-ci ou si une de celles-ci modifie sa manière de répondre aux messages de la première : simplicité de conception, économie d’écriture et évolution sans heurt : tout l’OO est là, dans le polymorphisme.

Figure 12-2

Diagramme de classe représentant le polymorphisme. La classe O2 déclenche sur la classe O1 le message jeFaisPresqueLaMemeChose qui se trouve redéfini dans trois sous-classes.

Un match de football polymorphique Nous allons illustrer ce mécanisme en nous repenchant sur notre simulation du match de football que nous avions juste esquissée dans le chapitre 10, consacré à l’UML. Dans un premier temps, nous avions délibérément évité toute mise en pratique de l’héritage. Or, si une classe se prête assez naturellement à cette mise en pratique, c’est bien la classe Joueur, comme montré dans le diagramme qui suit. Nous spécialisons la classe Joueur en trois sous-classes : Attaquant, Defenseur et Gardien. Rien, du côté des attributs, ne particularise vraiment les différentes sous-classes de joueur, sans doute une tenue un peu différente pour le gardien de but. Y a-t-il lieu de rajouter de nouvelles méthodes dans les sous-classes ? Là encore, le gardien de but peut, sans essuyer de punitions de la part de l’arbitre, attraper la balle avec les mains. Le mode d’interaction entre les

Redéfinition des méthodes CHAPITRE 12

257

joueurs et la balle sera donc quelque peu différent dans le cas du gardien qui peut, dans un premier temps, faire comme tous les joueurs, c’est-à-dire lui donner de violents coups de pied, mais, de surcroît, la caresser de ses douces mains. Figure 12-3

Un petit diagramme de classe centré sur les joueurs et leur relation à l’entraîneur.

Dans ce diagramme UML, très simplifié, aucun nouvel attribut ni méthode ne vient se rajouter dans les sousclasses de joueur. Ce qu’octroie l’héritage ici est la redéfinition des méthodes : interagitBalle() pour le gardien de but, et avance() pour tous les joueurs. Pour illustrer le polymorphisme de la méthode avance(), le scénario imaginé, est un entraîneur excité et paniqué en fin de partie, qui hurle d’avancer à tous ses joueurs. C’est son envoi de message à lui. Sans doute le dernier avant longtemps. Chaque joueur va donc avancer, mais tous le feront à leur manière. Surtout, dans ce hurlement de la dernière chance, il n’est plus question pour l’entraîneur d’en particulariser le contenu en fonction des joueurs auxquels il est adressé. Que ceux-ci exécutent à leur manière les ordres, ainsi qu’ils ont appris à le faire pendant les entraînements. On a rarement vu, sauf dans des cas vraiment désespérés, le gardien de but se retrouver à flirter avec son alter ego de l’équipe adverse. En fait, tous les joueurs se déplaceront, mais en respectant une zone de déplacement sur le terrain, liée à la place qu’ils occupent ainsi qu’au placement des joueurs adverses. Bel exemple de polymorphisme. Nous allons d’ailleurs illustrer cette première mise en musique du polymorphisme dans les cinq langages de programmation, et de manière très graduelle, afin d’en expliquer les avantages, les subtilités syntaxiques, et, là encore, les écarts commis par le C++. De nouveau, ce dernier a pris le pli de faire les choses de manière plus compliquée que les autres.

La classe Balle Commençons d’abord par la classe la moins problématique :

En Java class Balle { public Balle() {} public void bouge(){ System.out.println("la balle bouge"); } }

258

L’orienté objet

En C++ class Balle { public: Balle() {} void bouge(){ cout <<"la balle bouge"<
En C# class Balle{ public Balle() {} public void bouge(){ Console.WriteLine("la balle bouge"); } }

En Python class Balle: def __init__(self): pass def bouge(self): print "la balle bouge"

En PHP 5 class Balle { public function __construct() {} public function bouge() { print ("la balle bouge
\n"); } }

Passons maintenant à une classe plus sensible, la classe Joueur :

En Java class Joueur{ private int posSurLeTerrain; private Balle laBalle; public Joueur(Balle laBalle) { this.laBalle = laBalle; } public int getPosition() { return posSurLeTerrain; } public void setPosition(int position) { posSurLeTerrain = position; }

Redéfinition des méthodes CHAPITRE 12

259

public void interagitBalle() { System.out.println("Je tape la balle avec le pied"); laBalle.bouge(); } public String toString() // redéfinition de la méthode toString() définie dans la classe Object { return getClass().getName() ; } public void avance() { System.out.println("la position actuelle du " + this + " est " + posSurLeTerrain); /* this déclenche automatiquement l’appel de toString() pour obtenir la classe ultime de l'objet */ posSurLeTerrain += 20; } }

posSurLeTerrain est un attribut clé qui indiquera la position du joueur à un instant donné sur le terrain. Dans un souci de simplicité, on ne spécifie qu’une valeur, qui pourrait être la distance par rapport au but. C’est la valeur maximale de cette distance, selon la nature des joueurs, qui imposera de redéfinir leur déplacement. Comme nous utiliserons quelques fois cet attribut dans les sous-classes à venir, des méthodes d’accès, get() et set(), deviennent nécessaires.

Une autre addition, qui illustre parfaitement le thème principal de ce chapitre, est la redéfinition de la méthode toString(). Cette méthode existe par défaut dans la classe la plus haute de la hiérarchie Java, la classe Object, que nous retrouverons dans le chapitre 14 et dont par défaut, sans que l’on doive l’expliciter, hérite toute classe Java. Elle est appelée implicitement à chaque fois que l’on demande d’afficher le référent d’un objet objet (dans le code par la présence du this). Comme il nous importe d’afficher la classe dynamique de l’objet dans le corps de la méthode avance(), nous redéfinissons la méthode toString() afin qu’elle fasse précisément ceci : renseigner la classe dynamique de l’objet par le concours des deux méthodes introspectives getClass().getName(). Enfin, en plus de récupérer et d’afficher la classe de l’objet en question, à chaque exécution de la méthode avance(), le joueur incrémente sa position de 20.

En C++ class Joueur { private: int posSurLeTerrain; Balle* laBalle; public: Joueur(Balle* laBalle) { this->laBalle = laBalle; } /*virtual*/ void interagitBalle() { /* présence de "virtual" */ cout<<"Je tape la balle avec le pied" << endl; laBalle->bouge(); } /*virtual*/ void avance() { /* présence de "virtual" */ const type_info& t = typeid(*this); /* pour faire apparaître la classe de l'objet */ cout<<"la position actuelle du " << t.name() << " joueur est " << posSurLeTerrain << endl; posSurLeTerrain += 20; }

L’orienté objet

260

int getPosition() { return posSurLeTerrain; } void setPosition(int position) { posSurLeTerrain = position; } };

Plusieurs points doivent être détaillés dans la version C++. Tout d’abord, vous voyez apparaître un étrange mot-clé virtual, au début de la signature des méthodes qui vont se prêter à une redéfinition dans les sousclasses. Ce mot-clé marque une différence essentielle dans la manière dont les langages qui nous occupent abordent le polymorphisme, et que nous commenterons plus longuement par la suite. Dans un premier temps, nous laisserons ce mot-clé inactif (entre commentaires). La méthode avance() est un peu plus compliquée, car nous voulons, à l’instar du toString() en Java, récupérer, pendant l’exécution, la classe de l’objet sur lequel la méthode avance() s’exécute. C’est un type d’information, caractéristique de RTTI (Run-Time Type Information), assez récemment ajouté dans le fonctionnement du C++ (celui-ci n’était pas conçu pour fonctionner en mode polymorphique). Son utilisation est un peu délicate. Il vous faut en comprendre l’utilité sans nécessairement maîtriser sa syntaxe assez sibylline.

En C# class Joueur { private int posSurLeTerrain; private Balle laBalle; public Joueur(Balle laBalle) { this.laBalle = laBalle; } public /*virtual*/ void interagitBalle() { /* le même " virtual " qu'en C++ */ Console.WriteLine("Je tape la balle avec le pied"); laBalle.bouge(); } public /*virtual*/ void avance() { /* toujours virtual */ Console.WriteLine("la position actuelle du " + this + " est " + posSurLeTerrain); posSurLeTerrain += 20; } public int positionGet { /* remarquez encore la nature singulière des méthodes d'accès */ get { return posSurLeTerrain; } set { posSurLeTerrain = value ; /* « value » sera remplacé par n’importe quelle valeur que l’on passe à l’attribut au moment de l’appel de cette méthode */ } } }

Redéfinition des méthodes CHAPITRE 12

261

À nouveau, en C#, on note l’apparition de cet étrange virtual (pour les mêmes raisons qu’en C++) au début de la déclaration des méthodes, qui sera commenté par la suite. On relève à part cela quelques petites différences syntaxiques. Ainsi aurez-vous pu noter par vous-même l’utilisation des majuscules plutôt que des minuscules pour les méthodes (Main() et non main(), ToString() et non toString()). C’est toujours le cas et cela mériterait sans doute un procès ! La méthode ToString(), qui provient ici aussi de la superclasse Object dont héritent par défaut toutes les classes, n’a nul besoin d’une redéfinition ici car, dans sa version d’origine (celle dans la classe Object), elle fait ce qu’on souhaite qu’elle fasse, c’est-à-dire juste renvoyer la classe dynamique de l’objet en question. Toutefois, c’est également une méthode qui prête souvent à redéfinition de manière en tout point semblable à celle du code Java. Plus original, comme nous l’avons déjà vu, est la manière dont C # réalise les méthodes d’accès get et set. Il les réunit dans une même méthode, avec la syntaxe quelque peu singulière indiquée dans le code. Cette subtilité d’écriture permet d’appeler les méthodes d’accès comme s’il s’agissait directement des simples attributs. Par la présence de virtual, mis comme commentaire pour l’instant, vous aurez pu constater que C# n’est pas exactement à Java ce que Canada Dry est à l’alcool puisque, de temps en temps, comme ici, il lui fausse compagnie pour se rapprocher du C++. En Python class Joueur(object): # ici il faut explicitement hériter de la supersuperclasse object __posSurLeTerrain=0 __laBalle=None def __init__(self,laBalle): self.__laBalle=laBalle def getPosition(self): return self.__posSurLeTerrain def setPosition(self,position): self.__posSurLeTerrain=position def interagitBalle(self): print "Je tape la balle avec le pied" self.__laBalle.bouge() def __str__(self): # redéfinition de cette méthode return self.__class__.__name__ def avance(self): print "la position actuelle du " + self.__str__()+" est %s" %(self.__posSurLeTerrain) self.__posSurLeTerrain+=20

En Python, nous redéfinissons, comme en Java et en C#, la méthode de description des référents, ici la méthode __str__(). Cependant, une différence importante avec les deux langages précédents est l’obligation d’expliciter l’héritage de la superclasse object. De manière générale, Python ne fait rien de gratuit et vous oblige à déclarer toutes les initiatives que vous prenez, y compris celles qui pourraient apparaître automatiques à la plupart des programmeurs. En PHP 5 class Joueur { private $posSurLeTerrain; private $laBalle;

262

L’orienté objet

public function __construct($laBalle) { $this->posSurLeTerrain = 0; $this->laBalle = $laBalle; } public function getPosition() { return $this->posSurLeTerrain; } public function setPosition ($position) { $this->posSurLeTerrain = $position; } public function interagitBalle() { print ("Je tape la balle avec le pied
\n"); $this->laBalle->bouge(); } public function __toString () { return get_class($this); } public function avance() { print ("la position actuelle du "); print ($this); print (" est $this->posSurLeTerrain
\n"); $this->posSurLeTerrain += 20; } }

On retrouve un code très proche de Java et de C#.

Précisons la nature des joueurs Attaquons maintenant le principal sujet de ce chapitre, à savoir l’héritage et, surtout, la redéfinition des méthodes dans les sous-classes. Trois sous-classes : Gardien, Defenseur et Attaquant vont hériter de la classe Joueur. Dans la classe Gardien, les deux méthodes interagitBalle() et avance() seront redéfinies alors que, dans les deux autres sous-classes, seule la méthode avance() le sera. Redéfinir une méthode dans une sous-classe (ce qu’en anglais on désigne comme la pratique override) consiste à reprendre exactement sa signature, à ceci près que l’on rend l’accès à la méthode dans la sous-classe moins sévère qu’il ne l’est dans la superclasse. Un public ou protected peut remplacer un private, mais non l’inverse, par simple respect du principe de substitution. Une superclasse ne peut en faire plus qu’une sous-classe, pas plus qu’une méthode dans une sous-classe ne peut se rendre moins accessible que celle qu’elle redéfinit dans la superclasse. Si je peux envoyer un message à tous les objets issus d’une superclasse, je dois pouvoir envoyer ce même message à tous les objets issus de la sous-classe de celle-ci. A priori, une méthode définie comme private dans la superclasse, étant inaccessible et de l’extérieur et par ses enfants, ne devrait jamais pouvoir se prêter à une redéfinition. Elle ne le sera pas en effet et nous reviendrons sur ce point précis en fin de chapitre.

Redéfinition des méthodes CHAPITRE 12

263

La redéfinition des méthodes Une méthode redéfinie dans une sous-classe possédera la même signature que celle définie dans la superclasse avec, comme unique différence possible, un mode d’accès moins restrictif. En général, une méthode définie protected ou public dans la superclasse sera redéfinie comme protected ou public dans la sous-classe.

En Java class Gardien extends Joueur { /* héritage */ public Gardien(Balle laBalle) { super(laBalle); /* appel du constructeur de la superclasse */ setPosition(0); } public void interagitBalle() { /* redéfinition */ super.interagitBalle(); /* appel de la méthode originelle */ System.out.println("Je prends la Balle avec les mains"); } public void avance() { /* redéfinition */ if (getPosition() < 10) System.out.println("Moi gardien, je peux encore prendre la balle avec les mains"); if (getPosition() < 20) super.avance(); /* appel de la méthode originelle sous condition */ } } class Defenseur extends Joueur { public Defenseur(Balle laBalle) { super(laBalle); setPosition(20); } public void avance() { if (getPosition() < 100) super.avance(); } } class Attaquant extends Joueur { public Attaquant (Balle laBalle) { super(laBalle); setPosition(100); } public void avance() { if (getPosition() < 200) { super.avance(); if (getPosition() > 150) System.out.println("moi attaquant je fais attention au hors-jeu") ; } } }

264

L’orienté objet

Tout d’abord, penchons-nous sur le constructeur de Gardien. Celui-ci fait appel, par l’entremise de super(), au constructeur de la superclasse. Rappelez-vous que super pointe vers la superclasse. Nous avons vu ce principe dans le chapitre précédent, chaque classe s’occupe de l’initialisation de ses propres attributs. Comme l’attribut laBalle trouve son origine dans la superclasse Joueur, c’est automatiquement le constructeur de celle-ci (pour autant qu’il s’en occupait déjà dans la superclasse) qui devra à nouveau prendre en charge son initialisation dans la sous-classe Passons maintenant à la redéfinition des deux méthodes. La méthode interagitBalle(), redéfinie seulement chez le gardien, se comporte, dans un premier temps, comme la méthode déclarée initialement chez le Joueur (et c’est de nouveau la raison de l’utilisation du mot-clé super, indispensable afin d’éviter une récursion infinie), mais ensuite rajoute une fonctionnalité qui lui est propre : « prendre la balle avec les mains ». La méthode avance (), quant à elle, ne se produira que dans des limites permises par la fonction et le placement de chacun des joueurs. Ici, l’idée est plutôt de renforcer l’intégrité des sous-classes par rapport à la superclasse, en interdisant à l’attribut posSurLeTerrain de prendre toutes les valeurs possibles. Conditionner dans la rédéfinition de la méthode l’appel à la méthode originale est également une pratique assez courante. Une sous-classe a quelquefois ceci de plus spécifique que ses attributs, au contraire de ce qui se passe pour la superclasse, ne peuvent prendre toutes les valeurs. Cela colle parfaitement à la vision « ensembliste » de l’héritage, puisque seul un sous-ensemble de toutes les valeurs d’attributs possibles sera admis pour les sous-classes.

En C++ class Gardien : public Joueur { public: Gardien(Balle* laBalle):Joueur(laBalle) { /* appel du constructeur de la superclasse*/ setPosition(0); } void interagitBalle() { /* redéfinition */ Gardien::interagitBalle(); /* appel de la méthode originelle */ cout<<"Je prends la Balle avec les mains"<
Redéfinition des méthodes CHAPITRE 12

265

class Attaquant : public Joueur { public: Attaquant (Balle* laBalle) : Joueur(laBalle) { setPosition(100); } void avance() { if (getPosition() < 200) { Joueur::avance(); if (getPosition() > 150) { cout<<"moi attaquant je fais attention au hors-jeu" << endl; } } } };

L’écriture du constructeur contraste assez largement avec la version Java. Le rappel du constructeur de la superclasse, pour les mêmes raisons que celles évoquées pour le code Java, se fait, non plus dans le bloc d’instructions du constructeur, mais, plus directement, à même la déclaration de la signature. Par ailleurs, en C++, super n’existe pas, et pour cause, le multihéritage l’interdit. Qui serait le super parmi tous les candidats possibles ? Comme pour la désambiguïsation parfois nécessaire, suite à un multihéritage malheureux, l’appel aux méthodes des superclasses se fait donc par une évocation explicite des classes dont elles proviennent.

En C# class Gardien : Joueur { public Gardien(Balle laBalle):base(laBalle) { /* appel du constructeur de la superclasse */ positionGet = 0; } public /*override*/ new void interagitBalle() { /* override ou new */ base.interagitBalle(); /* appel de la méthode originelle */ Console.WriteLine("Je prends la balle avec les mains"); } public /*override*/ new void avance() { /* override ou new */ if (positionGet < 10) Console.WriteLine("Moi gardien, je peux encore prendre la balle avec les mains"); if (positionGet < 20) base.avance(); /* appel de la méthode originelle */ } } class Defenseur : Joueur { public Defenseur(Balle laBalle):base(laBalle) { positionGet = 20; } public /*override*/ new void avance() { if (positionGet < 100) base.avance(); } } class Attaquant : Joueur { public Attaquant (Balle laBalle):base(laBalle) { positionGet = 100; }

266

L’orienté objet

public /*override*/ new void avance() { if (positionGet < 200) { base.avance(); if (positionGet > 150) Console.WriteLine("moi attaquant je fais attention au hors-jeu"); } }

En C#, et en ce qui concerne le constructeur, on trouve une pratique hybride de la version Java (avec l’utilisation du mot-clé base en lieu et place de super), et de C++ (avec l’appel du constructeur de la superclasse lors de déclaration de la signature, plutôt que dans le corps de la méthode). On retrouve d’ailleurs ce même motclé base dans le corps des méthodes redéfinies. Vous constaterez également que la signature des méthodes redéfinies inclut le mot-clé new. Nous n’avons pas le choix, là encore, sous le regard coercitif du compilateur. Ne rien mettre, comme en Java, provoquerait cette fois un avertissement de la part du compilateur. Il s’agit donc, ou de déclarer les méthodes comme new (c’est en effet une nouvelle version de la « même » méthode), ou, alternativement, d’opter pour un couplage du mot-clé virtual, lors de la déclaration de la version première de la méthode, avec le mot-clé override, lors de la redéfinition de la méthode. Bien évidemment, l’effet n’est pas le même, comme nous allons le constater très bientôt. En Python class Gardien(Joueur): def __init__(self,laBalle): Joueur.__init__(self,laBalle) self.setPosition(0) def interagitBalle(self): Joueur.interagitBalle(self) print "Je prends la Balle avec les mains" def avance(self): if self.getPosition()<10: print "Moi gardien, je peux encore prendre la balle avec les mains" if self.getPosition()<20: Joueur.avance(self) class Defenseur(Joueur): def __init__(self,laBalle): Joueur.__init__(self,laBalle) self.setPosition(20) def avance(self): if self.getPosition()<100: Joueur.avance(self) class Attaquant(Joueur): def __init__(self,laBalle): Joueur.__init__(self,laBalle) self.setPosition(100) def avance(self): if self.getPosition()<200: Joueur.avance(self) if self.getPosition()>150: print "moi attaquant je fais attention au hors-jeu"

Redéfinition des méthodes CHAPITRE 12

267

En Python, point de base et de super, mais comme en C++, il est obligatoire de préciser de quelle superclasse provient la méthode que nous exploitons à la redéfinition de la méthode de la sous-classe. En PHP 5 class Gardien extends Joueur { public function __construct($laBalle) { parent::__construct($laBalle); // appel du constructeur de la superclasse $this->setPosition(0); } public function interagitBalle() { parent::interagitBalle(); print ("Je prends la Balle avec les mains
\n"); } public function avance() { if ($this->getPosition() < 10) { print ("Moi gardien, je peux encore prendre la balle avec les mains
\n"); } if ($this->getPosition() < 20) { parent::avance(); } } } class Defenseur extends Joueur { public function __construct($laBalle) { parent::__construct($laBalle); $this->setPosition(20); } public function avance() { if ($this->getPosition() < 100) { parent::avance(); } } } class Attaquant extends Joueur { public function __construct($laBalle) { parent::__construct($laBalle); $this->setPosition(100); } public function avance() { if ($this->getPosition() < 200) { parent::avance(); } if ($this->getPosition() > 150) { print ("Moi attaquant je fais attention au hors-jeu
\n"); } } }

268

L’orienté objet

Le code PHP 5 est très proche du Java et C# avec présence du mot-clé parent jouant un rôle équivalent au super de Java et base de C#. Cela a pratiquement épuisé toutes les possibilités sémantiques pour ce mot-clé. Que reste-t-il pour les langages à venir : « tonton » ou « maître » ?

Passons à l’entraîneur … avant qu’une crise cardiaque ne l’éloigne à jamais des terrains de football. Ils n’ont vraiment aucune classe, ces entraîneurs.

En Java class Entraineur{ private Joueur[] lesJoueurs; public Entraineur(Joueur[] lesJoueurs){ this.lesJoueurs = lesJoueurs; } public void panique(){ System.out.println("C'est la panique"); for (int i=0; i
Rien de bien spécial, si ce n’est, aspect capital et clé du polymorphisme, que l’entraîneur est associé à un tableau d’objets typé Joueur, ce qui se traduit par une relation 1 -> 1..n , apparaissant entre la classe Entraineur et la classe Joueur dans le diagramme de classe UML de la figure 12-3.. De son seul point de vue, tous les objets avec lesquels l’entraîneur se doit d’interagir sont issus de la classe Joueur. Il ne voit aucun gardien, attaquant ou défenseur parmi eux. On pourrait rajouter les sous-classes Avant-Centre ou Ailier-Droit qu’il n’en ferait aucun cas. C’est dans sa méthode panique() que l’entraîneur envoie le message désespéré d’avancer à tous les joueurs. L’entraîneur envoie ce message à tous les joueurs de son tableau, sans se préoccuper d’aucune sorte de la nature du joueur qui le recevra. Sa seule certitude (le compilateur le lui a assuré) c’est que tous ses joueurs seront en mesure de pouvoir l’exécuter. L’entraîneur, en plus de friser la crise d’apoplexie, fonctionne de manière totalement polymorphique. Il n’aura pas tout perdu. Lors de l’exécution du code, tous les joueurs qui recevront le message seront de type Attaquant, Gardien ou Defenseur. Il semble donc que les objets du tableau joueur, en fait tous les joueurs sur le terrain, peuvent bénéficier de deux typages, un typage dit statique, celui que seul le compilateur comprend et vérifie (le seul connu de l’entraîneur), et un typage dynamique, qui se révélera seulement à l’exécution.

En C++ class Entraineur { private: Joueur* lesJoueurs[]; public: Entraineur(Joueur* lesJoueurs[]) { for (int i=0; i<3; i++) this->lesJoueurs[i] = lesJoueurs[i]; } void panique() {

Redéfinition des méthodes CHAPITRE 12

269

cout << "C'est la panique" << endl; for (int i=0; i<3; i++) lesJoueurs[i] ->avance(); // le même message à tous les joueurs - attention !!! polymorphisme } };

Rien de très différent par rapport à la version Java, à ceci près que les joueurs apparaissent dans un tableau de pointeurs, ce qui nous oblige à assigner les pointeurs, un à un, à l’aide d’une boucle. Les tableaux en C++ ne sont pas des objets, et il est nécessaire de les manipuler élément par élément.

En C# class Entraineur { private Joueur[] lesJoueurs; public Entraineur(Joueur[] lesJoueurs) { this.lesJoueurs = lesJoueurs; } public void panique() { Console.WriteLine("C'est la panique"); for (int i=0; i
Exactement le même entraîneur qu’en Java. En Python class Entraineur: __lesJoueurs={} def __init__(self,lesJoueurs): self.__lesJoueurs=lesJoueurs def panique(self): print "C'est la panique" i=0 while i
En PHP 5 class Entraineur { private $lesJoueurs; public function __construct($lesJoueurs){ $this->lesJoueurs = $lesJoueurs; } public function panique() { print ("C'est la panique
\n"); for ($i=0; $i<3; $i++) { $this->lesJoueurs[$i]->avance(); } } }

L’orienté objet

270

En Python et en PHP 5, on retrouve le même entraîneur (c’est le sort des bons entraîneurs de devoir se partager à ce point) qu’en Java, en C# et en C++ à quelques détails syntaxiques insignifiants près.

Passons maintenant au bouquet final … c’est-à-dire la méthode principale, afin, qu’en plus des joueurs, les Romains en viennent à s’empoigner.

En Java public class Football { public static void main(String[] args) { Balle uneBalle = new Balle(); // création de la balle Joueur lesJoueurs[] = new Joueur[3]; // création de l'objet tableau lesJoueurs[0] = new Gardien(uneBalle); // création du premier joueur, un gardien lesJoueurs[1] = new Defenseur(uneBalle); // création du deuxième joueur, un défenseur lesJoueurs[2] = new Attaquant(uneBalle); // création du troisième joueur, un attaquant Entraineur unEntraineur = new Entraineur(lesJoueurs); // création de l'entraîneur System.out.println("******* d'abord les joueurs *****"); for (int i=0; i
Dans l’ordre, on crée d’abord l’objet Balle, puis un tableau de 3 joueurs (on se limitera pour des raisons évidentes à 3, mais à 11 ce serait pareil). À ce stade-ci, le tableau des joueurs est typé Joueur. En fait, on construit un tableau de référents, chacun des référents étant typé statiquement comme Joueur. Ensuite, on crée trois joueurs de type différent : un gardien, un défenseur et un attaquant. Comme la figure ciaprès l’illustre, quatre objets sont stockés en mémoire, un pour le tableau de référents et trois pour les joueurs. Au moment de l’exécution, les objets finaux, référés par les trois éléments du tableau, ne sont plus du type de la superclasse Joueur, mais chacun d’une sous-classe différente. Il faut se souvenir que l’opération new ne s’effectue que pendant l’exécution. Il n’est donc pas possible de prévoir, avant l’exécution, au moment de la compilation, de quel type dynamique sera l’objet. Nous pourrions très facilement nous retrouver dans une situation dans laquelle la création de l’objet serait conditionnée par une information à découvrir pendant l’exécution. Cela veut dire, qu’avant l’exécution, on ne peut présager avec certitude de la classe finale dont l’objet sera une instance. La seule garantie que l’on ait est le typage statique de cet objet, qui est forcément une superclasse de la classe finale. Dans de nombreux cas, la classe qui déclare l’objet et la classe qui suit l’opération new sont les mêmes, comme lorsque nous écrivons : O1 unObjetO1 = new O1(). Mais, ici, la donne a changé. Nous nous retrouvons dans une nouvelle situation, assez singulière, où la classe à gauche et à droite de cette instruction peuvent être différentes (mais pas indépendantes). La classe à droite,

Redéfinition des méthodes CHAPITRE 12

271

Figure 12-4

Les 4 objets nécessaires au stockage de l’objet tableau de joueurs et des 3 objets joueurs

c’est-à-dire, la classe finale, révélée au moment de l’exécution, se doit d’être absolument ou la même ou une sous-classe de la classe à gauche, c’est-à-dire la classe fournie par le typage statique. On peut écrire, sans heurter le compilateur : O1 unObjetO1 = new FilsO1(). En substance, le compilateur se satisfait, pour la justesse syntaxique, d’une superclasse, alors que le type final, à l’exécution, pourrait être une sous-classe de celle-ci. Rien de choquant à cela, étant donné le principe de substitution, qui nous dit que, si la superclasse peut le faire, toute sous-classe le fera également sans problème. Mais nous devrons, à partir de maintenant, nous efforcer de différencier le type statique, la classe à gauche, la seule importante pour le compilateur, du type dynamique, la classe à droite, la seule vraiment importante pour l’exécution du programme. Cette différenciation sera capitale pour comprendre le fonctionnement particulier et distinct des trois langages de programmation, C++, C# et Java, pour qui le typage explicite compte vraiment. En l’absence de compilateur, la situation est foncièrement différente pour Python et PHP 5.

Un même ordre mais une exécution différente Dans la suite de la méthode main de Java, on crée un objet entraîneur. Finalement, on envoie le même message interagitBalle() aux trois joueurs et le message panique() à l’entraîneur, en sachant que l’exécution de ce message par l’entraîneur, aura, à son tour, comme effet d’envoyer le message avance() aux trois joueurs. L’entraîneur hurle ce message 6 fois de suite. Lançons la simulation et affichons le résultat :

272

L’orienté objet

Résultat ******************** Résultat : ************************ ******* d'abord les joueurs ***** Je tape la balle avec le pied la balle bouge Je prends la balle avec les mains Je tape la balle avec le pied la balle bouge Je tape la balle avec le pied la balle bouge ******* puis l'entraineur ***** C'est la panique Moi gardien, je peux encore prendre la balle avec les mains la position actuelle du Gardien est 0 la position actuelle du Defenseur est 20 la position actuelle du Attaquant est 100 C'est la panique la position actuelle du Defenseur est 40 la position actuelle du Attaquant est 120 C'est la panique la position actuelle du Defenseur est 60 la position actuelle du Attaquant est 140 moi attaquant je fais attention au hors-jeu C'est la panique la position actuelle du Defenseur est 80 la position actuelle du Attaquant est 160 moi attaquant je fais attention au hors-jeu C'est la panique la position actuelle du Attaquant est 180 moi attaquant je fais attention au hors-jeu C'est la panique *********************************************************************

D’abord, le même message interagitBalle() est lancé aux trois joueurs. On s’aperçoit que ce message est, de fait, exécuté différemment selon le type de joueur. Le défenseur et l’attaquant exécutent celui défini par défaut pour tous les joueurs, alors que le gardien, lui, exécute sa version particulière, celle qu’il a redéfinie. Cela reflète bien le mode de découverte ascendant que Java met en œuvre pour découvrir la méthode à exécuter. Une fois le type de l’objet identifié lors de l’exécution, la méthode à exécuter sera d’abord recherchée dans la zone mémoire allouée à ce type. Si la méthode n’est pas trouvée, on « grimpera », en quête de celle-ci, dans les zones mémoires allouées aux superclasses. On dit de Java qu’il est un langage polymorphique par défaut, c’est-à-dire, qu’à défaut d’autre chose, il donne toujours priorité, dans le choix de la méthode à exécuter, à celle qui correspond au type dynamique. Aussi, si le type dynamique est donc une sous-classe du type statique ; le compilateur aura préalablement vérifié la cohérence et la justesse de la démarche. À l’exécution, le choix de la méthode adéquate se fera sur l’instant, après un processus de recherche, qui, comme souvent en OO, ralentit le processus d’exécution, mais de manière acceptable. Chaque objet possède en fait un attribut supplémentaire mais caché, son type dynamique. Lorsqu’il reçoit un ordre d’exécution de méthode, il « s’introspecte », découvre sa classe définitive et s’assure que la méthode exécutée est bien celle déclarée dans cette classe-là.

Redéfinition des méthodes CHAPITRE 12

273

Ensuite, l’entraîneur envoie le même message avance() aux trois joueurs. Ici, également, le message sera exécuté de trois manières différentes. Chaque joueur, grâce à la présence de la méthode toString(), nous informera sur la nature de sa classe et avancera de 20, si sa méthode le lui permet. On constate qu’au fur et à mesure, de moins en moins de joueurs pourront avancer, car tous arriveront à la limite de leur déplacement. Le rôle de la méthode toString() est de révéler, une fois de plus, le mécanisme polymorphique qui permet la participation des différentes sous-classes, bien que le tableau de joueurs reste typé d’une seule et même superclasse. Passons maintenant au C++, et apprêtons-nous à découvrir un comportement plutôt surprenant.

C++ : un comportement surprenant int main(int argc, char* argv[]){ Balle uneBalle; Joueur* lesJoueurs[3]; lesJoueurs[0] = new Gardien(&uneBalle); lesJoueurs[1] = new Defenseur(&uneBalle); lesJoueurs[2] = new Attaquant(&uneBalle); cout <<"******* d'abord les joueurs *****" << endl; for (int i=0; i<3; i++) lesJoueurs[i]->interagitBalle(); Entraineur unEntraineur(lesJoueurs); cout << "******* puis l'entraineur *****" << endl; for (int j=0; j<6; j++) unEntraineur.panique(); return 0; }

La fonction main() n’a rien de très particulier. Dans un premier temps, nous laisserons le mot-clé virtual, présent dans la déclaration des méthodes interagitBalle() et avance(), en commentaire, c’est-à-dire désactivé. Dans sa syntaxe, la version de code qui en résulte est la plus proche du code Java que nous venons d’exécuter. Pourtant, voici le résultat obtenu : Résultat Résultat de l’exécution du C++ sans déclarer les méthodes comme « virtual » : ******* d'abord les joueurs ***** Je tape la balle avec le pied la balle bouge Je tape la balle avec le pied la balle bouge Je tape la balle avec le pied la balle bouge ******* puis l'entraineur ***** C'est la panique la position actuelle du class Joueur joueur est 0 la position actuelle du class Joueur joueur est 20 la position actuelle du class Joueur joueur est 100 C'est la panique la position actuelle du class Joueur joueur est 20 la position actuelle du class Joueur joueur est 40 la position actuelle du class Joueur joueur est 120 C'est la panique

274

L’orienté objet

la position actuelle du class Joueur joueur est 40 la position actuelle du class Joueur joueur est 60 la position actuelle du class Joueur joueur est 140 C'est la panique la position actuelle du class Joueur joueur est 60 la position actuelle du class Joueur joueur est 80 la position actuelle du class Joueur joueur est 160 C'est la panique la position actuelle du class Joueur joueur est 80 la position actuelle du class Joueur joueur est 100 la position actuelle du class Joueur joueur est 180 C'est la panique la position actuelle du class Joueur joueur est 100 la position actuelle du class Joueur joueur est 120 la position actuelle du class Joueur joueur est 200 ******************************************************************

Que ce soit lors de l’exécution du message interagitBalle() sur les trois joueurs ou du message avance(), nous constatons qu’au contraire de Java, en C++, le type statique prime sur le type dynamique. Par exemple, la méthode avance() s’exécutera sans limitation, c’est-à-dire dans sa version par défaut, octroyant le droit au gardien de faire un pas de deux dans le rectangle adverse, et aux attaquants de se noyer dans la foule. On peut lire dans le résultat que tous les joueurs se comportent, en effet, comme des Joueurs, et non dans leur version plus spécifique. Indépendamment du type dynamique, c’est-à-dire de la sous-classe, celle qui caractérise vraiment les joueurs en définitive, le compilateur a le dernier mot et force le type statique au détriment du type dynamique, y compris lors de l’exécution. C’est plutôt déconcertant, car cela ne correspond pas du tout au comportement naturel que l’on serait en droit d’attendre. À quoi bon redéfinir des méthodes dans les sous-classes, si celles-ci n’ont pas la primeur lors du déclenchement du message qui les concerne ? En fait, encore une fois, C++ favorise l’optimisation sur la cohérence sémantique. La raison est à rechercher, en partie, toujours dans ce lourd tribut payé au C, langage procédural par excellence. Il est plus optimal de faire le lien entre la méthode et l’objet lors de l’étape de compilation que lors de l’étape d’exécution. Aucune recherche de méthode, ralentissant l’exécution du programme, ne sera plus nécessaire pendant l’exécution. On dit de C++ qu’il n’est pas un langage polymorphique par défaut, comme l’était historiquement Smalltalk, et comme devrait l’être, là encore selon la charte de l’OO, tous les langages OO dignes de cette étiquette. Encore une fois, également, C++ décide que vous êtes adultes et vaccinés, et que c’est à vous de faire le choix entre l’optimisation ou la cohérence sémantique. Vous voulez du polymorphisme, les performances en temps calcul risquent d’en prendre un coup, mais, qu’à cela ne tienne, il suffit de retirer les commentaires qui entourent le mot-clé virtual dans la déclaration des deux méthodes redéfinies. En rendant les deux méthodes « virtuelles », voilà le nouveau résultat obtenu par le code C++ parfaitement en phase avec le résultat obtenu précédemment par Java. Nouveau résultat C++ en déclarant les deux méthodes virtuelles : ******* d'abord les joueurs ***** Je tape la balle avec le pied la balle bouge Je prends la balle avec les mains Je tape la balle avec le pied la balle bouge

Redéfinition des méthodes CHAPITRE 12

275

Je tape la balle avec le pied la balle bouge ******* puis l'entraineur ***** C'est la panique Moi gardien, je peux encore prendre la balle avec les mains la position actuelle du class Gardien joueur est 0 la position actuelle du class Defenseur joueur est 20 la position actuelle du class Attaquant joueur est 100 C'est la panique la position actuelle du class Defenseur joueur est 40 la position actuelle du class Attaquant joueur est 120 C'est la panique la position actuelle du class Defenseur joueur est 60 la position actuelle du class Attaquant joueur est 140 moi attaquant je fais attention au hors-jeu C'est la panique la position actuelle du class Defenseur joueur est 80 la position actuelle du class Attaquant joueur est 160 moi attaquant je fais attention au hors-jeu C'est la panique la position actuelle du class Attaquant joueur est 180 moi attaquant je fais attention au hors-jeu C'est la panique

Moyennant la présence du mot-clé virtual dans la déclaration des méthodes, C++ se comporte, d’un point de vue polymorphique, comme Java. Pour rendre le polymorphisme possible, il faut que la liaison entre l’objet et la méthode qui s’exécutera sur lui soit établie pendant l’exécution, le compilateur s’étant simplement assuré de la possibilité de la chose. Pour que cette liaison s’effectue, il faut, comme indiqué dans la figure ci-après, que l’objet, parmi ses attributs, en possède un supplémentaire, caché au programmeur, qui contienne l’information sur la zone mémoire où se situe la méthode. En Java, c’est d’office le cas. En C++, ce sera le cas dès qu’une méthode de la classe est déclarée comme virtuelle. Un pointeur additionnel sera nécessaire par objet, pointant vers une table additionnelle par classe, indiquant pour chaque méthode virtuelle où se trouve la bonne implémentation à exécuter. En C++, la simple déclaration d’une méthode virtuelle provoque de ce fait un accroissement de mémoire, alloué pour les objets et pour la table, et un ralentissement résultant de la découverte de la méthode appropriée à l’aide des pointeurs présents dans la table. C’est pour cela que C++ donne la possibilité, au détriment de la simplicité et du comportement intuitif qui en résulte, de contourner le polymorphisme. Il favorise le temps calcul au détriment d’une certaine logique comportementale.

Polymorphisme : uniquement possible dans la mémoire tas Le polymorphisme, à la base, permet qu’un même objet soit typé statiquement et dynamiquement de manière différente. Si cela est parfaitement possible avec les objets stockés dynamiquement dans la mémoire tas, cela est beaucoup plus délicat avec les objets stockés statiquement dans la mémoire pile. En C++, la simple instruction O1 o1 crée l’objet o1. Pour modifier l’affectation dynamiquement, il faudrait dans le cours du programme pouvoir écrire : o1 = filsO1, avec l’objet filsO1 instance de FilsO1 sous-classe de O1. Avec le principe de substitution, l’écriture est possible, mais le résultat est partiellement satisfaisant, car il faut que la zone mémoire initialement prévue pour recevoir o1 puisse maintenant contenir filsO1.

276

L’orienté objet

Figure 12-5

La mise en mémoire de la pratique du polymorphisme.

Quand on sait qu’il est fréquent que les objets des sous-classes soient plus volumineux que les objets de la superclasse, on ne s’étonnera pas que cette affectation d’un objet d’une sous-classe à la place de celui d’une superclasse ait un prix : la perte des attributs propres à la sous-classe et surtout la perte du pointeur vers les méthodes virtuelles. Aucun typage dynamique n’est, de ce fait, possible pour des objets stockés sur la pile (cela revient toujours à une forme de typage statique, prédéterminé par le compilateur), et ils ne peuvent en aucun cas bénéficier du polymorphisme qui exige la manipulation de référents. En présence des référents, l’instruction o1 = filsO1 n’a comme seul effet que de faire pointer le référent o1 vers l’objet précédemment référé par filsO1. En C# public class Football{ public static void Main(){ Balle uneBalle = Joueur[] lesJoueurs = lesJoueurs[0] = lesJoueurs[1] = lesJoueurs[2] =

new new new new new

Balle(); Joueur[3]; Gardien(uneBalle); Defenseur(uneBalle); Attaquant(uneBalle);

Redéfinition des méthodes CHAPITRE 12

277

Entraineur unEntraineur = new Entraineur(lesJoueurs); Console.WriteLine("******* d'abord les joueurs *****"); for (int i = 0; i
On ne note rien de particulier dans l’écriture de la classe principale, qui ressemble à s’y méprendre à du Java. Cependant, du point de vue polymorphique, C# se situe entre les deux langages précédents. C’est l’avantage d’être le troisième, et c’est pour cela que, précisément, nous le traitons en troisième lieu. Si on laisse le programme tourner comme montré dans le code jusqu’à présent, c’est-à-dire, sans déclarer les méthodes à redéfinir virtual, il est alors obligatoire de rajouter le mot-clé new lors de la redéfinition des méthodes. En présence de ce mot-clé, le résultat est non polymorphique comme vous le constatez : Résultat Résultat du C# sans virtual/override mais en présence de new : ******* d'abord les joueurs ***** Je tape la balle avec le pied la balle bouge Je tape la balle avec le pied la balle bouge Je tape la balle avec le pied la balle bouge ******* puis l'entraineur ***** C'est la panique la position actuelle du Gardien est 0 la position actuelle du Defenseur est 20 la position actuelle du Attaquant est 100 C'est la panique la position actuelle du Gardien est 10 la position actuelle du Defenseur est 30 la position actuelle du Attaquant est 110 C'est la panique la position actuelle du Gardien est 20 la position actuelle du Defenseur est 40 la position actuelle du Attaquant est 120 C'est la panique la position actuelle du Gardien est 30 la position actuelle du Defenseur est 50 la position actuelle du Attaquant est 130 C'est la panique la position actuelle du Gardien est 40 la position actuelle du Defenseur est 60 la position actuelle du Attaquant est 140 C'est la panique la position actuelle du Gardien est 50 la position actuelle du Defenseur est 70 la position actuelle du Attaquant est 150

278

L’orienté objet

Résultat en déclarant les méthodes à re-définir virtual et les méthodes re-définies override : ******* d'abord les joueurs ***** Je tape la balle avec le pied la balle bouge Je prends la balle avec les mains Je tape la balle avec le pied la balle bouge Je tape la balle avec le pied la balle bouge ******* puis l'entraineur ***** C'est la panique Moi gardien, je peux encore prendre la balle avec les mains la position actuelle du Gardien est 0 la position actuelle du Defenseur est 20 la position actuelle du Attaquant est 100 C'est la panique la position actuelle du Defenseur est 40 la position actuelle du Attaquant est 120 C'est la panique la position actuelle du Defenseur est 60 la position actuelle du Attaquant est 140 moi attaquant je fais attention au hors-jeu C'est la panique la position actuelle du Defenseur est 80 la position actuelle du Attaquant est 160 moi attaquant je fais attention au hors-jeu C'est la panique la position actuelle du Attaquant est 180 moi attaquant je fais attention au hors-jeu C'est la panique

En revanche, en présence du couple virtual/override, on obtient bien le résultat polymorphique attendu. En fait, C# coupe vraiment la poire en deux, ou opte pour un jugement de Salomon. Il considère, d’abord, qu’il n’y a plus de comportement par défaut mais, à la place, qu’il y a deux comportements possibles. Les deux sont tout aussi adoptables, l’un met l’accent sur les performances l’autre sur une certaine logique comportementale, en optant pour une déclaration particulière des méthodes concernées. L’absence de comportement obtenu « gratuitement » ou par défaut contraint à maîtriser parfaitement ce que vous faites et les choix possibles. Enfin, tout cela ne concerne que les classes en C# et nullement les structures (comme les objets présents sur la pile dans le cas du C++), puisque celles-ci ne peuvent hériter entre elles. L’addition du new force la main, marque le coup, et indique explicitement que la redéfinition de cette méthode dans la sous-classe, ne se verra utilisée qu’en présence d’un objet typé statiquement par cette sous-classe. En Python uneBalle=Balle() lesJoueurs={} lesJoueurs[0]=Gardien(uneBalle) lesJoueurs[1]=Defenseur(uneBalle) lesJoueurs[2]=Attaquant(uneBalle) unEntraineur=Entraineur(lesJoueurs)

Redéfinition des méthodes CHAPITRE 12

print "****** d'abord les joueurs ******" i=0 while i
Résultats ****** d'abord les joueurs ****** Je tape la balle avec le pied la balle bouge Je prends la Balle avec les mains Je tape la balle avec le pied la balle bouge Je tape la balle avec le pied la balle bouge ****** puis l'entraineur ****** C'est la panique Moi gardien, je peux encore prendre la balle avec les mains la position actuelle du Gardien est 0 la position actuelle du Defenseur est 20 la position actuelle du Attaquant est 100 C'est la panique la position actuelle du Defenseur est 40 la position actuelle du Attaquant est 120 C'est la panique la position actuelle du Defenseur est 60 la position actuelle du Attaquant est 140 moi attaquant je fais attention au hors-jeu C'est la panique la position actuelle du Defenseur est 80 la position actuelle du Attaquant est 160 moi attaquant je fais attention au hors-jeu C'est la panique la position actuelle du Attaquant est 180 moi attaquant je fais attention au hors-jeu C'est la panique moi attaquant je fais attention au hors-jeu

En PHP 5 $uneBalle = new Balle(); $lesJoueurs[0] = new Gardien($uneBalle); $lesJoueurs[1] = new Defenseur($uneBalle); $lesJoueurs[2] = new Attaquant($uneBalle);

279

280

L’orienté objet

$unEntraineur = new Entraineur($lesJoueurs); print ("********* d'abord les joueurs *******
\n"); for ($i = 0; $i<3; $i++) { $lesJoueurs[$i]->interagitBalle(); } print ("******** puis l'entraineur ********
\n"); for ($i = 0; $i<6; $i++) { $unEntraineur->panique(); } i+=1

Agréable surprise enfin pour Python et PHP 5. Ils se conforment bien tous deux à la charte du bon langage OO car, à l’instar de Java, il sont polymorphiques par défaut. Sans rien ajouter pour ce faire, la classe dynamique prime sur la classe statique lors de l’exécution du code. Remarquez toutefois que cela ne leur pose pas trop de problème, puisque ces langages ont purement et simplement supprimé le typage statique, qui est le seul vérifié par le compilateur. C’est bien lors de la réception du message que l’objet vérifiera quelle version de celui-ci il doit exécuter, mais cela ne pose aucun problème, vu que nul compilateur ne les aura préalablement aiguillé sur une mauvaise piste. Polymorphisme possible mais différent dans les quatre langages La mise en place du polymorphisme différencie les cinq langages de programmation de manière sensible. C++ se comporte, par défaut, de manière non polymorphique, Java, Python et PHP 5 font le contraire, et C# considère qu’il n’y a plus lieu de laisser une version par défaut mais de préciser ce que vous cherchez à faire.

Quand la sous-classe doit se démarquer pour marquer Rajoutons dans notre simulation Java du match de football, et dans la sous-classe Attaquant, la méthode suivante : marqueUnBut(), comme indiqué ci-après : class Attaquant extends Joueur { public Attaquant (Balle laBalle) { super(laBalle); setPosition(100); } public void avance() { if (getPosition() < 200) { super.avance(); if (getPosition() > 150) System.out.println("moi attaquant je fais attention au hors-jeu"); } } public void marqueUnBut() { System.out.println("youpiiiii..... j'ai marqué... !!"); } }

On se place dans le cas extrême, où seuls les attaquants sont autorisés à marquer. Il serait somme toute assez naturel de les en autoriser. Or, le compilateur, et ce en dépit des cris désespérés de l’entraîneur, fait une totale obstruction. Si dans la méthode main, vous écrivez : lesJoueurs[2].marqueLeBut() ;

Redéfinition des méthodes CHAPITRE 12

281

Bien que tout leur permette de le faire, car ils sont bien attaquants et peuvent marquer des buts, cette instruction générera une erreur de compilation. C’est normal, puisque le rôle premier du compilateur est de vérifier que tout envoi de message est conforme au typage statique de l’objet. Nous avons bien dit au typage statique et non au typage dynamique, puisque ce type est supposé non connu au moment de la compilation. Vous pourriez vous étonner de l’étroitesse de vue du compilateur. Dans l’instruction : lesJoueurs[2] = new Attaquant(uneBalle);

ce même compilateur pourrait se rendre compte que le type final de l’objet, le type à l’exécution, le seul qui compte in fine, est Attaquant et, donc, que lesJoueurs[2] peuvent, de fait, marquer un but. Dans un cas semblable, vous avez tout à fait raison, il le pourrait. Mais considérons maintenant un cas plus général, correspondant au petit code suivant : int a; Joueur unJoueur; readConsole( a ); /* on imagine une instruction qui permet de donner au * clavier la valeur de a alors que le code s'exécute, * et qui existe dans tous les langages de programmation */ if (a > 1) unJoueur = new Gardien() ; else unJoueur = new Attaquant() ; unJoueur.marqueUnBut() ;

Ici, vous admettrez que, si le compilateur se basait sur le type dynamique, il serait bien en peine de savoir si la réception du message par le joueur est possible ou pas. Et c’est bien pour cela que le compilateur, féroce, mais néanmoins prudent, ne se base, pour sa vérification de la conformité des envois de message, que sur le typage statique. Les attaquants participent à un « casting » D’où un problème basique, comment détourner l’attention du compilateur ? Comment lui faire comprendre que, bien que leMarquageDeBut ne soit pas vrai de tous les joueurs, nous savons, nous, programmeurs compétents, que le joueur[2] est bien un attaquant et qu’il peut se le permettre. La solution est de recourir au « casting », traduit de différentes manières en français : « transtypage », « coercion » (on en passe et des meilleures), et qui consiste à forcer la main au compilateur de la manière suivante : ((Attaquant)lesJoueurs[2]).marqueUnBut()

Cela revient à dire ceci. On sait qu’il n’est pas prévu que tous les joueurs marquent des buts, mais on sait également quelque chose que toi, compilateur, tu ne peux pas savoir (car cette information sera obtenue seulement pendant l’exécution) : le deuxième joueur du tableau est bien un attaquant, et, en tant que tel, il peut marquer un but. Le compilateur acceptera un casting d’une classe dans une de ses sous-classes, mais dans aucune autre. Il est évident qu’il n’y aura jamais lieu d’opérer ce casting dans le sens contraire. Le principe de substitution nous permet toujours de faire passer une sous-classe pour sa superclasse (on parle alors de « casting implicite »). Cela, c’est complètement admis et parfaitement normal. Ce qui ne l’est plus, c’est de faire passer la superclasse pour sa sous-classe. Car, en effet, rien ne nous incite à penser que cela puisse fonctionner (rapellez-vous dans le chapitre précédent de la Mazda que l’on traiterait comme une Ferrari…

282

L’orienté objet

Et, de fait, cela pourrait ne pas marcher. Comme à chaque fois que vous désactivez un système de protection, cela peut se retourner contre vous. Supposons qu’alors que notre programme compile merveilleusement, un gardien plutôt qu’un attaquant soit installé à la place du joueur[2], comme indiqué ci-après : lesJoueurs[2] = new Gardien(); /* nous avons maintenant un gardien en place d’un attaquant.*/

Le compilateur ne tiquera pas quand il lira l’instruction : ((Attaquant)lesJoueurs[2]).marqueUnBut() puisqu’il ne connaît pas le type final du joueur[2]. Mais, lors de l’exécution, une erreur surviendra, de type « mauvais casting », comme montré ci-après, lorsque le code Java s’exécute : C'est la panique la position actuelle du Defenseur est 80 C'est la panique C'est la panique java.lang.ClassCastException: Gardien at Football.main(Football.java:162) Exception in thread "main"

Éviter les « mauvais castings » Une erreur de type « mauvais casting » apparaît. Le programme s’attendait à recevoir un gardien pendant l’exécution, et vous lui passez un attaquant à la place. Comme cette erreur se produit pendant l’exécution, et qu’il vaut mieux tenter de prévenir toute forme d’erreur dès l’écriture du code, il y a deux manières de procéder. Vous pourriez accepter l’erreur si elle survient, et recourir alors à une gestion d’exception que Java encourage toujours dans l’écriture du code. (il vous faudrait alors faire une gestion d’exception ClassCastException pour tout casting). Mais il y a mieux à faire : empêcher une telle erreur de se produire. Vous pouvez, à l’aide de l’opérateur instanceof qui, en Java (il existe le même en PHP 5), renvoie le type dynamique de l’objet, vérifier que vous opérez bien un « casting » possible. L’écriture devient alors : if (lesJoueurs[2] instanceof Attaquant) ((Attaquant)lesJoueurs[2]).marqueUnBut();

En fait, il vous revient de forcer pendant l’exécution la vérification du type, avant d’opérer le casting. Cela ne change évidemment rien à la compilation, mais cela permet de n’envoyer le message à l’objet que si celui-ci est apte à le recevoir et ainsi d’éviter que l’exécution ne se « plante ». Bien que les précautions à prendre soient les mêmes, et dans le même esprit, la manière de procéder se transforme légèrement en C++ et en C#. Ils autorisent également le casting, mais encourageraient et feraient la vérification de type plutôt de la manière suivante : En C++ class Attaquant : public Joueur { public: Attaquant (Balle* laBalle) : Joueur(laBalle) { setPosition(100); }

Redéfinition des méthodes CHAPITRE 12

283

void avance() { if (getPosition() < 200) { Joueur::avance(); if (getPosition() > 150) { cout<<"moi attaquant je fais attention au hors-jeu"<interagitBalle(); Entraineur unEntraineur(lesJoueurs); cout << "******* puis l'entraineur *****" << endl; for (int j=0; j<6; j++) unEntraineur.panique(); Attaquant *unAttaquant = dynamic_cast(lesJoueurs[2]) ; /* afin de vérifier le type ➥dynamique de l'objet */ if (unAttaquant != 0) unAttaquant->marqueUnBut(); return 0; }

En C# Attaquant unAttaquant = lesJoueurs[2] as Attaquant; if (unAttaquant!= null) unAttaquant.marqueUnBut();

En C# comme en C++, on force le casting. Ça passe ou ça « classe ». Si cela marche, c’est-à-dire si, à l’exécution, l’objet est bien du type dynamique de la classe dans laquelle on désire le « caster », le nouveau référent recevra la bonne adresse, sinon il recevra 0 ou null, mais l’envoi de message ne s’effectuera pas, bien heureusement. Les problèmes de casting de ce type ne concernent pas Python et PHP 5, étant donné leur absence de typage des attributs ou des référents. De fait, dans ces langages, aucun compilateur ne vérifie préalablement la syntaxe et les types statiques. C’est seulement au moment de l’exécution que l’on découvrira tout ce qu’il y a à découvrir sur le type des objets auxquels sont destinés les messages. Rien de préalable n’entravera le cours d’exécution des messages. Si vous observez les codes Python et PHP 5, jamais le vecteur des joueurs n’a dû être typé, comme pour les trois langages précédents, par la classe Joueur, et ce parce que la liste ou la collection peuvent exister sans typage statique.

284

L’orienté objet

Python et PHP 5 : tout se passe à l’exécution Python et PHP 5 s’interprétant, c’est-à-dire exécutant les instructions du code, au fur et à mesure de leur rencontre, la traduction en langage exécutable s’effectue « en ligne ». Il est suivi directement de l’exécution, laissant donc à cette phase d’exécution le soin de découvrir des erreurs qui, dans d’autres langages OO, seraient découvertes lors de la compilation. Tous les « viols » et les incohérences de typage, par exemple, l’envoi d’un message à un objet qui n’est pas destiné par sa classe à recevoir ce message, se découviront donc lors de l’exécution. Quand on sait le sang d’encre que se font C++, Java et C# pour prévenir ce type d’erreur par un typage fort et l’engagement à l’entrée du code d’un « videur-compilateur », employé à faire respecter à la lettre ce typage, on peut s’interroger sur cette option prise par ces deux langages. Leur défense s’appuie sur la simplicité et la rapidité de mise en œuvre pour aller directement à l’essentiel et ne se préoccuper que des fonctionalités premières du code. On peut donc les voir plus comme des langages de prototypage, susceptibles de céder la place, lors d’une phase plus « industrielle », à des langage plus contraignants, plus « safe » et plus rapide (surtout Python, PHP 5 restant un langage de prédilection pour les Maîtres du Web).

Le « casting » a mauvaise presse Avouons-le tout de go, l’opération de casting a, en général, mauvaise réputation en programmation. D’ailleurs Stroustrup, inventeur du C++, regrette son omniprésence dans la programmation en Java ou C#. Cette manière de détourner l’attention du compilateur pour faire passer quelque chose pour ce que ce n’est pas a été largement fauteur de troubles dans des langages comme C et C++. En effet, l’utilisation malveillante ou simplement distraite du compilateur peut entraîner des effets plutôt brutaux et inélégants. Il a justement comme rôle de faire une vérification de la bonne utilisation des types, pourquoi délibérément lui fausser compagnie ? Dans l’exemple décrit ci-dessus, il aurait été très facile d’éviter le casting en, comme le petit diagramme de classe l’illustre ci-dessous, rajoutant explicitement un référent de type Attaquant auprès de l’entraîneur, de manière à ce que ce dernier n’envoie qu’à ce seul Attaquant les seuls messages qui le concernent. Figure 12-6

Diagramme de classe alternatif qui évitera à l’entraîneur de recourir au « casting » pour demander à son attaquant de marquer un but.

Cette solution est clairement celle du puriste, qui tente d’éviter par la compilation et le typage fort toute mauvaise surprise lors de l’exécution du code. Nous pensons néanmoins que, dans ce cas précis, et vu l’omniprésence de cette opération de « casting » en Java ou C#, la critique n’a plus exactement la même portée. D’abord, ce casting n’est toujours autorisé que dans certaines limites : une classe dans sa sous-classe, et rien d’autre. Ensuite, il survient souvent comme le juste pendant du polymorphisme. Or si le polymorphisme est, lui, très encouragé, il est difficile de protester contre une situation qui lui est souvent conséquente. Enfin, si son évitement prête à plus de contorsions étranges de la part du programmeur que sa simple acceptation, mais maîtrisée, il n’y a plus lieu d’hésiter.

Redéfinition des méthodes CHAPITRE 12

285

Par exemple, sans recourir au référent Attaquant additionnel mentionné précédemment, une manière alternative de l’éviter serait, dans le cas du football, de déclarer la méthode marqueUnBut() chez tous les joueurs, mais de la vider de son contenu d’instructions, tant chez le gardien que le défenseur. Absurde non ? Faire comme si le gardien et le défenseur pouvaient marquer des buts alors qu’ils ne peuvent pas… L’unique consigne reste donc que le compilateur a toujours raison, mais que vous pouvez le forcer, de temps à autre, à relâcher son attention dans une partie du programme. Partie de programme que vous devez, en contrepartie, vous forcer de réaliser en redoublant d’attention, vu les possibles erreurs indésirables pendant l’exécution auxquelles vous exposez cette partie. Polymorphisme et casting Une conséquence du polymorphisme est le recours au « casting » qui vise à récupérer des fonctionnalités propres à l’une ou l’autre sous-classe lors de l’exécution d’un programme. Sa pratique est parfois délicate et demande une attention soutenue, car elle peut mener à des erreurs pendant la phase d’exécution. Java et C#, par exemple, par l’introduction de la généricité dans leurs dernières versions, tentent de diminuer ce recours. L’absence de typage statique dans Python et PHP 5 est bien évidemment une manière de contourner cette problématique, bien que cette absence ne les mette pas non plus à l’abri d’avatars ne survenant malheureusement qu’à l’exécution, lorsqu’on s’y attend le moins.

Redéfinition et encapsulation Que se passe-t-il si, comme dans le petit code Java ci-dessous, la méthode que nous cherchons à redéfinir est déclarée private dans la superclasse. class O1 { public void jeTravaillePourO1() { jeSuisPriveDansO1(); } private /*protected*/ void jeSuisPriveDansO1() { System.out.println("je suis O1"); } } public class FilsO1 extends O1 { public void jeSuisPriveDansO1() { System.out.println("je suis le Fils d'O1"); } public static void main(String[] args) { O1 o1 = new FilsO1(); o1.jeTravaillePourO1(); } }

Comme vous pouvez le voir à l’exécution, selon que vous déclariez la méthode jeSuisPriveDansO1 de la superclasse private ou protected, c’est la version de la superclasse ou de la sous-classe qui s’exécutera. Or, si l’on s’en tient à la découverte des méthodes fonctionnant de manière ascendante en Java, ce devrait toujours être la version redéfinie qui s’exécute (donc celle de la sous-classe). Cependant, les langages OO considèrent à juste titre qu’une méthode déclarée comme private dans la superclasse, n’étant nullement accessible par la sous-classe (ce qui n’est pas le cas d’une méthode protected), ne peut se prêter à une quelconque redéfinition.

286

L’orienté objet

En fait, c’est comme si la méthode de la sous-classe était renommée implicitement par Java, afin d’éviter toute confusion possible. Cela n’est plus la même, ç’en est une autre ! C# vous évite ce genre de problème et de confusion en forçant les deux méthodes à posséder le même niveau d’accessibilité. De plus, vous ne pourrez jamais redéfinir une méthode déclarée private en C# (un bon point pour ce langage qui évite là une source patente de confusion).

Figure 12-7

Illustration de la ligue simulation de la Robocup.

Redéfinition de méthodes et multihéritage Si deux classes redéfinissent toutes deux une méthode d’abord définie dans une superclasse qu’elles se partagent (et que toutes deux la redéfinissent en faisant appel à la méthode d’origine) et qu’à leur tour ces deux classes sont héritées par une seule classe (le type de situation problématique d’héritage en losange évoquée dans le chapitre précédent) et que cette dernière décide de redéfinir toujours la même méthode (en faisant également appel aux deux méthodes d’en haut) se pose, à nouveau, le problème de la présence une fois ou deux fois de la méthode de la superclasse du haut (le même problème que nous avons vu dans le chapitre précédent mais avec les méthodes et non plus les attributs de la classe au sommet). Tant Python que C++ offrent des solutions optionnelles pour faciliter le choix de l’implémentation.

Redéfinition des méthodes CHAPITRE 12

287

Polymorphisme contre case-switch On dit souvent que toute programmation qui exige qu’un objet, à la réception d’un message, teste sa nature intime par l’intermédiaire d’un case-switch ou d’une succession de if-then afin de savoir quelle methode exécuter, est une partie de code idéale pour réaliser un « polymorphisme ». Lorsque nous donnons cours de programmation OO, nous ne testons pas, au préalable les prérequis de chacun de nos élèves, de façon à adopter le cours pour chaucun. De même, chacun d’entre eux, à la réception du message que nous leur délivrons, ne s’interroge pas sur ses capacités propres afin de savoir comment digérer la matière. Une programmation polymorphique vous permet en effet d’éviter cette succesion de tests. Chaque sous-classe d’étudiant (n’y voyez rien de péjoratif) recevra ce cours à la manière de sa sous-classe. Un programme conçu de telle sorte évitera évidemment les réécritures qu’une série de tests ou un case-switch entraînerait suite au rajout d’une sous-classe.

La Robocup : Simulation League Cette petite incursion logicielle du côté du football nous donne envie de vous parler de la Robocup. Cet événement annuel met en compétition des équipes de football constituées soit de robots, soit de joueurs programmés. Il existe plusieurs ligues : trois robotiques, selon la nature et la taille des robots ; cette année (2002), une quatrième devrait voir s’affronter les premiers robots humanoïdes, et une ligue de simulation. L’un des auteurs de cet ouvrage envoie ses étudiants y participer depuis trois années de suite. On peut dire qu’il les envoie au casse-pipe, car l’équipe se retrouve éliminée chaque année au premier tour, et ce par des scores défiant toute concurrence. Néanmoins, Coubertin faisant foi, cette expérience nous incite à encourager plus de monde encore à y participer, et surtout des étudiants, car il s’agit d’un excellent véhicule didactique de la programmation orientée objet. En effet, vous aurez constaté que le football se prête merveilleusement à un développement de type OO, alors pourquoi ne pas y aller franco, et soumettre également une équipe. Nos étudiants passeront peut-être un tour l’année prochaine. L’idée d’un match de football entre robots germa dans la tête de chercheurs japonais (on aurait été étonné du contraire) dès 1993, mais il fallut attendre 1997 pour que la première compétition eut lieu à Nagoya au Japon. Depuis, cet événement se reproduit annuellement et réunit des milliers de participants. De nos jours, on peut considérer que 3000 chercheurs sont concernés de près ou de loin par la Robocup. Il est aujourd’hui, aussi paradoxal que celui puisse sembler au premier abord, beaucoup plus facile de concevoir, en informatique, un bon joueur d’échecs qu’un bon joueur de foot. La difficulté à reproduire nos facultés sensori-motrices y est pour beaucoup, bien évidemment, et pose d’extraordinaires défis pour les chercheurs en intelligence artificielle. Le constat majeur est qu’une intelligence désincarnée et découplée du monde environnant est bien plus à notre portée qu’une intelligence effective, utile et opérationnelle, à même ce monde. Le monde en simulation est autrement plus gérable que le monde réel, pour un être lui-même simulé. Par les nouveaux défis qu’elle pose : perception visuelle, motricité, temps réel, intelligence collective…, la Robocup est un irremplaçable moyen de promotion pour la recherche en robotique et en intelligence artificielle. Le grand avantage de la ligue de simulation sur celle des ligues robotiques est qu’un tournevis n’est plus nécessaire, mais plutôt une bonne connaissance de la programmation OO, un bon sens commun, quelques manuels d’IA et ne pas souffrir du manque de sommeil et de régime pizza marguerita. Si l’objectif ultime est d’atteindre pour la moitié du XXIe siècle une équipe de robots humanoïdes capables de rivaliser avec des joueurs humains, la Robocup nous apparaît, pour notre part, comme un excellent exercice de programmation OO, en synergie avec les apports de l’intelligence artificielle. Dans la version simulation, onze joueurs logiciels (le plus souvent programmés en C++) forment une équipe. Chacun de ces joueurs est un client du serveur, dont le programme décide de ce qu’il doit faire, et envoie le résultat de sa décision au serveur : accélérer, communiquer, frapper dans la balle, tourner la tête, tourner son corps. Chaque joueur possède une énergie décrémentée en fonction des actions entreprises, mais est capable également de doucement se régénérer. La puissance de ces actions dépend de cette énergie. Lorsque le serveur reçoit ces commandes, et afin de reproduire l’imprédictibilité inhérente au monde réel, il ajoute un bruit aléatoire aux mouvements des joueurs et de la balle, ainsi qu’aux décisions prises par les joueurs. La connexion client-serveur est de type UDP/IP. Les règles de la FIFA sont respectées autant que faire se peut : temps réglementaire (5 min par mi-temps), sortie de balle, hors-jeu, etc. Chaque joueur reçoit de la part du serveur des informations visuelles sur sa position (en fonction des objets qui l’entourent), l’imprécision de cette information s’accroissant avec la distance. Il perçoit également des communications provenant d’autres joueurs à sa portée, et des informations sur son état interne. Bon match !

288

L’orienté objet

Exercices Exercice 12.1 Réalisez le diagramme de classe UML, ainsi qu’une ébauche de code, dans un quelconque des trois langages de programmation, des classes suivantes. Une agence bancaire contient des comptes en banque de deux sortes : livret d’épargne et compte courant. Ce qui les différencie, c’est que, dans le premier compte, un retrait quelconque ne peut jamais rendre le solde négatif et que, dans le second, le retrait ne peut amener le solde en dessous de – 1000 euros. Une autre différence tient à la manière de calculer l’intérêt. Dans le premier cas, c’est la manière par défaut qui consiste à calculer l’intérêt multiplié par 2 % qui prévaut, dans le second, c’est la manière par défaut qui consiste à prendre la racine carrée. Utilisez l’appel des méthodes de la superclasse.

Exercice 12.2 Le « boot » par défaut d’un ordinateur consiste à charger le système d’exploitation présent sur le disque dur dans la mémoire RAM. On considérera deux catégories d’ordinateur, une première qui, avant de « booter », demande un mot de passe, et une autre qui, avant de « booter », demande quel système d’exploitation on désire lancer. Esquissez le code des trois classes correspondantes.

Exercice 12.3 Tentez de prédire ce que le code Java suivant fera apparaître à l’écran. Réalisez le diagramme de classe UML correspondant. class Electeur { private int age; private String adresse; protected Candidat[] lesCandidats; public Electeur(Candidat[] lesCandidats) { this.lesCandidats = lesCandidats; } public void jeVote() {} } class ElecteurIdiot extends Electeur { private int QI; public ElecteurIdiot(Candidat[] lesCandidats, int QI) { super(lesCandidats); this.QI = QI; } public void jeVote() { for (int i=0; i QI) || (lesCandidats[i].compareSlogan("vive la France, la semaine des 5 heures"))) { lesCandidats[i].accroitVoix(); break; } } } }

Redéfinition des méthodes CHAPITRE 12

class ElecteurIndecis extends Electeur { int age; public ElecteurIndecis(Candidat[] lesCandidats, int age) { super(lesCandidats); this.age = age; } public void jeVote() { for (int i=0; i
289

290

L’orienté objet

public String monSlogan() { return "vive la France, "; } public void accroitVoix() { nombreDeVoix ++; } public int donneNombreCasseroles() { return nbreCasseroles; } public int donneQI() { return QI; } public boolean compareSlogan(String unSlogan) { /* la methode String.compareTo(String) renvoie 0 seulement si les deux strings sont egaux */ if (unSlogan.compareTo(monSlogan())==0) return true; else return false; } public void donneNombreVoix() { System.out.println(nom + " a fait " + nombreDeVoix + " voix"); } } class CandidatDangereux extends Candidat { public CandidatDangereux(String nom, int age, int nbreCasseroles, int QI) { super(nom,age,nbreCasseroles,QI); } public String monSlogan() { return super.monSlogan() + "etranger dehors"; } } class CandidatEgoTrip extends Candidat { public CandidatEgoTrip(String nom, int age, int nbreCasseroles, int QI) { super(nom,age,nbreCasseroles,QI); } public String monSlogan() { return super.monSlogan() + "l'etat c'est moi"; } } class CandidatBrillant extends Candidat { public CandidatBrillant(String nom, int age, int nbreCasseroles, int QI) { super(nom,age,nbreCasseroles,QI); } public String monSlogan() { return super.monSlogan() + "regardez mon bilan"; } } class CandidatCasserole extends Candidat { public CandidatCasserole(String nom, int age, int nbreCasseroles, int QI) { super(nom,age,nbreCasseroles,QI); }

Redéfinition des méthodes CHAPITRE 12

public String monSlogan() { return super.monSlogan() + "regardez mon compte en banque"; } } public class Exo3 { public static void main(String[] args) { Candidat[] lesCandidats = new Candidat[8]; Electeur[] lesElecteurs = new Electeur[10]; lesCandidats[0] lesCandidats[1] lesCandidats[2] lesCandidats[3] lesCandidats[4] lesCandidats[5] lesCandidats[6] lesCandidats[7]

= = = = = = = =

new new new new new new new new

CandidatDangereux("LePon",75,1000,50); CandidatDangereux("Laguillerette",55,0,10); CandidatDangereux("StChasse",50,100,10); CandidatEgoTrip("LeChe",60,0,150); CandidatEgoTrip("Madeleine",55,0,100); CandidatCasserolle("SuperLier",70,1000,100); CandidatBrillant("Jaudepis",65,0,1000); CandidatBrillant("Tamere",55,0,800);

lesElecteurs[0] lesElecteurs[1] lesElecteurs[2] lesElecteurs[3] lesElecteurs[4] lesElecteurs[5] lesElecteurs[6] lesElecteurs[7] lesElecteurs[8] lesElecteurs[9]

= = = = = = = = = =

new new new new new new new new new new

ElecteurMalin(lesCandidats); ElecteurMalin(lesCandidats); ElecteurIndecis(lesCandidats, 20); ElecteurIndecis(lesCandidats,80); ElecteurIndecis(lesCandidats,60); ElecteurIndecis(lesCandidats,70); ElecteurIndecis(lesCandidats,40); ElecteurIdiot(lesCandidats,20); ElecteurIdiot(lesCandidats,10); ElecteurIdiot(lesCandidats,60);

for (int i=0; i
Exercice 12.4 Que donne l’exécution du programme Java suivant ? // fichier A.java public class A { public A() {} } // fichier B.java public class B extends A { public B() { super(); } public String toString() { return(" Hello " + super.toString()); }

291

292

L’orienté objet

} // fichier testAB.java public class TestAB { public TestAB() { A a = new B(); System.out.println(a); } public static void main(String[] args) { TestAB tAB = new TestAB(); } }

Exercice 12.5 Supprimez dans le code qui suit les lignes qui provoquent une erreur et indiquez si l’erreur se produit à la compilation ou à l’exécution. Quel est le résultat de l’exécution qui s’affiche à l’écran après suppression des instructions à problème ? class A { public void a() { System.out.println("a de A") ; } public void b() { System.out.println("b de A") ; } } class B extends A { public void b() { System.out.println("b de B") ; } public void c() { System.out.println("c de B") ; } } public class Correction2 { public static void main(String[] args) { A a1=new A() ; A b1=new B() ; B a2=new A() ; B b2=new B() ; a1.a() ; b1.a() ; a2.a() ; b2.a() ; a1.b() ; b1.b() ; a2.b() ; b2.b() ; a1.c() ; b1.c() ;

Redéfinition des méthodes CHAPITRE 12

a2.c() ; b2.c() ; ((B)a1).c() ((B)b1).c() ((B)a2).c() ((B)b2).c() }

293

; ; ; ;

}

Exercice 12.6 Que donne l’exécution du programme C++ suivant ? Réalisez le diagramme de classe UML correspondant. #include "stdafx.h" #include "iostream.h" class Animaux { private: int age; int id; void dormirEnFonctionDeMonAge() { if (age > 2) cout << "Je fais un petit "; else cout << "Je fais un gros "; } virtual void dormirAToutAge() { cout << "ronflement"; } public: Animaux(int _age, int _id) : age(_age), id(_id) {} void dormir() { dormirEnFonctionDeMonAge(); dormirAToutAge(); } }; class Employe { private: int age; int id; char *nom; void mangerDeTouteFacon() { cout <<"Je mange beaucoup de "; } virtual void mangerDifferemment() { cout <<"mes Animaux"; } public: Employe(int _age, int _id, char* _nom): age(_age),id(_id) { nom = _nom; } void manger() { mangerDeTouteFacon(); mangerDifferemment(); }

294

L’orienté objet

}; class Elephant: public Animaux { public: Elephant(int _age, int _id):Animaux(_age,_id) {}; private: void dormirAToutAge() { cout << "barrissement"; } }; class Lion: public Animaux { public: Lion(int _age, int _id):Animaux(_age,_id){} private: void dormirAToutAge() { cout << "rugissement"; } }; class Singe: public Animaux { public: Singe(int _age, int _id) : Animaux(_age,_id){} }; class EmployeDuZoo: public Employe { public: EmployeDuZoo(int _age,int _id,char* _nom):Employe(_age,_id,_nom){} void mangerDeTouteFacon() { cout << "Je mange enormement de "; } void mangerDifferemment() { cout << "choucroute"; } }; class MandaiDuZoo: public Employe { public: MandaiDuZoo (int _age, int _id, char* _nom):Employe(_age,_id,_nom){} void mangerDeTouteFacon() { cout << "Je mange tres peu de "; } void mangerDifferemment() { cout <<"radis beurre"; } }; class ResponsableDuZoo:public Employe { private: Employe* mesEmployes[3]; Animaux* mesAnimaux[3];

Redéfinition des méthodes CHAPITRE 12

public: ResponsableDuZoo (int _age, int _id, char* _nom,Employe* _mesEmployes[3], Animaux* _mesAnimaux[3]):Employe(_age,_id,_nom) { for (int i=0; i<3; i++) { mesAnimaux[i] = _mesAnimaux[i]; mesEmployes[i] = _mesEmployes[i]; } } void mangerDifferemment() { cout << "caviar"; } void faireLaTourneeDuSoir() { for (int i=0; i<3; i++) { mesAnimaux[i]->dormir(); cout << endl; } for (int j=0; j<3; j++) { mesEmployes[j]->manger(); cout << endl; } } }; int main(int argc, char* argv[]) { Employe *mesEmployes[3]; mesEmployes[0] = new EmployeDuZoo(30,2,"Dupont"); mesEmployes[1] = new EmployeDuZoo(25,3,"Durant"); mesEmployes[2] = new MandaiDuZoo(23,4,"Michel"); Animaux *mesAnimaux[3]; mesAnimaux[0] = new Lion(1,0); mesAnimaux[1] = new Elephant(3,1); mesAnimaux[2] = new Singe(2,2); ResponsableDuZoo JeanMarie(60,1,"JeanMarie", (Employe*[3])mesEmployes,(Animaux*[3])mesAnimaux); JeanMarie.faireLaTourneeDuSoir(); return 0; }

Exercice 12.7 Que donne l’exécution du programme C# suivant ? using System; class InstrumentDeMusique { private String nomInstrument; private double poidsInstrument; private Musicien joueurInstrument; private static int nombreInstrumentDansOrchestre;

295

296

L’orienté objet

public InstrumentDeMusique(String nomInstrument, double poidsInstrument, Musicien joueurInstrument) { this.nomInstrument = nomInstrument; this.poidsInstrument = poidsInstrument; this.joueurInstrument = joueurInstrument; nombreInstrumentDansOrchestre ++; } public double donneMonPoids() { return poidsInstrument; } public Musicien donneMonJoueur() { return joueurInstrument; } public Boolean seraiJeBienJoue() { if (joueurInstrument.donneExperience() > 10) return true; else return false; } public virtual void testDesaccordage() { Console.WriteLine("on teste"); } } class Violon : InstrumentDeMusique { private String marqueDesCordes; private int frequenceDaccordage; private int temperatureLimite; private int temperatureAmbiante; private static int nombreViolonDansOrchestre; public Violon(String nomInstrument, double poidsInstrument, Musicien joueurInstrument, String marqueDesCordes, int temperatureAmbiante) :base(nomInstrument, poidsInstrument, joueurInstrument) { this.marqueDesCordes = marqueDesCordes; frequenceDaccordage = 12; temperatureLimite = 40; this.temperatureAmbiante = temperatureAmbiante; } public override void testDesaccordage() { if (temperatureAmbiante > temperatureLimite) Console.WriteLine("Attention violon desaccorde"); else Console.WriteLine("Tout va bien"); } } class Piano : InstrumentDeMusique { private int frequenceDaccordage; private String nomDeLaccordeur;

Redéfinition des méthodes CHAPITRE 12

private static int nombrePianoDansOrchestre; public Piano(String nomInstrument, double poidsInstrument, Musicien joueurInstrument, String nomDeLaccordeur) :base(nomInstrument, poidsInstrument, joueurInstrument) { frequenceDaccordage = 24; this.nomDeLaccordeur = nomDeLaccordeur; } public String donneNomAccordeur() { return nomDeLaccordeur; } public override void testDesaccordage() { if (nomDeLaccordeur == "") Console.WriteLine("Attention pas d'accordeur de piano"); else Console.WriteLine("Tout va bien"); } } class Musicien { private String nom; private int experience; private int age; public Musicien(String nom, int experience, int age) { this.nom = nom; this.experience = experience; this.age = age; } public String donneNome() { return nom; } public int donneAge() { return age; } public int donneExperience() { return experience; } } public class Exo7 { public static void Main() { InstrumentDeMusique[] lesInstruments = new InstrumentDeMusique[4]; Musicien[] lesMusiciens = new Musicien[4]; lesMusiciens[0] lesMusiciens[1] lesMusiciens[2] lesMusiciens[3]

= = = =

new new new new

Musicien("Pat",5,25); Musicien("Herbie",15,22); Musicien("Brad",15,34); Musicien("Joe",5,18);

lesInstruments[0] = new Violon("stradivarius",2,lesMusiciens[0],"cordeMeilleure",42); lesInstruments[1] = new Piano("playel11",150,lesMusiciens[1],"");

297

L’orienté objet

298

lesInstruments[2] = new Piano("Playel12",135,lesMusiciens[2],"Albert"); lesInstruments[3] = new InstrumentDeMusique("Instrument",200,lesMusiciens[1]); for (int i=0; i<4; i++) lesInstruments[i].testDesaccordage(); } }

13 Abstraite, cette classe est sans objet Ce chapitre introduit la notion de classe abstraite et son exploitation lors du polymorphisme.

Sommaire : Classe abstraite — Méthode abstraite, virtuelle pure — Polymorphisme, encore — Les interfaces graphiques

Doctus — Les caractéristiques de nos objets sont un moyen de les identifier. L’ensemble de leurs méthodes permet de savoir ce qu’ils représentent et ce qu’ils font. Elles permettent même de définir nos objets. Candidus — Nous avons déjà parlé de ça, où veux-tu en venir ? Doc. — Rappelle-toi que la définition d’une méthode est constituée de sa signature : type retourné, nom de méthode et liste de ses arguments. Son implémentation est l’affaire du mécanisme d’héritage. Nous pouvons donc nous contenter, dans un premier temps, de déclarer l’existence de certaines méthodes tout en remettant à plus tard leur réalisation concrète. Cand. — Et qu’est-ce qu’on y gagne ? Doc. — Cela revient à dire que les objets savent faire certaines choses mais sans dire tout de suite comment. Cand. — Je pourrai donc créer une classe d’objets comme je le fais d’habitude mais, je pourrai, pour certaines méthodes, dire que l’implémentation doit être recherchée dans le type concret de l’objet ? Doc. — Il vaut mieux préciser que tu diras comment ailleurs plutôt que tout de suite. C’est en fait dans des sous-classes, bien concrètes celles-la, que tu devras réaliser les méthodes concernées. Cand. — Nos classes concrètes représentent les différentes formes que peuvent prendre les objets qu’on manipule par leur poignée abstraite, c’est ça ? Doc. — Ta poignée s’appelle une classe abstraite. Tu pourras t’en servir pour manipuler ces objets mais elle ne sera, du moins en partie, qu’une sorte de squelette que tu utiliseras pour regrouper tout ce qui est concret et commun à un groupe d’objets, tout en mentionnant des méthodes abstraites que tu te proposes de réaliser dans des sous-classes qui vont en hériter.

300

L’orienté objet

De Canaletto à Turner Le peintre vénitien Canaletto a peint de multiples vues de Venise au XVIIIe siècle. Elles sont extraordinaires par la précision et la profusion de détails qui nous sont rapportés. Canaletto réalisait ses œuvres afin de satisfaire des commandes de notables anglais, exigeant une vue précise de Venise, non avare de détails, sous la forme d’un reportage fidèle à la réalité. Il y a bien évidemment une « aspiration photographique » dans ce travail. Elles sont précises au point que, grâce à elles, on a pu déduire exactement de quelle hauteur, depuis l’époque du peintre, les eaux avaient monté dans Venise. Turner a lui aussi peint Venise quelque 100 ans plus tard. Venise y est moins nette, bien qu’on en devine les caractéristiques essentielles. Nous sommes aux sources de l’abstraction picturale, où la peinture exprime davantage la vision intérieure de l’artiste que la réalité. Il cherche à communiquer sa Venise à lui, et, ce faisant, à suggérer les émotions qu’elle provoque en lui. Néanmoins, cette abstraction conserve de nombreux traits de Venise, faisant l’économie de leur implémentation détaillée. Venise est entre les lignes. C’est la signature de Venise, bien plus que sa photo. Cela présente l’avantage de bien mieux vieillir et de ressembler à Venise aujourd’hui, bien plus que l’œuvre de Canaletto, qui n’est plus à jour. Les abstractions tiennent mieux la distance. Il en va un peu ainsi des classes abstraites par rapport aux classes concrètes. Dans une des vues de Venise de Turner, on devine un petit personnage peignant dans un coin. Vous aurez deviné de qui il s’agit.

Des classes sans objet En se replongeant dans les deux petits programmes illustrant les mécanismes d’héritage : l’écosystème et le match de football, force serait de faire le constat suivant : dès qu’une superclasse apparaît dans le code, elle n’a plus l’occasion de donner naissance à des objets. Dans le code de l’écosystème, ne figure aucun objet de type faune ou ressource, et dans le match de football ne joue aucun joueur. Au moment de la création de l’objet à proprement parler, lorsque les joueurs montent sur le terrain, lors de l’utilisation du « new », la classe qui suit ce « new » et qui type dynamiquement l’objet n’a plus lieu d’être une superclasse. Vous aurez tôt fait de nous rétorquer qu’en étant instance de la sous-classe, tous les objets le sont automatiquement de la superclasse. C’est exact, d’un point de vue déclaration, ou typage statique, et c’est vrai pour le compilateur (ce qui n’est déjà pas rien), mais cela ne reste que partiellement vrai lors de l’exécution. Les objets sont d’abord d’un type dynamique avant d’être également du type statique, comme nombre d’immigrés vous diront qu’ils sont d’abord français (ou devenus tels) avant d’être italien, algérien, polonais ou argentin. Tout objet peut être de plusieurs types statiques, hérités de leurs parents et grands-parents, mais ne sera que d’un, et un seul, type dynamique, sa véritable et ultime nature. Rien n’interdit, pour l’instant, de créer dynamiquement des objets de type superclasse. Mais on conçoit aisément que, dès que l’univers conceptuel qui nous intéresse est couvert de sous-classes, c’est-à-dire, et pour reprendre la théorie des ensembles, lorsque chaque élément de l’ensemble est repris dans un sous-ensemble, il ne soit plus justifié de créer encore des objets de la superclasse. Votre voiture est une Renault avant d’être une voiture, votre chien est un cocker avant d’être un chien, le joueur de football est un attaquant ou un défenseur avant d’être un joueur. Cela pourrait néanmoins être le cas, si on vous demande de rajouter un animal dans votre logiciel sans préciser son espèce, ou si on vous offre une voiture sans préciser sa marque, ou si l’entraîneur décide d’envoyer un joueur sur le terrain sans lui indiquer quel poste il occupe, mais c’est plutôt rare. On sait pertinamment de quelle nature intime sont les objets auxquels on a affaire. Dans la pratique courante de l’OO, les superclasses, bien qu’indispensables à la factorisation des caractéristiques communes aux sous-classes, ne donnent que très rarement naissance à des objets.

Abstraite, cette classe est sans objet CHAPITRE 13

301

Du principe de l’abstraction à l’abstraction syntaxique Ayez à l’esprit que ce ne serait pas une bourde syntaxique de créer des objets instances d’une superclasse, sauf dans un cas précis, que nous allons maintenant détailler, et qui se produit quand vous déclarez explicitement la superclasse comme étant « abstraite ». Nous nous baserons pour comprendre la nature et le rôle des classes abstraites sur le modèle de l’écosystème. Dans le code, la classe Jungle envoie de manière répétée le même message evolue() aux objets issus des deux sous-classes de Ressource : Eau et Plante. L’exécution de ce message n’a à ce point rien de commun entre l’eau (elle s’assèche) et la plante (elle pousse) qu’aucun corps d’instruction n’est repris dans la classe Ressource. L’eau et la plante, bien qu’évoluant toutes deux, et capable de recevoir ce même message, d’où qu’il provienne, ne partagent rien dans l’exécution de celui-ci. Dans le code Java qui suit, tant la classe Eau que la classe Plante intègrent la méthode évolue() : public class Plante { …… …….. …… …….. public void evolue() { ………. ……….. } }

public class Eau {

…… ……. public void evolue() { ……………………. ……… ……….. } }

Vous pourriez décemment vous demander à quoi cela sert de nommer ces méthodes de la même manière, si elles décrivent des réalités si distinctes. Rappelez-vous ce que nous vous disions sur la pauvreté de notre langage, quand il s’agit de décrire des modalités actives par rapport aux modalités structurelles. Voilà une première raison. Il en est une seconde qui tient plus à la pratique logicielle. Il est intéressant de pouvoir écrire le code de la jungle, la « tierce » classe, « cliente » de l’eau et de la plante, comme envoyant indifféremment un même message aux points d’eau et aux plantes. Une économie d’écriture sera véritablement réalisée s’il est possible, à l’instar des joueurs de football recevant le message avance() de l’entraîneur, de permettre à la Jungle d’envoyer le même message évolue(), en boucle, à toutes les ressources auxquelles elle est associée (sans se préoccuper du nombre et de la nature de celles-ci), comme ci-après : for (int i=0; i
On pourrait imaginer créer un ensemble de 100 points d’eau, par la simple instruction : for (int i=0 ; i<100 ; i++) lesRessources[i] = new Eau() ;

Et 200 plantes, au moyen de : for (int i=0 ; i<200 ; i++) lesRessources[100+i] = new Plante() ;

Et d’envoyer ensuite le message évolue() sur ces 300 ressources. C’est en effet ce que l’on cherche à faire, en tous les cas, au moment de l’exécution du code. On désirerait ajouter un nouveau type de ressource, par exemple, des cadavres en décomposition d’autres animaux, que le code de la jungle ne se modifierait en rien. Malheureusement pour nous (mais heureusement dans pratiquement tous les cas de figure), l’exécution est toujours précédée par une étape de compilation (comme nous le savons, Python et PHP 5 se distinguent ici)

302

L’orienté objet

qui, parmi d’autres choses, fait office de correcteur syntaxique plutôt sévère. Or, comme dans tous les langages de programmation, un tableau se doit d’être typé. Si nous voulons donc installer toutes les ressources dans un tableau, il faudra typer ce dernier au moyen d’une instruction telle que : Ressource [] lesRessources = new Ressource[300].

Pouvions-nous typer ce tableau comme Plante ? Non, car il y a des points d’eau dans l’affaire. Et comme Eau ? Non, car il y a des plantes dans l’affaire. La seule solution est de le typer comme Ressource, puisqu’en effet, tant les plantes que les eaux en sont. Et nous nous retrouvons, comme dans le chapitre précédent, en présence d’objet dont le type statique, Ressource, diffère du type dynamique : Eau ou Plante. Nous nageons à nouveau, avec bonheur, en plein polymorphisme. La classe Jungle pourrait également recevoir dans une de ses méthodes un argument de type Ressource, sur lequel elle enverrait un message commun à la plante ou l’eau, et qui serait par la suite exécuté différemment. Une nouveauté, essentielle ici, est que ni la classe Eau ni la classe Plante ne redéfinisse une méthode evolue(), qui aurait une part d’instructions déjà prévue dans la superclasse. Pourtant, si nous typons le tableau des ressources comme Ressource, et que nous envoyons le message « évolue » sur chacun de ces objets, le compilateur, pour qui seul le type statique a voix au chapitre, ne pourra accepter qu’aucune méthode evolue() ne soit en effet présente dans la classe Ressource. Dilemme, dont la seule issue possible est d’installer une méthode evolue() dans la classe Ressource, tout en déclarant cette méthode « abstraite », c’est-à-dire sans corps d’instruction. Une méthode abstraite est une méthode qui se limite à sa seule signature, une méthode qui ne fait rien, à part se présenter. En Java, en C# et en PHP 5, nous la déclarons dans la classe Ressource de la manière suivante : abstract public void evolue();

En C++, elle serait dite méthode « virtuelle pure », et se déclare ainsi : public : void virtual evolue() = 0;

Elle est virtuelle par la présence de virtual. En ajoutant = 0, on la rend abstraite. Nous verrons par la suite une manière de réaliser l’abstraction dans Python.

Classe abstraite Toute classe contenant au moins une méthode abstraite devient d’office abstraite. D’ailleurs, tant Java que C# et PHP 5 forcent le trait, en vous obligeant à rajouter le mot-clé abstract dans la déclaration de la classe, comme suit : public abstract class Ressource extends ObjetJungle { ……. }

C++ reste plus sobre et sait que l’abstraction d’au moins une méthode entraîne l’abstraction de toute la classe. Il n’y a d’autre moyen de rendre une classe abstraite qu’en y installant une méthode abstraite ou virtuelle pure. En fait, Java, C# et PHP 5 n’accepteraient pas qu’une méthode abstraite ne fût définie dans une classe, elle-même déclarée comme abstraite, mais le contraire ne s’applique pas. Les trois langages acceptent d’une classe qu’elle soit abstraite, alors qu’aucune méthode abstraite ne s’y trouve. Ils bloquent ainsi la possibilité pour certaines classes de donner naissance à des objets, indifféremment du fait qu’elles intègrent une méthode abstraite. Dans la pratique, très logiquement, une classe ne sera généralement abstraite que si une méthode abstraite s’y trouve.

Abstraite, cette classe est sans objet CHAPITRE 13

303

« new » et « abstract » incompatibles Au début de ce chapitre, nous vous expliquions que, souvent, les superclasses ne donnent pas naissance à des objets. Dorénavant, elles le pourront d’autant moins qu’elles seront déclarées abstraites. new et abstract sont deux mots-clés totalement incompatibles, en ce sens qu’aucune allocation de mémoire ne peut être effectuée pour des instances de classe abstraite. Si nous revenons à la définition première des classes abstraites, c’est-à-dire qu’elles contiennent au moins une méthode abstraite, cette interdiction doit vous paraître logique. Supposons une classe contenant une méthode abstraite et pouvant donner naissance à des objets. Tout objet se doit être capable d’exécuter tous les messages reçus. Qu’en serait-il du message issu de la méthode abstraite ? Le compilateur ne tiquerait pas, car la syntaxe du message est parfaitement correcte. Mais que faire à l’exécution, face à un corps d’instruction absent ? On enverrait un message qui dit de ne rien faire ? Cette possibilité a d’office été bannie par les langages OO, car un message se doit de faire quelque chose. Notez pour l’anecdote qu’un corps d’instruction vide est considéré comme distinct de pas de corps d’instruction du tout : public void evolue() {} est différent de abstract public void evolue(). Tous les langages OO interdisent l’envoi de messages à partir de méthodes sans corps d’instruction, mais cette interdiction est levée pour des méthodes dont le corps d’instruction, bien qu’existant, est vide. Seules les premières méthodes sont abstraites, les autres sont stupides mais concrètes !

Abstraite de père en fils Au contraire des superclasses concrètes, les superclasses abstraites obligent à redéfinir (ne serait-il sans doute pas plus approprié de simplement dire « définir » ?) les méthodes abstraites dans leurs sous-classes. Tant que la méthode abstraite n’est pas redéfinie dans les sous-classes, chacune de ces sous-classes se doit de rester abstraite, et aucune ne donnera naissance au moindre objet. Le compilateur se chargera de vérifier que vous maintenez l’abstraction, de sous-classes en sous-classes, jusqu’à ce que toutes les méthodes abstraites soient redéfinies. Ci-après, vous voyez le code Java de la superclasse abstraite Ressource et de la sous-classe concrète Eau. La méthode dessineToi(), qui représente graphiquement la ressource, est abstraite dans la classe Ressource, car il est nécessaire de savoir de quelle ressource il s’agit avant de la dessiner. Tous les objets se dessinent, mais tous le feront à leur manière. La méthode evolue(), pour des raisons déjà évoquées, est également abstraite. Les plantes évoluent en grandissant, les points d’eau en diminuant de taille. public abstract class Ressource extends ObjetJungle { /* classe abstraite */ private int temps; private int quantite; Ressource () { super(); temps = 0; quantite = 100; } abstract public void dessineToi(Graphics g); /* méthode abstraite */ abstract public void evolue(); /* méthode abstraite */ public void incrementeTemps() { temps++; } public int getTemps() { return temps; } public void diminueQuantite() { decroitTaille(2); }

304

L’orienté objet

} public class Eau extends Ressource { Eau() { super(); } public void dessineToi(Graphics g) { /* définition de cette méthode abstraite */ g.setColor(Color.blue); g.fillOval(getMaZone().x, getMaZone().y, getMaZone().width, getMaZone().height); } public void evolue() { /* définition de cette méthode abstraite */ incrementeTemps(); if ((getTemps()%10) == 0) decroitTaille(2); } }

Un petit exemple dans les cinq langages de programmation Ci-après, vous trouverez en Java, C#, PHP 5 et C++ un même exemple d’une superclasse abstraite, dû à la présence en son sein d’une méthode abstraite, ainsi que deux sous-classes concrétisant cette même méthode de deux manières différentes. En Java abstract class O1 { abstract public void jexisteSansRienFaire(); } class FilsO1 extends O1 { public void jexisteSansRienFaire() { System.out.println("ce n'est pas vrai, je fais quelque chose"); } } class AutreFilsO1 extends O1 { public void jexisteSansRienFaire() { System.out.println("c'est de nouveau faux, moi aussi je fais quelque chose"); } } public class ExempleAbstract { public static void main(String[] args) { /* O1 unO1 = new O1(); impossible */ O1 unFilsO1 = new FilsO1(); O1 unAutreFilsO1 = new AutreFilsO1(); unFilsO1.jexisteSansRienFaire(); unAutreFilsO1.jexisteSansRienFaire(); } }

Résultat ce n’est pas vrai, je fais quelque chose c’est de nouveau faux, moi aussi je fais quelque chose

Abstraite, cette classe est sans objet CHAPITRE 13

305

Remarquez que nous avons délibérément typé statiquement et dynamiquement nos objets de manière différente, le type statique ne pouvant être qu’une superclasse du type dynamique. Alors qu’il n’est pas possible de typer dynamiquement un objet par une classe abstraite, comme le montre le code (impossible de créer un objet comme étant typé « définitivement » par une classe abstraite), il n’y a aucun problème pour le typer statiquement avec une classe abstraite. C’est de fait une pratique très courante et inhérente au polymorphisme. En C# using System; abstract class O1 { abstract public void jexisteSansRienFaire(); } class FilsO1 : O1 { public override void jexisteSansRienFaire() { Console.WriteLine("ce n'est pas vrai, je fais quelque chose"); } } class AutreFilsO1 : O1{ public override void jexisteSansRienFaire() { Console.WriteLine("c'est de nouveau faux, moi aussi je fais quelque chose"); } } public class ExempleAbstract { public static void Main() { /* O1 unO1 = new O1(); impossible */ O1 unFilsO1 = new FilsO1(); O1 unAutreFilsO1 = new AutreFilsO1(); unFilsO1.jexisteSansRienFaire(); unAutreFilsO1.jexisteSansRienFaire(); } }

Résultat ce n’est pas vrai, je fais quelque chose c’est de nouveau faux, moi aussi je fais quelque chose

Rien d’essentiellement différent par rapport au code Java, si ce n’est l’addition du mot-clé override, lors de la concrétisation des méthodes abstraites. Le mot-clé virtual, lors de la déclaration des méthodes abstraites, n’est plus nécessaire, comme il l’est lors de la déclaration des méthodes, non plus abstraites, mais concrètes et à redéfinir. En PHP 5 Héritage et abstraction

Héritage et abstraction




306

L’orienté objet

\n"); } } class AutreFilsO1 extends O1 { public function jexisteSansRienFaire() { print ("c'est de nouveau faux, moi aussi je fais quelque chose
\n"); } } $unFilsO1 = new FilsO1(); $unAutreFilsO1 = new AutreFilsO1(); $unFilsO1->jexisteSansRienFaire(); $unAutreFilsO1->jexisteSansRienFaire(); ?>

Point de typage statique différent d’un typage dynamique, car point de compilation. Mais à cette différence essentielle près, l’abstraction se réalise comme dans les deux langages précédents (et elle est plutôt très inspirée de Java, comme l’est toute la partie « héritage » de PHP 5). En C++ #include "stdafx.h" #include "iostream.h" class O1 { public: virtual void jexisteSansRienFaire() = 0; }; class FilsO1 : public O1 { public: void jexisteSansRienFaire() { cout <<"ce n'est pas vrai, je fais quelque chose" << endl; } }; class AutreFilsO1 : public O1 { public: void jexisteSansRienFaire() { cout <<"c'est de nouveau faux, moi aussi je fais quelque chose" << endl; } }; int main(int argc, char* argv[]) { FilsO1 unFilsO1;

Abstraite, cette classe est sans objet CHAPITRE 13

307

AutreFilsO1 unAutreFilsO1; /* O1 unO1; impossible */ unFilsO1.jexisteSansRienFaire(); unAutreFilsO1.jexisteSansRienFaire(); O1* unFilsO1Pointeur = new FilsO1(); O1* unAutreFilsO1Pointeur = new AutreFilsO1(); unFilsO1Pointeur->jexisteSansRienFaire(); unAutreFilsO1Pointeur->jexisteSansRienFaire(); return 0; }

Résultat ce n’est c’est de ce n’est c’est de

pas vrai, je fais nouveau faux, moi pas vrai, je fais nouveau faux, moi

quelque chose aussi je fais quelque chose quelque chose aussi je fais quelque chose

En C++, le mot-clé abstract disparaît. La déclaration de la méthode comme « virtuelle pure » suffit à rendre la classe abstraite. Nous présentons deux versions du programme, selon que les objets sont créés dans la mémoire pile ou la mémoire tas. Nous voyons que, dans le premier cas, il n’est pas possible de typer statiquement des objets par une classe abstraite car, comme dans ce cas, la seule déclaration suffit à la création de l’objet, les deux types se doivent d’être égaux. L’utilisation du polymorphisme à partir de classe abstaite n’est donc possible qu’avec des pointeurs et sur des objets installés dans la mémoire tas. Classe abstraite Une classe abstraite en Java, C##, PHP 5 et C++ (dans ce cas, en présence d’une méthode virtuelle pure) ne peut donner naissance à des objets. Elle a comme unique rôle de factoriser des méthodes et des attributs communs aux sous-classes. Si une méthode est abstraite dans cette classe, il sera indispensable de redéfinir cette méthode dans les sous-classes, sauf à maintenir l’abstraction pour les sous-classes et à opérer la concrétisation quelques niveaux en dessous.

L’abstraction en Python Bien qu’il ne soit pas possible de déclarer une classe abstraitre en Python (à cause de la simplification de la syntaxe et de l’affaiblissement du typage), une petite pirouette, illustrée dans le code ci-dessous, permet d’empêcher la classe O1 de donner naissance à des objets. class O1: def __init__(self): assert self.__class__ is not O1 def jexisteSansRienFaire(self): pass class FilsO1(O1): def jexisteSansRienFaire(self): print "ce n'est pas vrai, je fais quelque chose"

308

L’orienté objet

class AutreFilsO1(O1): def jexisteSansRienFaire(self): print "c'est de nouveau faux, moi aussi je fais quelque chose" # unO1=O1() devenu impossible unFilsO1=FilsO1() unAutreFilsO1=AutreFilsO1() unFilsO1.jexisteSansRienFaire() unAutreFilsO1.jexisteSansRienFaire()

L’intruction « assert » évalue la condition qui suit (ici, dans le constructeur, vérifie si l’objet en création n’est pas de la classe O1). Si cette condition est vérifiée, cette instruction ne fait rien. Dans le cas contraire, une exception est produite et levée (elle peut alors être try-catch comme expliqué dans le chapitre 7). La classe O1 pourra être héritée et aura dès lors comme seul rôle de factoriser des méthodes à redéfinir dans les classes filles. Comme Python n’a que faire du typage statique, les classes abstraites ne joueront pas un rôle exactement semblable à celui joué dans les situations polymorphiques possibles et fréquentes dans les trois autres langages.

Un petit supplément de polymorphisme Les enfants de la balle Monsieur Loyal, dans son costume rouge mité, centré au milieu du disque lumineux, d’un revers de la main interrompt les cuivres et les cymbales un peu rouillés, et dans un éclat de voix strident hurle : « Que tous les artistes fassent leur numéro. » Et, bientôt, l’un après l’autre, tous les artistes viendront s’exécuter sur la piste. Mais ce qu’ils ne savent pas tous ces artistes, ces jongleurs, ces clowns, ces trapézistes, ces funambules et ces dompteurs, tous ces enfants ou, devrais-je dire, toutes ces sous-classes de la balle, c’est qu’ils feront leur numéro, ils le feront mais ne le feront pas à la manière Bouglione ou à la manière Pinter, ils le feront à la manière polymorphique. Tous ont reçu cinq sur cinq le message de Monsieur Loyal, tous exécuteront leur numéro, tous l’exécuteront avec méthode, mais chacun à sa façon. Monsieur Loyal envoie un même message à un tableau d’artistes de cirque, artiste abstrait (la méthode faireMonNuméro étant abstraite dans la classe Artiste), et chacun se lancera dans le numéro qu’il a concrétisé et si longtemps répété dans sa sous-classe d’artiste, devenue pour le coup concrète, elle aussi : Jongleur, Trapéziste, Dompteur… Il est important pour M. Loyal de savoir que la classe Artiste existe pour pouvoir interagir avec tous ces artistes d’une seule et même manière, quitte à ce que ce qui leur soit demandé se prête à une réalisation différente. S’il convoque un de ces artistes dans son bureau pour le payer, il lui enverra un message, ayant comme but l’établissement des prestations. Il est, là encore, bien possible que ces prestations doivent être établies différemment selon l’artiste en question. Un clown pourrait coûter moins cher qu’un dompteur ou un trapéziste, d’où sa tristesse.

Cliquez frénétiquement Empoignez votre souris d’ordinateur, et cliquez frénétiquement, un clic, deux clics, cliquez où vous pouvez, cliquez où vous voulez, mais cliquez. Ce qui se produit sur l’écran, en réponse à ces clics, dépend de l’endroit où vous cliquez, de l’objet graphique sur lequel vous cliquez. Un menu apparaît, une fenêtre se ferme, une autre s’ouvre, un onglet passe au premier plan, une nouvelle police de caractère est mémorisée, le curseur se transforme en croix, etc. Il s’en passe des choses en réaction à ce clic, et pourtant le clic est toujours le même.

Abstraite, cette classe est sans objet CHAPITRE 13

309

Parfois, vous pouvez juste le doubler rapidement, parfois votre souris possède un, deux ou trois boutons, et le clic se fait sur l’un ou l’autre. Cela laisse malgré tout très peu de modalités d’action, en comparaison au nombre d’objets graphiques qui réagiront différemment en réaction à ces quelques modalités d’action. Une poignée de modalités d’action, effectives sur une large panoplie d’objets, réagissant tous différemment en regard de ces actions, les interfaces graphiques des systèmes exploitations, Windows, Mac OS ou Linux, sont de merveilleux exemples de polymorphisme. Une souris, un clic, et une véritable taxonomie d’objets graphiques, capables de réagir à ce clic, voilà comment on peut résumer très schématiquement l’interaction de l’utilisateur avec ces systèmes d’exploitation. Pourquoi ces objets graphiques sont-ils organisés de façon hiérarchique ? Car une fenêtre est un de ces objets qui peut se déplacer, s’agrandir, et possède un système de glissement qui permet de dévoiler, en partie seulement, la fenêtre. Une icône est un objet graphique qui peut juste se déplacer, un menu est un objet graphique qui ne peut pas se déplacer mais peut s’ouvrir, etc. Il est clair que certaines modalités d’action sont partagées par certains de ces objets et, ainsi, sont-elles factorisables dans des superclasses abstraites. D’autres seront spécifiées de manière polymorphique, au bas de l’arbre taxonomique, comme les effets de la souris. La figure 13-2 montre les différents objets graphiques Java, et leur structure d’héritage. Figure 13-2

La hiérarchie des classes graphiques en Java.

310

L’orienté objet

Ce détour par les interfaces graphiques des systèmes d’exploitation est loin d’être innocent, car l’histoire de l’orienté objet est concomitante, en partie, à l’histoire des interfaces graphiques. Lorsque Steve Jobs, célèbre gourou de l’informatique et créateur des Macintosh, se rend au Xerox PARC en 1979, Alan Kay, leader d’un groupe de recherche, lui présente les trois technologies innovantes sur lesquelles il planche. Tout d’abord, la mise en réseau des ordinateurs selon un protocole encore balbutiant : Internet. Steve Jobs n’y voit rien de très prometteur. Ensuite, une nouvelle manière de programmer, implémentée, en grande partie, dans un nouveau langage de programmation, inspiré de Simula, mais largement amélioré : Smalltalk. À nouveau, Steve Jobs ne voit pas là de quoi fouetter un programmeur. Alan Kay décrit pourtant cette manière de programmer, OO comme il se doit, comme l’approche la plus élégante et la plus directe pour réaliser ce qui est son troisième domaine de recherche : la conception de nouvelles modalités d’interaction avec l’ordinateur : fenêtres, souris, menus… On connaît aujourd’hui cette musique par cœur, mais, en 1979, toute interaction se faisait via des lignes de commande tapées au clavier. En découvrant cette dernière recherche, Steve Jobs est subjugué, il n’en croit pas ses oreilles et ses yeux, et il comprend que c’est la manière la plus innovante et à la fois la plus naturelle de penser l’utilisation de l’ordinateur. Il part avec, sous le bras, son projet d’un nouveau système d’exploitation pour les Macs. On connaît la suite de l’histoire… Un certain Bill Gates passa aussi par là, jeta un œil par la fenêtre, et Windows, comme par hasard, vi le jour. Chaque fois que vous cliquez à l’écran, ouvrez une fenêtre ou déroulez un menu, c’est en partie à Alan Kay que vous le devez.

Alan Kay Il obtient son doctorat de l’université d’Utah en 1969 ; le sujet en est le développement d’interfaces graphiques 3-D. L’approche OO lui semble la plus naturelle pour la réalisation de ces interfaces graphiques. De ses 10 années passées au légendaire Xerox PARC (et il n’est pas pour rien dans la naissance de cette légende), il aura apporté une contribution essentielle à la mise au point du langage Smalltalk qui, bien que venant après Simula, est sans conteste le premier langage OO populaire et diffusé. Il participe aussi activement à la mise au point des protocoles réseaux Ethernet et Internet. Son dernier sujet de préoccupation, et qui l’occupe encore activement aujourd’hui, est de repenser l’interaction hommeordinateur, afin de ne pas laisser l’ordinateur se cantonner à une machine à écrire sophistiquée, mais de le transformer en un réel support à la créativité et l’imagination. C’est en observant, auprès de Seymour Papert, les enfants utiliser l’ordinateur, qu’il se rend compte qu’il y a dans cette « machine » un potentiel largement sous-exploité pour l’enrichissement intellectuel des enfants et des adultes en général. Depuis toutes ces années d’observation, l’éducation des enfants (dès l’âge de 6 ans), par le biais d’une utilisation mieux pensée des technologies de l’information, est devenue pour lui une croisade. On sait tout ce que Steve Jobs lui doit et, de fait, il s’associe au succès d’Apple pendant les 10 années qui suivent. Il apporte sa touche essentielle dans la mise au point du Netwon d’Apple, mais le succès n’est pas au rendez-vous, car son rêve d’un système permettant une vraie interaction intuitive et créative, un compagnon aussi indispensable que discret, ne parvient à aboutir, suite à des difficultés techniques et des freinages commerciaux. Il devient ensuite pendant 5 ans le vice-président pour la recherche et le développement de la « Walt Disney Company » où il a l’occasion de se convaincre chaque jour davantage que, selon ses propres termes, la « révolution informatique ne s’est toujours pas produite » (mais qu’est-ce qui lui faut ?) et que « la meilleure manière de prédire le futur est de l’inventer ». Sa présence auprès de Walt Disney lui permet d’approfondir l’interaction entre les enfants et l’ordinateur, afin de faire de ce dernier une aide véritable à la pédagogie, une stimulation à la créativité et une vraie réponse à la soif insatiable d’apprentissage de l’enfant.

Abstraite, cette classe est sans objet CHAPITRE 13

311

À plus de 60 ans, et de façon à définitivement se donner les moyens de ses ambitions, il fonde sa propre compagnie : « Viewpoints Research Institute », consacrée au développement d’outils informatiques, permettant une meilleure appréhension des systèmes complexes, et destinés tant aux adultes qu’aux enfants. Une des productions de cette nouvelle compagnie est le langage de développement orienté objet, « Squeak », au sujet duquel la maison d’édition Eyrolles a récemment produit un livrea. De par sa grande facilité d’utilisation, ce langage devrait permettre tant aux enfants qu’aux éducateurs de « jouer » et « d’expérimenter » de manière créative, ludique et plus intuitive, les maths et la science. Ce nouveau langage, directement issu de Smalltalk, devrait aider Alan Kay à imposer tant ses nouvelles idées sur la pédagogie que sa vision très personnelle, et dont le manque de réceptivité l’obsède, sur l’interaction homme-machine. Enfin, ne soyez plus étonnés que ce livre reprenne des exemples de biologie et de chimie, nous ne pouvons rêver d’une meilleure compagnie, car Alan Kay obtint son premier diplôme universitaire en biologie moléculaire. C’est à partir de là, et sans arrêt depuis, qu’il se mit à penser l’informatique, son informatique à lui, en des termes biologiques. Ainsi l’idée de messages entre objets est-elle directement inspirée de l’idée de messages chimiques entre les cellules. Alan Kay considère qu’un ordinateur idéal se doit de fonctionner comme un organisme vivant, complexe mais se divisant la tâche en un ensemble de modules simples, chaque module devant agir de concert avec les autres (les communications ayant lieu par messages chimiques) afin de réaliser une fonctionnalité commune. Mais tous les modules doivent conserver une certaine autonomie et préserver leur intégrité. Steve Jobs doit se mordre les doigts de n’avoir saisi qu’une seule innovation importante parmi tout ce dont lui parla Alan Kay, car tout de ce qui fait l’informatique aujourd’hui : les réseaux, la programmation OO, les interfaces graphiques, les imprimantes laser, les ordinateurs de poche, l’informatique ubiquitaire, est redevable en partie à l’extraordinaire imagination et la créativité débordante d’Alan Kay. Allant de déception en déception, à force de voir ses idées spoliées en ce qu’elles ont de plus rémunératrices, il espère par cette nouvelle entreprise arriver enfin, sans doute par sa juste réappropriation et son adoption par les enfants, à redonner toutes ses lettres de noblesse à l’ordinateur, tel qu’il souhaiterait le voir utiliser. Ayant lu tout cela, vous ne serez guère surpris d'apprendre qu'il a reçu en 2004 le prix Turing (la version informatique du Nobel). a. Squeak, Xavier Briffault, Stéphane Ducasse, Eyrolles 2001.

Le Paris-Dakar L’organisateur du Paris-Dakar doit planifier à l’avance la consommation de tout le convoi de véhicules qui participent à cette course : moto, camions, buggy, voiture, hélicoptère… Tous ces véhicules consomment, mais tous le font différemment en fonction des kilomètres parcourus. Il est capital de comprendre ici que c’est la manière de calculer la consommation qui diffère d’un véhicule à l’autre. Si la seule différence se ramenait à une valeur, par exemple, simplement la consommation par km, et que le calcul de la consommation revenait à multiplier cette valeur par le nombre de km à parcourir, il n’y aurait nul besoin d’héritage, nul besoin de polymorphisme. Il n’y aurait, qui plus est, qu’une seule classe de véhicule, dont les objets se distingueraient, notamment par la valeur de cette consommation kilométrique. Si seule la valeur des attributs différencie les objets entre eux, point n’est besoin de sous-classe. L’usage des langues naturelles, en général, ne permet pas de distinguer le lien existant entre un objet et sa classe d’un côté, et le lien entre une sous-classe et sa superclasse de l’autre. Ma Renault, objet, est une Renault, classe. Une Renault, sous-classe, est une voiture, superclasse. La même expression « est une » est utilisée ici, alors qu’en programmation OO, ces deux contextes d’utilisation mènent à des développements logiciels très différents : simple instanciation d’objet dans le premier, mise en place d’une structure d’héritage à deux niveaux dans le second. Ne vous trompez pas et n’abusez pas d’héritage inutile. Il se doit d’alléger et non pas d’alourdir votre conception. Le gain de son apport, en clarté, économie, maintenance et extensibilité, doit être suffisamment important. Autrement, n’y songez même pas et contentez-vous d’un seul niveau taxonomique. C’est déjà bien assez.

312

L’orienté objet

Le polymorphisme en UML Dans les trois exemples de polymorphisme, présentés plus haut, ce qui apparaît à chaque fois est un type d’architecture logicielle comme celle décrite en UML ci-après. Figure 13-3

Diagramme de classes UML classique associé au polymorphisme.

Dans UML, toute classe ou méthode abstraite est indiquée en italique. Ici, la superclasse est, de ce fait, abstraite. Trois sous-classes concrètes redéfinissent la méthode faireMonNumero(), une méthode dont le corps d’instruction sera radicalement différent pour chacune des sous-classes. Une « classe cliente » envoie indifféremment le message faireMonNumero() à tous les référents qu’elle possède comme attribut. Comme indiqué dans le diagramme, cet ou ces attributs seront typés par la superclasse. Il peut s’agir d’un et un seul référent, typé statiquement d’une unique manière, mais qui, lors de l’exécution du message, peut endosser plusieurs types dynamiques différents. Cela peut être un tableau de référents, qui lors de l’envoi d’un même message, en boucle sur tous les éléments du tableau, donnera lieu à des exécutions différentes. Aucun objet n’est ici encore créé, sauf un tableau de référents. Il n’y a donc, à ce stade, pas de tentatives, qui seraient rejetées à la compilation, de création d’objet d’une classe abstraite (la superclasse ici). Les référents pointeront vers des objets, qui eux, lors de leur création, seront de type dynamique, type correspondant à une des sous-classes héritant de la superclasse. L’opération de création d’objets se fera à partir des sous-classes, bien concrètes cette fois. Chaque objet sera donc typé statiquement par son référent superclasse, et typé dynamiquement par sa sous-classe. Il n’y aurait aucun problème à approfondir l’arbre d’héritage, et à laisser, nonobstant le lien client-serveur entre la classe cliente et la superclasse, la manière dont le message serait exécuté être définie bien plus bas dans l’héritage. Plusieurs couches de classes abstraites peuvent précéder l’arrivée, tout en bas, des classes concrètes. Rappelez-vous qu’à entendre certains gourous de l’OO, les superclasses ne pourraient être, en principe, qu’abstraites. Principe discutable, nullement contraint par la syntaxe des langages de programmation, mais dont, néanmoins, on perçoit la pertinence en jetant un simple coup d’œil au monde qui nous entoure.

Abstraite, cette classe est sans objet CHAPITRE 13

313

Exercices Exercice 13.1 Expliquez pourquoi la présence d’une méthode abstraite dans une classe interdit naturellement la création d’objets issus de cette classe.

Exercice 13.2 Justifiez pourquoi l’absence de concrétisation dans une sous-classe d’une méthode définie abstraite dans sa superclasse oblige, sans autre forme de procédé, la sous-classe à devenir abstraite.

Exercice 13.3 Expliquez pourquoi C++ ne recourt pas à l’utilisation du mot-clé abstract pour définir une classe abstraite.

Exercice 13.4 Justifiez pourquoi C# n’impose pas de définir une méthode abstraite virtual pour pouvoir la redéfinir.

Exercice 13.5 Réalisez un code dans un des trois langages, dans lequel une superclasse MoyenDeTransport contiendrait une méthode consomme() abstraite, qu’il faudrait redéfinir dans les trois sous-classes Voiture, Moto, Camion.

Exercice 13.6 Réalisez un code dans un des trois langages, dans lequel une superclasse ExpressionAlgebrique contiendrait un String comme « 2 + 5 » ou « 7 * 2 » ou « 25 : 5 » et une méthode abstraite evalue(), et trois sous-classes Addition, Multiplication, Division, code qui redéfinirait cette méthode selon que l’expression mathématique en question serait une addition, une multiplication ou une division.

Exercice 13.7 Retrouvez ce qu’écrirait à l’écran le code Java suivant. Dessinez également le diagramme de classe UML correspondant. abstract class Militaire { private int age; private String nationalite; private int QI; public Militaire(int age, String nationalite, int QI) { this.age = age; this.nationalite = nationalite; this.QI = QI; } abstract public void partirEnManoeuvre();

314

L’orienté objet

public void deserter() { System.out.println("Salut les cocos"); } public void executer() { System.out.println("A vos ordres chef"); } public int getQI() { return QI; } } abstract class Plouc extends Militaire { public Plouc(int age, String nationalite, int QI) { super(age,nationalite,QI); } abstract public void partirEnManoeuvre(); } abstract class Grade extends Militaire { public Grade(int age, String nationalite, int QI) { super(age,nationalite,QI); } public void commander (Militaire unTroufion) { unTroufion.executer(); } abstract public void partirEnManoeuvre(); } class Colonel extends Grade { private Plouc[] mesTroufions; public Colonel(int age, String nationalite, int QI, Militaire[] mesTroufions) { super(age,nationalite,QI); this.mesTroufions = new Plouc[4]; for (int i=0; i<4; i++) { this.mesTroufions[i] = (Plouc)mesTroufions[i]; } } public void partirEnManoeuvre() { for (int i=0; i<4; i++) { commander(mesTroufions[i]); System.out.println(); } } } class General extends Grade { private Colonel monColonel; public General(int age, String nationalite, int QI, Colonel monColonel) { super(age,nationalite,QI); this.monColonel = monColonel; } public void partirEnManoeuvre() { commander(monColonel); } }

Abstraite, cette classe est sans objet CHAPITRE 13

class Abruti extends Plouc { public Abruti(int age, String nationalite, int QI) { super(age,nationalite,QI); } public void partirEnManoeuvre() { System.out.println("C'est super"); } } class TireAuFlanc extends Plouc { public TireAuFlanc(int age, String nationalite, int QI) { super(age,nationalite,QI); } public void executer() { super.executer(); deserter(); System.out.println("et merde"); } public void partirEnManoeuvre() { if (getQI() < 5) System.out.println("vivement le bar"); else System.out.println("vivement la bibliotheque"); } } public class Armee { public static void main(String[] args) { Militaire[] unRegiment = new Militaire[6]; unRegiment[0] = new Abruti(20, "Belge", 1); unRegiment[1] = new Abruti(23, "Francais", 8); unRegiment[2] = new TireAuFlanc(20, "Italien", 1); unRegiment[3] = new TireAuFlanc(25, "Italien", 8); unRegiment[4] = new Colonel(50, "Belge", 2, (Militaire[])unRegiment); unRegiment[5] = new General(60, "Francais", 2, (Colonel)unRegiment[4]); for (int i=0; i<6; i++) unRegiment[i].partirEnManoeuvre(); } }

Exercice 13.8 Retrouvez ce qu’écrirait à l’écran le code C# suivant : using System; abstract class Animaux { private int monAge; private String monNom; public Animaux(int age, String nom) { monAge = age; monNom = nom; }

315

316

L’orienté objet

public virtual void crierSuivantLesAges() { if (monAge <= 2) { Console.WriteLine("je fais un petit"); } else { Console.WriteLine("je fais un gros"); } } protected virtual void crierAToutAge() { Console.WriteLine("chuuuuut"); } abstract public void dormir(); } class Fermier { private int nbreAnimaux; private Animaux[] mesAnimaux; public Fermier(Animaux[] lesAnimaux, int nombre) { mesAnimaux = lesAnimaux; nbreAnimaux = nombre; } public void jeFaisMaTourneeDuSoir() { for (int i=0; i
Abstraite, cette classe est sans objet CHAPITRE 13

317

class Taureau : Animaux { public Taureau(int age, String nom) : base(age, nom) {} public override void dormir() { crierSuivantLesAges(); crierAToutAge(); Console.WriteLine("et je m'endors a côte de ma vache"); } } public class Ferme { public static void Main() { Animaux[] laFamilleRoyale = new Animaux[10]; Fermier AlphonseII = new Fermier(laFamilleRoyale,3); laFamilleRoyale[0] = new Cochon(1,"Phil"); laFamilleRoyale[1] = new Poule(1,"Astrud"); laFamilleRoyale[2] = new Taureau(3,"Lorenzio"); AlphonseII.jeFaisMaTourneeDuSoir(); } }

Exercice 13.9 Retrouvez ce qu’écrirait à l’écran le code Java suivant. Ce code utilise la classe Vector qui est un tableau dynamique dont nous utilisons les méthodes suivantes : • size() pour obtenir la taille du tableau, • elementAt(i) pour extraire le énième élément du tableau (attention, cette méthode renvoie un object, et il est nécessaire d’utiliser le casting pour récupérer le vrai type), • addElement() rajoute un nouvel élément en queue du tableau. Dessinez également le diagramme de classe UML correspondant. import java.util.*; public class UneSerre { Vector maSerre = new Vector(); Jardinier j; public static void main(String[] args) { new UneSerre(); } public UneSerre() { maSerre.addElement(new Bananier(3,150)); maSerre.addElement(new Olivier(5,300)); maSerre.addElement(new Magnolia()); j=new Jardinier(maSerre); j.occupeToiDesPlantes(0,2,1); j.occupeToiDesPlantes(3,3,4); j.occupeToiDesPlantes(4,0,1); } }

318

L’orienté objet

class Jardinier { Vector maSerre; public Jardinier(Vector maSerre) { this.maSerre = maSerre; } public void occupeToiDesPlantes(int periode, int lumiere, int humidite) { for (int k=0; k
protected static String[] lumiere=

{

"je me desseche !", "plus d'eau", "ok pour l'eau", "Mes racines pourrissent", "blou bloup"

}; { "more light please", "lumosite parfaite", "je suis aveuglee", "je crame!!" };

public Vegetal(int a, int h) { age = a; hauteur = h; } abstract public void jeGrandis(int lumiere, int humidite, int periode); } abstract class Fruitier extends Vegetal { Fruitier(int a, int h) { super(a,h); } public void jeDonneDesFruits() { System.out.println(" .... et je donne des fruits"); } } class Bananier extends Vegetal { Bananier (int a, int h) { super(a,h); } public void jeDonneDesFruits() { System.out.println(" .... et je donne de bonnes bananes"); }

Abstraite, cette classe est sans objet CHAPITRE 13

public void jeGrandis(int l, int h, int periode) { System.out.println("le bananier dit: "); if (l>1) l=1; System.out.println(lumiere[l]+" "); System.out.println(humidite[h]); if (periode==3 && age>3 && age>10) jeDonneDesFruits(); age++; } } class Olivier extends Fruitier { Olivier(int a, int h) { super(a,h); } public void jeGrandis(int l, int h, int periode) { System.out.println("l'olivier dit: "); if (l>1) l=1; System.out.println(lumiere[l]+" "); System.out.println(humidite[h]); if (periode==1 && age>3 && age>10) jeDonneDesFruits(); age++; } } abstract class Plante extends Vegetal { Plante(int a, int h) { super(a,h); } public void jeDonneDesFleurs() { System.out.println("je donne des jolies fleurs"); } } class Magnolia extends Plante { Magnolia() { super(0,0); } Magnolia(int a, int h) { super(a,h); } public void jeGrandis(int l, int h, int periode) { System.out.println("le magnolia dit: "); System.out.println(lumiere[l]+" "); System.out.println(humidite[h]); if (periode==1 && age>1 && age>6) jeDonneDesFleurs(); age++; } public void jeDonneDesFleurs() { System.out.println("Les Magnolias fleurissent"); } }

319

320

L’orienté objet

Exercice 13.10 Retrouvez ce qu’écrirait à l’écran le code C++ suivant. Dessinez également le diagramme de classe UML correspondant. #include "stdafx.h" #include "iostream.h" class Instrument { public: Instrument() {} virtual void joue() = 0; }; class Guitare : public Instrument { public: void joue() { cout << "je fais ding ding" << endl; } }; class Trompette : public Instrument { public: void joue() { cout << "je fais pouet pouet" << endl; } }; class Tambour : public Instrument { public: void joue() { cout << "je fais badaboum" << endl; } }; class Musicien { private: Instrument* monInstrument; public: Musicien(Instrument *monInstrument) { this->monInstrument = monInstrument; } void joue() { monInstrument->joue(); } }; class Orchestre { private: Musicien *lesMusiciens[3]; int nombreMusicien;

Abstraite, cette classe est sans objet CHAPITRE 13

public: Orchestre(Musicien* lesMusiciens[3]) { for (int i=0; i<3; i++) { this->lesMusiciens[i] = lesMusiciens[i]; } } void joue() { for (int i=0; i<3; i++) { lesMusiciens[i]->joue(); } } }; int main() { Instrument* lesInstruments[10]; Musicien* lesMusiciens[8]; lesInstruments[0] lesInstruments[1] lesInstruments[2] lesInstruments[3] lesInstruments[4] lesInstruments[5] lesInstruments[6] lesInstruments[7] lesInstruments[8] lesInstruments[9] lesMusiciens[0] lesMusiciens[1] lesMusiciens[2] lesMusiciens[3] lesMusiciens[4] lesMusiciens[5] lesMusiciens[6] lesMusiciens[7]

= = = = = = = = = = = = = = = = = =

new new new new new new new new new new new new new new new new new new

Guitare(); Guitare(); Trompette(); Trompette(); Guitare(); Tambour(); Tambour(); Tambour(); Trompette(); Guitare(); Musicien(lesInstruments[2]); Musicien(lesInstruments[5]); Musicien(lesInstruments[0]); Musicien(lesInstruments[9]); Musicien(lesInstruments[9]); Musicien(lesInstruments[4]); Musicien(lesInstruments[2]); Musicien(lesInstruments[1]);

Musicien* lesMusiciensDOrchestre[3]; lesMusiciensDOrchestre[0] = lesMusiciens[2]; lesMusiciensDOrchestre[1] = lesMusiciens[5]; lesMusiciensDOrchestre[2] = lesMusiciens[3]; Orchestre *unOrchestre unOrchestre->joue(); return 0; }

= new Orchestre(lesMusiciensDOrchestre);

321

322

L’orienté objet

Exercice 13.11 Corrigez le code des classes A et B pour que le code compile et que son exécution affiche à l’écran : « 1, 2, trois, quatre ». Expliquez et corrigez les erreurs à même le code. abstract class A extends Object { private int a,b ; private String c ; public A(int a,int b, String c) { super() ; this.a=a ; this.b=b ; this.c=c ; } public abstract void decrisToi() ; }

class B extends A { private String d ; public B(int a, int b, String c, String d) { this.a=a ; this.b=b ; this.c=c ; this.d=d ; } } public class Correction1 { public static void main(String[] args) { B b=new B(1,2,"trois","quatre") ; b.decrisToi() ; } }

Exercice 13.12 Dans le code C++ qui suit, seuls la classe C et le main contiennent des erreurs. Supprimez-les sans altérer les fonctionnalités du code et indiquez ce que ce code écrirait dans sa version correcte. #include "stdafx.h" #include "iostream.h" class A { private: int a ;

Abstraite, cette classe est sans objet CHAPITRE 13

public: A(int a) { this->a=a ; } int getA() { return a ; } virtual void action(){ cout << "je travaille" << endl ; } virtual void actionA() = 0 ; void actionA2() { cout << "je travaille pour A et A" << endl ; } } ; class B : A { private: int a,b ; public: B(int a, int b): A(a) { this->a=a ; this->b = b ; } void action(){ actionA() ; cout << "je ne travaille pas" << endl ; } virtual void actionA(){ cout << "je travaille pour A" << endl ; } void actionA2() { cout << "je travaille pour A et A" << endl ; } } ; class C : public A,B { public: C(int a, int b):A(a),B(a,b) {} void action() { cout << getA() << "je travaille pour C"<
323

324

L’orienté objet

int main() { A *a = new A(1) ; A *c1 = new C(1,2) ; B c2 ; c1->action() ; c2.action() ; return 0 ; }

14 Clonage, comparaison et assignation d’objets Ce chapitre va nous permettre, par l’entremise de la super-superclasse Object en Java, Python et en C# et, différemment en C++ et PHP 5, de comprendre comment l’installation en mémoire des objets et la définition de leurs relations aux autres objets sont déterminantes lors du clonage de ces objets, lors de leur comparaison deux à deux, et lors de l’affectation de l’un d’entre eux à un autre.

Sommaire : La classe Object — Cloner un objet — Le test d’égalité d’objet deux à deux — Affecter un objet à un autre — La surcharge d’opérateur en C++ et en C# — Traitement en surface et traitement en profondeur Candidus — Jusqu’à quel point peut-on manipuler un objet comme s’il s’agissait d’une simple valeur primitive… un entier par exemple ? Doctus — La structure d’un objet étant plus complexe, tu devras décider ce qui pourra déterminer que deux objets sont égaux. Cand. — Qu’une voiture en vaut une autre ou qu’un animal en vaut un autre, par exemple… Doc. — Tu vois que ça n’a rien d’évident, c’est une question de goûts et de couleurs ! Mais c’est bien par là qu’il faut aborder le problème : les attributs. Il s’agit de bien choisir ceux qu’il faut considérer pour évaluer l’équivalence de deux objets. Cand. — Je vois : mais quitte à choisir les attributs à comparer deux à deux, pourquoi ne pas les prendre tous ? Et le tour est joué ! Doc. — Mais non, car que feras-tu en présence de deux objets composites ? Te suffira-t-il que deux voitures possèdent un moteur et quatre roues pour les déclarer égales ? Cand. — Ah ! Je vois le hic. Il faudra donc ouvrir le capot pour voir si un des moteurs n’a pas fait trois tours de compteur alors que l’autre est tout neuf ou que l’un a un turbo et pas l’autre… Doc. — Autre chose. Lorsque tu vas copier un objet, il te faudra distinguer parfaitement la copie, la représentation de cet objet mémoire étant différente d’un langage à l’autre. Cand. — Alors là, je sens que ça se complique. Les objets, c’est pas si simple en fait. Doc. — Non, mais c’est presque simple. Ils savent se cloner. Ils héritent ça de leur grand-mère !

326

L’orienté objet

Cand. — D’accord, mais tu me disais qu’il faudra que je m’en mêle pour qu’ils fassent ça comme il faut… Doc. — Tu devras en effet leur indiquer ce que tu considères équivalent pour guider leur duplication. Cand. — C’est moi le maître des clones !

Introduction à la classe Object Si, comme tout membre du règne humain, auquel ils appartiennent malgré leur boulimie de pizza, les informaticiens sont partagés sur l’existence d’une entité spirituelle supérieure, aucun praticien de Java ne doute de l’existence de la super-superclasse, « Ze Klass ». Le sommet de la hiérarchie, la classe des classes, s’appelle en Java, de façon très finaude, la classe Object. Les concepteurs de ce langage ont dû trop faire la java la veille du jour décisif, quand il a été décidé du nom de cette classe. Notez dans la série, qu’il existe également une classe class en Java dont les objets sont en fait les classes de tous les objets. Insensé non ? Quand on vous disait qu’ils n’étaient pas tout à fait « nets » chez Sun, au point que .Net a décidé de considérablement revoir cette terminologie. Toutes classes, quelles qu’elles soient, les vôtres comme les nôtres, comme celles de Java, héritent de la classe Object. Ce qui nous amène à dire, ce qui pourrait sembler comme la dernière des tautologies, doublée de la première des lapalissades, et pire encore, qu’en Java : tous les objets sont des « Objects ». Il faut croire que ce choix en a convaincu plus d’un sur les vertus de la java, car les concepteurs de C# et Python n’ont pas hésité une seconde pour nommer de façon semblable leur première de la classe. En C#, non seulement toutes les classes, mais également toutes les structures, héritent de la classe Object. En Python, il la trouve également suprême, mais pas au point de lui décerner la majuscule (tous ces langages différencient bien évidemment minuscules et majuscules) et cette superclasse a pour petit nom object. Elle n’existe ni en C++ ni en PHP 5. Python et son papa Guido Van Rossum Python est très souvent plébiscité pour la simplicité de son fonctionnement, ce qui en ferait un langage de choix pour l’apprentissage de la programmation. La programmation est, de fait, très interactive (vous tapez « 1+1 » à l’écran et comme par magie « 2 » apparaît). On arriverait par des écritures plus simples et plus intuitives, donc plus rapidement, au résultat escompté. Si print "Hello World" est incontestablement plus simple à écrire que public static void main (String[] args) {System.out.println "Hello World"}, nous restons un peu sceptiques quant à l’extension de cette même simplicité à la totalité de la syntaxe. Python reste un langage puissant car il est à la fois OO et procédural. Pour l’essentiel, il n’a rien à envier à des langages comme Java ou C+ + et exige donc de maîtriser, comme pour ceux-ci, toutes les bases logiques de la programmation afin de parvenir à des codes d’une certaine sophistication. D’où notre réserve. Il est incontestable, et c’est d’ailleurs la raison de son apparition dans la nouvelle édition de ce livre, que sa popularité n’a cessé de croître au cours des ans, et que de nombreuses solutions logicielles dans le monde Linux ou Microsoft y ont de plus en plus souvent recours. Un Python.Net est sur le point d’éclore. Ce langage de programmation veut préserver la simplicité qui caractérise les langages de script interprétés plutôt que compilés (les exécutions sont traduites dans un bytecode intermédiaire et s’exécutent au fur et à mesure qu’elles sont rencontrées) grâce à son aspect multi-plates-formes, à l’absence de typage ou à la présence de structures de données très simples d’emploi (listes, dictionnaires et chaînes de caractères). Il souhaite en outre préserver toutes les fonctionnalités qui caractérisent les langages puissants (OO, ramasse-miettes, héritage multiple et librairies d’utilitaires très fournies, comme nous le verrons dans les chapitres qui suivent). Une telle hybridation, pour autant qu’elle soit réussie, en ferait un langage de tout premier choix pour le prototypage de projet, quitte à revenir par la suite à des langages plus rapides pour la phase finale du projet (comme C++ et Java, avec lequels, d’ailleurs, Python se couple parfaitement : il peut hériter ou spécialiser des classes Java ou C++). Une telle hybridation est-elle possible ? En quoi cette motivation serait-elle innovante par rapport à celle qui a présidé à la création de Java et C#. Ce mélange idéal entre puissance fonctionnelle et simplicité d’usage n’est-il pas le Graal dont tous les langages de programmation d’aujourd’hui sont en quête ?

Clonage, comparaison et assignation d’objets CHAPITRE 14

327

Python, écrit en C et exécutable sur toutes les plates-formes, est développé par les Python Labs de Zope Corporation qui comprennent une demi-douzaine de développeurs. Ce noyau est dirigé par Guido Van Rossum, inventeur et « superviseur » du langage, qui se targue de porter le titre très ambigu de Dictateur Bénévole à Vie. Toute proposition de modification du langage est débattue par le noyau et soumis à la communauté Python, mais la prise en compte de celle-ci dans le langage reste la prérogative de Guido, qui conserve le dernier mot (d’où son titre : gentil et à l’écoute… mais dictateur tout de même, ces modifications n’étant pas soumises à un vote majoritaire). Guido est hollandais d’origine, détenteur de maîtrises en mathématiques et en informatique de l’université libre d’Amsterdam. Il participa, à la fin des années 1980, à un groupe de développeurs dont le but était la mise au point d’un langage de programmation abordable par des non-experts, d’où son nom « ABC ». Dès 1991, il s’attaque à Python. (Ce nom ne doit rien à l’horrible reptile mais se réfère aux extraordinaires humoristes anglais que sont les Monthy Python, qui ont révolutionné dans les années 1970 et 1980 et l’humour et la télévision, et dont l’humour nous manque tellement aujourd’hui, d’où la nécessité de les remplacer par un jumeau ouvert sur le Web et la programmation.) Guido Van Rossum travaille alors au CWI (Centrum voor Wiskunde en Informatica). En 1995, il prend la direction des États-Unis, comme tout bon informaticien qui se respecte et qui veut percer, et travaille pour le CNRI (Corporation for National Research Initiatives) jusqu’en 2000. À cette époque, il publie Computer Programming for Everybody, sa profession de foi pour l’enseignement de la programmation. C’est également à cette époque qu’il est nommé directeur des Python Labs de Zope Corporation. Depuis 2005, il travaille pour Google, entreprise connue pour son immense succès commercial, et qui semble investir beaucoup dans le langage Python. La communauté Python reste très structurée autour d’un seul site Web : http://www.python.org, ce qui s’avère un atout considérable, surtout lors de l’apprentissage et de la découverte du langage. À quelques subilités près, Python est dans le prolongement de l’aventure Open Source, dont le représentant le plus emblématique reste Linux. Il pourrait devenir le langage de programmation phare de l’Open Source, tout comme Linux est celui de l’OS. Toutes les sources restent accessibles et disponibles, monsieur-tout-le-monde peut participer à l’évolution du produit, mais la prise en charge officielle de ces évolutions reste sous la responsabilité de quelques-uns (ici, un seul). Cela n’est évidemment pas le cas de Java (propriété Sun) et C# (propriété Microsoft), ni même du C++, dont le contrôle s’est quelque peu éparpillé. Quant aux références pour aborder ce langage, le site officiel de Python reste une source idéale. Des livres, essentiellement publiés par les éditions O’Reilly, sont disponibles, parmi lesquels : – Apprendre à programmer avec Python, de GÉRARD SWINNEN : très introductif et très axé procédural ; – Programming Python, de MARK LUTZ : pour une présentation très complète ; – Python en concentré – Manuel de référence (ce qu’il fut indéniablement pour l’écriture de notre livre), d’ALEX MARTELLI.

Une classe à compétence universelle Une telle classe présente ce premier avantage que vous pouvez l’utiliser comme argument ou type de retour d’une méthode, que vous souhaitez à compétence universelle (pouvant s’opérer sur toute sorte d’objet), méthode que vous pourrez, par la suite, spécialiser selon le type d’objet en question. Cette classe Object est donc, le plus souvent, candidate à une utilisation de type « universelle ». Vous pouvez l’utiliser quand vous concevez un type de structure particulière, qui concerne tous les objets sans distinction de classe, comme une liste liée ou un tableau extensible. En Java et C #, toute une panoplie de classes collections fait largement usage de la classe Object. Les exemples les plus connus en sont les classes Vector et ArrrayList, tableaux extensibles, qui peuvent contenir un nombre indéterminé d’objets de toute classe. Par exemple, les méthodes de ce Vector les plus usitées sont addElement(Object unObjet), qui permet de rajouter n’importe quel type d’objet à la fin de ce Vector, et Object elementAt(int i), méthode qui renvoie l’objet positionné à la « énième position » du Vector. Comme vous le voyez, c’est bien le type Object que l’on retrouve dans la définition de ces méthodes. C’est normal, car rien dans la fonctionnalité de ce Vector n’exige de connaître le type particulier de ce qui y est

328

L’orienté objet

contenu. Les Vectors étant généralement composés d’objets de classe quelconque, l’utilisation de la méthode elementAt(int i) est, presque toujours, accompagnée d’un « casting », afin de récupérer les caractéristiques qui sont propres à l’objet extrait du Vector. Un exemple d’utilisation de la classe Vector est donné ci-après. Nous y découvrons également pourquoi et comment la possibilité, depuis les dernières versions de Java et de .Net, de typer ces collections, permet d’éviter l’utilisation (fort décriée nous l’avons vu précédemment) du « casting ». Comme nous le verrons aussi, les objets se nomment et se clonent tous, mais tous peuvent le faire d’une manière qui leur est particulière.

Code Java illustrant l’utilisation de la classe Vector et innovation de Java 5 import java.util.*; class O1 { public void jeTravaillePourO1() { System.out.println("Salut, je travaille pour O1"); } } class O2 { public void jeTravaillePourO2() { System.out.println("Salut, je travaille pour O2"); } } public class TestVector { public static void main(String[] args) { Vector unVecteur = new Vector() ; /* dans le nouveau Java, vous pouvez créer : ➥Vector unVecteur = new Vector */ unVecteur.addElement(new O2()); if (unVecteur.elementAt(0) instanceof O1) ((O1)unVecteur.elementAt(0)).jeTravaillePourO1(); /* il faut " caster " pour récupérer ➥le bon type, mais plus dans la version nouvelle du langage */ if (unVecteur.elementAt(1) instanceof O2) ((O2)unVecteur.elementAt(1)).jeTravaillePourO2(); } }

Résultat Salut, je travaille pour O1 Salut, je travaille pour O2

Nouvelle version du code import java.util.*; class O1 { public void jeTravaillePourO1() { System.out.println("Salut, je travaille pour O1"); } }

Clonage, comparaison et assignation d’objets CHAPITRE 14

329

class O2 { public void jeTravaillePourO2() { System.out.println("Salut, je travaille pour O2"); } } public class TestVector2 { public static void main(String[] args) { Vector unVecteur = new Vector(); // depuis le nouveau Java unVecteur.add(new O1()); // « unVecteur.add(new O2()); » n’est plus possible unVecteur.elementAt(0).jeTravaillePourO1(); // Plus besoin de caster !!!! } }

Bien sûr, s’il n’est plus besoin de caster, il devient impossible d’utiliser ce même vecteur pour des objets de type différent (sauf si issus d’une même superclasse). Dans notre exemple, il n’est plus possible d’insérer des objets de la classe O1 et O2 dans le même Vector.

Décortiquons la classe Object Voici maintenant la classe Object, telle qu’elle est spécifiée par Sun. Onze méthodes y sont prédéfinies, représentatives de la compétence universelle de chaque objet en Java. public class Object { private static native void registerNatives(); static { registerNatives(); } public final native Class getClass(); public native int hashCode(); public boolean equals(Object obj) { return (this == obj); } protected native Object clone() throws CloneNotSupportedException; public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) throw new IllegalArgumentException("timeout value is negative"); if (nanos < 0 || nanos > 999999) throw new IllegalArgumentException( "nanosecond timeout value out of range"); if (nanos >= 500000 || (nanos != 0 && timeout == 0)) timeout++; wait(timeout); }

330

L’orienté objet

public final void wait() throws InterruptedException { wait(0); } protected void finalize() throws Throwable { } }

Décrire chacune de ces méthodes dépasserait largement le cadre de ce voyage initiatique dans le monde de l’OO ; nous devrions rentrer trop profondément dans des arcanes syntaxiques propres à Java. Ainsi, la présence du mot native dans la déclaration des méthodes signale que celles-ci sont écrites dans un autre langage de programmation, pour des raisons d’optimisation ou de proximité intime avec le fonctionnement du processeur, généralement C ou C++. Aucune instruction n’est donc présente, et la version exécutable de la méthode est déjà pré-installée dans la machine virtuelle Java, dédiée à la plate-forme que vous utilisez. De même, les méthodes notify et wait jouent un rôle clé lors de la mise en pratique du multithreading, que nous introduirons au chapitre 17. Notez finalement (et nous l’illustrerons par la suite) que les deux méthodes dont l’encapsulation est du type controversé protected sont précisément celles qui sont les plus susceptibles de subir une redéfinition dans les sous-classes, redéfinition faisant appel à la version d’origine (d’où le protected). En cela, elles font partie du SPM (Société pour la Protection des Méthodes). Penchons-nous plutôt sur ces méthodes qui aident à la compréhension des mécanismes OO, car il est intéressant de voir ce que les brillants ingénieurs de SUN considèrent comme étant des méthodes à caractère universel, méthodes pouvant être exécutées sur tous les objets informatiques qui peuplent notre galaxie. La preuve en est que certaines de ces méthodes se retrouvent, au nom près, dans la classe Object du langage C#. Nous les indiquons ci-après : public static bool Equals (Object objA , Object objB) public virtual bool Equals (Object obj) public virtual int GetHashCode () public Type GetType() public static bool ReferenceEquals(Object objA, Object objB) public virtual string ToString() protected object MemberwiseClone()

Nous avons déjà vu la méthode ToString(), qui permet à l’objet de se présenter, tout en nous renseignant sur sa classe. Des méthodes comme GetType() en C# ou GetClass() en Java permettent également un semblant d’introspection où l’objet, lui-même, peut informer celui qui le manipule sur la nature de sa classe et, par là même, obtenir toutes les informations désirées sur les méthodes ou les attributs qui caractérisent cette classe. Ainsi, il est possible de créer un nouvel objet unAutreO à partir d’un objet existant, unO, au moyen de la simple instruction : Object unAutreO = unO.getClass().newInstance(); // En Java

La classe object en Python, dont il faut, au contraire de Java et C#, explicitement hériter si l’on souhaite récupérer certaines fonctionnalités, se caractérise également par un ensemble de méthodes à compétence universelle comme __init__ pour construire n’importe quel objet et __new__ pour le construire à partir d’un objet existant. Nous avons déjà vu la méthode __str__ qui permet à l’objet un début d’introspection et de nous renseigner à l’exécution sur son typage dynamique. Toute sous-classe de object peut redéfinir ces méthodes. Mais revenons à la classe Object de Java et surtout à deux méthodes universelles qui nous intéressent plus particulièrement ; tout d’abord, la méthode : equals(Object o), qui sert à tester l’égalité de deux objets. Elle est appelée sur le premier objet, en Java, afin de le comparer au second. Elle peut être appelée de la même

Clonage, comparaison et assignation d’objets CHAPITRE 14

331

manière en C#, ou appelée en lui passant en argument les deux objets à comparer (dans ce dernier cas, elle devient légitimement static). La seconde méthode est clone() qui, en Java, permet de dupliquer un objet. Par les petits codes suivants, nous allons illustrer au mieux le fonctionnement de ces deux méthodes. Notre compréhension des problèmes de stockage et d’organisation en mémoire tas des objets devrait s’en trouver améliorée. Nous allons étudier en premier lieu la méthode equals(Object o).

Test d’égalité de deux objets Code Java pour expérimenter la méthode equals(Object o) Dans le premier programme qui suit, nous codons nos habituelles classes O1 et O2. La classe O2 possède juste un attribut entier, alors que la classe O1 possède un attribut entier et un attribut de type référent vers O2. Nous créons ensuite trois objets O2 et deux objets O1 dont nous testerons l’égalité. Dans un premier temps, plusieurs instructions seront mises en commentaire afin de les désactiver. class O1{ private int unAttributO1; private O2 lienO2; public O1(int unAttributO1, O2 lienO2){ this.unAttributO1 = unAttributO1; this.lienO2 = lienO2; } public boolean equals(Object unObjet) /* la méthode qui nous intéresse, d'abord désactivée ➥puis activée */ { if (this == unObjet) { return true; /* renvoie true si les objets sont les mêmes */ } else { if (unObjet instanceof O1) { O1 unAutreO1 = (O1)unObjet; /* effectue un " casting " */ if ((unAttributO1 == unAutreO1.unAttributO1) &&(lienO2.getAttribut() == unAutreO1.lienO2.getAttribut())){ return true; } else{ return false; } } } return false; } } class O2{ private int unAttributO2; public O2(int unAttributO2){ this.unAttributO2 = unAttributO2; }

L’orienté objet

332

public int getAttribut(){ return unAttributO2; } /* public boolean equals(Object unObjet) { if (this == unObjet) { return true; } else { if (unObjet instanceof O2) { O2 unAutreO2 = (O2)unObjet; if (unAttributO2 == unAutreO2.unAttributO2) return true; else return false; } } return false; } */ } public class CloneEqual{ public static void main(String[] args){ O2 unO2 = new O2(5); O2 unAutreO2 = new O2(5); O2 unTroisièmeO2 = new O2(10); unTroisièmeO2 = unO2; O1 unO1 = new O1(10, unO2); O1 unAutreO1 = new O1(10, unAutreO2); if (unO2 == unAutreO2) /* teste l'égalité des référents */ System.out.println("unO2 et unAutreO2 ont la même référence"); if (unO2.equals(unAutreO2)) /* sans redéfinition, teste l'égalité des référents, avec ➥redéfinition teste l'égalité des états */ System.out.println("unO2 et unAutreO2 ont le même état") ; if (unO2 == unTroisièmeO2) System.out.println("unO2 et unTroisièmeO2 ont la même référence"); if (unO2.equals(unTroisièmeO2)) System.out.println("unO2 et unTroisièmeO2 ont le même état"); if (unO1 == unAutreO1) System.out.println("unO1 et unAutreO1 ont la même référence"); if (unO1.equals(unAutreO1)) System.out.println("unO1 et unAutreO1 ont le même état"); } }

Clonage, comparaison et assignation d’objets CHAPITRE 14

333

Nous avons, dans un premier temps, désactivé la redéfinition de la méthode equals dans les classes O1 et O2. Voici le résultat du code, en l’absence de cette redéfinition : Résultat unO2 et unTroisièmeO2 ont la même référence unO2 et unTroisièmeO2 ont le même état

On comprend, au vu de ce résultat, le mode de fonctionnement par défaut de la méthode equals(), celle qui est héritée de la classe Object. En fait, cette méthode, sans redéfinition, se comporte exactement comme le double « = », opération d’égalité logique venant du C++ (à ne pas confondre avec le simple « = », qui est l’opération d’affectation). Par défaut, l’égalité porte sur les référents, c’est-à-dire les adresses des objets. Il est clair que, si les référents sont égaux, on sait que l’on pointe vers un seul et même objet, et donc l’égalité est tout à fait vérifiée. Ce test d’égalité des référents est précieux, quand la complexité du programme devient telle qu’il est nécessaire, à certaines étapes du code, de vérifier que deux référents continuent à pointer vers un même objet. Cela se produit très souvent quand les référents sont perdus dans d’immenses vecteurs ou tableaux. Ce que l’on aimerait pourtant, comme illustré dans la figure qui suit, c’est élargir cette égalité aux objets qui, bien qu’installés dans des zones mémoire différentes, possèdent un même état, c’est-à-dire des attributs ayant la même valeur. Lorsque le programme écrit « unO2 et unTroisièmeO2 ont le même état », il a raison, mais cela n’apprend rien, puisqu’il s’agit du même objet en mémoire. Or, deux objets d’une même classe, et caractérisés par un même état, pourraient légitimement être considérés comme égaux, où qu’ils se trouvent dans la mémoire. Vous pourriez, dès lors, conserver le double « = » pour le test d’égalité des référents, et redéfinir la méthode equals() pour le test d’égalité des états. Car c’est parce que Java sait que vous aurez tôt ou tard besoin ou envie de redéfinir cette nouvelle procédure de comparaison qu’il a créé et installé la méthode equals() dans la classe des classes, méthode qui ne demande qu’à être redéfinie, comme suit : public boolean equals(Object unObjet) { if (this == unObjet) { return true; } else { if (unObjet instanceof O2) { O2 unAutreO2 = (O2)unObjet; if (unAttributO2 == unAutreO2.unAttributO2) return true; else return false; } } return false; }

D’abord, on teste s’il s’agit oui ou non du même objet. Si ce n’est pas le cas, on teste si ces deux objets sont bien issus de la même classe. Enfin, dans le cas positif, on compare les valeurs des attributs deux à deux.

334

L’orienté objet

Figure 14-1

Différence entre l’égalité des référents et l’égalité des états.

Cette redéfinition des deux méthodes equals() dans les deux classes était déjà présente en commentaire. Si vous supprimez les marques de commentaire et ré-exécutez le programme vous obtenez : Résultat unO2 unO2 unO2 unO1

et et et et

unAutreO2 ont unTroisièmeO2 unTroisièmeO2 unAutreO1 ont

le même état ont la même référence ont le même état le même état

Égalité en profondeur On s’aperçoit que, bien qu’unO2 et unAutreO2 ne représentent plus physiquement le même objet, ils sont malgré tout déclarés comme égaux, car ils partagent les mêmes valeurs d’attributs. Quitte à redéfinir la méthode equals(), il est important de la redéfinir le plus « profondément » possible. En effet, deux objets seront égaux, si, avant tout, ils possèdent les mêmes valeurs attributs. Cependant, comme vous le montre l’exemple de la classe O1 et la figure qui suit, il faut que les objets qu’ils réfèrent par leur attribut de type « référent », possèdent, à leur tour, les mêmes valeurs d’attributs, et ainsi de suite, de référents en référents. Cette procédure de comparaison doit donc s’effectuer récursivement, en suivant le fil rouge des référents, et en parcourant tout le réseau relationnel des objets. Il ne faut pas seulement que les attributs soient égaux, au premier niveau, il faut également que les objets vers lesquels pointent les attributs référents soient eux-mêmes égaux. La redéfinition de la méthode equals() dans le code de la classe O1 illustre ce mécanisme de comparaison en cascade. Et c’est ainsi que, dans le résultat de l’exécution du programme, les objets unO1 et unAutreO1 sont également déclarés égaux.

Clonage, comparaison et assignation d’objets CHAPITRE 14

335

Parler de récursivité est parfaitement adéquat, car une manière plus élégante et vraiment récursive de redéfinir la méthode equals dans la classe O1 aurait été : public boolean equals(Object unObjet) /*la méthode qui nous intéresse, d'abord désactivée puis activée*/ { if (this == unObjet) { return true; //renvoie true si les objets sont les mêmes } else { if (unObjet instanceof O1) { O1 unAutreO1 = (O1)unObjet; //effectue un " casting " if ((unAttributO1 == unAutreO1.unAttributO1) &&(lienO2.equals(unAutreO1.lienO2))){ // appel vraiment récursif de “equals” return true; } else{ return false; } } } return false;

Les bonnes utilisation et redéfinition de cette méthode sont conditionnées par une compréhension adéquate des modes d’adressages et de stockage des objets en mémoire. Il en va de même de la méthode clone(), permettant de dupliquer un objet, et que nous illustrons en enrichissant le code précédent, par la possibilité de cloner les objets O1 et O2. Figure 14-2

Pour que deux objets soient égaux, il faut non seulement que leurs attributs soient égaux, mais que les objets vers lesquels ils pointent soient égaux également.

336

L’orienté objet

Le clonage d’objets Code Java pour expérimenter la méthode clone() class O1 implements Cloneable { /* implémenter l'interface Cloneable */ private int unAttributO1; private O2 lienO2; /* l'attribut référent */ public O1(int unAttributO1, O2 lienO2) { this.unAttributO1 = unAttributO1; this.lienO2 = lienO2; } public void donneAttribut() { System.out.println("valeur attribut = " + unAttributO1); } public O2 getO2() { return lienO2; } public boolean equals(Object unObjet) { if (this == unObjet) { return true; } else { if (unObjet instanceof O1) { O1 unAutreO1 = (O1)unObjet; if ((unAttributO1 == unAutreO1.unAttributO1) &&(lienO2.getAttribut()== unAutreO1.lienO2.getAttribut())) { return true; } else { return false; } } } return false; } public Object clone() throws CloneNotSupportedException { /* la méthode clone */ O1 unNouveauO1 = (O1)super.clone() ; /* copie superficielle et rappel de la version d’origine */ unNouveauO1.lienO2 = (O2)lienO2.clone(); /* copie en profondeur */ return unNouveauO1; } } class O2 implements Cloneable { private int unAttributO2; public O2(int unAttributO2) { this.unAttributO2 = unAttributO2; } public int getAttribut() { return unAttributO2; }

Clonage, comparaison et assignation d’objets CHAPITRE 14

public void setAttribut(int unAttributO2) { this.unAttributO2 = unAttributO2 ; } public boolean equals(Object unObjet) { if (this == unObjet) { return true; } else { if (unObjet instanceof O2) { O2 unAutreO2 = (O2)unObjet; if (unAttributO2 == unAutreO2.unAttributO2) return true; else return false; } } return false; } public Object clone() throws CloneNotSupportedException { /* la méthode clone */ return super.clone(); } } public class CloneEqual { public static void main(String[] args) { /* Test de la méthode equal() */ O2 unO2 = new O2(5); O2 unAutreO2 = new O2(5); O2 unTroisiemeO2 = new O2(10) ; O1 unO1 = new O1(10, unO2); O1 unAutreO1 = new O1(10, unAutreO2); if (unO2 == unAutreO2) System.out.println("unO2 et unAutreO2 ont if (unO2.equals(unAutreO2)) System.out.println("unO2 et unAutreO2 ont if (unO2 == unTroisiemeO2) System.out.println("unO2 et unTroisiemeO2 if (unO2.equals(unTroisiemeO2)) System.out.println("unO2 et unTroisiemeO2 if (unO1 == unAutreO1) System.out.println("unO1 et unAutreO1 ont if (unO1.equals(unAutreO1)) System.out.println("unO1 et unAutreO1 ont

la même référence"); le même état"); ont la même référence") ; ont le même état ") ; la même référence"); le même état");

/* Test de la méthode Clone */ O2 unQuatriemeO2 = null ; try { unQuatriemeO2 = (O2)unTroisiemeO2.clone() ; /* clonage */ } catch(Exception e) {} /* vérification de l'égalite de " unTroisiemeO2 " et " unQuatriemeO2 " */

337

L’orienté objet

338

System.out.println(unTroisiemeO2.getAttribut() + " = ? " + unQuatriemeO2.getAttribut()) ; O1 unTroisiemeO1 = null ; try { unTroisiemeO1 = (O1)unAutreO1.clone() ; } catch(Exception e) {} if (unO1.equals(unTroisiemeO1)) System.out.println("unO1 et unTroisiemeO1 ont le même état") ; unTroisiemeO1.getO2().setAttribut(7) ; /* on modifie l'état de l'objet "unTroisiemeO1" et on vérifie que cela n'affecte pas l'objet un O1 */ if (unO1.equals(unTroisiemeO1)) System.out.println("unO1 et unTroisiemeO1 ont le même état") ; else System.out.println("unO1 et unTroisiemeO1 n'ont pas le même état") ; } }

Résultat unO2 unO1 10 = unO1 unO1

et unAutreO2 ont et unAutreO1 ont ? 10 et unTroisiemeO1 et unTroisiemeO1

le même état le même état ont le même état n’ont pas le même état

Java nécessite quelques additions syntaxiques pour pouvoir cloner un objet. D’abord, il faut, par l’implémentation d’une interface particulière (le chapitre 15 sera entièrement consacré à l’implémentation d’interfaces), déclarer que les deux classes O1 et O2 peuvent être clonées. Cette addition est une simple étiquette apposée aux deux classes, nécessaire lors de l’exécution du clonage, réalisée par une méthode native en Java, donc pouvant poser problème. En effet, le clonage pouvant échouer et lever, comme vous le constatez dans la signature de la méthode clone(), une exception, Java oblige à prévoir cette exception, que vous devenez contraints et forcés de gérer, comme toute exception. Une fois ces additions effectuées, l’appel de la méthode clone() de la classe Object a pour effet de créer une copie de l’objet, attribut par attribut. Cela ne pose aucun problème, comme le résultat du code l’indique, pour la classe O2, car il suffit de dupliquer la valeur du seul attribut qu’elle possède, et de l’installer dans le clone. La redéfinition de clone() dans la classe O2 se limite, de fait, à rappeler la version de la classe Object. Mais comme cette méthode a un accès protected dans la classe Object, vous être forcés de la redéfinir en la déclarant public dans la classe O2 de manière à pouvoir l’utiliser (souvenez-vous que l’encapsulation ne peut que s’affaiblir en restriction, lors d’une redéfinition des méthodes dans les sous-classes). La présence de ce protected est de nouveau une incitation de Java à maîtriser au mieux l’utilisation du clonage par un rappel de la méthode d’origine. En effet, en matière de clonage, vous n’êtes pas à l’abri d’une surprise, et vous pourriez vous retrouver avec un petit objet aussi inattendu que Doly… Surprise il peut en effet y avoir, car la situation est de nouveau plus délicate pour O1, étant donné que deux solutions s’offrent à vous, comme l’illustre la figure suivante. Soit vous optez pour une copie superficielle de tous les attributs de l’objet O1 dans un nouvel objet, appelé dans le code unTroisièmeO1. Dans ce cas, et en ce qui concerne l’attribut de O1 référent vers l’objet O2, le nouvel objet O1, clone du premier, partagera la valeur

Clonage, comparaison et assignation d’objets CHAPITRE 14

339

de cet attribut et, simplement, se mettra également à référer le même objet O2. Mais il peut sembler préférable qu’à l’instar du clonage de l’objet O1, vous cloniez également tous les attributs référés par cet objet. De nouveau, le clonage pourrait se propager récursivement de référent à référent, de manière à reproduire, à partir d’un premier objet, tout le réseau relationnel dans lequel il s’inscrit. C’est l’option prise par le code ici, qui rajoute comme attribut référent du nouvel objet O1, un clone de cet attribut. De manière à illustrer ce mécanisme de clonage en profondeur, à la fin du programme, on modifie l’attribut de l’objet O2 pointé vers l’objet unTroisièmeO1. Si l’objet O2 était pointé deux fois par les deux objets O1, le résultat du test de comparaison serait différent. Il y a donc bien deux objets O1 et deux objets O2 distincts. Figure 14-3

Différence entre clonage en superficie et clonage en profondeur.

Comme nous l’avions déjà constaté lors de l’étude de la méthode equals(), et à nouveau ici pour le clonage, il y a plusieurs options dans la manipulation des objets, selon que l’on entraîne dans ces mêmes manipulations les objets référés ou pas. Ces deux méthodes equals et clone peuvent se prêter semblablement à une redéfinition récursive. Il sera toujours indispensable de maîtriser les conséquences que ces choix entraînent, bien que Java et C# (qui, dans les opérations que nous avons effectuées ici, lui ressemble comme deux gouttes d’eau) vous forcent la main et vous épaulent largement pendant ces manipulations. Ainsi, le « ramasse-miettes » pourra vous débarrasser d’objets maladroitement créés lors de ces manipulations.

Égalité et clonage d’objets en Python Code Python pour expérimenter l’égalité et le clonage Dans le code Python qui suit, la pratique de l’égalité d’objets est singulière et passe forcément par l’utilisation de l’opérateur ==. Toutefois, à vous de décider quel type d’égalité vous choisissez de réaliser et cela pour chaque

340

L’orienté objet

classe. Cela se fait par la définition de la méthode __eq__, qui sera automatiquement et implicitement appelée dans l’exécution du code dès que l’opération == entre deux objets est rencontrée. Il en va de même pour les méthodes __ge__, __gr__, __le__, __lt__ et __ne__, automatiquement appelées dès que sont rencontrés respectivement les opérateurs : >=, >, <=, < et !=. Nous avons, dans ce code, décidé de réaliser la version de « l’égalité d’état en profondeur ». En l’absence de définition de la méthode __eq__, la version par défaut est, comme en Java, celle de l’égalité des référents. En revanche, aucune fonctionnalité ne vous mâche la besogne pour le clonage d’objets et nous nous sommes limités à définir de toutes pièces une méthode clone (uniquement dans la classe O2), qui renvoie une nouvelle instance avec l’attribut de l’instance que l’on choisit de cloner. class O1: __unAttributO1 = 0 __lienO2 = None def __init__(self,unAttributO1,lienO2): self.__unAttributO1 = unAttributO1 self.__lienO2 = lienO2 def __eq__(self, unObjet): if isinstance(unObjet,O1) : if (self.__unAttributO1 == unObjet.__unAttributO1 and self.__lienO2.getAttribut() == unObjet.__lienO2.getAttribut()): return True else: return False else: return False

class O2: __unAttributO2 = 0 def __init__(self,unAttributO2): self.__unAttributO2 = unAttributO2 def __eq__(self, unObjet): if isinstance(unObjet,O2): if (self.__unAttributO2 == unObjet.__unAttributO2): return True else: return False else: return False def clone(self): return O2(self.__unAttributO2) def getAttribut(self): return self.__unAttributO2 unO2 = O2(5)

Clonage, comparaison et assignation d’objets CHAPITRE 14

341

unAutreO2 = O2(5) unTroisiemeO2 = O2(10) unTroisiemeO2 = unO2 unO1 = O1(10, unO2) unAutreO1 = O1(10,unAutreO2) if unO2 == unAutreO2: print "unO2 et unAutreO2 ont le meme etat" if unO2 == unTroisiemeO2: print "unO2 et unTroisiemeO2 ont le meme etat" if unO1 == unAutreO1: print "unO1 et unAutreO1 ont le meme etat" unQuatreO2 = unO2.clone() print unAutreO2.getAttribut()

Résultats unO2 et unAutreO2 ont le même état unO2 et unTroisièmeO2 ont le même état unO1 et unAutreO1 ont le même état 5

Égalité et clonage d’objets en PHP 5 Code PHP 5 pour expérimenter l’égalité et le clonage Dans l’esprit, PHP 5 qui suit est très proche des codes qui précèdent mais à nouveau en présence d’une syntaxe considérablement modifiée. Il est tout d’abord nécessaire de différencier l’égalité des référents, assurée avec un === (l’inégalité avec !==) de l’égalité des états des objets (c’est-à-dire des attributs), égalité qui sera réalisée de manière récursive (et ceci par défaut) et en présence d’un « == », l’inégalité avec !=. En ce qui concerne le clonage, il en existe par défaut une forme de clonage implicite (appelé par la syntaxe clone $object) qui recopie les attributs un à un dans le nouvel objet, mais qu’il est possible de redéfinir par la définition d’une fonction __clone() dans la classe concernée. Si celle-ci existe, c’est elle qui sera appelée lors du clônage de l’objet, comme le code qui suit l’illustre au mieux. Clonage et comparaison d'objets

Clonage et comparaison d'objets



342

L’orienté objet

public function __construct($unAttributO1, $lienO2) { $this->unAttributO1 = $unAttributO1; $this->lienO2 = $lienO2; } public function donneAttribut() { print ("valeur attribut = $this->unAttributO1
\n"); } public function getO2() { return $this->lienO2; } public function __clone() { $this->lienO2 = clone $this->lienO2; } } class O2 { private $unAttributO2; public function __construct($unAttributO2) { $this->unAttributO2 = $unAttributO2; } public function getAttribut() { return $this->unAttributO2; } public function setAttribut($unAttributO2) { $this->unAttributO2 = $unAttributO2; } } $unO2 = new O2(5); $unAutreO2 = new O2(5); $unTroisiemeO2 = new O2(10); $unO1 = new O1(10, $unO2); $unAutreO1 = new O1(10, $unAutreO2); if ($unO2 === $unAutreO2) { print ("unO2 et unAutreO2 ont } if ($unO2 == $unAutreO2) { print ("unO2 et unAutreO2 ont } if ($unO2 === $unTroisiemeO2) { print ("unO2 et unTroisiemeO2 } if ($unO2 == $unTroisiemeO2) { print ("unO2 et unTroisiemeO2 } if ($unO1 === $unAutreO1) { print ("unO1 et unAutreO1 ont }

la même référence
\n");

le même état
\n");

ont la même référence
\n");

ont le même état
\n");

la même référence
\n");

Clonage, comparaison et assignation d’objets CHAPITRE 14

343

if ($unO1 == $unAutreO1) { print ("unO1 et unAutreO1 ont le même état
\n"); } $unQuatriemeO2 = clone $unTroisiemeO2; print ($unTroisiemeO2->getAttribut()); print ($unQuatriemeO2->getAttribut()); $unTroisiemeO1 = clone $unAutreO1; if ($unO1 == $unTroisiemeO1) { print ("unO1 et unTroisiemeO1 ont le même état
\n"); } $unTroisiemeO1->getO2()->setAttribut(7); if ($unO1 == $unTroisiemeO1) { print ("unO1 et unTroisiemeO1 ont le même état
\n"); } else { print ("unO1 et unTroisiemeO1 n'ont pas le même état
\n"); } ?>

Toutes les aides présentes dans Java, C#, PHP 5 et Python, disparaissent du C++ qui, à nouveau, non seulement vous rend la vie plus compliquée, mais, pire encore, vous juge suffisamment aptes à affronter ces complications. Le test d’égalité des états d’objet et la possibilité de dupliquer des objets sont également présents dans C++. Cependant, ces différentes opérations sont à ce point attachées au mode de stockage des objets qu’il sera nécessaire, dans les codes et les explications qui suivent, d’étudier ce qui se passe en mémoire pile comme en mémoire tas. Le code qui suit s’en trouve considérablement allongé, et demande que l’on redouble d’attention par rapport à la version Java, Python ou PHP 5. Traitement en surface et en profondeur Les objets se référant mutuellement en mémoire, et constituant ainsi un graphe connecté dans cette même mémoire, il sera important, dans tout traitement qu’ils subissent, de penser à prolonger ces traitements le long du graphe ou pas, différenciant ainsi un traitement en surface d’un traitement en profondeur.

Égalité, clonage et affectation d’objets en C++ Code C++ illustrant la duplication, la comparaison et l’affectation d’objets class O2 { private: int unAttributO2; public: O2(int unAttributO2) { this->unAttributO2 = unAttributO2; } int getAttribut() { return unAttributO2; } /* constructeur par copie */

L’orienté objet

344 /*

O2(const O2& unO2) { unAttributO2 = unO2.unAttributO2; } */ O2& operator=(const O2& unO2) { /* surcharge de l'affectation */ unAttributO2 = unO2.unAttributO2; return *this; } /* déclaration de la surcharge de la comparaison */ /* friend bool operator==(const O2& unO2, const O2& unAutreO2); */ }; class O1 { private: int unAttributO1; O2* lienO2; public: O1(int unAttributO1, O2* lienO2){ this->unAttributO1= unAttributO1; this->lienO2 = lienO2; } void donneAttribut(){ cout <<"valeur attribut = " << unAttributO1 << endl; } /* constructeur par copie */ /* O1(const O1& unO1) { unAttributO1 = unO1.unAttributO1; lienO2 = new O2(*unO1.lienO2); } */ O1& operator=(const O1& unO1) { /* surcharge de l'affectation */ unAttributO1 = unO1.unAttributO1; if (lienO2) delete lienO2; lienO2 = new O2(*unO1.lienO2); return *this; } /* déclaration de la surcharge de l'opérateur de comparaison */ /* friend bool operator==(const O1& unO1, const O1& unAutreO1); */ }; /* redéfinition des opérations de comparaison */ /* bool operator==(const O2& unO2, const O2& unAutreO2) { if (unO2.unAttributO2 == unAutreO2.unAttributO2) return true; else return false; }

Clonage, comparaison et assignation d’objets CHAPITRE 14

bool operator==(const O1& unO1, const O1& unAutreO1) { if ((unO1.unAttributO1 == unAutreO1.unAttributO1) && (unO1.lienO2->getAttribut()== unAutreO1.lienO2->getAttribut()) ) return true; else return false; } */ int main(int argc, char* argv[]) { /* Test de la méthode equal() */ /* Objets créés dans le tas */ O2* unO2Tas = new O2(5); O2* unAutreO2Tas = new O2(5); O2* unTroisiemeO2Tas = new O2(10); O2* unQuatriemeO2Tas = new O2(10); *unTroisiemeO2Tas = *unO2Tas; unQuatriemeO2Tas = unO2Tas; O2* unCinquiemeO2Tas = new O2(*unO2Tas); O1* unO1Tas O1* unAutreO1Tas O1* unTroisiemeO1Tas O1* unQuatriemeO1Tas *unQuatriemeO1Tas

= = = = =

new O1(10, unO2Tas); new O1(10, unAutreO2Tas); new O1(*unAutreO1Tas); new O1(10,unO2Tas); *unAutreO1Tas;

if (unO2Tas == unAutreO2Tas) cout << "unO2Tas et unAutreO2Tas ont la même référence" << endl; if (unO2Tas == unTroisiemeO2Tas) cout << "unO2Tas et unTroisiemeO2Tas ont la meme reference" << endl; if (unO2Tas == unQuatriemeO2Tas) cout << "unO2Tas et unQuatriemeO2Tas ont la meme reference" << endl; if (unO2Tas == unCinquiemeO2Tas) cout << "unO2Tas et unCinquiemeO2Tas ont la meme reference" << endl; if (unO1Tas == unAutreO1Tas) cout << "unO1Tas et unAutreO1Tas ont la même référence" << endl; if (unAutreO1Tas == unTroisiemeO1Tas) cout << "unO1AutreTas et unTroisièmeO1Tas ont la même référence" << endl; if (unAutreO1Tas == unQuatriemeO1Tas) cout << "unO1AutreTas et unQuatriemeO1Tas ont la même référence" << endl; if (*unO2Tas == *unAutreO2Tas) cout << "unO2Tas et unAutreO2Tas ont le meme etat" << endl; if (*unO2Tas == *unTroisiemeO2Tas) cout << "unO2Tas et unTroisiemeO2Tas ont le meme etat" << endl; if (*unO2Tas == *unQuatriemeO2Tas) cout << "unO2Tas et unQuatriemeO2Tas ont le meme etat" << endl; if (*unO2Tas == *unCinquiemeO2Tas) cout << "unO2Tas et unCinquiemeO2Tas ont le meme etat" << endl;

345

346

L’orienté objet

if (*unO1Tas == *unAutreO1Tas) cout << "unO1Tas et unAutreO1Tas ont le meme etat" << endl; if (*unAutreO1Tas == *unTroisiemeO1Tas) cout << "unAutreO1Tas et unTroisiemeO1Tas ont le meme etat" << endl; if (*unAutreO1Tas == *unQuatriemeO1Tas) cout << "unAutreO1Tas et unQuatriemeO1Tas ont le meme etat" << endl; /* Objets créés dans la O2 unO2Pile = O2 unAutreO2Pile = O2 unTroisiemeO2Pile = O2 &unQuatriemeO2Pile = unTroisiemeO2Pile = O2 unCinquiemeO2Pile =

pile */ O2(5); O2(5); O2(10); unO2Pile; unO2Pile; O2(unO2Pile);

O1 unO1Pile O1 unAutreO1Pile O1 unTroisiemeO1Pile O1 unQuatriemeO1Pile unQuatriemeO1Pile

O1(10, &unO2Pile); O1(10, &unAutreO2Pile); O1(unAutreO1Pile); O1(10, &unO2Pile); unAutreO1Pile;

= = = = =

if (&unO2Pile == &unAutreO2Pile) cout << "unO2Pile et unAutreO2Pile ont la meme reference" << endl; if (&unO2Pile == &unTroisiemeO2Pile) cout << "unO2Pile et unTroisiemeO2Pile ont la meme reference" << endl; if (&unO2Pile == &unQuatriemeO2Pile) cout << "unO2Pile et unQuatriemeO2Pile ont la meme reference" << endl; if (&unO2Pile == &unCinquiemeO2Pile) cout << "unO2Pile et unCinquiemeO2Pile ont la meme reference" << endl; if (&unO1Pile == &unAutreO1Pile) cout << "unO1Pile et unAutreO1Pile ont la meme reference" << endl; if (&unAutreO1Pile == &unTroisiemeO1Pile) cout << "unO1AutrePile et unTroisiemeO1Pile ont la meme reference" << endl; if (&unAutreO1Pile == &unQuatriemeO1Pile) cout << "unO1AutrePile et unQuatriemeO1Pile ont la meme reference" << endl; if (unO2Pile == unAutreO2Pile) cout << "unO2Pile et unAutreO2Pile ont le meme etat" << endl; if (unO2Pile == unTroisiemeO2Pile) cout << "unO2Pile et unTroisiemeO2Pile ont le meme etat" << endl; if (unO2Pile == unQuatriemeO2Pile) cout << "unO2Pile et unQuatriemeO2Pile ont le meme etat" << endl; if (unO2Pile == unCinquiemeO2Pile) cout << "unO2Pile et unCinquiemeO2Pile ont le meme etat" << endl; if (unO1Pile == unAutreO1Pile) cout << "unO1Pile et unAutreO1Pile ont le meme etat" << endl; if (unAutreO1Pile == unTroisiemeO1Pile) cout << "unAutreO1Pile et unTroisiemeO1Pile ont le meme etat" << endl; if (unAutreO1Pile == unQuatriemeO1Pile) cout << "unAutreO1Pile et unQuatriemeO1Pile ont le meme etat" << endl; return 0; }

Clonage, comparaison et assignation d’objets CHAPITRE 14

347

Nous allons décrire ce code ligne par ligne. Initialement, de nombreuses instructions seront maintenues en commentaire, que nous ré-activerons au fur et à mesure de leur justification. D’abord, la classe O2 a, comme à l’habitude, son attribut, son constructeur et sa méthode d’accès. Les trois méthodes qui suivent sont désactivées pour l’instant. Il s’agit du constructeur par copie, qui participe à la duplication des objets et joue un rôle équivalent à la méthode clone() de Java. Ensuite, nous trouvons la surcharge de l’opérateur d’affectation, qui permettra de réaliser l’assignation d’un objet dans un autre (ce qui requerra également une duplication de l’objet assigné et une destruction en partie de l’objet affecté). Finalement, nous trouvons la surcharge de l’opérateur de comparaison ==, de manière à lui permettre de comparer des objets entre eux (rôle équivalent à la méthode equals() en Java). Une différence importante de C++ par rapport à Java est la mise en œuvre du mécanisme de surcharge d’opérateur (mécanisme qui existe également en C# et est très proche de son mode d’emploi en C++ ; nous verrons à la fin du chapitre comment C# joint la pratique de Java à celle de C++) pour réaliser l’égalité et l’assignation d’un objet dans un autre. Surcharge d’opérateur La surcharge d’opérateur consiste en général à simplement étendre la portée des opérateurs unaires (tel « ++ ») ou binaires (telle l’addition) à de nouveaux types, par exemple, de nouvelles classes. Ces opérateurs sont généralement prédéfinis pour des types primitifs. On peut, par défaut, additionner et comparer des entiers ou des réels entre eux, mais C++ vous offre la possibilité de comparer et d’additionner des fleurs, des animaux, des proies, des footballeurs, pour autant que vous surchargiez les opérateurs correspondants pour ces nouveaux types. Ainsi, vous pourriez définir l’addition de deux fleurs comme l’obtention d’une nouvelle fleur possédant un nombre de pétales égal à l’addition des deux fleurs, ou celle de deux footballeurs, comme l’obtention d’un troisième, aussi absurde cela soit-il, ayant comme numéro ou comme QI la somme des deux autres. En fait, la surcharge d’opérateur est un jeu d’écriture, sous la responsabilité du compilateur, qui traduira dans une forme d’utilisation classique l’emploi d’un de ces opérateurs dans un contexte nouveau. Nous verrons le comment et le pourquoi de la surcharge des opérateurs de comparaison (==) et d’affectation (=) dans la suite.

Traitons d’abord la mémoire tas La classe O1 ressemble à la classe O2, si ce n’est l’addition d’un référent vers un objet O2. De nouveau, on retrouve le constructeur par copie et la surcharge des opérateurs d’affectation et de comparaison. Passons maintenant à la partie principale, l’intérieur de la fonction main(). Tout d’abord, neuf objets sont créés dans la mémoire tas, cinq objets de la classe O2 et quatre objets de la classe O1. Les quatre premiers objets O2 sont créés sans surprise. Dans l’instruction suivante, *unTroisiemeO2Tas = *unO2Tas, le premier objet, dé-référé, c’est-à-dire l’objet vraiment, pas son adresse, est assigné au troisième, comme la figure suivante l’illustre. Il y a, en conséquence, un clonage du premier objet qui s’opère, de manière à l’installer dans la mémoire, préalablement affectée au troisième. L’ancien objet, unTroisiemeO2Tas, a complètement disparu pour reproduire l’objet unO2Tas. Rien de bien particulier à signaler, car l’assignation d’un objet dans un autre se fait comme pour n’importe quelle variable dans n’importe quel langage de programmation (figure 14-4). Dans l’instruction suivante, unQuatriemeO2Tas = unO2Tas, une autre affectation s’opère mais, cette fois- ci, ce sont les référents qui sont concernés et non plus les objets à proprement parler. Dorénavant, deux référents pointeront vers le même objet : unQuatrièmeO2Tas et un O2Tas. La figure précédente illustre la différence que présentent ces deux types d’assignation. Le dernier type est de façon classique celui que l’on rencontre pour les objets en Java et en C#. Dans ce cas, un objet, en l’absence de son référent, est en perdition en mémoire, et ne demande qu’à être récupéré par un « ramasse-miettes », malheureusement inexistant en C+ +.

348

L’orienté objet

Figure 14-4

Assignation de l’objet unO2Tas dans l’objet unTroisiemeO2Tas, et du référent de l’objet unO2Tas dans le référent de l’objet unQuatriemeO2Tas.

L’instruction suivante, O2* unCinquiemeO2Tas = new O2(*unO2Tas), crée un nouvel objet, en utilisant le constructeur par copie de la classe O2. Le constructeur par copie sert donc, à partir d’un référent vers un premier objet, ici unO2Tas, à définir ce qu’il faut récupérer dans le premier pour le transmettre au second, ici unCinquiemeO2Tas. Il est obligatoire que ce constructeur par copie reçoive un référent comme argument car, s’il recevait un objet, il faudrait copier ce même objet, ce qui provoquerait un nouvel appel au constructeur par copie, et ainsi de suite ad vitam aeternam (même en latin, on préfère éviter cela en informatique). Le constructeur par copie, dont le rôle et le fonctionnement ne vont pas sans rappeler l’opérateur d’assignation, intervient principalement lors du passage d’un objet par argument dans une méthode quelconque, puisque à chaque appel de la méthode concernée un objet sera dupliqué. Les deux instructions suivantes créent deux objets O1 en mémoire tas, en leur passant comme argument les référents vers deux objets O2. L’instruction suivante crée unTroisièmeO1 en appelant à nouveau le constructeur par copie, mais sur le référent de l’objet unAutreO1Tas cette fois. Finalement, un quatrième objet O1 est créé, mais auquel est assigné unAutreO1Tas à la place. Les pointeurs étant dé-référés, ce sont de nouveaux les objets et non les référents qui sont concernés par cette assignation.

Surcharge de l’opérateur d’affectation O1& operator=(const O1& unO1) { // surcharge de l’affectation unAttributO1 = unO1.unAttributO1; if (lienO2) delete lienO2;

Clonage, comparaison et assignation d’objets CHAPITRE 14

349

lienO2 = new O2(*unO1.lienO2); return *this; }

Le petit code qui précède reprend la surcharge de l’opérateur d’affectation ou d’assignation, lorsqu’il porte sur les objets eux-mêmes, et non leur référent . Pour la classe O1, il s’agit tout d’abord de récupérer la valeur de l’attribut unAttributO1. Ensuite, il faut dupliquer l’objet O2 pointé vers l’attribut référent et l’installer dans l’objet assigné. Une étape importante est l’effacement de l’ancien objet référé par l’attribut référent. Étant donné que ce référent se mettra à pointer sur le nouvel objet O2, et en l’absence de tout ramasse-miettes, il est important d’effacer l’ancien objet O2 qui n’est plus référé par personne.

Comparaisons d’objets Après la création de ces neuf objets, le code se lance dans des comparaisons de ces objets deux à deux. Les 7 premières comparaisons se font sur les référents. Cela ne pose aucun problème pour le compilateur car, comme les référents sont des adresses, il n’y a rien de gênant à les comparer deux à deux. Ces comparaisons se révéleront vraies à chaque fois que les deux référents pointeront vers un même objet. En revanche, les sept comparaisons qui suivent seront refusées par le compilateur (c’est pour cette raison que nous les plaçons en commentaire dans un premier temps). En effet, il s’agit maintenant de comparer, non plus des référents, mais des objets, et l’opérateur de comparaison n’est pas initialement prévu pour cela. On comprend, dès lors, que la seule façon d’éviter le joug du compilateur est de surcharger l’opérateur de comparaison pour les objets issus des classes O2 et O1.

La mémoire pile Les neuf objets suivants sont créés dans la mémoire pile, mais de manière semblable à la création des objets tas. On retrouve de même les opérations de comparaison, d’abord entre les référents, ce que le compilateur accepte, ensuite entre les objets eux-mêmes, ce que le compilateur refuse à nouveau, sans surcharge de l’opérateur de comparaison. À ce stade, le résultat de l’exécution est le suivant unO2Tas et unQuatriemeO2Tas ont la même référence unO2Pile et unQuatriemeO2Pile ont la même référence

En effet, le premier objet O2 et le quatrième partagent le même référent. Toutes les autres comparaisons sont, soit fausses, soit inactives, en l’absence de surcharge de l’opérateur de comparaison. Nous allons de ce pas le surcharger…

Surcharge de l’opérateur de comparaison Comme indiqué dans le code, tant pour la classe O1 que la classe O2, l’opération se fait en deux temps. D’abord, a lieu la surcharge de l’opérateur en dehors des deux classes. La syntaxe de la signature, quelque peu alambiquée, doit telle qu’elle est faciliter par la suite le travail de récriture du compilateur : bool operator==(const O2& unO2, const O2& unAutreO2){ if (unO2.unAttributO2 == unAutreO2.unAttributO2) return true;

350

L’orienté objet

else return false; } bool operator==(const O1& unO1, const O1& unAutreO1){ if ( (unO1.unAttributO1 == unAutreO1.unAttributO1) && (unO1.lienO2 != unAutreO1.lienO2) && (unO1.lienO2->getAttribut() == unAutreO1.lienO2->getAttribut()) ) return true; else return false; }

Comme cette opération de surcharge doit avoir accès aux attributs des classes O1 et O2, déclarés private, une manière élégante d’autoriser cet accès est de déclarer la procédure de surcharge friend de la classe. C’est la raison de la présence de la signature de la surcharge, précédée du mot-clé friend, dans les deux classes. Au même titre que les classes, des fonctions définies en dehors de toute classe (ce que C++ est le seul parmi les trois langages à permettre), peuvent être déclarées comme friend, si elles désirent un accès privilégié aux caractéristiques privées de ces classes. Vous pourrez constater que la comparaison, dans le cas de la classe O1, porte, non seulement sur son attribut, mais également sur l’attribut de l’objet référé. Les deux objets auront le même état si, bien qu’ils pointent vers des objets O2 différents, toutes les valeurs d’attribut sont égales. Voyons le résultat, en réalisant la surcharge de l’opérateur de comparaison, autorisant, de ce fait, les comparaisons d’objet, que le compilateur nous laisse maintenant passer sans coup férir. Résultat du code en rendant les comparaisons d’objet possible unO2Tas et unQuatriemeO2Tas ont la même référence unO2Tas et unAutreO2Tas ont le même état unO2Tas et unTroisiemeO2Tas ont le même état unO2Tas et unQuatriemeO2Tas ont le même état unO2Tas et unCinquiemeO2Tas ont le même état unO1Tas et unAutreO1Tas ont le même état unO2Pile et unQuatriemeO2Pile ont la même référence unO2Pile et unAutreO2Pile ont le même état unO2Pile et unTroisiemeO2Pile ont le même état unO2Pile et unQuatriemeO2Pile ont le même état unO2Pile et unCinquiemeO2Pile ont le même état unO1Pile et unAutreO1Pile ont le même état

Que constatons-nous ? Quel que soit leur mode de création, par assignation ou par constructeur par copie, les objets O2 partagent le même état. Cela prouve qu’il existe, par défaut dans toute classe en C++, un constructeur par copie, qui se borne à recopier tous les attributs d’un objet à l’autre, et une opération d’affectation par défaut qui fait de même. Nous constatons, également, que les grands absents de ces comparaisons sont les objets unAutreO1 et unTroisiemeO1, et unAutreO1 et unQuatriemeO1. À l’instar de ce qui se passait en Java pour la méthode clone(), cette comparaison et cette affectation par défaut ne permettent qu’un traitement en surface des objets. Les valeurs d’attributs sont dupliquées mais, dès le moment où un de ces attributs réfère une autre classe, il est important de se préoccuper de la duplication ou non des objets référés. D’autant qu’en C++, cela pourra vous éviter de mauvaise surprise, si vous effacez le référent dans un des objets, en oubliant qu’il est encore et toujours référé par l’objet affecté ou l’objet affectant. Ce n’est pas possible en Java, vu la présence heureuse du « ramasse-miettes », mais cela peut faire

Clonage, comparaison et assignation d’objets CHAPITRE 14

351

énormément de dégâts en C++, dégâts dont la probabilité croît avec la taille du logiciel et la difficulté qu’il y a à suivre à la trace de multiples référents.

Dernière étape Pour conclure avec le C++, nous allons enlever les derniers commentaires du code, ce qui revient à surcharger le constructeur par copie et l’opérateur d’affectation. Ces opérations de surcharge sont tellement répandues, sinon automatisées, que dans de nombreux générateurs de code automatique, à partir d’UML par exemple, elles sont ajoutées systématiquement dans le squelette de code C++ produit. On parle alors de la définition d’une « classe canonique ». Ci-après apparaît le code C++ de la classe O1, tel qu’il est automatiquement généré par Rational Rose. On y trouve un squelette de constructeur, de destructeur, mais tout est aussi en place pour pourvoir à la surcharge des opérateurs d’assignation et de comparaison (les deux comparaisons s’y trouvent, == et != qui signifie « non égal » ).

Code C++ de la classe O1 généré automatiquement par Rational Rose //## begin module.cm preserve=no // %X% %Q% %Z% %W% //## end module.cm //## begin module.cp preserve=no //## end module.cp //## Module: O1; Pseudo Package body //## Subsystem: //## Source file: C:\Program Files\Rational\Rational Rose C++ Demo 4.0\O1.cpp //## begin module.additionalIncludes preserve=no //## end module.additionalIncludes //## begin module.includes preserve=yes //## end module.includes // O1 #include "O1.h" //## begin module.additionalDeclarations preserve=yes //## end module.additionalDeclarations // Class O1 O1::O1() //## begin O1::O1%.hasinit preserve=no : unAttributO1(10) //## end O1::O1%.hasinit //## begin O1::O1%.initialization preserve=yes //## end O1::O1%.initialization { //## begin O1::O1%.body preserve=yes //## end O1::O1%.body } O1::O1(const O1 &right) //## begin O1::O1%copy.hasinit preserve=no : unAttributO1(10) //## end O1::O1%copy.hasinit //## begin O1::O1%copy.initialization preserve=yes //## end O1::O1%copy.initialization {

352

L’orienté objet

//## begin O1::O1%copy.body preserve=yes //## end O1::O1%copy.body } O1::~O1() { //## begin O1::~O1%.body preserve=yes //## end O1::~O1%.body } const O1 & O1::operator=(const O1 &right) { //## begin O1::operator=%.body preserve=yes //## end O1::operator=%.body } int O1::operator==(const O1 &right) const { //## begin O1::operator==%.body preserve=yes //## end O1::operator==%.body } int O1::operator!=(const O1 &right) const { //## begin O1::operator!=%.body preserve=yes //## end O1::operator!=%.body } //## Other Operations (implementation) return O1::getAttribut(argtype argname) { //## begin O1::getAttribut%1021245027.body preserve=yes //## end O1::getAttribut%1021245027.body } // Additional Declarations //## begin O1.declarations preserve=yes //## end O1.declarations

Résultat final du code C++ unO2Tas et unQuatriemeO2Tas ont la même référence unO2Tas et unAutreO2Tas ont le même état unO2Tas et unTroisiemeO2Tas ont le même état unO2Tas et unQuatriemeO2Tas ont le même état unO2Tas et unCinquiemeO2Tas ont le même état unO1Tas et unAutreO1Tas ont le même état unAutreO1Tas et unTroisiemeO1Tas ont le même état unAutreO1Tas et unQuatriemeO1Tas ont le même état unO2Pile et unQuatriemeO2Pile ont la même référence unO2Pile et unAutreO2Pile ont le même état unO2Pile et unTroisiemeO2Pile ont le même état unO2Pile et unQuatriemeO2Pile ont le même état unO2Pile et unCinquiemeO2Pile ont le même état unO1Pile et unAutreO1Pile ont le même état unAutreO1Pile et unTroisiemeO1Pile ont le même état unAutreO1Pile et unQuatriemeO1Pile ont le même état

Découvrons le résultat obtenu en supprimant tous les commentaires du code. On s’aperçoit que les égalités d’état se sont étendues à présent sur et entre tous les objets O1, qu’ils soient dans la pile ou dans le tas. Tant le clonage, l’affectation que les comparaisons se font maintenant en profondeur. Cette étude fouillée des méthodes equals() et clone() de la superclasse Object, pour Java, de la version très simplifiée du Python, du rôle du

Clonage, comparaison et assignation d’objets CHAPITRE 14

353

constructeur par copie et des opérateurs de comparaison et d’affectation en C++ (C# permet au programmeur d’être mangé, comme nous allons le voir, indifféremment à la sauce Java ou à la sauce C++), avait un but principal. Il s’agit d’enfoncer le clou sur la structure relationnelle des objets en mémoire, tant dans la pile que dans le tas. Toute opération de lecture, de sauvegarde (nous verrons cette sauvegarde au chapitre 19, mais, par avance, celle-ci, également, devra prendre grand soin à la structure relationnelle des objets entre eux), d’accès et d’effacement doit prendre en considération, et ce avec un maximum de soin et de prudence, tout le réseau relationnel des objets, au travers duquel voyage une ribambelle de messages. Un tel réseau est susceptible de s’installer extrêmement vite pendant l’exécution d’un programme, ce qui fait toute la puissance de l’OO, mais également sa fragilité, dès lors que le fonctionnement de ce réseau n’est pas suffisamment compris et maîtrisé.

En C#, un cocktail de Java et de C++ Comme le code présenté ci-après l’indique, la version C# permet d’opter indifféremment pour la manière Java, en redéfinissant la méthode Equals() et en définissant la méthode Clone(), ou pour la manière C++, en surchargeant les opérateurs appropriés. Nous avons vu que C# permet de stocker des objets dans la mémoire pile, en en faisant des instances de structure plutôt que de classe. Ces objets se créent comme ceux issus des classes, mais leur processus de destruction ainsi que les mécanismes d’affectation sont très différents. Dans le code, nous gardons les classes O1 et O2 pratiquement égales à celles définies dans Java, mais nous rajoutons les structures SO1 et SO2 pour traiter les objets créés dans la pile.

Pour les structures Pour les structures, il n’y a pas lieu de s’occuper de surcharger quoi que ce soit en ce qui concerne l’assignation d’objets, puisque le fonctionnement par défaut est le seul que l’on puisse imaginer. L’assignation se fait d’office en suivant le fil des référents. Pour la comparaison, on peut soit surcharger les opérateurs de comparaison, soit redéfinir la méthode Equals() héritée de la superclasse Object. En effet, au même titre que les classes, les structures héritent également de la classe Object. C’est la seule classe dont elles peuvent hériter, les pauvres. En général, C# vous incite à favoriser la redéfinition de la méthode Equals() par rapport à la surcharge d’opérateurs. Même si vous avez déjà surchargé les opérateurs appropriés, il vous avisera de redéfinir la méthode Equals() afin de pouvoir faire fonctionner la procédure de comparaison de manière polymorphique, vu que cette méthode provient de la classe Object. Vous ne pouvez, par ailleurs, pas surcharger l’opérateur == sans surcharger également son dual, l’opérateur !=. Intéressant, non ? C# vous oblige à ne pas mourir idiot et à rester cohérent : si vous surchargez un opérateur logique, vous êtes contraint et forcé de surcharger son contraire. Le compilateur en fait son affaire.

Pour les classes Pour les classes, vous pouvez à nouveau utiliser la surcharge ou la redéfinition de la méthode Equals() pour la comparaison. En revanche, la duplication d’un objet ne peut se faire qu’à l’aide de la méthode Clone(), vu l’impossibilité de surcharger l’opérateur d’affectation ou d’assignation. Cette dernière procède d’abord à une copie superficielle de l’objet, par l’entremise de la méthode MemberwiseClone(), héritée de la superclasse Object, puis crée un clone afin de reproduire l’attribut référent. La présence de MemberwiseClone() se justitifie par l’impossibilité en C# de redéfinir une méthode définie comme protected dans la superclasse, en assignant à la version redéfinie une priorité d’accés plus large (public ici).

354

L’orienté objet

Code C# using System; struct SO1 { private int unAttributO1; private SO2 lienO2; public SO1(int unAttributO1, SO2 lienO2) { this.unAttributO1 = unAttributO1; this.lienO2 = lienO2; } public SO2 getO2() { return lienO2; } public void changeO2(int nouvelleValeur) { lienO2.setAttribut(nouvelleValeur); } public void donneAttribut() { Console.WriteLine("valeur attribut = " + unAttributO1); } public static bool operator==(SO1 unO1, SO1 unAutreO1) { /* surcharge de l'opérateur de comparaison */ if ((unO1.unAttributO1 == unAutreO1.unAttributO1) && (unO1.lienO2.getAttribut() == unAutreO1.lienO2.getAttribut()) ) return true; else return false; } public static bool operator!=(SO1 unO1, SO1 unAutreO1) { if ((unO1.unAttributO1 != unAutreO1.unAttributO1) || (unO1.lienO2.getAttribut() != unAutreO1.lienO2.getAttribut()) ) return true; else return false; } public override bool Equals(Object unObjet) { /* Redéfinition de la méthode Equals */ if (unObjet != null) { if (unObjet is SO1) { if ((unAttributO1 == ((SO1)unObjet).unAttributO1) && (lienO2.getAttribut() == ((SO1)unObjet).lienO2.getAttribut()) ) return true; else return false; } } return false; } }

Clonage, comparaison et assignation d’objets CHAPITRE 14

class O1 { private int unAttributO1; private O2 lienO2; public O1(int unAttributO1, O2 lienO2) { this.unAttributO1 = unAttributO1; this.lienO2 = lienO2; } public O2 getO2() { return lienO2; } public void donneAttribut() { Console.WriteLine("valeur attribut = " + unAttributO1); } public override bool Equals(Object unObjet) { /* redéfinition de la méthode Equals */ if (this == unObjet) { return true; } else { O1 unAutreO1 = unObjet as O1; if (unObjet != null) { if ( (unAttributO1 == unAutreO1.unAttributO1) && (lienO2.getAttribut() == unAutreO1.lienO2.getAttribut()) ) return true; else return false; } } return false; } public O1 Clone() { /* définition du clonage */ O1 unNouveauO1 = (O1)this.MemberwiseClone(); unNouveauO1.lienO2 = lienO2.Clone(); return unNouveauO1; } } struct SO2 { private int unAttributO2; public SO2(int unAttributO2) { this.unAttributO2 = unAttributO2; } public int getAttribut() { return unAttributO2; } public void setAttribut(int nouvelleValeur) { unAttributO2 = nouvelleValeur; }

355

356

L’orienté objet

public static bool operator==(SO2 unO2, SO2 unAutreO2) { /* surcharge de la comparaison*/ if (unO2.unAttributO2 == unAutreO2.unAttributO2) return true; else return false; } public static bool operator!=(SO2 unO2, SO2 unAutreO2) { if (unO2.unAttributO2 != unAutreO2.unAttributO2) return true; else return false; } public override bool Equals(Object unObjet) { /* redéfinition de la méthode Equals */ if (unObjet != null) { if (unObjet is SO2) { if (unAttributO2 == ((SO2)unObjet).unAttributO2) return true; else return false; } } return false; } } class O2 { private int unAttributO2; public O2(int unAttributO2) { this.unAttributO2 = unAttributO2; } public int getAttribut() { return unAttributO2; } public void setAttribut(int nouvelleValeur) { unAttributO2 = nouvelleValeur; } public override bool Equals(Object unObjet) { /* redéfinition de la méthode Equals */ if (this == unObjet) { return true; } else { O2 unAutreO2 = unObjet as O2; if (unObjet != null) { if (unAttributO2 == unAutreO2.unAttributO2) return true; else return false; } } return false; } public O2 Clone() { return((O2) MemberwiseClone()); } }

Clonage, comparaison et assignation d’objets CHAPITRE 14

public class CloneEqual { public static void Main() { /* Test de la méthode equal() */ /* Objets créés dans le tas */ O2 unO2 = new O2(5); O2 unAutreO2 = new O2(5); O2 unTroisièmeO2 = new O2(10); unTroisièmeO2 = unO2; O1 unO1 = new O1(10, unO2); O1 unAutreO1 = new O1(10, unAutreO2); if (unO2 == unAutreO2) Console.WriteLine("unO2 et unAutreO2 ont if (unO2.Equals(unAutreO2)) Console.WriteLine("unO2 et unAutreO2 ont if (unO2 == unTroisièmeO2) Console.WriteLine("unO2 et unTroisièmeO2 if (unO2.Equals(unTroisièmeO2)) Console.WriteLine("unO2 et unTroisièmeO2 if (unO1 == unAutreO1) Console.WriteLine("unO2 et unAutreO2 ont if (unO1.Equals(unAutreO1)) Console.WriteLine("unO1 et unAutreO1 ont

la même référence"); le même état"); ont le même état"); ont le même état"); la même référence"); le même état");

/* Test de la méthode Clone */ O2 unQuatrièmeO2 = null; unQuatrièmeO2 = (O2)unTroisièmeO2.Clone(); Console.WriteLine(unTroisièmeO2.getAttribut() + " = ? " + unQuatrièmeO2.getAttribut()); O1 unTroisièmeO1 = null; unTroisièmeO1 = (O1)unAutreO1.Clone(); if (unO1.Equals(unTroisièmeO1)) Console.WriteLine("unO1 et unTroisièmeO1 ont le même état"); unTroisièmeO1.getO2().setAttribut(7); if (unO1.Equals(unTroisièmeO1)) Console.WriteLine("unO1 et unTroisièmeO1 ont le même état"); else Console.WriteLine("unO1 et unTroisièmeO1 n'ont pas le même état"); /* Objets créés dans la pile */ SO2 unO2Pile = new SO2(5); SO2 unAutreO2Pile = new SO2(5); SO2 unTroisiemeO2Pile = new SO2(10); unTroisiemeO2Pile = unO2Pile; SO1 unO1Pile = new SO1(10, unO2Pile); SO1 unAutreO1Pile = new SO1(10, unAutreO2Pile); SO1 unTroisiemeO1Pile = unAutreO1Pile; if (unO2Pile == unAutreO2Pile) Console.WriteLine("unO2Pile et unAutreO2Pile ont le meme etat");

357

L’orienté objet

358

if (unO2Pile == unTroisiemeO2Pile) Console.WriteLine("unO2Pile et unTroisiemeO2Pile ont le meme etat"); if (unO1Pile == unAutreO1Pile) Console.WriteLine("unO1Pile et unAutreO1Pile ont le meme etat"); if (unO1Pile.Equals(unAutreO1Pile)) Console.WriteLine("unO1Pile et unAutreO1Pile ont le meme etat"); if (unAutreO1Pile == unTroisiemeO1Pile) Console.WriteLine("unO1AutrePile et unTroisiemeO1Pile ont le meme etat"); unTroisiemeO1Pile.changeO2(7); if (unO1Pile == unTroisiemeO1Pile) Console.WriteLine("unO1Pile et unTroisièmeO1Pile ont le même état"); else Console.WriteLine("unO1Pile et unTroisièmeO1Pile n'ont pas le même état"); } }

Résultat unO2 et unAutreO2 ont le même état unO2 et unTroisièmeO2 ont le même état unO2 et unTroisièmeO2 ont le même état unO1 et unAutreO1 ont le même état 5 = ? 5 unO1 et unTroisièmeO1 ont le même état unO1 et unTroisièmeO1 n’ont pas le même état unO2Pile et unAutreO2Pile ont le même état unO2Pile et unTroisiemeO2Pile ont le même état unO1Pile et unAutreO1Pile ont le même état unO1Pile et unAutreO1Pile ont le même état unO1AutrePile et unTroisiemeO1Pile ont le même état unO1Pile et unTroisièmeO1Pile n’ont pas le même état.

Le code étant très proche des codes Java et C++ précédents, le résultat devrait apparaître très logique. On s’aperçoit que l’objet unTroisiemeO1 est bien une copie en profondeur de l’objet unO1 car, en changeant la valeur de l’attribut pointé par le premier, l’égalité n’a plus court. Il en va de même pour les objets pile unO1Pile et unTroisièmeO1Pile, bien que nulle redéfinition de mécanisme d’affectation n’ait été nécessaire. La répétition de la phrase « unO1Pile et un AutreO1Pile ont le même état » est dû à ce que l’on a effectué la comparaison des deux manières possibles proposées par C#, soit à l’aide de l’opérateur == surchargé, soit à l’aide de la méthode Equals() redéfinie.

Clonage, comparaison et assignation d’objets CHAPITRE 14

359

Exercices Exercice 14.1 Créez une classe CompteEnBanque munie d’un seul attribut solde, et définissez l’égalité de deux objets comptes en banque, de telle manière qu’elle soit vérifiée dès lors que les deux soldes sont égaux. Réalisez cet exercice en C# et en C++.

Exercice 14.2 Surchargez dans ces deux mêmes langages l’opérateur d’addition pour cette même classe, de telle manière que la somme de deux comptes en banque en donne un troisième, dont le solde est la somme des deux soldes.

Exercice 14.3 Pourquoi Java a-t-il déclaré protected la méthode equals() dans la superclasse Object ?

Exercice 14.4 Pourquoi C# a-t-il déclaré static la version de la méthode Equals(object a, object b) dans la superclasse Object, et qui compare les deux objets reçus en tant qu’argument ?

Exercice 14.5 Pourquoi est-il plus dans l’esprit OO en C# de redéfinir la méthode Equals() plutôt que de surcharger l’opérateur == ?

Exercice 14.6 Créez une nouvelle classe Emprunt munie d’un seul attribut montant et modifiez la classe CompteEnBanque de telle manière que plusieurs objets emprunts puissent être associés à un même objet compte en banque. Redéfinissez l’égalité de deux comptes en banque comme vérifiée, si la somme des montants des emprunts est égale dans les deux cas.

Exercice 14.7 Redéfinissez la méthode clone() en Java pour la classe CompteEnBanque de manière que les emprunts se trouvent également clonés lors du clonage du compte.

15 Interfaces Ce chapitre présente les interfaces, structures de code qui se bornent à introduire les seules signatures des méthodes. Il décrit dans les quatre langages de programmation qui en font usage les trois rôles que ces interfaces sont appelées à jouer : forcer l’implémentation de leurs méthodes, permettre le multihéritage, faciliter et stabiliser la décomposition de l’application logicielle.

Sommaire : Interface — Multihéritage d’interfaces en Java, C# et PHP 5 — Interface = contrat de médiation — L’essor des interfaces dans UML 2 — Fichiers .h et fichiers .cpp en C++ — Décomposition de l’application

Candidus — Ce mode d’emploi que représente l’interface, s’agit-il en quelque sorte d’une liste d’ingrédients permettant d’identifier un médicament générique ? Doctus — Ton image est bonne. Tout ce qu’on attend d’un objet peut être exprimé par son interface. Seuls les attributs nécessaires à la concrétisation des objets en sont exclus. Cand. — Les interfaces ne sont-elles qu’une façade publique pour l’implémentation ? N’ont-ils aucun impact sur l’exécution des programmes ? Doc. — Bien sûr que si. Une interface joue certains des rôles d’une classe, elle représente un sous-ensemble de méthodes – que tu pourras invoquer comme s’il s’agissait d’une classe à part entière. Même le compilateur se contente des signatures qu’elles contiennent. Tu peux compiler une classe dépendant d’une interface avant même d’avoir réalisé la moindre classe d’implémentation concrète. Cand. — S’il ne s’agit que de listes de leurs signatures, on peut imaginer que n’importe quel sous-ensemble des méthodes d’une classe suffit à définir une interface ? Doc. — Absolument. C’est même cette simplicité qui nous permet de réaliser en Java quelque chose d’équivalent au multihéritage. Un multihéritage réel repose sur une simple déclaration de parenté multiple donnée au compilateur. Java n’autorisant qu’une seule classe parent, tu devras recourir aux interfaces pour obtenir du compilateur qu’il te demande d’implémenter toutes les méthodes déclarées par une classe. Cand. — Une interface ne serait donc qu’une classe dont toutes les méthodes sont abstraites ? Doc. — Attention tout de même ! En Java, tu pourras implémenter plusieurs interfaces mais tu ne pourras hériter que d’une seule superclasse. Cand. — Si je pousse à l’extrême, juste pour voir, il devrait donc être possible de créer un objet ne dépendant que d’un ensemble d’interfaces. Il ne dépendra alors jamais directement des classes concrètes avec lesquelles il doit communiquer.

362

L’orienté objet

Doc. — Rien ne l’interdit en effet, à tel point que de tels objets peuvent être mis en œuvre sur un réseau. Chacun des ordinateurs ne devra disposer que des seules interfaces nécessaires pour la communication. Pour envoyer une requête à un objet situé sur une machine distante, les seules signatures de méthodes de cet objet suffiront. Le message sera envoyé à l’objet concret de la machine distante qui se chargera d’exécuter la méthode associée.

Peter Coad et TogetherJ Comme nous recourrons plus d’une fois à TogetherJ dans cet ouvrage, pour la réalisation des diagrammes UML, le moins que nous puissions faire est de donner un grand coup de chapeau à Peter Coad, le directeur de TogetherSoft, l’entreprise qui produit ce remarquable logiciel et depuis rachetée par Borland (dont Coad fut le vice-président jusqu’en 2004), qui s’est précipité de l’intégrer dans ses nouveaux environnements de développement. Ce qui nous impressionne le plus dans l’utilisation de ce logiciel est la synchronisation qu’il fut le premier à permettre entre le code (Java en l’occurrence, mais des versions du logiciel existent pour d’autres langages, y compris C#, C++ ou VB.Net) et les diagrammes UML 2. Changez quoi que ce soit dans un diagramme de classes et le code « encaisse » immédiatement ce changement, changez quoi que ce soit dans le code et les diagrammes UML s’adaptent pour intégrer cette modification, et tout cela immédiatement devant vos yeux ébahis. TogetherJ fut le premier à parvenir à cette complète synchronisation, suivi depuis par d’autres environnements de développement UML, à l’instar d’un de ses concurrents les plus sérieux comme Rational Rose, Umondo, Argo et beaucoup d’autres. Cette synchronisation est à ce point effective, qu’il nous est arrivé de perdre toute une partie de code car nous avions, sans y prendre garde, effacé quelques rectangles dans le diagramme de classe. De fait, cette synchronisation est discutée par pas mal de praticiens, selon le statut que l’on accorde aux diagrammes UML (nous approfondissons ce point dans le chapitre 10). Cela peut se limiter (mais c’est déjà indispensable) à une aide à la conception, et qui se maintient quelque peu à « l’écart » de la réalisation matérielle finale, un peu comme un plan d’architecte ou une partition de musique, qu’il est impensable d’introduire dans une quelconque machine afin que puisse en résulter la maison ou la symphonie. Mais la voie tracée par Peter Coad est d’en faire un vrai outil de développement qui prend une part active à l’obtention des produits finaux, comme une partition musicale que l’on enrichirait de tout ce qui est nécessaire à la production de la musique. Les deux visions coexistent, mais la seconde (que nous avons poussée à l’extrême dans le chapitre 10 en assimilant UML 2 à un nouveau type de langage de programmation) semble gagner chaque jour un peu plus de terrain. C’est clairement celle qui accompagne le MDA de l’OMG. Peter Coad n’a pas réellement besoin de cette publicité tant celle-ci est omniprésente sur le Web, produite par d’autres ou, plus effectivement encore, par le principal intéressé. On retrouvait, par exemple, sur la page Web de TogetherSoft, des annonces rien de moins emphatiques telles que « Peter Coad is a business builder, a model builder and a thought leader », excusez du peu. Avec cette société, il est vrai, Peter Coad n’en est pas à son premier coup de maître. Il est reconnu comme un gourou OO (auteur de la méthodologie Coad/Yourdon pour l’analyse orientée objet) depuis pas mal d’années, et a produit un ensemble d’ouvrages dans la même maison d’édition, Prentice Hall, intitulés : Java Modeling in Color with UML – un livre où Peter Coad colorie les diagrammes UML pour y intégrer des informations temporelles sur le développement des parties de ces diagrammes –, et plusieurs autres comme Java Design : Building Better Apps and Applets ou Object Models: Strategies, Patterns and Applications, dans lesquels il développe sa vision de la modélisation objet, avec des points forts et discutables à la fois, comme la préférence qu’il accorde à la composition sur l’héritage (et que nous discutons plusieurs fois dans ce livre). Par ailleurs, il dirige une collection dans cette même maison d’édition. Il dépense aussi une énergie folle à promouvoir un type de développement (qui, bien sûr, privilégie l’usage des logiciels Together), reposant sur un ensemble de principes devenus très populaires ces jours-ci et tenant de l’Extreme Programminga, comme d’encourager les développeurs à produire très vite et à fréquence élevée des exécutables, modestes dans leur portée, mais de haute qualité ; mais aussi à intégrer plusieurs acteurs dans le processus de développement, le client et les experts du domaine, bien sûr les programmeurs, et à les faire communiquer au mieux (UML est là pour cela). Pour cela, l’analyse, le design, la programmation et la mise à l’essai doivent être faits de manière quasi simultanée (d’où l’utilisation de logiciel intégrateur comme Together). Surtout, le développement doit se dérouler en une succession de courtes itérations, débouchant à chaque fois sur un produit exécutable et facilement évaluable. a. L’Extreme Programming, Bénard et al., Eyrolles 2002.

Interfaces CHAPITRE 15

363

Interfaces : favoriser la décomposition et la stabilité Nous avons entr’aperçu les interfaces, dans un chapitre précédent, comme une structure de code dont la finalité première est d’extraire d’une classe l’ensemble des signatures de ses services, afin d’en informer toutes celles qui voudraient y faire appel. Ces classes n’ont nul besoin des détails d’implémentation, c’est-à-dire de la manière précise dont ces services seront réellement exécutés (le corps d’instruction) par l’objet qui les fournit. Il suffira d’appeler le service par son nom pour le voir s’exécuter. L’utilisation d’interfaces conduit naturellement à des applications facilement décomposables, réparties à travers une large équipe de programmeurs, tout en permettant une forte résistance aux changements d’implémentation. Ce sont les interfaces qui circuleront de programmeur en programmeur car ce sont les seuls éléments de code dont chacun d’eux, occupé à la réalisation de sa classe, a besoin dans son interaction avec les autres classes. Une fois sa classe bien entamée, ce programmeur en extraira également les services qu’il doit rendre disponibles aux autres. C’est l’aboutissement naturel de l’encapsulation, quand elle est pratiquée à l’extrême. On dissimule tout ce qui concerne l’implémentation des classes, au point d’en faire un fichier distinct et inaccessible, au contraire du fichier reprenant les noms des seuls services rendus par cette classe, qui est disponible, lui, pour tout utilisateur.

Java, C# et PHP 5 : interface via l’héritage En Java, C# et PHP 5 (Python n’intègre pas dans sa syntaxe cette structure de données, tout comme il ne permet pas les méthodes abstraites), des classes abstraites aux interfaces il n’y a qu’un pas, puisqu’une interface est une classe abstraite dont toutes les méthodes sont déclarées abstraites. Nous verrons plus avant dans ce chapitre qu’en matière d’interface, C++ voit les choses un peu différemment. Tout en conservant les avantages qu’elles permettent dans la décomposition et la stabilisation des applications logicielles, C++ a détaché la pratique des interfaces de l’héritage. En Java, les seuls attributs pouvant encore figurer dans la définition d’une interface sont public, final (c’està-dire constant, une fois leur valeur déterminée ils ne sont plus modifiables) et static (ce qui est assez logique, puisque, à l’instar d’une classe abstraite, vous ne pouvez créer des objets instances des interfaces). C# et PHP 5 n’autorisent aucun attribut dans la définition de ses interfaces. En Java et PHP 5, on n’hérite pas d’une interface, mais on l’implémente, en respectant la syntaxe suivante : class O1 extends O2 implements IO2

désignant une classe O1 qui hérite d’une classe O2 et implémente une interface IO2. C# ne fait pas de différence syntaxique entre l’héritage de classe et d’interface, et la version C# de l’instruction précédente se réduit simplement à : class O1 : O2 , IO2

à ceci près que les interfaces doivent être héritées en dernier, c’est-à-dire à la suite de la seule classe dont on peut hériter. Les interfaces peuvent normalement hériter entre elles, comme le diagramme de classe présenté ci-après l’indique. En UML, le lien continu représente l’héritage et le lien en pointillé représente l’implémentation (C# ne faisant pas de différence entre les deux). Quand ils concernent les interfaces, les graphes d’héritage peuvent être plus complexes que quand ils se limitent aux seules classes. Ces graphes peuvent, en effet, présenter des ramifications multiples, tant descendantes qu’ascendantes (sinon ils se restreindraient à des structures d’arbre).

364

L’orienté objet

Figure 15-1

Structure d’héritage des interfaces. Dans ce diagramme de classes UML, figurent quatre interfaces O2, O1, OO1 et OO2, et deux classes, OOO1 et Interface.

Les trois raisons d’être des interfaces Forcer la redéfinition Les interfaces ont principalement trois raisons d’être. La première, largement exploitée par Java, est de forcer le programmeur à réutiliser des fonctionnalités déjà prédéfinies dans les librairies Java, librairies d’utilitaires écrites très souvent sous forme d’interfaces. Par exemple, toute classe Java peut implémenter l’interface MouseListener, dont toutes les méthodes abstraites seront déclenchées par des événements souris, comme public void mouseClicked() ou public void mouseReleased(). En implémentant l’interface MouseListener, vous voilà contraints et forcés d’exprimer, par la concrétisation de ces méthodes abstraites, ce que votre code exécutera en réponse à un clic de souris. En gros, en cliquant sur la souris, vous passez la main au système d’exploitation de votre ordinateur, qui cherche par quelle voie redonner la main au programme. Il le fera en se servant des méthodes appropriées, telle mouseClicked() (qui s’exécute d’un simple clic de souris), que vous aurez concrétisées dans votre programme. D’autres interfaces très utiles, comme Runnable (pour la redéfinition de la méthode public void run() qui contient le corps d’instruction à exécuter par un thread [voir chapitre 17]) ou KeyListener (pour l’utilisation du clavier), sont souvent implémentées ensemble par une même classe (d’où la nécessité d’autoriser la multi-implémentation d’interfaces).

Interfaces CHAPITRE 15

365

Implémentation d’interfaces Quand, en Java, C# et PHP 5, une classe implémente ou hérite d’une interface, elle est contrainte et forcée de concrétiser les méthodes qu’elle hérite de celle-ci. Ne pas le faire génère une erreur lors de la compilation ou directement à l’exécution pour PHP 5. L’interface oblige à utiliser et à redéfinir ses méthodes. En Java, c’est par le biais des interfaces que sont implémentées les fonctionnalités GUI et multithread. Nous verrons que C# et Python procèdent différemment.

Dans la suite, nous présentons deux petits code Java, ayant pour mission d’illustrer l’utilisation de deux interfaces : ActionListener et KeyListener. Nous vous renvoyons aux manuels de programmation Java (ils ne manquent pas sur les étals des librairies ou sur le Web) pour approfondir la composition et l’utilisation de ces interfaces. Nous y faisons juste une rapide illusion pour comprendre comment ces structures d’interface peuvent jouer un rôle majeur dans le développement d’utilitaires. Le premier code concrétise une méthode abstraite provenant de l’interface ActionListener dont il hérite, qui se déclenche en cliquant sur un objet graphique, tel un bouton. Lorsque vous l’exécutez, une fenêtre apparaît avec trois boutons. En cliquant sur un des boutons, vous changez le « look » de votre application. Bien que plusieurs éléments de ce code soient très liés aux librairies graphiques Java, que nous ne voyons pas ici, le fonctionnement devrait rester compréhensible. L’unique mécanisme que nous cherchons à mettre en évidence, c’est l’obligation d’utiliser et de redéfinir une méthode (ici public void actionPerformed()) héritée de l’interface ActionListener où elle est abstraite, afin d’indiquer ce qui se passera lors d’un clic sur un des trois boutons. Code Java illustrant l’interface ActionListener import java.awt.*; import java.awt.event.*; import javax.swing.*; class PlafPanel extends JPanel implements ActionListener { /* on implémente l'interface ActionListener */ private JButton metalButton; private JButton motifButton; private JButton windowsButton; public PlafPanel() { /* on ajoute trois boutons */ metalButton = new JButton("Metal"); motifButton = new JButton("Motif"); windowsButton = new JButton("Windows"); add(metalButton); add(motifButton); add(windowsButton); /* on rend ces boutons sensibles à ce qui est dit dans les méthodes redéfinies à partir de l'interface, c’est-à-dire la méthode « actionPerformed » */ metalButton.addActionListener(this); motifButton.addActionListener(this); windowsButton.addActionListener(this); } /* la méthode à absolument redéfinir, et qui dit ce qui se passe en cliquant sur les boutons */ public void actionPerformed(ActionEvent evt) { Object source = evt.getSource(); String plaf = " ";

366

L’orienté objet

if (source == metalButton) // Teste pour savoir de quel bouton il s’agit. plaf = "javax.swing.plaf.metal.MetalLookAndFeel"; else if (source == motifButton) plaf = "com.sun.java.swing.plaf.motif.MotifLookAndFeel"; else if (source == windowsButton) plaf = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"; try { UIManager.setLookAndFeel(plaf); SwingUtilities.updateComponentTreeUI(this); } catch(Exception e) {} } } public class ExempleDInterface1 extends JFrame { /* JFrame est la fenêtre qui apparaît à l’exécution ➥du code */ public ExempleDInterface1() { setTitle("Test Plateforme"); // titre de la fenêtre setSize(300, 200); // taille de la fenêtre addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } } ); getContentPane().add(new PlafPanel()); /* on ajoute sur le JFrame le panneau avec les trois ➥boutons */ } public static void main(String[] args) { ExempleDInterface1 unTest = new ExempleDInterface1(); unTest.setVisible(true); // on fait apparaître la fenêtre } }

Résultat Le second code, illustrant le rôle et l’utilisation de l’interface KeyListener, vous permet de vous servir des quatre touches fléchées du clavier, afin de dessiner à l’écran en une succession de traits verticaux et horizontaux. Si vous appuyez sur la touche « shift » en même temps, le dessin sera exécuté plus rapidement. Là encore, la connaissance des librairies graphiques Java est indispensable à une compréhension complète du code (et sort largement du cadre de cet ouvrage, un petit manuel Java vous sera utile), mais les fonctionnalités principales, et surtout la redéfinition des méthodes liées au clavier et à l’interface KeyListener, devraient apparaître suffisamment compréhensibles. Trois méthodes abstraites sont définies dans cette interface. L’implémentation de l’interface oblige la redéfinition des trois méthodes, y compris si nous n’avons rien de précis à demander à certaines d’entre elles (comme c’est le cas dans ce code).

Interfaces CHAPITRE 15 Figure 15-2

Illustration de l’interface ActionListener. Quand on clique sur un des trois boutons, l’apparence de l’application change.

Code Java illustrant l’interface KeyListener import java.awt.*; import java.awt.event.*; import javax.swing.*; class SketchPanel extends JPanel implements KeyListener { // l’interface qui nous importe private Point start,end; public SketchPanel() { start = new Point(0,0); end = new Point(0,0); addKeyListener(this); } /* une méthode à nécessairement redéfinir, mais la seule que l'on redéfinit vraiment */ public void keyPressed(KeyEvent evt) { int keyCode = evt.getKeyCode(); int modifiers = evt.getModifiers(); int d; if ((modifiers & InputEvent.SHIFT_MASK) != 0) // si on appuie sur Shift d = 5; else d = 1; if (keyCode == KeyEvent.VK_LEFT) add (-d,0); // selon la touche sur laquelle on appuie else if (keyCode == KeyEvent.VK_RIGHT) add (d,0); else if (keyCode == KeyEvent.VK_UP) add (0,-d); else if (keyCode == KeyEvent.VK_DOWN) add (0,d); } public void keyReleased(KeyEvent evt) {}; /* on la redéfinit sans vraiment la redéfinir, sinon gare au compilateur */ public void keyTyped(KeyEvent evt) {}; /* on la redéfinit sans vraiment la redéfinir */ public boolean isFocusable() { return true; } public void add (int dx, int dy) { // c’est la méthode qui trace les lignes end.x += dx; end.y += dy; Graphics g = getGraphics();

367

368

L’orienté objet

g.drawLine(start.x, start.y, end.x, end.y); g.dispose(); start.x = end.x; start.y = end.y; } } public class ExempleDInterface2 extends JFrame { public ExempleDInterface2() { setTitle("Test Jeu Graphique"); setSize(300, 200); addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } } ); getContentPane().add(new SketchPanel()); } public static void main(String[] args) { ExempleDInterface2 unTest = new ExempleDInterface2(); unTest.setVisible(true); } }

Résultat Figure 15-3

Illustration de l’interface KeyListener. En utilisant les 4 touches fléchées, vous pouvez dessiner au moyen d’une succession de traits horizontaux et verticaux.

Permettre le multihéritage Comme nous l’avons dit, plusieurs interfaces seront très souvent implémentées en même temps. De fait, un autre apport des interfaces est d’avoir levé l’interdiction du multihéritage : d’interdit pour les classes, il devient autorisé pour les interfaces. Il s’agit plutôt d’une multi-implémentation en Java et PHP 5, quand une

Interfaces CHAPITRE 15

369

classe implémente plusieurs interfaces, mais c’est vraiment un multihéritage, quand une interface en hérite de plusieurs autres ou, dans tous les cas de figure, en C#. La disparition du corps d’instruction dans les méthodes permet de contourner toutes les difficultés posées par le multihéritage en C++ (discutées au chapitre 12). Comme dans le diagramme UML précédent, rien n’interdit deux interfaces de posséder deux signatures de méthodes égales (dans ce diagramme, la même signature de méthode jeTravaillePourOO1() apparaît deux fois), puisque seule la classe, plus bas dans le graphe d’héritage, fournira un contenu à ces méthodes. De même, une classe et une interface pourraient partager la même signature de méthode. Si elles se trouvent héritées par une sous-classe commune, c’est de la seule version concrétisée de la méthode qu’héritera réellement la sous-classe. Concernant les attributs, leur déclaration restreinte à des constantes statiques en Java, et leur disparition pure et simple du C# et PHP 5, réduisent également les problèmes posés par des noms égaux.

La carte de visite de l’objet Finalement, l’interface peut s’assimiler à la carte de visite d’une classe (et de tous les objets auxquels elle donne naissance), qu’un développeur d’une autre classe consulte et s’engage à respecter dans la conception de la sienne. Le diagramme UML qui suit illustre une collaboration idéale entre deux programmeurs, le premier de la classe O1 et l’autre de la classe O2. Figure 15-4

Diagramme de classes d’interaction entre les classes O1 et O2, passant par la médiation des deux interfaces implémentées par les classes IO1 et IO2.

370

L’orienté objet

Ce qui apparaît dans ce diagramme, c’est que l’unique dépendance de la classe O1 avec la classe O2 passe par un lien avec la seule interface de la classe O2, c’est-à-dire IO2. L’unique fichier dont doit disposer le développeur de la classe O1 est un fichier contenant le code de l’interface IO2, et aucun autre. L’existence d’une implémentation quelconque de cette interface par une classe, ici la classe O2, peut ne le préoccuper en rien. Son contrat de développement, il le signe avec l’interface et non avec la classe. C’est la responsabilité des autres développeurs de la classe O2, de faire en sorte que ce soit bien l’interface IO2, et aucune autre, qui soit implémentée. Les codes Java, C# et PHP 5 correspondant à ce diagramme sont présentés ci-après, répartis, dans les deux cas, et comme cela doit l’être idéalement, sur 5 fichiers séparés : les deux classes, les deux interfaces et la classe principale. Le développeur de la classe O1 n’aura à manipuler que trois d’entre eux, au début, IO1, pour le donner aux autres, puis IO2 pour savoir comment s’adresser à la classe O2 et, plus longuement, O1, car c’est surtout pour cela qu’on le paie. Code Java Fichier 1 : IO1.java public interface IO1 { public void jeTravaillePourO1();}

Fichier 2 : IO2.java public interface IO2 { public void jeTravaillePourO2();}

Fichier 3 : O1.java public class O1 implements IO1 { private IO2 unO2; public O1() { } public void setO2(IO2 unO2){ this.unO2 = unO2; } public void jUtiliseO2() { System.out.println("j'utilise O2"); unO2.jeTravaillePourO2(); } public void jeTravaillePourO1(){ System.out.println("je travaille pour O1"); jImplementeLeServiceO1(); } private void jImplementeLeServiceO1() { System.out.println("je suis prive dans O1"); } }

Fichier 4 : O2.java public class O2 implements IO2 { private IO1 unO1;

Interfaces CHAPITRE 15

371

public O2 (IO1 unO1){ this.unO1 = unO1; } public void jeTravaillePourO2() { System.out.println("je travaille pour O2"); jImplementeLeServiceO2(); } public void jUtiliseO1(){ System.out.println("j'utilise O1"); unO1.jeTravaillePourO1(); } private void jImplementeLeServiceO2() { System.out.println("je suis prive dans O2"); } }

Fichier 5 : Principale.java public class Principale { public static void main(String[] args) { O1 unO1 = new O1(); O2 unO2 = new O2(unO1); unO1.setO2(unO2); unO1.jUtiliseO2(); unO2.jUtiliseO1(); } }

Résultat j’utilise O2 je travaille pour O2 je suis privé dans O2 j’utilise O1 je travaille pour O1 je suis privé dans O1

Rien de spécial dans ce code Java. Le code de la classe O1 contient un référent vers l’interface IO2 et vice versa. Aucun lien n’est établi syntaxiquement entre la classe O1 et la classe O2. Ce n’est qu’à l’exécution, et de manière polymorphique, qu’un objet O1 enverra explicitement un message vers un objet de la classe O2. Le compilateur n’y verra que du feu, et c’est comme cela que ça doit être. Tous les services rendus par la classe O2 sont bien une implémentation de ceux prévus par l’interface. Lors de l’exécution, l’interaction pourra se faire en toute tranquillité d’esprit, car rien de ce qui sera fait n’aura pas d’abord été prévu dans les interfaces, et vérifié par le compilateur. Pour ce dernier, les interfaces sont une garantie du respect des types, et en conséquence de la sécurité d’exécution. Notez qu’en Java, la seule compilation de la classe Principale entraîne la compilation de toutes les classes dont celles-ci dépendent, comme indiqué par le code. Les liens à la compilation se font, simplement, par le jeu de dénomination des fichiers et des classes.

372

L’orienté objet

Code C# Fichier 1 : IO1.cs public interface IO1 { void jeTravaillePourO1();}

À la différence de Java, point de mode d’accès dans la définition des méthodes, car ce mode d’accès ne peut être spécifié que lors de leur implémentation. Comme le seul mot-clé permis à ce niveau par Java est public, cela revient au même. Fichier 2 : IO2.cs public interface IO2 { void jeTravaillePourO2();}

Fichier 3 : O1.cs using System; public class O1 : IO1 { private IO2 unO2; public O1(){} public void setO2(IO2 unO2) { this.unO2 = unO2; } public void jUtiliseO2(){ Console.WriteLine("j'utilise O2"); unO2.jeTravaillePourO2(); } public void jeTravaillePourO1(){ Console.WriteLine("je travaille pour O1"); jImplementeLeServiceO1(); } private void jImplementeLeServiceO1(){ Console.WriteLine("je suis prive dans O1"); } } /* on ajoute une sous-classe qui doit à son tour implémenter l'interface */ public class FilsO1 : O1, IO1 /* l’interface n’est pas automatiquement héritée*/ { public new void jeTravaillePourO1() { Console.WriteLine("je travaille pour fils O1"); } }

Fichier 4 : O2.cs public class O2 : IO2 { private IO1 unO1, unAutreO1; public O2 (IO1 unO1, IO1 unAutreO1) { this.unO1 = unO1; this.unAutreO1 = unAutreO1;

Interfaces CHAPITRE 15

373

} public void jeTravaillePourO2(){ Console.WriteLine("je travaille pour O2"); jImplementeLeServiceO2(); } public void jUtiliseO1(){ Console.WriteLine("j'utiliseO1"); unO1.jeTravaillePourO1(); unAutreO1.jeTravaillePourO1(); // c’est ici que la réimplantation de l’interface devient nécessaire } private void jImplementeLeServiceO2() { Console.WriteLine("je suis prive dans O2"); } }

Fichier 5 : Principale.cs public class Principale { public static void Main() { O1 unO1 = new O1(); FilsO1 unAutreO1 = new FilsO1(); O2 unO2 = new O2(unO1, unAutreO1); unO1.setO2(unO2); unO1.jUtiliseO2(); unO2.jUtiliseO1(); } }

Résultat j’utilise O2 je travaille pour O2 je suis privé dans O2 j’utilise O1 je travaille pour O1 je suis privé dans O1 je travaille pour fils O1

À première vue, le code ressemble à s’y méprendre au code Java. Cependant, il sera toujours important en C# de se préoccuper des problèmes de typage statique et dynamique (et de l’emploi soigné des mots-clés : abstract, virtual, new et override), si l’héritage se propage vers le bas, et que de nouvelles classes héritent de la classe O1. C’est le cas ici avec la classe FilsO1. Comme seule la classe O1 implémente l’interface, si nous ne forçons pas la classe FilsO1 à ré-implémenter à son tour la même interface, par défaut et à cause du typage statique, le comportement de la méthode jeTravaillePourO1() sera celui de la superclasse O1, la première à implémenter l’interface et non pas celui de FilsO1, comme ce serait effectivement le cas en Java. N’oubliez pas que C# n’est pas polymorphique par défaut. Il a fait le choix de ne rien être par défaut et de vous obliger à préciser vos intentions. C# a rendu cette pratique plus contraignante et moins intuitive que ne l’a fait Java, en évitant que vous vous laissiez guider par le seul fonctionnement par défaut (entièrement polymorphique en Java). Ainsi, cette ré-implémentation à plusieurs niveaux des interfaces pourrait conduire le programmeur C# à se trouver confronté à des problèmes

374

L’orienté objet

de signatures de méthodes partagées entre plusieurs interfaces, qu’il ne pourra trancher qu’en précisant de quelle interface est issue la méthode (par une écriture comme « IO1.jeTravaillePourO1() {….} »). À lire les développeurs de .Net, il semble que la raison essentielle de ce choix, ainsi que la présence du new dans la redéfinition des méthodes, soient liées à la possibilité de faire coexister dans un même code plusieurs versions des mêmes fonctionnalités. Tant que vous n’êtes pas convaincu du développement en cours de la méthode jeTravaillePourO1() dans la classe FilsO1, n’implémentez pas l’interface IO1 dans celle-ci. Par défaut, c’est la version de la classe O1 qui s’exécutera. Dès que vous êtes sûr de vous, vous pouvez ajouter l’implémentation de l’interface IO1. C’est automatiquement la nouvelle version qui sera exécutée, bien qu’il suffise d’un simple retrait pour revenir à la version précédente. Ainsi, les deux versions peuvent co-exister dans un même code. Cela peut aider le développeur à innover, puisqu’il a la certitude qu’une version ancienne continue à fonctionner comme roue de secours. Si tous ces fichiers sont clairement tenus séparés, leur liaison, nécessaire lors de la compilation, recourt à une pratique moins souple qu’en Java, mais qui, en revanche, apparaîtra plus familière aux habitués du monde Microsoft. Pour qu’une classe, à la compilation, puisse en référer une autre, il faut d’abord la transformer en une interface .dll (le même nom d’interface désignent ici deux types de fichier un peu différents, bien qu’ils servent tous deux à une forme de médiation entre d’autres fichiers, et c’est la raison pour laquelle, nous féminisons le second). L’instruction de compilation se transforme alors en : csc /t:library /out :IO1.dll IO1.cs pour faire de l’interface IO1 un code récupérable à la compilation par les autres codes qui en dépendent. Ensuite, la compilation de la classe O1 utilisant cette interface se fera par : csc /r:IO1.dll /t:library O1.dll O1.cs

Et, ainsi de suite, « rebroussant » le fil des dépendances, jusqu’à la classe Principale, la seule à contenir le point de départ de l’exécution, c’est-à-dire la méthode Main. La classe Principale se compilera à l’aide de l’instruction : csc /r:O1.dll /r:O2.dll /r:IO1.dll /r:IO2.dll Principale.cs

La connexion entre les interfaces et les fichiers « .dll » ne devraient pas surprendre les programmeurs habitués à l’environnement Microsoft. On retrouve ces interfaces dans le langage Visual Basic. On les retrouve également dans toute l’approche COM/OLE, si chère à Microsoft. Ainsi, en C#, même les structures peuvent hériter des interfaces et permettre à leur méthode de « s’extérioriser ». Ces interfaces serviront encore dans la nouvelle plate-forme .Net, en tant que pont entre les différents langages de programmation supportés par la plate-forme, tels que VB.Net, C#, C++, Python.Net et JScript. Ainsi, il sera possible d’utiliser du code C#, par l’entremise de son interface .dll, à l’intérieur d’une macro VB.Net. Les interfaces, ici, serviront non seulement à la médiation entre des classes écrites dans des environnements différents, mais élargiront cette médiation à des langages de programmation différents. Code PHP 5 Interfaces

Interfaces



Interfaces CHAPITRE 15

375

interface IO1 { public function jeTravaillePourO1(); } interface IO2 { public function jeTravaillePourO2(); } class O1 implements IO1 { private $unO2; public function __construct(){} public function setO2(IO2 $unO2) { // Il est possible de typer l’argument avec l’interface $this->unO2 = $unO2; } public function jUtiliseO2(){ print("j'utilise O2
\n"); $this->unO2->jeTravaillePourO2(); } public function jeTravaillePourO1(){ print("je travaille pour O1
\n"); self::jImplementeLeServiceO1(); } private function jImplementeLeServiceO1(){ print("je suis prive dans O1
\n"); } } class O2 implements IO2 { private $unO1; public function __construct (IO1 $unO1) { // Il est possible de typer l’argument avec l’interface $this->unO1 = $unO1; } public function jeTravaillePourO2(){ print("je travaille pour O2
\n"); self::jImplementeLeServiceO2(); } public function jUtiliseO1(){ print("j'utiliseO1
\n"); $this->unO1->jeTravaillePourO1(); } private function jImplementeLeServiceO2() { print("je suis prive dans O2
\n"); } } $unO1 = new O1(); $unO2 = new O2($unO1);

376

L’orienté objet

$unO1->setO2($unO2); $unO1->jUtiliseO2(); $unO2->jUtiliseO1(); ?>

Nous installons tout le code dans un seul fichier .php. Rien de bien particulier à signaler. Le code est très proche du Java, sans nul besoin de typage statique, sauf lors du passage d’arguments où l’on peut, dans un souci de clarification, spécifier le type qui sera vérifié à l’exécution.

Les Interfaces dans UML 2 L’importance des interfaces dans le développement des logiciels OO est telle qu’UML a, dans sa deuxième version, enrichi son offre de symboles grapiques en matière d’interface. Ainsi, les liens entre les classes O1, O2 et interfaces IO1 et IO2 décrits dans le chapitre précédent se représentent désormais, comme dans la figure 15-5, à l’aide d’un petit cercle lorsqu’il s’agit d’une implémentation d’interface et d’un « socket » (un arc de cercle comme un petit grippeur) lorsqu’il s’agit d’une utilisation d’interface. En général, le « socket interface » d’une classe se connecte sur le « cercle interface » d’une autre. C’est ainsi que les classes se connectent au mieux, par interfaces interposées. Ce nouveau symbole graphique s’est substitué au stéréotype « interface » apposé jusqu’alors sur les classes qui l’étaient. Cela nous permet également de comprendre la notion de stéréotype en UML, qui s’écrit entre guillemets, se rajoute sur n’importe quel élément graphique d’UML pour en particulariser l’usage et, en général, effecue la transition jusqu’à l’arrivée d’un nouveau symbole graphique. Figure 15-5

La classe O1 implémente l’interface IO1 et utilise l’interface IO2 et vice versa pour la classe O2.

IO1

O1

O2 IO2

Parmi d’autres additions, d’UML 2 figure le diagramme dit de « structure composite », dans lequel on trouve des éléments graphiques comme celui, extension de la figure précédente, représenté dans la figure 15-6. Dans la figure 15-6, vous découvrez un composant logiciel et ses trois manières d’interagir respectivement avec ses trois « composants intelocuteurs ». Il peut s’agir d’une classe ou d’un composant plus important incluant plusieurs classes. Chaque interaction est symbolisée par un port comprenant une implémentation et une utilisation d’interface. L’implémentation correspond à la manière de ce composant de se « présenter » à son interlocuteur. La partie utilisatrice reprend les services que ce composant attend à son tour de la part de cet interlocuteur. Chaque interlocuteur se voit associé à un « port » à part. Il reprend le protocole de communication pour cet interlocuteur précis, chaque interlocuteur ayant le sien. À l’intérieur du composant, on découvre les parties fonctionnelles reliées à ces ports et qui concrétisent ces interfaces. Cette manière de représenter les composants logiciels par leur façon de s’imbriquer entre eux, les implémentations faisant office de « sortie » du composant et les utilisations des « entrées », ramène la conception logicielle à la réalisation d’un immense Lego ou puzzle, où l’on tente au mieux de construire un ensemble fonctionnel en assemblant entre elles des parties pré-existantes en fonction de leur « entrée-sortie ».

Interfaces CHAPITRE 15

377

Figure 15-6

Partie d’un diagramme de « structure composite ».

En C++ : fichiers .h et fichiers .cpp Dans ce langage, le rôle de l’interface se réduit au plus essentiel de tous, c’est-à-dire une structure de code utilisée pour la médiation entre les différents acteurs d’un programme en cours de développement. Sa mise en œuvre est pourtant fondamentalement différente, car elle ne repose plus sur la pratique de l’héritage, mais plutôt sur la séparation dans l’écriture d’une classe entre un fichier dit d’interface, et portant l’extension .h, et un fichier dit d’implémentation, et portant l’extension.cpp. L’interface n’est plus une addition syntaxique du langage, comme c’est le cas en Java et C#, mais elle répond plutôt à un mode d’organisation de l’application logicielle en un ensemble de fichiers présentant les classes aux autres, les .h, et de fichiers implémentant ces classes, les .cpp. L’interface n’est plus un élément essentiel de la syntaxe mais un type de fichier. Dans la version C++ des diagrammes UML, elle ne figurera plus dans le diagramme de classe, mais on la retrouvera dans le diagramme qui décrit l’organisation des fichiers de l’application : le diagramme de composant, comme la figure ci-dessous l’illustre. Figure 15-7

Diagramme UML de composants associé au code C++ présenté plus haut.

Nous avons, jusqu’à présent, réalisé nos exemples de code C++ sans les faire précéder et les accompagner de fichiers d’interface. La raison en est simple, ils ne sont pas utiles à la compréhension première des briques de base de l’OO. Cependant, toute formation en C++ accorde, à juste titre, beaucoup d’importance à la décomposition, dans l’écriture des classes, entre fichiers d’interface et fichiers d’implémentation.

378

L’orienté objet

Excepté ce rôle de médiation, la définition du corps des méthodes dans le fichier d’implémentation, ou directement dans le fichier d’interface, n’est pas sans conséquence sur la taille et la vitesse d’exécution du code. Implémenter une méthode dès la déclaration de la classe, comme nous l’avions fait jusqu’ici, plutôt que dans un fichier séparé, comme nous le ferons par la suite, conduit à une version différente de l’exécutable. Nous négligerons ces aspects, car notre ouvrage n’a pas pour vocation une compréhension exhaustive du C++. Cependant, tout développeur dans ce langage doit garder à l’esprit que cette séparation interface/implémentation, et l’endroit où se trouve déclarée l’implémentation des méthodes, est responsable d’un ensemble d’effets assez conséquents. Nous allons reprendre l’application réalisée précédemment en Java, C# et PHP 5, en présentant et commentant à nouveau les cinq fichiers C++ qui supportent cette application. Fichier 1 : IO1.h #include "IO2.h" class O1 { private: O2 *unO2; void jImplementeLeServiceO1(); public: O1(); void jeTravaillePourO1(); void setO2(O2 *unO2); void jUtiliseO2(); };

Nous voyons qu’il n’est plus possible d’isoler dans l’interface un sous-ensemble des méthodes, les seules que nous voudrions rendre disponibles aux autres classes. Toutes les méthodes existant dans la classe devront être prévues dans l’interface. Néanmoins, l’interface se borne à n’en présenter que leur signature. C’est bien en cela qu’elle constitue, à nouveau, un partenaire essentiel à la décomposition de l’application logicielle. La classe O1 include l’interface IO2.h, et elle possède un attribut de type O2. C’est de cette manière qu’en C++ les classes interagiront entre elles. Chacune, dans le développement de son interface, « inclura » l’interface de celle qu’elle utilise, et en fera un attribut supplémentaire. Fichier 2 : IO2.h class O1; class O2 { private: O1 *unO1; void jImplementeLeServiceO2(); public: O2 (O1 *unO1); void jeTravaillePourO2(); void jUtiliseO1(); };

Pour éviter des problèmes de récursivité sans fin, dus au simple fait que les deux classes se réfèrent mutuellement, nous ne procéderons pas ici à l’inclusion de l’interface IO1.h. En revanche, nous rappellerons simplement la classe O1 au début du code. De nouveau, toutes les méthodes de la classe O2 sont là, mais dans leur version la plus sobre, sans instruction. La classe O2 possède, à son tour, un attribut de type O1.

Interfaces CHAPITRE 15

379

Fichier 3 : O1.cpp #include "stdafx.h" #include "iostream.h" #include "IO1.h" O1::O1() { } void O1::jeTravaillePourO1() { cout <<"je Travaille pour O1" << endl; jImplementeLeServiceO1(); } void O1::jImplementeLeServiceO1() { cout << "je suis prive dans O1"<unO2 = unO2; } void O1::jUtiliseO2() { cout << "j'utilise O2" << endl; unO2->jeTravaillePourO2(); }

Comme nous le voyons, ce premier fichier d’implémentation, O1.cpp, doit commencer par inclure l’interface qu’il a pour mission d’implémenter. Ensuite, toutes les méthodes sont concrètement définies. Elles s’écrivent en faisant précéder leur signature de la classe à laquelle elles appartiennent. Fichier 4 : O2.cpp #include "stdafx.h" #include "iostream.h" #include "IO1.h" O2::O2(O1 *unO1) { this->unO1 = unO1; } void O2::jUtiliseO1() { cout << "j'utilise O1" << endl; unO1->jeTravaillePourO1(); } void O2::jImplementeLeServiceO2() { cout <<"je suis prive dans O2" << endl; } void O2::jeTravaillePourO2() { cout << "je Travaille pour O2" << endl; jImplementeLeServiceO2(); }

Rien de spécial à signaler, si ce n’est qu’il ne faut pas inclure I02.h, vu que l’inclusion de la première interface s’en sera déjà occupée.

380

L’orienté objet

Fichier 5 : TestInterface.cpp #include "stdafx.h" #include "IO1.h" int main(int argc, char* argv[]) { O1* unO1 = new O1(); O2* unO2 = new O2(unO1); unO1->setO2(unO2); unO1->jUtiliseO2(); unO2->jUtiliseO1(); return 0; }

Finalement, le main, détaché de toute classe, se borne à inclure seulement I02.h, pour les mêmes raisons que le fichier précédent. Résultat j'utilise O2 je travaille pour O2 je suis privé dans O2 j'utilise O1 je travaille pour O1 je suis privé dans O1 Séparation .h/ .cpp En C++, la séparation des fichiers d’interface .h et d’implémentation .cpp est une autre façon d’améliorer la stabilité des logiciels, en forçant les développeurs, par l’organisation des fichiers (et non plus du code comme en Java, C# ou PHP 5), à désolidariser physiquement le catalogue des objets de leur accès direct.

Interfaces : du local à Internet Par fichiers séparés Ce qu’il y a de vraiment commun aux trois langages de programmation OO, c’est le besoin de séparer dans deux fichiers différents les signatures des méthodes de leur implémentation, ce qui est visible et accessible de l’extérieur selon la manière dont cela s’exécute de l’intérieur. Java, C# et PHP 5 permettent une extraction encore supplémentaire et plus fine, par le jeu de l’héritage. Dans ces langages, l’interface est re-solidarisée à l’implémentation par ce mécanisme d’héritage, alors qu’en C++ l’interface est include dans le fichier d’implémentation.

Nous allons généraliser dans le prochain chapitre cette interaction entre objets, par le truchement de leur interface, à Internet. Les interfaces sont à ce point suffisantes à la communication entre objets que seuls les fichiers qui les contiennent devront être installés sur les ordinateurs séparés, mais appelés à communiquer. La véritable implémentation des services sera non seulement encapsulée dans les fichiers classes, mais également dans des ordinateurs séparés, ordinateurs simplement connectés par Internet et son protocole de communication : TCP/IP.

Interfaces CHAPITRE 15

381

Exercices Exercice 15.1 Écrivez en Java ou en C# le code correspondant à ce diagramme de classe, composé de quatre interfaces et d’une classe : Figure 15-7

Exercice 15.2 Si la classe B cherche à interagir avec la classe A, comment allez-vous représenter la flèche d’association dirigée dans le diagramme de classe ci-après ? Figure 15-8

L’orienté objet

382

Exercice 15.3 Expliquez en quoi l’utilisation des interfaces rend possible le multihéritage.

Exercice 15.4 Décomposez en un fichier d’interface .h et un fichier d’implémentation .cpp, le seul fichier d’implémentation C++ suivant : class A { private: int a1, a2; bool b1, b2; int faireA() { return a1 + a2; } public: A(int a1, int a2) { this->a1 = a1; this->a2 = a2; b1 = false; } void faire2A(int c) { faireA() + c; } };

Exercice 15.5 Lors de la compilation du code Java ci-après, deux erreurs sont signalées. Lesquelles ? interface IA { private int faireA(); public void faire2A(); public int faire3A(); } class A implements IA { public int faireA() { return 2; } public void faire2A() { System.out.println(faireA()); } } public class Exercice5 { public static void main(String[] args) { A unA = new A(); unA.faire2A(); } }

Interfaces CHAPITRE 15

383

Exercice 15.6 Lors de la compilation du code C# suivant, trois erreurs sont signalées. Lesquelles ? using System; interface IA { public int faireA(); public void faire2A(); } class B { public int faireB() { return 3; } } class A : IA, B { public int faireA() { return 2; } public void faire2A() { Console.WriteLine(faireA()); } } public class Exercice6 { public static void Main() { A unA = new A(); unA.faire2A(); } }

Exercice 15.7 Dans quel cas de figure une classe abstraite héritera-t-elle d’une interface ?

Exercice 15.8 Pourquoi en C++, au contraire de C# et de Java, le fichier implémentation contiendra-t-il le même nombre ou moins de méthodes que le fichier interface ?

Exercice 15.9 En C# et en Java, un attribut peut-il posséder en tant que type une interface ? Si oui, est-ce là une bonne pratique ?

Exercice 15.10 Expliquez les trois pratiques de compilation liées à la présence des interfaces, et pourquoi la pratique de Java apparaît comme la moins lourde à mettre en œuvre.

16 Distribution gratuite d’objets : pour services rendus sur le réseau En s’interrogeant tout d’abord sur les raisons de leur utilisation, ce chapitre introduit à la pratique des applications informatiques distribuées, applications réalisées par le biais d’objets distribués, s’activant sur des ordinateurs séparés et communiquant à travers la couche physique : Internet ou un niveau sémantique audessus : le Web. Différentes implémentations de ces objets distribués seront évoquées et expérimentées, comme RMI, Corba, Jini et les services Web.

Sommaire : Objets distribués : pourquoi ? — Invocation statique — RMI — Corba — Jini — Invocation dynamique — Services Web Candidus — Et si nous en venions à l’essentiel… Tous ces objets représentent bien plus qu’un nouveau type d’organisation. J’ai le sentiment que c’est même presque une révolution des modes de pensée. Doctus — Je suis très content de t’entendre dire ça Il était temps que les informaticiens se mettent à suivre l’exemple des constructeurs de matériel informatique. Depuis un certain temps, ces derniers nous en font une tous les deux ans en matière de performance et de nouvelles technologies. Cand. — Alors que les programmeurs se contentent de leurs bons vieux « if then else » ! Doc. — Internet fait la démonstration tous les jours de ce qu’on peut faire en matière de logiciels distribués. Nos objets ne sont rien d’autre que des moyens modernes pour répartir les tâches de manière démocratique, à travers toute la planète. C’est l’OO à l’heure de la mondialisation, n’en déplaise aux alterégOO. Cand. — Une démocratie à la fois utopique et bien réelle alors ! Dans laquelle chacun a voix au chapitre pour exercer son droit à la liberté d’expression mais où les responsabilités devront être pleinement assumées ! Doc. — Il s’agit là d’un aspect très fort de notre défi. Cette responsabilisation n’est possible que si elle est basée sur de réelles compétences. Cand. — Il faudra donc veiller à la bonne distribution des rôles en s’assurant que chacun disposera de tout le nécessaire pour effectuer son travail. Chaque objet devra également pouvoir communiquer avec tous les spécialistes qu’il lui faudra connaître pour compléter son savoir-faire. Doc. — Les langages actuels sont assez avancés pour exploiter tout le travail réalisé avant l’OO. Ce qu’il reste à faire ne concerne que l’aspect d’ouverture des programmes existants pour les faire communiquer, afin qu’on puisse s’en servir, dans la situation et le moment voulus.

386

L’orienté objet

Objets distribués sur le réseau : pourquoi ? Faire d’Internet un ordinateur géant La possibilité d’utiliser des objets qui s’exécutent, indépendamment, sur des processeurs séparés, mais tout en continuant à s’envoyer des messages, cette fois-ci à travers Internet, se justifie par un ensemble d’avantages, pour la plupart liés à l’exploitation des réseaux informatiques en général. Le premier d’entre eux est d’accroître les ressources computationnelles mises à disposition. On connaît le fameux slogan de Sun : « The computer is the network » (la traduction ne nous paraît pas indispensable). Cela reste évidemment le cas, tant que le coût des communications entre les objets (par exemple la durée d’une communication multipliée par le nombre de communications) est inférieur à l’épargne effectuée par la répartition de l’exécution sur différents processeurs. Internet est un immense ordinateur, le plus grand et le plus puissant, pour autant que l’on arrive à paralléliser efficacement l’application logicielle qui nous intéresse sur tous les nœuds qui le constituent. C’est avec une mélancolie certaine que les utilisateurs gourmands en temps calcul rêvent à tous ces ordinateurs dormants qui, quand ils se réveillent, ne tournent qu’à 10 % de leur capacité de temps calcul, et ce quand ils tournent vraiment, entre deux e-mails et deux téléchargements de la photo osée de la dernière vedette d’un reality show. L’exemple le plus connu d’une telle utilisation des ressources Internet est le projet « [email protected] », où tous les ordinateurs du réseau qui le désirent dédient une partie de leurs ressources (temps calcul et mémoire) à la recherche de motifs tangibles dans des signaux radio perçus dans l’univers entier, bouts de signal révélateurs d’une éventuelle présence intelligente dans l’univers (en dehors du bureau des auteurs, bien entendu). Les extraterrestres auraient démarré un projet semblable, mais leurs ordinateurs sont, depuis, pris dans une boucle infinie et à cours de ressources, dans l’analyse des écrits de Heidegger, des chansons de Léo Ferré, des films de Greenaway, des tableaux de Pollock et des musiques de Boulez. « [email protected] » est très facilement parallélisable car chaque ordinateur jouant le jeu, ne s’occupe que d’une partie du ciel, tous font la même chose mais chacun se limite à une partie des données. C’est également le cas dans de nombreux projets bioinformatiques comme le séquençage de motifs d’ADN, le repliement des protéines ou la prédiction climatique, eux aussi facilement parallélisables.

La motivation essentielle des nouveaux projets d’informatique distribuée comme le « Grid Computing » qui répartit sur tous les ordinateurs d’Internet qui acceptent d’y consacrer un peu de leurs ressources des tâches très exigeantes en mémoire et temps calcul est que, quel que soit l’accroissement des performances informatiques (comme la loi de Moore nous l’indique), cet accroissement sera toujours négligeable par rapport à celui des performances du réseau dans sa totalité qui, non seulement bénéficie de l’accélération de chacun de ses membres, mais également de l’accroissement du nombre de ceux-ci dans le réseau. Le mélange du « Grid Computing » et des services Web devrait connaître une évolution très naturelle.

Répartition des données Une autre raison d’utiliser Internet pour la communication entre objets tient au simple fait que les objets doivent parfois s’exécuter sur des processeurs distribués, car les données à manipuler sont elles-mêmes réparties dans la mémoire connectée à ces processeurs. C’est souvent le cas de la lecture ou de la mise à jour de base de données, laquelle ne peut s’effectuer que sur le serveur où cette base est installée. Déplacer toute la base serait incommensurablement plus coûteux que simplement déplacer l’objet, qui nécessite quelques informations en provenance de cette base. La plupart des applications distribuées, appliquées au commerce électronique, sont de ce type. On les dit « 2-tier », lorsqu’un client interagit, via son navigateur Internet, directement avec le serveur de

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

387

la base de données. C’est plutôt rare, car les connexions directes sur une base de données sont lentes, coûteuses et concurrentielles. De ce fait, on rajoute le plus souvent quelques processeurs intermédiaires ; l’application est dite alors « 3-tier ou multi-tier ». Ces processeurs récoltent les requêtes clients et, avant d’entamer une interaction avec la base de données (interaction lente et à réaliser avec parcimonie), vérifient les requêtes (pour cela, ils peuvent s’embarquer dans quelques interactions supplémentaires avec le client), compilent ces requêtes, les transforment, les homogénéisent puis, enfin, les transmettent à la base de données. Tous ces allers-retours entre les clients, les serveurs intermédiaires et les bases de données, peuvent se faire idéalement avec les objets distribués, et sont d’ailleurs très fréquemment réalisés à partir du protocole propre à Java (dû au positionnement stratégique de Java pour les applications Web), « RMI », étudié à la prochaine section. Ici, les objets sont distribués, purement et simplement, car les données qu’ils doivent traiter, pour des raisons historiques, économiques, stratégiques ou de confidentialité, le sont également.

Répartition des utilisateurs et des responsables Au contraire des lofteurs ou des star académitiens, tous les utilisateurs de l’informatique ne partagent pas un même lieu géographique, et ne l’utilisent pas pour les mêmes raisons. Ils sont tout autant distribués que l’est leur machine. Et leur mobilité est devenue celle de leur portable. L’existence des objets distribués conduit à faciliter l’intégration de services rendus par des entreprises commerciales distinctes mais, éventuellement, complémentaires. À ce titre, on parle souvent et de plus en plus, d’architecture informatique orientée service (SOA). Par exemple, l’organisation d’un voyage implique des services pour la prise en charge du déplacement, d’autres pour celle du séjour, d’autres encore pour celle des assurances, etc. Une agence de voyage pourra plus simplement interagir avec ces différentes entreprises, de manière à centraliser aisément toute la gestion du voyage. Chaque utilisateur peut collaborer avec les autres en déléguant respectivement leurs tâches à des objets distribués. On entend souvent parler sur Internet d’agents « intelligents », capables de prises de décision, d’achats, de ventes, au profit de son « maître ». Ces agents se trouvent le plus souvent incarnés dans des objets distribués. C’est aussi derrière ce type d’informatique que courent les services Web : permettre à moindre frais d’intégrer et de faire collaborer des compétences distribuées géographiquement. Vous achetez un disque et le magasin dans lequel vous vous trouvez vous permet de réserver des places pour le concert de l’artiste auteur du disque que vous achetez. L’achat des logiciels informatiques devrait être lentement mais sûrement remplacé par la location de ces mêmes logiciels le temps nécessaire à leur utilisation. On assiste au développement d’une informatique éclatée, distribuée sur le réseau. Les programmes, qui s’exécutent sur des machines distantes se sollicitent mutuellement au fur et à mesure de leur exécution. Les deux petits exemples qui suivent permettront aisément de comprendre ce nouvel emploi du Web, à portée de souris aujourd’hui. Si vous utilisez un traitement de texte et désirez corriger votre texte à l’aide du meilleur correcteur orthographique existant sur le marché, vous êtes limité au seul programme disponible dans le traitement de texte que vous utilisez. Demain, lorsque vous solliciterez dans le menu de votre traitement de texte la fonction « correcteur orthographique », de nombreuses options s’offriront à vous, différenciées par leur temps d’utilisation, leur prix, leur qualité, etc. Il vous sera alors possible de choisir celle qui vous convient. Automatiquement, ce choix entraînera l’empaquetage de votre texte, de manière à le faire circuler sur le Web, à l’acheminer vers le correcteur orthographique en question (à l’Académie française ou à Oxford), qui le corrigera et vous le renverra dans sa version corrigée. Un système de facturation totalement intégré vous permettra de ne payer que ce que vous aurez dépensé (accès et temps de correction). Vous ne serez plus obligé d’acheter et de mettre constamment à jour sur votre ordinateur un correcteur unique. Ce scénario est évidemment extensible à tous les logiciels utilisés, y compris le traitement de texte de départ, dont nous supposons que vous êtes en train de vous énerver à installer la version 2010, n’en déplaise à Microsoft.

388

L’orienté objet

Peer-to-peer Le modèle « peer-to-peer », dont un logiciel comme Napster fut à l’origine (mais sa totale vulnérabilité en raison de la présence d’un serveur central l’a fait disparaître au profit d’autres implantations comme Gnutella ou Kazaa dont les serveurs sont plus distribués) est très représentatif d’une vision idéalisée d’Internet, « new age » et « collectiviste », défendue par beaucoup. Tout ordinateur devrait être, à la fois, serveur de données et client de ces mêmes serveurs, en évitant au maximum de passer par des ordinateurs pivots, jouant un rôle trop stratégique de concentrateurs ou de médiateurs. Si les réseaux informatiques ont eu et continuent à avoir cet extraordinaire impact psychologique et économique sur le fonctionnement de nos sociétés, c’est que leur diffusion s’accompagne, tout en les amplifiant, de deux phénomènes d’importance croissante : la communication et la dématérialisation de ce que l’on communique. De plus, ces deux phénomènes s’amplifient mutuellement : l’accroissement des communications pousse à la dématérialisation des supports d’échange (voyez la musique, voyez les films) et l’accroissement de cette dématérialisation banalise la communication de ceux-ci (voyez les musées Internet ou le commerce électronique en général). La dématérialisation des supports d’échange est une tendance constante, qui nous accompagne depuis l’origine des temps. Le troc, qui consistait à échanger des produits mais exigeait de multiples relations interpersonnelles, fit place à la monnaie métallique, qui facilitait l’échange, parce qu’elle désolidarisait la transaction des produits échangés. De nos jours, cette monnaie matérielle est de plus en plus souvent remplacée par des transactions électroniques, ce qui accroît la distance dans le temps et dans l’espace entre acheteurs et vendeurs. Pièces, billets, chèques s’effacent chaque jour un peu plus au profit des cartes de crédit et des sites Web. Cela montre que l’objet de l’échange survit très souvent et très bien aux mutations que subit le support de l’échange. Le besoin d’échanger demeure tandis que le médiateur se métamorphose. L’informatique a largement accompagné cette tendance à la dématérialisation, avant d’en devenir un vecteur et un accélérateur prépondérant. Ainsi, en matière d’économie, nous vivons une évolution conséquente qui nous fait passer des échanges matériels de biens et de marchés à des relations fondées sur le seul accès dématérialisé à un bien, accès limité dans le temps et facilité par les réseaux. De nombreux économistes pensent que nous paierons dans l’avenir moins pour le transfert de propriété d’un bien dans l’espace que pour le « flux d’expérience » auquel nous aurons accès dans le temps. Par exemple, nous n’achèterons plus un DVD mais nous paierons le visionnage d’un film, téléchargé en direct à partir d’une agence de location vidéo. Le marketing des objets les plus matériels risque lui aussi d’être modifié par cette pratique nouvelle. En effet, nous n’achèterons plus une voiture, mais nous la louerons le temps d’un voyage, tout comme vous le faites aujourd’hui lorsque vous louez une place dans un avion, mais probablement pour d’autres raisons. Cette parfaite mise à plat du réseau Internet, ce projet par trop égalitaire, deviendrait, ce faisant, un réseau de communication fantastique pour des objets voués à se rendre mutuellement des services à travers le réseau. « Passe-moi ton texte que je te le corrige avec le merveilleux correcteur sémantique dont j’ai fait l’acquisition récemment, et j’en profite pour t’envoyer une photo à retoucher avec ton merveilleux système de traitement d’images, et puis, surtout, n’hésite pas à m’envoyer ta dernière production musicale dont mon merveilleux compositeur automatique fera du Mozart. » Il ne s’agit pas ici de simples transferts de fichiers, mais plutôt de déclencher des applications à distance, en leur transmettant en paramètres les données sur lesquelles ces applications vont devoir opérer. C’est toute la différence entre un envoi de message, qui est un ordre d’exécution, et un envoi de données. Les autoroutes de l’information se transformeront en des réseaux de petits boulots, où l’on troque des services plutôt que des biens, et où les 35 heures seront très difficiles à faire respecter. Sun a beaucoup misé sur cette évolution et favorise le développement d’une plate-forme informatique nommée « JXTA », qui devrait faciliter grandement le développement, par chacun, d’applications « peer-to-peer ».

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

389

L’informatique ubiquitaire Une autre évolution, plus récente, veut que les ordinateurs, tels qu’on les connaît, c’est-à-dire une boîte grise ou fluo, sur la table ou sur les genoux, soient de moins en moins les seules machines à vouloir communiquer. Dans un avenir très proche, dans le monde de l’Internet sans fil, n’importe quelle machine, dotée d’un processeur, pourrait chercher à communiquer avec n’importe quelle autre. C’est ce qu’on appelle aujourd’hui l’informatique ubiquitaire ou disparaissante, à votre convenance. Bien que contradictoires, les deux adjectifs décrivent des réalités qui se conditionnent. C’est parce qu’il y a des ordinateurs partout qu’il n’est plus opportun de parler d’ordinateur, un peu comme les chauves, qui sont les seuls à parler de cheveux (oui, on sait, comme analogie, c’est un peu tiré par les cheveux…). Rappelez-vous le premier chapitre (si vous y parvenez, c’est si loin…). La voiture que nous y décrivions pourrait réellement envoyer un message au feu rouge, comme « passe au vert, j’arrive ». La brosse à dents pourrait demander au percolateur de lancer le café, ou l’inverse. Le frigo pourrait commander au supermarché voisin le renouvellement de ses denrées périmées. Votre appareil photo pourrait envoyer la prise de vue passionnante du coucher de soleil que vous venez de faire, sur la télévision de votre fils, en train de retransmettre le match Allemagne-Brésil. Imaginez-vous dans un pays lointain avec votre appareil photo numérique. Vous souhaitez imprimer avec la meilleure qualité possible la photo que vous venez de faire et n’avez aucune imprimante le permettant. Dans votre appareil photo un petit menu affiche la liste des boutiques qui peuvent s’occuper de cette impression dans un rayon d’un kilomètre. Vous en choisissez une, là encore selon certains critères : prix, proximité, qualité. Une fois votre choix effectué, un seul clic suffit à lancer l’impression. Vous n’aurez plus alors qu’à aller chercher votre photo imprimée dans la boutique en question. La facturation s’effectuera, à nouveau, le plus automatiquement et simplement qui soit sous forme électronique. Vous en rêviez… Les objets distribués l’ont fait. Nous reviendrons sur cet exemple dans la suite du chapitre. Rien n’est vraiment fictif dans tout cela, la technologie le permet ; il reste à rendre toutes ces idées utiles, puis, un jour, indispensables.

Robustesse Un dernier avantage à la répartition à travers le réseau d’une application informatique tient à la robustesse de cette application face aux pannes de machines, si le même traitement s’effectue, en toute redondance, sur plusieurs ordinateurs à la fois. L’ambition idéale d’Internet était de renforcer, par ses multiples ordinateurs et ses multiples liaisons, la fiabilité du système informatique. Plus un réseau contient de nœuds et de connexions, plus la fiabilité de ses communications est assurée. En clair, la multiplication des ressources mises à disposition, pour un projet computationnel unique, garantit une quasi parfaite fiabilité dans l’exécution de ce projet.

RMI (Remote Method Invocation) Très peu de modifications devront être apportées aux fichiers Java utilisés dans le chapitre précédent, dès lors que nous souhaitons simplement « migrer » cette même application sur Internet. Nous débutons la pratique des objets distribués par la méthodologie RMI car, d’une certaine manière, c’est elle qui satisfait le mieux l’ambition poursuivie par toutes les autres : rendre transparente la distribution géographique des objets s’échangeant des messages pour le programmeur, ce dernier ayant la possibilté de programmer « distribué » exactement comme il programme en local. Bien qu’elle partage l’ambition de rendre le développement d’applications globales presque aussi immédiat que le développement d’applications locales, c’est la force de la technologie RMI (Remote Method Invocation), par rapport à la technologie Corba, que nous discuterons dans la suite, de passer, effectivement, le plus facilement,

390

L’orienté objet

d’une application Java purement locale à une version distribuée de cette même application. Les cinq fichiers Java nécessaires, comme dans le chapitre précédent, sont indiqués ci-après. Nous allons détailler uniquement les additions qui font de cette application, précédemment locale, une application Internet. Pour les différencier du cas précédent, nous avons simplement ajouté « RMI » à la fin du nom des classes et interfaces concernées. Comme nous travaillons à travers le réseau Internet, des problèmes sont à prévoir, panne de réseau, connexion impossible, que Java nous force, de fait, à anticiper. En conséquence de quoi, les seules additions requises concerneront surtout des mécanismes de gestion d’exception. Nous allons développer les côtés serveur et client de la communication, bien que nous sachions qu’en matière d’objets distribués, ces deux appellations sont parfaitement interchangeables.

Côté serveur Fichier 1 : IO2RMI.java – Définition de l’interface public interface IO2RMI extends java.rmi.Remote { public void jeTravaillePourO2() throws java.rmi.RemoteException; public String jeRenvoieUnString() throws java.rmi.RemoteException; }

Nous avions anticipé dans les chapitres précédents l’importance prise par les « interfaces » dans la réalisation des applications distribuées. Il est en effet plus important encore, lorsque les programmeurs se trouvent largement séparés aussi bien dans l’espace que dans le temps, de baser leur communication sur un protocole d’échange minimal, leur laissant les mains libres pour d’éventuelles transformations dans les applications dont ils ont la charge. Nous découvrons dans la suite, leur exploitation dans ce cadre précis d’objets distribués sur des processeurs distincts. Pour indiquer qu’elle sera utilisée à distance, l’interface doit hériter de la classe java.rmi.Remote. Nous prévoyons deux services dans cette interface, le premier se borne à écrire sur le serveur, alors que le second transmet un String que le client doit recevoir. Il faut prévoir un éventuel déclenchement de l’exception java.rmi.RemoteException, quand on enverra le message prévu par cette signature. Bien des choses pourraient se passer sur le réseau, rendant l’envoi du message impossible. Le message, dès l’écriture de sa signature, doit également contenir les exceptions auxquelles il peut donner lieu. Les problèmes qui peuvent se produire, et qu’il est nécessaire de prévoir, font partie de la signature du message. Fichier 2 : O2RMI.java – Définition de la classe, implémentation de l’interface par la classe O2RMI import java.rmi.*; import java.rmi.server.UnicastRemoteObject; public class O2RMI extends UnicastRemoteObject implements IO2RMI { public O2RMI() throws RemoteException { super(); /*System.setSecurityManager(new RMISecurityManager()); */ System.out.println("Je mets mon objet sur le net"); try { Naming.rebind("unObjetO2", this); /* enregistrement de l'objet */ } catch (Exception e) {System.exit(0);} System.out.println("C'est fait"); }

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

391

public void jeTravaillePourO2() { /* implémentation du premier service */ System.out.println("je travaille pour O2"); jImplementeLeServiceO2(); } public String jeRenvoieUnString() { /* implémentation du deuxième service */ return "un bonjour en provenance d'O2"; } private void jImplementeLeServiceO2() { System.out.println("je suis prive dans O2"); } }

La classe qui implémente les deux messages possibles (et donc l’interface contenant leur signature) doit également hériter de UnicastRemoteObject, si elle désire redéfinir le comportement à distance des méthodes, telles equals ou toString, qu’il peut être important de repenser vu la distribution des objets sur le réseau. La principale différence avec une application locale se situe dans le constructeur de la classe O2RMI. Il faut maintenant prévoir que l’objet en question, qui exécutera les services du côté serveur, soit référencé par un nom. Il faut installer cet objet sur le Web avec un nom symbolique, ici unObjetO2, au moyen duquel tout client pourra y faire appel. Ce nom sera installé dans un « registre » que tout client pourra consulter. Lors de la déclaration du nom du référent dans le « registre », par l’opération Naming.rebind(), un port, autre que celui spécifié par défaut (this), peut être précisé. Un service de sécurité peut également être activé (ici, mis en commentaire) qui, souvent, se limite à celui par défaut. On admettra, sans s’en préoccuper davantage, que toute interaction de type réseau ne va pas sans un souci sécuritaire (dissimulation du contenu du message, identification de l’expéditeur ou du destinataire…) qu’il faudra prendre en charge. Les deux messages à exécuter, jeTravaillePourO2() et jeRenvoieUnString(), sont là, mais cette fois avec leur implémentation. Même si leur mode d’exécution se fera à distance plutôt que localement, rien ne change vraiment dans l’écriture de leur « envoi ». Fichier 3 : PrincipaleServeurRMI.java – Fichier final côté serveur public class PrincipaleServeurRMI { public static void main(String[] args) { try { new O2RMI(); } catch (Exception e) {System.exit(0);} } }

Il nous faut un exécutable principal côté serveur, mais rien de particulier n’est à signaler dans le code. Il se borne à déclencher toute la mise en disponibilité de l’objet côté serveur, en appelant simplement le constructeur de la classe O2RMI, afin de créer l’objet qui sera installé sur Internet.

Côté client Fichier 4 : O1RMI.java – Fichier réalisant l’appel des services côté client import java.rmi.*; public class O1RMI { IO2RMI unObjetO2 = null;

L’orienté objet

392

public O1RMI(){ System.out.println("je vais chercher l'objet sur le net"); try { /* il faut retrouver l'objet sur lequel déclencher les services */ unObjetO2 = (IO2RMI)Naming.lookup("unObjetO2"); } catch (Exception e) { System.out.println("ça marche pas " + e); System.exit(0);} System.out.println("c'est fait"); } public void jUtiliseO2() { System.out.println("j'utilise O2"); try /* on envoie les deux messages */ { unObjetO2.jeTravaillePourO2(); System.out.println(unObjetO2.jeRenvoieUnString()); } catch (Exception e) { System.out.println("ça marche pas" + e); System.exit(0); } System.out.println("c'est fait"); } }

Afin de pouvoir envoyer son message, le client doit, avant tout, identifier son destinataire. Il le fait en utilisant le même « registre » que le serveur. Il cherche sur ce « registre » l’objet qui correspond au nom unObjetO2, qui est en effet le nom que nous lui avons donné côté serveur. Il est important, à ce stade, de compléter le nom de l’objet par l’adresse Internet de l’ordinateur sur lequel il se trouve, ainsi que le port de communication qui est dédié à cette interaction. En général, cette même opération s’écrira plutôt de la manière suivante : Naming.lookup (" rmi ://iridia.ulb.ac.be :1234/unObjetO2 ")

iridia.ulb.ac.be étant l’adresse Internet du serveur en question, et 1234 le port de communication. Nous ne l’avons pas fait ici, puisque nous exécutons toute l’application en local. Un objet dans chaque port Un port est un « canal de sortie » dédié à une communication d’un ordinateur donné avec le monde extérieur. Chaque communication avec chaque interlocuteur doit faire l’objet d’un port distinct et dédié à cette seule communication. Du côté du serveur, l’objet est également « présent » et « à l’écoute » sur ce même port.

À ce stade, une véritable distribution du service sur Internet ne demande en fait qu’une modification de l’adresse dans le code du client, les deux envois de message sur unObjetO2 se déroulant exactement comme si cette application ne fonctionnait qu’en local. Comme discuté dans le chapitre précédent, nous voyons que le client exige de connaître l’interface IO2RMI, qui sert effectivement de médiateur entre les deux acteurs : du côté serveur, on l’implémente, du côté client, on l’utilise. L’interface encore La structure syntaxique d’interface permet une véritable séparation physique des développements informatiques, non seulement entre les programmeurs mais également entre les machines exécutant les programmes. La seule information en provenance du serveur dont le client dispose est l’interface des services rendus par celui-ci afin qu’il puisse compiler.

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

393

Fichier 5 : PrincipaleClientRMI.java – Finalement le fichier principal côté client public class PrincipaleClientRMI { public static void main (String args[]) { O1RMI unObjetO1 = new O1RMI(); unObjetO1.jUtiliseO2(); } }

Rien à signaler.

RMIC : stub et skeleton Déclencher une application distribuée, via le protocole RMI, est à peine plus exigeant qu’une simple application locale. Une étape additionnelle nécessaire, dans le cas d’un processus dit « invocation statique », est celle de la création du « stub » et du « skeleton », par la ligne de commande rmic O2RMI, à exécuter sur le fichier compilé en Java (.class) de la classe qui effectue le service, côté serveur. Suite à cette instruction, deux nouveaux fichiers s’ajoutent, l’un sur l’ordinateur côté client, le « stub » : O2RMI_Stub.class, et l’autre sur l’ordinateur côté serveur, le « skeleton » : O2RMI_Skel.class. À quoi servent ces deux fichiers additionnels, générés automatiquement par Java ? Comme la figure ci-après le montre, ils sont indispensables à l’envoi du message. En substance, le stub « empaquette » le message côté client, et s’occupe, en pratique, de la transmission du message vers le serveur. De son côté, le skeleton « dépaquette » le message côté serveur, l’active sur l’objet destinataire, « empaquette » les résultats (le return du message s’il y en a un) et transmet ce résultat au client. Le stub, encore lui, sera chargé de récupérer et de « dépaqueter » le return, afin de permettre au client d’aller de l’avant avec son exécution. C’est la présence de ces deux exécutables additionnels qui permet aux développeurs d’écrire une application distribuée, aussi simplement qu’une application locale. Dans un processus d’invocation statique, la plus grosse partie du boulot est ainsi faite, gracieusement, par le stub et le skeleton. Dans les dernières versions de Java, le skeleton n’est plus nécessaire et ne sera plus généré par la commande rmic. Le serveur découvre par un mécanime de « réflexion », c’est-à-dire directement à partir de l’objet serveur, les méthodes qui sont accessibles sur celui-là. À cet allègement près, rien ne change dans la mise au point de la solution RMI.

igure 16-1

Les rôles joués par le stub et le skeleton.

394

L’orienté objet

Invocation statique versus invocation dynamique L’exemple de RMI que nous présentons pour l’instant, de même que son équivalent Corba, réalise cet envoi de message à travers Internet grâce à un mécanisme d’invocation statique pour lequel un stub reste indispensable afin de jouer la doublure du serveur du côté client. En substance, lors de l’invocation statique, plus exigeante mais plus simple à mettre en œuvre que l’invocation dynamique, le client doit disposer d’une copie de l’interface du serveur (pour compiler) et du stub, également en provenance du serveur (pour pouvoir s’exécuter). En revanche, l’invocation dynamique ne nécessite plus la présence de cette doublure et de ce stub et permet de découvrir sur le moment et « à chaud » les services dont un premier objet a besoin en provenance de l’interface d’un deuxième. Nous reparlerons de l’invocation dynamique.

Lors d’une invocation statique, le stub serait comme une doublure ou un substitut du serveur pour le client. En son absence, le programme ne pourrait s’exécuter côté client, car il n’y aurait personne à qui envoyer le message. Par exemple, il est important que les paramètres du message soient empaquetés de telle manière que le skeleton puisse aisément les dépaqueter. L’un ne va donc pas sans l’autre. Le skeleton apparaît comme le dual du stub côté serveur, faisant, lui, office d’une doublure ou d’un substitut du client. La présence de ces deux modules, stub et skeleton (répétons que le skeleton a disparu des dernières versions de Java), est générale à toutes les applications distribuées qui fonctionnent par invocation statique. On les retrouvera de manière très semblable dans la technologie Corba, et en partie dans la technologie .Net de Microsoft. C’est une donnée inhérente aux applications distribuées d’avoir du côté serveur et du côté client un substitut de l’autre. On se rappellera qu’il s’agit par ailleurs d’un des 23 design patterns mis au point par le « Gang des quatre » (présentés au chapitre 23). Stub et skeleton Dans chacune des technologies distribuées qui se disputent le marché, et dans leur version dite statique, le « stub » s’occupe de recevoir l’envoi, de le décomposer en les différentes parties qui le composent, puis de le transmettre. Le « skeleton », généré explicitement dans les anciennes versions de Java, ou fonctionnant implicitement dans les nouvelles, le reçoit, le recompose, l’exécute sur l’objet concerné, récupère en retour la réponse du message, qu’il transmet vers le stub. Ce dernier reprend alors la main, récupère le retour et l’intègre comme il se doit dans l’exécutable. Une autre manière de penser le stub, qui s’accorde parfaitement aux applications distribuées, lorsque celles-ci concernent des processeurs embarqués dans des machines autres qu’informatiques, est de le considérer comme un « pilote » du serveur. En effet, toute interaction de votre ordinateur avec un périphérique nécessite l’intégration d’un « pilote », qui sert d’intermédiaire à cette communication. Il reçoit les commandes du processeur, mais les ré-interprète, les temporise, les remet en forme, pour que le périphérique les « comprenne » et puisse les exécuter. Il en va également ainsi du stub dans l’interaction client-serveur. Ce stub n’est plus indispensable dans le cas de l’invocation dynamique.

Lancement du registre Le lancement du « registre » est une opération additionnelle qui permet aux objets serveurs de s’enregistrer, et à l’application client de les retrouver. La ligne de commande rmiregistry s’en occupe. Sur DOS, il convient de plutôt exécuter start rmiregistry, de manière à pouvoir continuer de lancer des instructions sur la même fenêtre. La dernière étape côté serveur est d’exécuter le programme principal : java PrincipaleServeurRMI. Côté client, il faudra simplement exécuter le programme principal du client : java PrincipaleClientRMI. La procédure à suivre, ainsi que les différentes fenêtres DOS qui s’afficheront sur l’écran de l’ordinateur, si le tout est exécuté en local, sont montrées ci-après.

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

395

Figure 16-2

Déroulement de l’interaction RMI client-serveur, en local, et à partir d’une fenêtre DOS.

Corba (Common Object Request Broker Architecture) Le protocole RMI de communication entre objets distribués fonctionne très bien, à ceci près que, s’il accepte en effet que des objets se parlent à travers Internet, il n’accepte, en revanche, de ne les voir se parler que dans une, et une seule, langue, le Java. Cette restriction est la condition première de la facilité d’emploi. C’est un peu court jeune homme, quand on connaît le nombre de langages informatiques qui existent aujourd’hui, et dans lesquels les informaticiens continuent à développer… Par ailleurs, cela ne permet nullement de récupérer tout ce qui a déjà été développé, dans d’autres langages, afin de le transformer, car ce n’est finalement qu’une question d’apparence, en services disponibles sur le réseau Internet ou intranet. Corba est une réponse à ce souci d’universalité, et à ce refus de faire table rase des milliards de lignes de codes, traînant dans les sillons des disques durs. Tous les objets devraient pouvoir communiquer entre eux, qu’ils aient été écrits en Java, C++, Smalltalk, VB, Pascal, Fortran, même Cobol (ne dites pas à ma mère que je programme encore en Cobol, elle me croit programmeur Java, webmestre chez Google!). Les applications existantes, même non orientées objet, et que l’on cherche à récupérer, devraient pouvoir s’envelopper dans une sorte de revêtement objet, afin que les procédures qui les animent se métamorphosent en message.

396

L’orienté objet

Un standard : ça compte Corba, non seulement, a vocation universelle mais a également vocation de standard. Au même titre qu’UML pour l’analyse et la modélisation des applications informatiques, l’OMG, toujours en quête de cet universalisme logiciel, a élu Corba standard pour les applications distribuées. L’objectif de Corba est d’assurer l’interopérabilité entre des applications hétérogènes, tant du point de vue du langage de programmation utilisé pour les rédiger que du point de vue du système d’exploitation au-dessus duquel les exécutables de ces applications tournent. Si vous voulez échanger des services à travers Internet, parlez Corba et ne parlez plus Java ou .Net (nous verrons que, si .Net de Microsoft, accepte à l’heure actuelle plusieurs langages de programmation, la présence de Windows comme OS est une condition première), du moins, si vous désirez vous faire comprendre par tout le monde. Malheureusement, malgré l’extraordinaire avancée que constitue Corba sur le chemin de l’interopérabilité, un peu comme l’espéranto face à l’anglais, et alors que RMI et les services Web, continuent sur leur lancée, et de plus belle, Corba a un peu plus de mal à se faire entendre dans les entreprises. La raison première tient, sans nul doute, au prix à payer pour son universalité. Corba est plus compliqué que les autres protocoles à mettre en œuvre, car il exige, avant tout, d’apprendre un nouveau langage de programmation, un de plus, IDL. Et c’est, paradoxalement, cette seule complication qui nous intéresse ici, car l’acronyme IDL signifie : « Interface Definition Langage ». Ce langage permet, en effet, d’écrire les seules lignes de code vraiment nécessaires à la réalisation d’applications distribuées : la définition des interfaces. Alors qu’en Java, vous définissez les interfaces en Java, en XML pour les services Web, en Corba, vous les définissez en IDL. Ce langage est proche du C++, et permet de complexifier davantage encore, par comparaison avec Java (mais est-ce une si bonne chose pour la diffusion de Corba ?), la manière dont vous définissez les services qu’une application serveur peut offrir à un client. Corba a trouvé une solution très classique pour se rendre indépendant de tous les langages de programmation existants : en proposer un nouveau, au-dessus de tous les autres. Dans le cas très simple de l’exemple de ce chapitre, l’interface sera définie pratiquement comme en Java (ce code doit se trouver dans un fichier portant l’extension « idl ») : module ExempleCorba { interface IO2Corba { void jeTravaillePourO2(); string jeRenvoieUnString(); }; };

IDL Les interfaces sont déclarées au sein d’un module, ici, ExempleCorba. Un module constitue d’abord un espace de nommage, c’est-à-dire que tout ce qui sera déclaré à l’intérieur du module portera toujours au départ le nom du module (c’est équivalent aux « packages » ou « assemblage » en Java et aux « namespace » en C# et C++). Les modules, tout comme les répertoires, peuvent s’imbriquer les uns dans les autres. Un module peut contenir beaucoup d’autres déclarations que les interfaces, seuls éléments syntaxiques distribuables en Java. Dans un module, IDL permet de déclarer, en plus des interfaces et des signatures de méthodes qu’ils contiennent, des constantes, de définir des types nouveaux (en récupérant le typedef, les struct ou enum du C++), de définir des exceptions. De plus, à la différence de Java et de C#, dans les interfaces, il est possible de rajouter des attributs. Ces attributs peuvent n’être que lisibles, « readonly », ou parfaitement modifiables par le client.

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

397

Digne héritier du C++, Corba ne force pas par sa syntaxe la pratique de l’encapsulation. Les interfaces peuvent, comme en Java et C#, être héritées entre elles, simplement ou de façon multiple. Les arguments des méthodes peuvent être passés par référence ou par valeur, ce qui peut à nouveau compliquer la traduction en Java. Lorsque le fichier idl est finalisé, pour passer à l’implémentation, il est nécessaire de projeter tout cela vers un langage de programmation classique. C’est là que l’universalité de Corba fait merveille : il permet en effet la « projection » du fichier idl dans n’importe quel langage de programmation OO ou éventuellement non OO du côté client comme du côté serveur. En se limitant à Java et C++, un module sera traduit en package en Java et en namespace en C++. Une interface sera traduite en interface en Java mais – car on sait depuis le chapitre précédent qu’aucune structure syntaxique équivalente n’existe en C++ – en classe abstraite dans ce langage. Dans les deux langages, chaque attribut de l’interface entraînera l’existence d’une paire de méthodes pour lire et pour modifier cet attribut. Les exceptions seront traduites en exception et ainsi de suite…

Compilateur IDL vers Java Nous allons, dans le cas d’une projection en Java, utiliser le compilateur « IDLJ » fourni par Sun (l’outil Corba offert par Sun dans le toolkit Java). Bien sûr, d’autres implémentations de Corba existent, comme Visibroker de Borland, OrbixWeb de Iona Technologies, WebSphere d’IBM, ou d’autres encore, généralement payantes. Celle de Sun est gratuite, comme le toolkit Java. Il est important de séparer les spécifications énoncées par l’OMG de l’implémentation logicielle concrète de ces spécifications, qui peut prêter à quelques variations selon les constructeurs. Nous compilons notre fichier .idl dans le monde Java (on pourrait le faire dans bien d’autres langages) au moyen de l’instruction suivante : idlj –fall ExempleCorba.idl

Cette instruction a pour effet de créer un nouveau répertoire ExempleCorba, dans lequel plusieurs nouveaux fichiers .class sont installés, comme indiqué ci-après : Volume in drive C is SYS Volume Serial Number is 0C6E-0209 Directory of C:\Test\TestCorba\ExempleCorba 18/07/2004 18/07/2004 18/07/2004 18/07/2004 18/07/2004 18/07/2004 18/07/2004 18/07/2004

16:56 . 16:56 .. 16:57 351 IO2Corba.java 16:57 2.002 IO2CorbaHelper.java 16:57 842 IO2CorbaHolder.java 16:57 360 IO2CorbaOperations.java 16:57 2.256 IO2CorbaPOA.java 16:57 2.775 _IO2CorbaStub.java 6 File(s) 8.586 bytes 2 Dir(s) 21.581.393.920 bytes free

Ces fichiers Java doivent servir à faciliter la conception en Java de tout ce que vous avez prévu dans la définition du module. Observons-les de plus près. Tout d’abord, nous retrouvons le « stub » _IO2CorbaStub.java qui implémente l’interface, puisqu’elle apparaît comme un « substitut » du serveur pour le client, dans une optique tout à fait semblable à RMI. Depuis la nouvelle version de Corba, nous trouvons également un fichier IO2CorbaPOA. Le POA (Portable Object Adapter) sert d’intermédiaire entre l’ORB et l’objet Corba et pemet

398

L’orienté objet

de définir différentes « politiques » d’activation et de désactivation de l’objet serveur lors de l’exécution de ses services (un par méthode, activation uniquement lors de l’appel, problème de sollicitation concurrentielle de l’objet…). Toute application Corba doit posséder au minimum une instance de POA qui peut provenir soit directement du RootPOA (la politique par défaut) ou d’une version plus spécialisée. Associé à ce POA, un POAManager est à l’écoute du côté serveur. Il se conforme aux fonctionnalités du type de POA choisi pour le serveur et finalement crée l’objet serveur en le référant par un ID. Ce POA maintient une table réalisant le lien entre les ID de chaque objet et les politiques d’activation associées. C’est le POA qui recevra la requête (il remplace donc le skeleton de l’ancienne version de Corba) et qui, en fin de compte, invoque la méthode en question en suivant sa politique d’activation et de désactivation des objets. Le fichier IO2CorbaOperations.java est le plus évident à saisir, puisqu’il se limite à reproduire en Java l’interface idl., et ce y compris la signature des deux méthodes. Cette interface est implémentée par IO2CorbaPOA. IO2CorbaHelper.java contient un ensemble de fonctions auxiliaires, notamment la méthode narrow(), que nous utiliserons par la suite, et qui permet d’effectuer l’équivalent d’un « casting » dans le type de l’interface. Finalement, le fichier IO2CorbaHolder.java a comme raison d’être de traiter les arguments des méthodes qui peuvent être passés par référent, et qui ne sont pas pris en compte de manière automatique par Java. Il reste maintenant à concrétiser le côté client et le côté serveur de l’application, en écrivant deux nouveaux fichiers : ExempleCorbaClient.java et ExempleCorbaServeur.java, que nous allons détailler avec plus d’attention, car il s’agit bien d’un travail que le programmeur est d’office amené à faire.

Côté client Le fichier ExempleCorbaClient.java import ExempleCorba.*; import org.omg.CosNaming.*; import org.omg.CosNaming.NamingContextPackage.*; import org.omg.CORBA.*; public class ExempleCorbaClient { public static void main(String args[]) { try { ORB orb = ORB.init(args, null); /* creation d'un objet CORBA */ /* ensuite débute une suite d'instructions pour la mise en oeuvre du nommage */ org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService"); NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef); NameComponent nc = new NameComponent("unObjetO2", ""); NameComponent path[] = {nc}; /* il faut retrouver l'objet sur lequel déclencher les services */ IO2Corba unObjetO2 = IO2CorbaHelper.narrow(ncRef.resolve(path)); System.out.println("j'utilise O2"); /* on envoie les deux messages */ unObjetO2.jeTravaillePourO2(); System.out.println(unObjetO2.jeRenvoieUnString()); } catch(Exception e) { System.out.println("Error : " + e); e.printStackTrace(System.out); } } }

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

399

Quelques « imports » sont d’abord nécessaires pour récupérer les fonctionnalités Corba, par exemple, le service de « nommage » prévu dans org.omg.CosNaming.*. Nous plaçons l’essentiel du code dans un bloc try-catch de gestion d’exception, puisque de nombreux problèmes pourraient survenir dans la communication entre le client et le serveur. Il faut ensuite créer un objet Corba, orb, pour pouvoir utiliser le bus Corba, qu’on appelle l’ORB (l’Object Request Broker), et qui permet l’empaquetage et le désempaquetage des messages, et la circulation de ceux-ci sur TCP/IP. Le service le plus important que Corba met à notre disposition est le service de « nommage » qui permet, comme dans le cas du registre RMI, de récupérer un objet par ses nom et adresse, où qu’il se situe sur Internet. Les services de Corba Corba, répondant ainsi aux spécifications de l’OMG, met d’autres services à notre disposition, tous implémentés en partie par les constructeurs, tels que le service de nommage, mais aussi le « cycle de vie », qui définit la manière dont les objets sont créés, déplacés ou copiés, le « service d’événements », qui permet à des objets de répondre à des événements, le « service de transaction », qui permet à une succession de messages de s’inverser si une étape se passe mal, le « service d’accès concurrentiel », qui permet à un même objet de traiter simultanément plusieurs clients, un « service de requête », par lequel l’objet peut nous informer sur son état et ses méthodes, et d’autres encore.

Nous n’allons pas passer trop de temps sur la manière dont les objets utilisent ce service de nommage. Il faut d’abord récupérer un « naming context », car les noms des objets peuvent se structurer différemment selon l’implémentation spécifique de Corba qu’on utilise. Un tableau de NameComponent reprendra le nom entier de l’objet. Ici, nous nous limiterons à juste un nom : unObjetO2, le même nom d’objet que nous avions donné lors de l’utilisation de RMI. Notre tableau se réduit ici à un seul élément. L’équivalent du unObjetO2 = (IO2RMI)Naming.lookup("unObjetO2") de RMI se transforme ici en IO2Corba unObjetO2 = IO2CorbaHelper.narrow(ncRef.resolve(path)) . L’effet est le même : réaliser l’association entre un objet local dont le référent est unObjetO2 et l’objet distant, lequel recevra in fine les messages. C’est cette pratique qui permet de rendre l’écriture d’une application distribuée très proche de l’écriture d’une application s’exécutant en local. La classe IO2CorbaHelper permet de réaliser le « casting » de l’objet dans l’interface désirée. Finalement, l’invocation des deux méthodes se fait exactement comme pour RMI, sans différence aucune avec une application locale. Cette écriture simple dissimule pourtant une procédure laborieuse, comprenant le codage des messages et des arguments dans une forme transférable sur le réseau, le transport de ces messages à travers le Web, l’activation et l’exécution de ces méthodes sur l’objet serveur, la récupération des « retours », le codage de ceux-ci, leur retransmission sur le Web vers le client, et finalement la récupération de ces « retours » par le client.

Côté serveur Le fichier ExempleCorbaServer.java import import import import import

org.omg.CosNaming.*; org.omg.CosNaming.NamingContextPackage.*; org.omg.CORBA.*; org.omg.PortableServer.*; ExempleCorba.*;

L’orienté objet

400

public class ExempleCorbaServer{ public static void main(String args[]){ try { ORB orb = ORB.init(args, null); /* creation d'un objet CORBA */ /* creation de l'objet serveur */ O2CorbaServant o2Ref = new O2CorbaServant(); /* Obtention d'une référence au rootpoa et activation du POA manager */ POA rootpoa = POAHelper.narrow(orb.resolve_initial_references("RootPOA")); rootpoa.the_POAManager().activate(); org.omg.CORBA.Object ref = rootpoa.servant_to_reference(o2Ref); IO2Corba href = IO2CorbaHelper.narrow(ref); /* Obtention d'une référence à l'objet serveur */ org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService"); NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef); NameComponent path[] = ncRef.to_name("unObjetO2"); // dénomination de l'objet serveur System.out.println("Je mets mon objet sur le net"); ncRef.rebind(path, href); /* on enregistre l'objet serveur */ orb.run(); // on attend l'invocation des clients System.out.println("c'est fait"); } catch(Exception e) { System.err.println("Error: " + e); e.printStackTrace(System.out); } } } class O2CorbaServant extends IO2CorbaPOA { /* implémentation de l'objet serveur */ public void jeTravaillePourO2() { /* implémentation du premier service */ System.out.println("je travaille pour O2"); jImplementeLeServiceO2(); } public String jeRenvoieUnString() { /* implémentation du deuxième service */ return "un bonjour en provenance d'O2"; } private void jImplementeLeServiceO2() { System.out.println("je suis prive dans O2"); } }

Comme pour le fichier du côté client, nous devons, dans un premier temps, créer un objet Corba, un orb. Une différence importante avec le protocole RMI est que Corba sépare les responsabilités du côté du serveur en un ensemble d’objets « servant » et un « serveur », à proprement parler, qui a pour rôle d’instancier ces objets servants selon le protocole d’activation du POA. Ce sont les objets servants qui implémentent l’interface IO2CorbaPOA responsable de la définition des services à délivrer. Deux classes sont donc à l’œuvre ici, la classe serveur qui s’occupe de créer les objets servants pour les rendre disponibles sur le bus Corba, et la classe servant qui type les objets rendant les services. C’est par l’instruction org.omg.Corba.Object ref = rootpoa.servant_to_reference(o2Ref); que le servant se rend disponible

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

401

sur le serveur doté du POA choisi. Le service de nommage fonctionne comme pour le client. L’enregistrement de l’objet servant, avec le nom symbolique qui permettra de le référer sur Internet, se fait par l’instruction : ncRef.rebind(path, href); équivalente à l’instruction Naming.rebind("unObjetO2", this); du protocole RMI (on retrouve l’expression rebind). À ce stade, la dernière instruction est de lancer le serveur et d’attendre simplement qu’un client adresse une requête. C’est de cela dont s’occupe l’instruction :  orb.run();.

Exécutons l’application Corba De manière à faire tourner cette application, il faut d’abord compiler tous les fichiers Java, ceux du répertoire ExempleCorba, et les deux que l’on vient de réaliser, installés quant à eux, dans le répertoire supérieur. Au même titre que le rmiregistry de RMI, il faut déclencher le service de nommage par la ligne de commande suivante : tnameserv. Ensuite, les deux dernières étapes sont, toujours comme en RMI, l’exécution du serveur suivie de l’exécution du client. Si tout se déroule comme attendu, les commandes à écrire, ainsi que les résultats obtenus dans un environnement DOS, devraient apparaître comme figure 16-3.

Figure 16-3

Déroulement de l’interaction Corb client-serveur, en local, et à partir d’une fenêtre DOS.

402

L’orienté objet

Corba vise à offrir un environnement d’exécution pour des millions d’objets à granularité variable : de simples instances d’objet C++, de quelques centaines de bits, jusqu’à des objets plus volumineux, encapsulant des millions de lignes de code Cobol, déjà programmées et à récupérer par un emballage IDL. Bien entendu, tous ces objets ne sont pas utilisés en même temps. Ainsi, pour éviter d’encombrer inutilement la mémoire centrale, Corba propose également plusieurs stratégies d’activation des objets définies par le POA. Par défaut, c’est lors de l’envoi de message sur le serveur que Corba activera l’objet pour le rendre capable de traiter le message. Mais plusieurs stratégies d’activation restent possibles, à choisir par le programmeur quand il développe la partie serveur, comme un processus par activation d’objet, un processus partagé par plusieurs activations, ou, encore, un processus par exécution de méthodes sur un objet. Ce même exemple est bien évidemment transposable dans n’importe quel langage de programmation OO, que cela soit côté serveur ou côté client.

Corba n’est pas polymorphique Un autre point particulièrement intéressant ici, car il touche aux fondements de l’OO, est la façon dont RMI, au contraire de Corba, permet un vrai polymorphisme, grâce à la possibilité de transférer entre le client et le serveur, non plus, seulement, des signatures de méthodes empaquetées, mais, par exemple, tout le code d’une classe. Pourquoi est-ce nécessaire à l’implémentation du polymorphisme ? Supposez qu’en réponse à un message envoyé par le client, le serveur renvoie un objet typé dynamiquement (c’est-à-dire pendant l’exécution), FilsO1. Or, dans la signature du message, déclarée dans l’interface, le type statique de ce retour est la superclasse O1, qui est la seule connue par le client. Recevant cet objet, vers lequel un nouveau message propre à la classe FilsO1 (c’est-à-dire redéfini dans la classe FilsO1) pourrait être envoyé, il est nécessaire de connaître le code de ce message. La classe FilsO1 n’étant pas connue par le client, elle sera téléchargée pendant l’exécution du programme. RMI permet donc, dans le feu de l’action, de compenser par un transfert de code ce qui manque comme information, soit chez le serveur, soit chez le client. Le polymorphisme en est directement responsable, car il permet de spécifier, seulement pendant l’exécution, parmi plusieurs méthodes celle qui devra réellement être exécutée. Corba ne le peut pas, car les codes des classes ne peuvent être transférés côté client ou côté serveur en cours d’exécution. C’est la raison pour laquelle RMI, là encore à la différence de Corba, génère les « stubs » et les « skeletons » à partir des classes implémentant les interfaces, et non plus directement à partir des interfaces. La possibilité pour un code Java distribué de créer ce « stub », sur le vif, face à une classe manquante du côté serveur ou client, et de le transférer vers l’un ou l’autre, est la base du fonctionnement de RMI et de Jini, comme nous allons le voir.

Rajoutons un peu de flexibilité à tout cela Nous avons établi dans le chapitre précédent une analogie entre le rôle du « stub » et celui d’un pilote de périphérique. Essayons de la pousser un peu. Prenons, par exemple, le cas bien connu de l’imprimante. Avant de pouvoir utiliser l’imprimante d’un ordinateur, il faut charger le pilote, qui est un programme servant d’intermédiaire entre l’imprimante et le processeur. Reprenons notre exemple de l’appareil photo et de l’imprimante, de plus en plus probable, avec l’accroissement de la mobilité de tous les utilitaires embarquant un processeur. Vous arrivez dans un lieu quelconque avec votre portable, mais cela pourrait être votre agenda électronique, votre appareil photo, et vous désirez, sans plus attendre, imprimer un document, contenu dans l’un de ces appareils. Dans une ville, il est possible

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

403

que plusieurs imprimantes soient disponibles dans un rayon de 100 m, presque à portée de main, toutes pouvant imprimer votre document. Comment choisir ? Plusieurs critères pourraient dicter votre choix : la proximité, la qualité de l’impression, le prix, et, si le document est volumineux, la durée d’impression. L’idéal serait que sur votre portable ou dans le viseur de votre appareil photo, à la suite d’une petite investigation de votre part sur des possibles « services » d’impression existant à proximité, vous voyiez apparaître une liste des imprimantes possibles, chacune avec son emplacement, son prix, sa qualité et sa durée d’impression. Une fois votre choix effectué (vous pourriez même imaginer que votre portable ou votre appareil photo connaît vos préférences au point de pouvoir automatiser ce choix), c’est seulement à ce moment-là, qu’il devient nécessaire de télécharger le pilote de l’imprimante, le temps de l’interaction de votre portable ou de l’appareil photo avec celle-ci. Il est crucial que vous soyez également tenu informé des soudaines défaillances d’un appareil, jusqu’alors disponible sur le réseau, que vous le soyez aussi du rajout dans le réseau, d’une nouvelle imprimante, ou de la disparition d’une d’elles du marché. Bref, il faudrait avoir la possibilité de ne charger un « stub » ou un « pilote » qu’à l’issue d’un choix entre plusieurs d’entre eux, de ne le conserver que le temps de l’exécution, et de pouvoir à tout moment remettre ce choix en question. Tout cela conduit à une vision de l’informatique plus mobile, souple, adaptable et, surtout, plus en phase avec les innovations technologiques de type réseau sans fil, la mobilité des utilisateurs et l’accélération des changements que la pression économique induit. Les applications informatiques évoluent, en effet, très vite, et il est important d’être tenu au courant des nouveautés dont vous pourriez tirer un profit immédiat. Il faudrait pour cela pouvoir rapidement charger un nouveau stub, sans vouloir le figer à jamais sur votre disque dur, et sans qu’il entre en compétition avec ceux qui le précédaient (suivez le regard vers certaines .dll que nous ne nommerons pas). Que les applications informatiques soient plutôt à louer, le temps de leur utilisation, qu’acquises définitivement est en passe d’entrer dans les mœurs de nombreuses entreprises, lassées de ces mises à jour incessantes et coûteuses de suites bureautiques ou autres logiciels graphiques. Cette nouvelle vision ne s’accorde pas avec la pratique trop statique, consistant à télécharger le stub avant l’exécution du programme, et à partir d’une interface déjà identifiée.

Corba : invocation dynamique versus invocation statique Corba, le premier, a infléchi cette contrainte, en permettant que le stub soit comme généré pendant l’exécution du client, et ne soit plus un préalable à cette exécution. On parle alors d’invocation dynamique en lieu et place de l’invocation statique que nous avons illustrée dans notre petite application précédente. Le client Corba peut, à partir d’objets distribués dont il connaît l’existence et l’adresse, spécifier le nom de la méthode devant être invoquée, ainsi que les paramètres désirés. Pour peu qu’un tel objet existe, un stub et un skeleton seront générés tant du côté client que serveur, permettant l’envoi de messages, exactement comme dans le cas statique. Toutes ces informations sont stockées dans une sorte d’annuaire des services (l’interface repository) associés à chaque objet entre lesquels le client pourra faire son choix. Ainsi le client peut-il toujours, pendant l’exécution, se renseigner sur ce qui est disponible, construire son message, et l’envoyer vers le serveur de son choix. Dans le cas d’une invocation dynamique, dont la réalisation est nettement plus complexe que celle de l’invocation statique (et dépasse le cadre de cet ouvrage), pendant son exécution, le code client peut obtenir la référence d’un objet Corba, instancier une variable de type Request, obtenir l’interface de l’objet à partir de « l’interface repository », en extraire les méthodes et les attributs et, à partir de ceux-ci, construire la requête, l’invoquer et finalement obtenir les résultats de son exécution en retour. Le tout sans aucune compilation préalable pour adapter le client au serveur.

404

L’orienté objet

Jini Du côté de Java-RMI, Sun a également accru la souplesse de l’approche, avec la possibilité de découvrir des services plutôt que de s’y conformer dès le départ, comme l’exige le téléchargement du stub en préambule de l’interaction. En quelques lignes, car il en faudrait tellement plus pour rendre justice à cette avancée technologique importante, Jini doit tout d’abord être perçu comme la continuation de RMI. Jini comprend une addition structurelle, essentielle à son fonctionnement, comme en Corba un annuaire de services (appelé « lookup »). Ce dernier simplifie la médiation entre le client et le serveur, en gardant continûment la liste des services disponibles sur le réseau, et en permettant au client de s’informer pour choisir le service qui réponde le mieux à ses attentes. Rappelez-vous la petite histoire de l’imprimante. Seule une approche de type Jini permet à ce scénario de se réaliser. Cet « annuaire » doit également prendre en charge toutes les modifications se produisant sur le réseau de service, et en informer les clients le souhaitant : disparition, addition ou modification des services existants. L’utilisation de Jini se déroule de la manière suivante. D’abord, si un artefact quelconque cherche à rendre ses services disponibles sur un réseau, il doit découvrir un annuaire pour y enregistrer son offre. Soit il possède l’adresse IP de cet annuaire, soit il se met en quête dans le réseau local d’un annuaire disponible. Une fois cet annuaire découvert, l’artefact met en dépôt sur celui-ci un « proxy », qui peut être soit un stub, permettant l’interaction directe entre un client et lui, soit, plus simplement encore, l’ensemble du service exécutable, c’est-à-dire, un objet et les méthodes à exécuter sur celui-ci. En possession de cet ensemble, le client pourra exécuter le service, sans plus avoir à passer par le serveur. Lorsque, à son tour, un client (rappelons-nous que les rôles, comme dans la vision peer-to-peer, sont tout à fait interchangeables) désire s’offrir un de ces services, il passe également par ce même annuaire, afin de s’informer sur l’offre disponible. Il transmet à l’annuaire les services désirés, de manière à récupérer le proxy qui lui permettra de les utiliser. Ce proxy sera, soit le service complet, soit le stub qui, par RMI, servira de pont entre le client et le serveur. Un problème important à solutionner devient alors la robustesse de cette architecture flexible, car les services peuvent apparaître ou disparaître à tout moment. Il faut imaginer un mécanisme qui tolère cette flexibilité, tout en limitant les impacts nuisibles qu’elle peut causer. Lors d’un envoi de message qui échoue, Jini, dans le prolongement de Java, délègue à un mécanisme de gestion d’exception la possibilité de remédier à cet échec. Par ailleurs, un service qui décide de quitter le réseau en avisera l’annuaire qui détient son proxy, de manière que celui-ci soit mis hors d’état. L’annuaire avisera les utilisateurs courants de ce proxy qu’il est temps de mettre fin à cette utilisation. Un service pourrait également quitter le réseau, de manière plus brutale, sans en informer l’annuaire. Les dommages causés par un tel départ sont atténués par un mécanisme dit de « leasing », qui demande à l’artefact qui fournit le service d’informer l’annuaire sur la durée de mise à disposition du service, et de la fréquence selon laquelle l’artefact se rappellera au bon souvenir de l’annuaire. Si lors d’une de ces sessions de contact, le service ne peut plus être contacté ou s’il ne désire pas renouveler le bail de son proxy, ce dernier sera détruit, et les clients seront avertis de sa disparition.

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

405

XML : pour une dénomination universelle des services Un dernier problème à relever concerne la façon dont les services sont présentés sur l’annuaire, et celui dont le message ainsi que son retour sont codés. Corba le fait à sa sauce, Jini à la sauce Java, mais rien de tout cela n’est vraiment conforme au style dans lequel les concepteurs et les acteurs principaux du Web ont décidé de coder toute information installée et circulant sur ce Web. Pour autant que les services à disposition, ainsi que les messages qui circulent sur Internet, soient à assimiler à n’importe quel autre type d’information disponible sur le Web, un standard aujourd’hui s’impose, qui a pour nom XML. La circulation ne se fait plus véritablement via Internet mais via le Web, en utilisant le protocole http. Dans l’approche des services Web, tout comme dans n’importe quel échange au-dessus du protocole http, un « serveur Web » est indispensable pour recevoir et traiter la requête côté serveur (par exemple : « Apache » sur Linux ou « IIS » sur Windows »). Alors que Corba fait jouer à l’ORB le rôle de bus de communication des requêtes entre clients et serveurs, dans le cas des services Web, ce rôle incombe au protocole http, à travers lequel ne circule que du texte et non plus des ordres prêts pour l’exécution, comme c’est le cas pour Corba ou RMI. Par un jeu de balises imbriquées (voir encart), XML permet de structurer de manière très homogène tout le contenu sémantique (à différencier de sa seule mise en forme) des documents disponibles sur le Web. XML pourrait ainsi constituer un mode de représentation des services et des messages envoyés sur le Web, à utiliser au-dessus des modes de représentation propres à Corba ou RMI. Cela permet également d’homogénéiser, dans leur représentation, tous les services disponibles sur le Web, quelle que soit l’implémentation finale de ces services : Corba, RMI ou DCOM. L’interopérabilité redevient possible à partir d’XML. Ce mariage entre l’informatique distribuée, la possibilité qu’ont deux applications informatiques de se solliciter mutuellement à travers le Web et le langage XML porte le nom de « services Web ». Cela permet aussi d’assimiler tout service à n’importe quel type de documentation circulant à travers le Web, et de bénéficier ainsi des mécanismes de recherche de documentation et d’extraction d’information (par un « parsing » des documents XML), devenus aussi indispensables que courants sur le Web. Microsoft a parfaitement anticipé avec XML cette homogénéisation du contenu des messages et des services dans sa nouvelle plate-forme .Net, en automatisant la traduction en XML de tous services codés, au départ, dans un langage de programmation comme C# ou VB.Net. Sun fait de même, en proposant un ensemble de librairies Java dénommées JAX-RPC (concaténant Java, XML et RPC – Remote Procedure Call), et permettant également une parfaite interopérabilité entre les langages de programmation et les systèmes d’exploitation. PHP ne pouvait pas ne pas suivre étant donnée l’importance du monde Web pour ce langage, et les services Web ainsi que les protocoles (Soap…) qui les accompagnent sont parfaitement intégrés. Nous nous limiterons dans la suite à décrire la manière dont .Net nous invite à développer des services Web, la plus simple à mettre en œuvre, en prenant conscience que cette manière d’intégrer XML dans la dénomination des services et des messages est en passe d’être adoptée par tous les grands constructeurs informatiques, tous les langages de programmation, et de devenir de facto un standard Web.

406

L’orienté objet

Tim Berners-Lee et le Web sémantique Dans cette dernière partie de chapitre, nous avons fait allusion à un souci permanent accompagnant le développement explosif du Web. Il s’agit de l’uniformisation de la structure des documents qui y sont accessibles, y compris des documents reprenant la signature d’objets exécutables mis à notre disposition sur certaines machines. Voir le Web comme un dépositaire d’une infinité d’informations, qui ne se limitent pas à apparaître (comme de simples photos dans un album), mais qui, de surcroît, facilitent leur exploitation par des humains dans le plus de contextes opérationnels possibles, cela a toujours été la préoccupation essentielle de Tim Berners-Lee, son inventeur. Ce dernier veut rendre le Web le plus utile qui soit aux humains, en rendant toute l’information qu’il contient facilement accessible et surtout traitable par les programmes informatiques euxmêmes. Il chérit son enfant comme tout bon père le fait, en espérant, qu’en grandissant, il prenne la direction souhaitée, même si, toute éducation ne fait qu’esquisser quelques chemins de vie possibles parmi lesquels, non seulement l’enfant effectue son choix, mais dévie très vite des premières balises rencontrées. En matière d’autonomie et de contrôle difficiles, le Web est incontestablement un sommet. Tim Berners-Lee, diplômé de l’université d’Oxford, a consacré les premières années de sa vie professionnelle à l’élaboration de solutions de type réseau temps réel, technologie code-barres, traitement de texte, système d’exploitation multitâche, et bien d’autres secteurs de l’informatique. C’est en 1980, lors d’un séjour de six mois comme consultant informatique du CERN (le laboratoire européen de physique des particules), qu’il écrit un programme appelé « Enquire », qui permet à des documents, non seulement d’encoder leur organisation spatiale et leur visualisation, mais également, basé sur la technologie hypertexte, de se référencer mutuellement. Le protocole http avait vu le jour. Il faudra cependant attendre encore dix ans pour que Tim Berners-Lee publie officiellement cette idée et l’impose comme la principale utilisation d’Internet et sans doute la plus aboutie (à côté des protocoles ftp, e-mail, telnet…). En 1994, il fonde le consortium W3 qui préside aux destinées d’http et veille à ce que le Web évolue comme le souhaite son créateur. Il en assure donc la direction depuis sa création. Il est également détenteur d’une chaire de recherche au MIT. La récriture des signatures des messages sous forme d’XML s’inscrit parfaitement dans cette préoccupation d’uniformisation de tout document circulant sur le Web. Écrire les exécutables de la même manière que tout autre document permet d’étendre tous les systèmes de recherche et d’extraction des documents à ces mêmes exécutables. Le futur des agents, prétendument intelligents, et circulant sur le Web au service de l’un ou l’autre utilisateur, ne pourra s’en trouver que facilité. C’est la vision ultime du « Web sémantique », que toutes les données qui s’y trouvent puissent être définies et reliées d’une manière qui les rende facilement utilisables par les applications logicielles. Une première uniformisation sera de type syntaxique et anticipée par des langages comme XML, qui permettent de simplifier la structuration des documents et de les détacher de leur visualisation. Une seconde, extraordinairement plus ambitieuse, se doit d’être de type sémantique, et viser à la terminologie et au vocabulaire employé, terminologie unique, dont il faudra forcer l’adoption par de multiples communautés d’utilisateurs qui, aujourd’hui, se comprennent plus entre les lignes que grâce aux lignes. Cette terminologie, que l’on tente d’organiser en ontologies reprenant les termes d’un domaine et les relations logiques entre ces termes, devra faciliter tant l’indexation que la recherche des services Web, et résoudre le problème que rencontrent aujourd’hui les moteurs de recherche de sites Web. Cette simplification se trouve anticipée, encore très timidement, par des solutions comme les DTD ou les schémas XML et la mise au point du RDF (Resource Description FrameWork, sorte de résurrection des réseaux sémantiques inventés par l’IA dans les années 1970). L’auteur, plébiscité par le magazine Time, comme l’un des 100 plus grands esprits de ce siècle, défend cette vision dans un récent ouvrage intitulé Weaving the Web et publié chez Hardcover. Depuis fin 2004, il détient une chaire de professeur au département d’électronique et d’informatique de l’université de Southampton et y poursuit son projet d’enrichissement sémantique du Web. Il est aussi un ardent défenseur d’un Web préservant une parfaite « neutralité » quant aux équipements qui s’y connectent et au contenu de l’information qui le traverse, un Web libertaire, démocratique et gratuit !

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

407

XML XML installe l’information dans une structure imbriquée de balises, comme l’exemple ci-après l’illustre, lorsqu’il s’agit d’encoder un livre et ses auteurs :

Hugues Bersini Ivan Wellesz Il est très facile dans une telle structure récursive d’effectuer une recherche ou de traiter l’information. L’utilisation massive d’XML devrait permettre une bien plus importante homogénéisation de toute l’information contenue dans Internet, impossible par HTML (qui est une simple manière d’organiser la disposition de cette information mais non pas de définir son contenu). La dénomination des balises est laissée au libre choix des concepteurs. Cependant, l’intérêt est de s’accorder sur des dénominations et des structurations de document communes, qui seront définies dans un format appelé DTD (Data Type Dictionnary), par exemple, toujours pour le livre :

< < < <

!ELEMENT !ELEMENT !ELEMENT !ELEMENT

livre (auteur)+> auteur(prénom, nom)> prénom (#PCDATA)> nom (#PCDATA)>

Une autre possibilité, plus récente, d’écriture des documents de conformation a été adoptée dans le cadre des services Web. Ce sont les schémas XML, plus proches dans leur syntaxe des documents XML de base (à nouveau, un système de balises imbriquées, ce qui permettra d’uniformiser le traitement). En permettant la prise en compte de type de données plus complexes que la simple composition récursive propre à XML, ces schémas faciliteront la traduction dans un format XML des bases de données relationnelles ainsi que des diagrammes de classe UML. Le schéma XML de l’exemple du haut ressemblerait plus ou moins à ceci :

< ElementType name= “livre” content = “eltOnly” model = “closed” >

408

L’orienté objet

Les services Web sur .Net La plate-forme .Net de Microsoft a pour vocation de fournir un environnement qui simplifie la conception, le développement, le déploiement et l’exécution d’applications distribuées. Corba, RMI et Jini ont en commun de concevoir ces applications distribuées en termes d’invocation de méthodes à distance. Ces appels de méthodes et envoi de messages seront perçus – dans la vision Microsoft, partagée par HP, IBM, Sun, PHP, et plusieurs autres acteurs mammouths du monde informatique – comme la mise à disposition de services Web. La nouveauté essentielle par rapport à Corba ou Jini est l’exploitation intensive d’XML comme langage de description de ces services. Reprenons notre exemple de RMI et de Corba et développons en C#, cette fois, un service que nous désirons rendre disponible sur le Web.

Code C# du service Fichier TestService.asmx <%@ WebService Language="C#" Class="TestService" %> using System; using System.Threading; using System.Web.Services; public class TestService : WebService { [WebMethod] public string jeTravaillePourLeWeb (String unNom) { return "Salut, " + unNom + jeSuisPriveDansLaClasse(); } private string jeSuisPriveDansLaClasse() { return ", je travaille pour le web"; } }

Un service Web doit se coder dans un fichier de type asmx. À la différence de Corba et de RMI, qui exigent de débuter l’écriture des services sur le Web par la définition d’une interface, suivie pour son implémentation d’une classe donnée dans laquelle est défini le corps d’exécution, .Net permet de partir directement de l’implémentation. La présence de [WebMethod] rend la signature de la méthode disponible sur le Web, sans qu’il y ait besoin de détacher cette signature pour l’installer dans un code à part (on dit que l’on « expose » la méthode sur le Web). De manière à rendre ce service disponible, mais surtout « lisible », sur le Web, .Net crée automatiquement une version XML de ce même service, reprenant ce qu’il y a lieu de connaître pour utiliser ce service : son nom, les arguments à passer et ce que le service renvoie en retour. Le type de langage XML utilisé à cette fin s’appelle Web Service Description Language: WDSL. Ci-après, vous pouvez voir une partie du fichier TestService.asmx?WSDL qui reprend la description du service.

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

409

- - - - - - - - - - - - -

WDSL Parmi les différentes informations encodées en XML (nous n’en détaillerons pas la structure) vous pouvez deviner le nombre et le type des arguments, le retour du service ainsi qu’à la fin, l’emplacement URL de ce dernier. Ici, car nous travaillons en local, cet emplacement est : http://localhost/6152/TestService.asmx. Afin de visualiser les services en format WDSL, il faut créer une URL virtuelle, ici le localhost/6152 et éditer le fichier TestService.asmx?WSDL à partir de votre navigateur. La lecture du service Web sur le navigateur n’est possible que si le « serveur web » est actif (Apache sous Linux ou IIS sous Windows). En effet, c’est lui qui reçoit la requête, l’interprète comme un « service web » et vous en expose le contenu. Mais, bien évidemment, l’adresse URL de ce service variera en fonction de l’emplacement de l’objet à même de l’exécuter. Au même titre que Corba ou RMI, le service doit pouvoir être localisé sur Internet, mais cette localisation est directement codée sous forme XML et fait partie intégrante de la définition du service. Nous justifierons la présence de l’expression Soap par la suite.

410

L’orienté objet

WDSL L’existence de ce standard de description de services, WDSL, rendra toute application distribuée utilisable par l’ensemble des technologies d’objets distribués, tout environnement Web (par exemple, ce service pourrait être utilisé à partir d’un navigateur Internet) et sur toute plate-forme informatique confondue.

Création du proxy Une fois le service disponible sur Internet et prêt à être exécuté sur un serveur donné, comment un client peutil y avoir accès ? Comme pour RMI et Corba, il faut créer un « proxy » ou un « stub », qui permettra au client, localement, de procéder, comme s’il s’adressait directement au serveur. C’est ce proxy qui sert de passerelle entre le client et le serveur. Comme dans tous les mécanismes d’invocation statique d’objet distribué décrits jusqu’à présent, c’est un intermédiaire essentiel. Ce proxy, créé par « rmic » en RMI et par « idlj » en Corba, se construit dans .Net de la manière suivante : C:\TestService>wsdl /l:cs /o:TestServiceProxy.cs http://localhost/6152/TestService.asmx?WDSL Microsoft (R) Web Services Description Language Utility [Microsoft (R) .NET Framework, Version 1.0.3705.0] Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. Writing file 'TestServiceProxy.cs'. C:\TestService>

Le proxy se crée à partir de l’instruction wsdl et s’installe dans un fichier TestServiceProxy.cs. Remarquez la localisation Internet du fichier asmx. Toute classe devant être utilisée par une autre dans Windows se doit d’être transformée en une .dll. Il faut donc, du côté client maintenant, compiler ce proxy et le transformer en une .dll, pour qu’il puisse être utilisé par le client. Cela se fait au moyen de l’instruction suivante : C:\TestService>csc /out:TestServiceProxy.dll /t:library /r:system.web.services.dll TestServiceProxy.cs Microsoft (R) Visual C# .NET Compiler version 7.00.9466 for Microsoft (R) .NET Framework version 1.0.3705 Copyright (C) Microsoft Corporation 2001. All rights reserved. C:\TestService>

Le proxy, côté client, est maintenant prêt à jouer son rôle d’intermédiaire entre le client et le serveur. Il reste encore à créer le code du client, comme indiqué ci-après :

Code C# du client TestClient.cs using System; public class TestClient { public static void Main() { TestService unTest = new TestService(); Console.WriteLine(unTest.jeTravaillePourLeWeb(" moi le service ")); } }

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

411

Il faut compiler ce code en le rattachant au proxy, comme indiqué ci-après : C:\TestService>csc /r:TestServiceProxy.dll TestClient.cs Microsoft (R) Visual C# .NET Compiler version 7.00.9466 for Microsoft (R) .NET Framework version 1.0.3705 Copyright (C) Microsoft Corporation 2001. All rights reserved. C:\TestService>

Il ne nous reste plus qu’à exécuter le client : C:\TestService>TestClient Salut, moi le service, je travaille pour le Web C:\TestService>

Et le tour est joué.

Soap (Simple Object Access Protocol) Soap est le protocole XML d’écriture des messages à envoyer au serveur (le nom des méthodes et leurs paramètres) et d’écriture de la réponse obtenue, suite à l’exécution des messages. Les deux paquets Soap d’appel et de réponse de la méthode sont reproduits ci-après : POST /6152/TestService.asmx HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: length SOAPAction: "http://tempuri.org/jeTravaillePourLeWeb" moi le service HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: length Salut, moi le service, je travaille pour le Web

412

L’orienté objet

Ce message Soap sera transmis au proxy qui le traduira comme il se doit pour le transmettre au serveur. On conçoit que l’utilisation de « parseur » XML soit indispensable, de manière à extraire les informations nécessaires pour transmettre le message au serveur, son nom et ses arguments. Une fois ce message exécuté et la réponse obtenue, cette dernière sera empaquetée à son tour dans un format Soap, que le proxy dépaquettera, afin de la rendre disponible dans le code client.

Invocation dynamique sous .Net Malgré l’existence du proxy et des compilations préalables, on constate que sous .Net l’invocation dynamique et non statique est le mode d’invocation standard. Observons par exemple le code du fichier TestServiceProxy.cs généré automatiquement à partir du fichier .asmx (il nous servira par la suite lors de la description des appels asynchrones) : Fichier TestServiceProxy.cs using using using using using using

System.Diagnostics; System.Xml.Serialization; System; System.Web.Services.Protocols; System.ComponentModel; System.Web.Services;

/// [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute(Name="TestServiceSoap", Namespace="http://tempuri.org/")] public class TestService : System.Web.Services.Protocols.SoapHttpClientProtocol { /// public TestService() { this.Url = "http://localhost/6152/TestService.asmx"; } /// [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://tempuri.org/jeTravaillePourLeWeb", RequestNamespace="http://tempuri.org/", ResponseNamespace="http://tempuri.org/", Use= System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle= System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] public string jeTravaillePourLeWeb(string unNom) { object[] results = this.Invoke("jeTravaillePourLeWeb", new object[] { unNom}); return ((string)(results[0])); } /// public System.IAsyncResult BeginjeTravaillePourLeWeb(string unNom, System.AsyncCallback callback, object asyncState) { return this.BeginInvoke("jeTravaillePourLeWeb", new object[] { unNom}, callback, asyncState); } /// public string EndjeTravaillePourLeWeb(System.IAsyncResult asyncResult) { object[] results = this.EndInvoke(asyncResult); return ((string)(results[0])); } }

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

413

On y trouve l’instruction this.invoke (« nom de la méthode », « description des paramètres ») caractéristique des invocations dynamiques, puisque l’on passe toute la description du service au moment de l’exécution.

Invocation asynchrone en .Net En .Net, tout comme en Corba, il est possible, après l’envoi du message, de ne pas bloquer l’expéditeur le temps de l’exécution. L’expéditeur peut alors, s’il le désire, exécuter la suite de son code jusqu’à ce qu’il soit informé du service s’exécutant côté serveur et qu’il en obtienne le résultat. On parle alors d’invocation asynchrone plutôt que synchrone. Nous en donnons un exemple ci-dessous à partir du fichier précédent. Nous l’avons renommé TestServiceLong.cs. Son exécution est beaucoup plus longue à cause d’une boucle ridicule qui justifie que le client continue de dérouler son code jusqu’à être informé de la fin du service. Fichier TestServiceLong.cs <%@ WebService Language="C#" Class="TestService" %> using System; using System.Threading; using System.Web.Services; public class TestService : WebService { [WebMethod] public string jeTravaillePourLeWeb (String unNom) { return "Salut, " + unNom + jeSuisPriveDansLaClasse(); } private string jeSuisPriveDansLaClasse() { int a = 0; for (int i=0; i<1000000000; i++) { // rallongement idiot de la méthode=boucle ridicule a++; } return ", je travaille pour le web"; } }

Fichier TestClientLong.cs using System; using System.Runtime.Remoting.Messaging; public class TestClient { private static bool bEnd = false; public static void CallbackService(IAsyncResult arResult) { // afin d'obtenir l'état initial de la proxy TestService unTest = (TestService)arResult.AsyncState;

L’orienté objet

414

// obtenir les résultats du service Web en appelant la méthode End du Proxy Console.WriteLine(unTest.EndjeTravaillePourLeWeb(arResult)); Console.WriteLine("Le service Web vient de se terminer"); bEnd = true; } public static void Main() { TestService unTest = new TestService(); // J'appelle le service Web de manière asynchrone - utilisation des délégués AsyncCallback acb = new AsyncCallback(CallbackService); Console.WriteLine(unTest.BeginjeTravaillePourLeWeb("moi le service",acb,unTest)); // Je continue comme si de rien n'était while (!bEnd) { Console.WriteLine("je vaque a mes occupations en attendant"); } } }

Résultats ………………………………………………. ………………………………………………. je vaque à mes occupations je vaque à mes occupations je vaque à mes occupations je vaque à mes occupations je vaque à mes occupations je vaque à mes occupations je vaque à mes occupations je vaque à mes occupations je vaque à mes occupations Salut, moi, le service, je Le service Web vient de se

en attendant en attendant en attendant en attendant en attendant en attendant en attendant en attendant en attendant travaille pour le web terminer

La réalisation de cet appel asynchrone exige de modifier le code du client en exploitant assez naturellement les deux méthodes Begin et End générées automatiquement par .Net dans le fichier proxy (voir plus haut).

Mais où sont passés les objets ? Du côté serveur, l’objet est créé à la volée, le temps de l’exécution du service. Il n’est donc pas nécessaire de désigner un objet précis, enregistré dans un registre comme dans RMI ou la version par défaut de Corba, sur lequel s’exécutera le service. Un nouvel objet est créé pour chaque message qui arrive au serveur, et est détruit à la fin de l’exécution de ce dernier. Cela simplifie grandement les choses et doit pouvoir suffire dans une majorité d’applications. On peut dès lors légitimement se demander l’intérêt qu’il y a à maintenir un objet serveur toute la durée de l’interaction, comme nous l’avons fait en expérimentant RMI et Corba.

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

415

S’il est difficile de percevoir ce que cet objet pourrait nous apprendre au début de son activation, il n’en est pas moins vrai que, durant l’interaction, l’objet peut maintenir un ensemble d’informations, du côté serveur, propice à cette interaction. Par exemple, un second envoi de message pourrait ne pas avoir le même effet selon les résultats du premier envoi (résultats enregistrés dans l’état de l’objet serveur). Imaginez un jeu informatique ayant cours sur le réseau ; le comportement de chaque objet, en réponse à un message envoyé par un autre, dépendra de son état. De même, dans une négociation commerciale entre deux objets, les décisions prises par chaque objet lors de cette interaction dépendront de leur connaissance courante de cette négociation. Une manière de procéder pourrait consister à sauvegarder cet état intermédiaire sur le disque dur (par exemple, dans une base de données). Cependant, vu les temps d’accès disque, cela pourrait considérablement ralentir et alourdir le déroulement de l’application. Il serait plus efficace de retrouver une situation, inhérente à RMI et Corba, de maintien d’information du côté serveur le temps de l’interaction. Les services Web ne fonctionnent pas directement au-dessus de TCP/IP comme RMI et Corba mais, en raison de leur homogénéisation Web et de leur codage XML, un étage plus haut, au-dessus du protocole HTTP (le protocole de communication Web). Lorsque, au-dessus de ce protocole, une interaction client-serveur doit se dérouler, en maintenant, du côté serveur, des informations sur son état, on invoque souvent la présence de « cookies », comme nous allons le voir. Nous allons reproduire les services Web de l’exemple précédent, en maintenant du côté serveur le nombre de fois que le service est appelé, et en modifiant la réponse du serveur au message en fonction de ce nombre. La nouvelle implémentation asmx du service est la suivante : Fichier TestServiceAvecMemoire.asmx <%@ WebService Language="C#" Class="TestServiceAvecMemoire" %> using System; using System.Web.Services; [WebService( Description = "Un service Web avec mémoire")] /* il est possible de passer des informations sur la nature du service */ /* il faut maintenant que la classe hérite de WebService pour utiliser l'objet " Session " */ public class TestServiceAvecMemoire : WebService { public TestServiceAvecMemoire() { if (nouvelleSession) /* debut de la session */ { nouvelleSession = false; nbreConnexions = 0; } } /* il est possible également de passer des informations sur la nature de la méthode - attention à l'addition de " EnableSession = true ", indispensable si cette méthode doit utiliser des informations mémorisées côté serveur */ [WebMethod( Description="Un Service avec memoire", EnableSession = true )] public string jeTravaillePourLeWeb (String unNom) { /* le retour sera différent suivant qu'il est invoqué une première fois ou non */

416

L’orienté objet

if (nbreConnexions == 0) { nbreConnexions++; return " Salut, " + unNom + jeSuisPriveDansLaClasse(); } else { nbreConnexions++; return " Encore toi, " + "salut, " + unNom + jeSuisPriveDansLaClasse(); } } private string jeSuisPriveDansLaClasse() { return ", je travaille pour le Web"; } private int nbreConnexions { /* méthode d'accès */ /* utilisation capitale de l'objet Session pour mémoriser l'état du serveur */ get { return (int) Session["nbreConnexions"]; } set { Session["nbreConnexions"] = value; } } private bool nouvelleSession { /* méthode d'accès */ get { if (Session["nouvelleSession"] == null) return true; return (bool) Session["nouvelleSession"]; } set { Session["nouvelleSession"] = value; } } }

On relève plusieurs adjonctions par rapport à la version précédente. D’abord, tant dans la définition de la classe service que dans la méthode qui rend le service, des informations supplémentaires peuvent être transmises par l’utilisation d’un attribut Description. Ensuite, la classe doit maintenant hériter de WebService pour pouvoir utiliser l’objet Session qui maintient la mémoire de l’interaction. Cet objet Session enregistre un ensemble de variables arbitraires que l’on désigne par Session["variable"]. C’est l’inélégance de cette écriture pour traiter les variables sessions qui nous fait recourir aux méthodes d’accès pour l’attribut entier nbreConnexions (qui mémorisera le nombre d’invocations de la méthode) et l’attribut booléen nouvelleSession (qui indiquera si oui on non il s’agit d’une nouvelle session). Toute méthode utilisant des informations sur la session doit le signaler dans sa déclaration par : EnableSession = true. Voici maintenant le code du côté client qui doit, lui aussi, par l’addition de l’instruction unTest.CookieContainer = new CookieContainer() signaler que cette interaction se fera en maintenant des informations sur l’état du serveur. Les « cookies » apparaissent. using System; using System.Net; public class TestClient2 {

Distribution gratuite d’objets : pour services rendus sur le réseau CHAPITRE 16

417

public static void Main() { TestServiceAvecMemoire unTest = new TestServiceAvecMemoire(); unTest.CookieContainer = new CookieContainer(); Console.WriteLine(unTest.jeTravaillePourLeWeb(" moi le service ")); /* le même envoi de message, mais l'effet sera différent */ Console.WriteLine(unTest.jeTravaillePourLeWeb(" moi le service ")); } }

Résultat Salut, moi le service, je travaille pour le Web Encore toi, salut, moi le service, je travaille pour le Web

Un annuaire des services XML universel : UDDI Enfin, existe-t-il dans cette nouvelle infrastructure d’objets distribués, un mode d’organisation et de présentation des services comparable à l’annuaire de Jini ? Oui, car tous les constructeurs se sont mis d’accord sur un mode uniforme de présentation de ces services, dénommé UDDI (Universal Description Discovery and Integration). Tous les services décrits dans le langage WDSL peuvent y être affichés et consultés, ainsi que leur emplacement et la façon de les activer. Comme dans Jini, dès qu’un de ces services se révèle utile à un client, ce dernier pourra télécharger le proxy du service, de manière, par exemple, à pouvoir communiquer directement avec le serveur responsable du service. La spécification UDDI décrit une série de standards que les fournisseurs de service Web doivent respecter, afin de présenter leur service dans cet annuaire. Dans l’annuaire UDDI, chaque enregistrement contient trois types d’information, décrits sur le modèle des bottins téléphoniques (nom, adresse, contact de l’entreprise), des « Pages jaunes » (catégorie de l’entreprise) et « Pages vertes » (informations plus techniques concernant le service Web). Logiquement centralisé mais physiquement réparti, cet annuaire universel recueille les inscritpions des fournisseurs de services Web et permet à tout un chacun d’effectuer des recherches selon ses besoins.

Services Web vs RMI et Corba Malgré l’avance prise par Java et le label de standard unique de Corba, il est incontestable que les services web sont en train de damer le pion de leurs concurrents. En premier lieu, du fait de la généralisation d’XML à tout le contenu du Web, que celui-ci soit statique (sites) ou plus dynamique (services). Ensuite, grâce à la facilité et à la rapidité (due à l’automatisation) de mise en œuvre de ces mêmes services – en tout cas en ce qui concerne la plate-forme de développement .Net. Cependant, les services web ne sont pas à l’abri des critiques. Le contenu d’un message doit être « parsé » avant de pouvoir s’exécuter. Ce processus est long, et doit en outre faire l’objet d’une standardisation (par exemple : types de données que l’on peut passer en arguments et en retour des méthodes) afin qu’un message XML envoyé par un client soit interprétré correctement par le serveur. C’est loin d’être le cas aujourd’hui avec la multiplication des standards Soap. Les services web sont donc bien plus lents et moins standards que ne l’est Corba aujourd’hui. Comme progrès technologique, on a déjà vu mieux… Malgré sa dénomination, Soap (Simple Object Access Protocol), le protocole d’envoi et de réception de message, n’est pas du tout orienté objet : rien n’est prévu pour la référence, la sauvegarde ou le maintien de l’état des objets durant une session. Comme nous l’avons vu précédemment, il n’y a pas vraiment d’objets exécutant les

418

L’orienté objet

services le temps de l’interaction. Les objets sont les grands absents des services Web. Il semble en fait que l’on soit revenu aux anciens RPC (Remote Procedure Call), par lesquels les applications informatiques, simplement, se sollicitaient mutuellement les exécutions de procédures. Sans doute, le seul véritable avantage des services Web s’avère être un contrôle plus facile de la sécurité, surtout grâce aux pare-feu qui empêchent toute circulation sur des ports autres que http (le port 80 utilisé par les services Web). Corba et RMI se caractérisent par un processus d’allocation de port dynamique, ce qui rend la sécurité des échanges nettement plus délicate à assurer.

Exercices Exercice 16.1 Expliquez pourquoi la pratique des objets distribués repose dans une large mesure sur la structure syntaxique d’interface.

Exercice 16.2 Comment le compilateur Corba, idl -> C++, traduit-il dans ce langage une interface IDL ?

Exercice 16.3 Expliquez en quoi RMI est plus polymorphique que Corba.

Exercice 16.4 Justifiez l’apport de XML dans le développement des services Web.

Exercice 16.5 Réalisez l’application suivante en Corba : un appareil de retrait d’argent automatique programmé en Java sur un premier ordinateur débite ou crédite des com