Cours JAVA

Cours JAVA : Arbres de AWT (ppt)


Télécharger Cours JAVA : Arbres de AWT (ppt)

★★★★★★★★★★3.5 étoiles sur 5 basé sur 1 votes.
Votez ce document:

Télécharger aussi :


Cours complet de JAVA : Arbres de AWT

...

1.7          Environnement et durée de vie des objets

Techniquement, les spécificités de la programmation orientée objet se résument au typage abstrait des données, à l'héritage et au polymorphisme, mais d'autres particularités peuvent se révéler aussi importantes. Le reste de cette section traite de ces particularités.

L'une des particularités les plus importantes est la façon dont les objets sont créés et détruits. Où se trouvent les données d'un objet et comment sa durée de vie est-elle contrôlée ? Différentes philosophies existent. En C++, qui prône que l'efficacité est le facteur le plus important, le programmeur a le choix. Pour une vitesse optimum à l'exécution, le stockage et la durée de vie peuvent être déterminé quand le programme est écrit, en plaçant les objets sur la pile (ces variables sont parfois appelées automatiques ou de portée) ou dans l'espace de stockage statique. La vitesse d'allocation et de libération est dans ce cas prioritaire et leur contrôle peut être vraiment appréciable dans certaines situations. Cependant, cela se fait aux dépends de la flexibilité car il faut connaître la quantité exacte, la durée de vie et le type des objets pendant qu'on écrit le programme. Si le problème à résoudre est plus général, tel que de la modélisation assistée par ordinateur, de la gestion d'entrepôts ou du contrôle de trafic aérien, cela se révèle beaucoup trop restrictif.

La deuxième approche consiste à créer les objets dynamiquement dans un pool de mémoire appelé le segment. Dans cette approche, le nombre d'objets nécessaire n'est pas connu avant l'exécution, de même que leur durée de vie ou leur type exact. Ces paramètres sont déterminés sur le coup, au moment où le programme s'exécute. Si on a besoin d'un nouvel objet, il est simplement créé dans le segment au moment où on en a besoin. Comme le stockage est géré de manière dynamique lors de l'exécution, le temps de traitement requis pour allouer de la place dans le segment est plus important que le temps mis pour stocker sur la pile (stocker sur la pile se résume souvent à une instruction assembleur pour déplacer le pointeur de pile vers le bas, et une autre pour le redéplacer vers le haut). L'approche dynamique fait la supposition généralement justifiée que les objets ont tendance à être compliqués, et que le surcoût de temps dû à la recherche d'une place de stockage et à sa libération n'aura pas d'impact significatif sur la création d'un objet. De plus, la plus grande flexibilité qui en résulte est essentielle pour résoudre le problème modélisé par le programme.

Java utilise exclusivement la deuxième approche . Le mot clef new est utilisé pour créer une instance dynamique d'un objet à chaque fois qu'on en a besoin.

Une autre particularité importante est la durée de vie d'un objet. Avec les langages qui autorisent la création d'objets dans la pile, le compilateur détermine combien de temps l'objet est amené à vivre et peut le détruire automatiquement. Mais si l'objet est créé dans le segment, le compilateur n'a aucune idée de sa durée de vie. Dans un langage comme le C++, il faut déterminer dans le programme quand détruire l'objet, ce qui peut mener à des fuites de mémoire si cela n'est pas fait correctement (et c'est un problème courant en C++). Java propose une fonctionnalité appelée ramasse-miettes (garbage collector) qui découvre automatiquement quand un objet n'est plus utilisé et le détruit. Java propose donc un niveau plus élevé d'assurance contre les fuites de mémoire. Disposer d'un ramasse-miettes est pratique car cela réduit le code à écrire et, plus important, le nombre de problèmes liés à la gestion de la mémoire (qui ont mené à l'abandon de plus d'un projet C++).

Le reste de cette section s'attarde sur des facteurs additionnels concernant l'environnement et la durée de vie des objets.

1.7.1      Collections et itérateurs

Si le nombre d'objets nécessaires à la résolution d'un problème est inconnu, ou combien de temps on va en avoir besoin, on ne peut pas non plus savoir comment les stocker. Comment déterminer l'espace nécessaire pour créer ces objets ? C'est impossible car cette information n'est connue que lors de l'exécution.

La solution à la plupart des problèmes en conception orientée objet est simple : il suffit de créer un nouveau type d'objet. Le nouveau type d'objets qui résout ce problème particulier contient des références aux autres objets. Bien sûr, un tableau ferait aussi bien l'affaire. Mais il y a plus. Ce nouvel objet, appelé conteneur (ou collection, mais la bibliothèque Java utilise ce terme dans un autre sens ; nous utiliserons donc le terme « conteneur » dans la suite de ce livre), grandira automatiquement pour accepter tout ce qu'on place dedans. Connaître le nombre d'objets qu'on désire stocker dans un conteneur n'est donc plus nécessaire. Il suffit de créer un objet conteneur et le laisser s'occuper des détails.

Heureusement, les langages orientés objet décents fournissent ces conteneurs. En C++, ils font partie de la bibliothèque standard (STL, Standard Template Library). Le Pascal Objet dispose des conteneurs dans sa Bibliothèque de Composants Visuels (VCL, Visual Component Library). Smalltalk propose un ensemble vraiment complet de conteneurs. Java aussi propose des conteneurs dans sa bibliothèque standard. Dans certaines bibliothèques, un conteneur générique est jugé suffisant pour tous les besoins, et dans d'autres (Java par exemple), la bibliothèque dispose de différents types de conteneurs suivant les besoins : des vecteurs (appelé ArrayList en Java) pour un accès pratique à tous les éléments, des listes chaînées pour faciliter l'insertion, par exemple, on peut donc choisir le type particulier qui convient le mieux. Les bibliothèques de conteneurs peuvent aussi inclure les ensembles, les files, les dictionnaires, les arbres, les piles, etc...

Tous les conteneurs disposent de moyens pour y stocker des choses et les récupérer ; ce sont habituellement des fonctions pour ajouter des éléments dans un conteneur et d'autres pour les y retrouver. Mais retrouver des éléments peut être problématique, car une fonction de sélection unique peut se révéler trop restrictive. Comment manipuler ou comparer un ensemble d'éléments dans le conteneur ?

La réponse à cette question prend la forme d'un itérateur, qui est un objet dont le travail est de choisir les éléments d'un conteneur et de les présenter à l'utilisateur de l'itérateur. En tant que classe, il fournit de plus un niveau d'abstraction supplémentaire. Cette abstraction peut être utilisée pour séparer les détails du conteneur du code qui utilise ce conteneur. Le conteneur, via l'itérateur, est perçu comme une séquence. L'itérateur permet de parcourir cette séquence sans se préoccuper de sa structure sous-jacente - qu'il s'agisse d'une ArrayList (vecteur), une LinkedList (liste chaînée), une Stack (pile) ou autre. Cela permet de changer facilement la structure de données sous-jacente sans perturber le code du programme. Java commença (dans les versions 1.0 et 1.1) avec un itérateur standard, appelé Enumeration, pour toutes ses classes conteneurs. Java 2 est accompagné d'une bibliothèque de conteneurs beaucoup plus complète qui contient entre autres un itérateur appelé Iterator bien plus puissant que l'ancienne Enumeration.

Du point de vue du design, tout ce dont on a besoin est une séquence qui peut être manipulée pour résoudre le problème. Si un seul type de séquence satisfaisait tous les besoins, il n'y aurait pas de raison d'en avoir de types différents. Il y a deux raisons qui font qu'on a besoin d'un choix de conteneurs.  Tout d'abord, les conteneurs fournissent différents types d'interfaces et de comportements. Une pile a une interface et un comportement différents de ceux d'une file, qui sont différents de ceux fournis par un ensemble ou une liste. L'un de ces conteneurs peut se révéler plus flexible qu'un autre pour la résolution du problème considéré. Deuxièmement, les conteneurs ne sont pas d'une même efficacité pour les mêmes opérations. Prenons le cas d'une ArrayList et d'une LinkedList. Les deux sont de simples séquences qui peuvent avoir la même interface et comportement. Mais certaines opérations ont des coûts radicalement différents. Accéder à des éléments au hasard dans une ArrayList est une opération qui demande toujours le même temps, quel que soit l'élément auquel on souhaite accéder. Mais dans une LinkedList, il est coûteux de se déplacer dans la liste pour rechercher un élément, et cela prend plus de temps pour trouver un élément qui se situe plus loin dans la liste. Par contre, si on souhaite insérer un élément au milieu d'une séquence, c'est bien plus efficace dans une LinkedList que dans une ArrayList. Ces opérations et d'autres ont des efficacités différentes suivant la structure sous-jacente de la séquence. Dans la phase de conception, on peut débuter avec une LinkedList et lorsqu'on se penche sur l'optimisation, changer pour une ArrayList. Grâce à l'abstraction fournie par les itérateurs, on peut passer de l'une à l'autre avec un impact minime sur le code.

En définitive, un conteneur n'est qu'un espace de stockage où placer des objets. Si ce conteneur couvre tous nos besoins, son implémentation réelle n'a pas grande importance (un concept de base pour la plupart des objets). Mais il arrive que la différence de coûts entre une ArrayList et une LinkedList ne soit pas à négliger, suivant l'environnement du problème et d'autres facteurs. On peut n'avoir besoin que d'un seul type de séquence. On peut même imaginer le conteneur « parfait », qui changerait automatiquement son implémentation selon la manière dont on l'utilise.

1.7.2      La hiérarchie de classes unique

L'une des controverses en POO devenue proéminente depuis le C++ demande si toutes les classes doivent être finalement dérivées d'une classe de base unique. En Java (et comme dans pratiquement tous les autres langages OO) la réponse est « oui » et le nom de cette classe de base ultime est tout simplement Object. Les bénéfices d'une hiérarchie de classes unique sont multiples.

Tous les objets dans une hiérarchie unique ont une interface commune, ils sont donc tous du même type fondamental. L'alternative (proposée par le C++) est qu'on ne sait pas que tout est du même type fondamental. Du point de vue de la compatibilité ascendante, cela épouse plus le modèle du C et peut se révéler moins restrictif, mais lorsqu'on veut programmer en tout objet il faut reconstruire sa propre hiérarchie de classes pour bénéficier des mêmes avantages fournis par défaut par les autres langages OO. Et dans chaque nouvelle bibliothèque de classes qu'on récupère, une interface différente et incompatible sera utilisée. Cela demande des efforts (et éventuellement l'utilisation de l'héritage multiple) pour intégrer la nouvelle interface dans la conception. Est-ce que la « flexibilité » que le C++ fournit en vaut réellement le coup ? Si on en a besoin - par exemple si on dispose d'un gros investissement en C - alors oui. Mais si on démarre de zéro, d'autres alternatives telles que Java se révèlent beaucoup plus productives.

Tous les objets dans une hiérarchie de classes unique (comme celle que propose Java) sont garantis d'avoir certaines fonctionnalités. Un certain nombre d'opérations élémentaires peuvent être effectuées sur tous les objets du système. Une hiérarchie de classes unique, accompagnée de la création des objets dans le segment, simplifie considérablement le passage d'arguments (l'un des sujets les plus complexes en C++).

Une hiérarchie de classes unique facilite aussi l'implémentation d'un ramasse-miettes (qui est fourni en standard en Java). Le support nécessaire est implanté dans la classe de base, et le ramasse-miettes peut donc envoyer le message idoine à tout objet du système. Sans une hiérarchie de classe unique et un système permettant de manipuler un objet via une référence, il est difficile d'implémenter un ramasse-miettes.

Comme tout objet dispose en lui d'informations dynamiques, on ne peut se retrouver avec un objet dont on ne peut déterminer le type. Ceci est particulièrement important avec les opérations du niveau système, telles que le traitement des exceptions, et cela permet une plus grande flexibilité dans la programmation.

1.7.3      Bibliothèques de collections et support pour l'utilisation aisée des collections

Parce qu'un conteneur est un outil qu'on utilise fréquemment, il est logique d'avoir une bibliothèque de conteneurs conçus de manière à être réutilisables, afin de pouvoir en prendre un et l'insérer dans le programme. Java fournit une telle bibliothèque, qui devrait satisfaire tous les besoins.

1.7.3.1  Transtypages descendants vs. patrons génériques

Pour rendre ces conteneurs réutilisables, ils stockent le type universel en Java précédemment mentionné : Object. La hiérarchie de classe unique implique que tout est un Object, un conteneur stockant des Objects peut donc stocker n'importe quoi. Cela rend les conteneurs aisément réutilisables.

Pour utiliser ces conteneurs, il suffit d'y ajouter des références à des objets, et les redemander plus tard. Mais comme le conteneur ne stocke que des Objects, quand une référence à un objet est ajoutée dans le conteneur, il subit un transtypage ascendant en Object, perdant alors son identité. Quand il est recherché par la suite, on récupère une référence à un Object, et non une référence au type qu'on a inséré. Comment le récupérer et retrouver l'interface de l'objet qu'on a stocké dans le conteneur ?

On assiste ici aussi à un transtypage, mais cette fois-ci il ne remonte pas dans la hiérarchie de classe à un type plus général, mais descend dans la hiérarchie jusqu'à un type plus spécifique, c'est un transtypage descendant ou spécialisation, ou soustypage. Avec la généralisation, on sait par exemple qu'un Cercle est un type de Forme, et que le transtypage est donc sans danger ; mais on ne sait pas qu'un Object est aussi un Cercle ou une Forme, il est donc rarement sur d'appliquer une spécialisation à moins de savoir exactement à quoi on a affaire.

Ce n'est pas trop dangereux cependant, car si une spécialisation est tentée jusqu'à un type incompatible le système d'exécution générera une erreur appelée exception, qui sera décrite plus loin. Quand une référence d'objet est rapatriée d'un conteneur, il faut donc un moyen de se rappeler exactement son type afin de pouvoir le spécialiser correctement.

La spécialisation et les contrôles à l'exécution génèrent un surcoût de temps pour le programme, et des efforts supplémentaires de la part du programmeur. Il semblerait plus logique de créer le conteneur de façon à ce qu'il connaisse le type de l'objet stocké, éliminant du coup la spécialisation et la possibilité d'erreur. La solution est fournie par les types paramétrés, qui sont des classes que le compilateur peut personnaliser pour les faire fonctionner avec des types particuliers. Par exemple, avec un conteneur paramétré, le compilateur peut personnaliser ce conteneur de façon à ce qu'il n'accepte que des Formes et ne renvoie que des Formes.

Les types paramétrés sont importants en C++, en particulier parce que le C++ ne dispose pas d'une hiérarchie de classe unique. En C++, le mot clef qui implémente les types paramétrés est « template ». Java ne propose pas actuellement de types paramétrés car c'est possible de les simuler - bien que difficilement - via la hiérarchie de classes unique. Une solution de types paramétrés basée sur la syntaxe des templates C++ est actuellement en cours de proposition.

1.7.4      Le dilemme du nettoyage : qui en est responsable ?

Chaque objet requiert des ressources, en particulier de la mémoire. Quand un objet n'est plus utilisé il doit être nettoyé afin de rendre ces ressources pour les réutiliser. Dans la programmation de situations simples, la question de savoir comment un objet est libéré n'est pas trop compliquée : il suffit de créer l'objet, l'utiliser aussi longtemps que désiré, et ensuite le détruire. Il n'est pas rare par contre de se trouver dans des situations beaucoup plus complexes.

Supposons qu'on veuille concevoir un système pour gérer le trafic aérien d'un aéroport (ou pour gérer des caisses dans un entrepôt, ou un système de location de cassettes, ou un chenil pour animaux). Cela semble simple de prime abord : créer un conteneur pour stocker les avions, puis créer un nouvel avion et le placer dans le conteneur pour chaque avion qui entre dans la zone de contrôle du trafic aérien. Pour le nettoyage, il suffit de détruire l'objet avion correspondant lorsqu'un avion quitte la zone.

Mais supposons qu'une autre partie du système s'occupe d'enregistrer des informations à propos des avions, ces données ne requérant pas autant d'attention que la fonction principale de contrôle. Il s'agit peut-être d'enregistrer les plans de vol de tous les petits avions quittant l'aéroport. On dispose donc d'un second conteneur des petits avions, et quand on crée un objet avion on doit aussi le stocker dans le deuxième conteneur si c'est un petit avion. Une tâche de fond s'occupe de traiter les objets de ce conteneur durant les moments d'inactivité du système.

Le problème est maintenant plus compliqué : comment savoir quand détruire les objets ? Quand on en a fini avec un objet, une autre partie du système peut ne pas en avoir terminé avec. Ce genre de problème arrive dans un grand nombre de situations, et dans les systèmes de programmation (comme le C++) où les objets doivent être explicitement détruits cela peut devenir relativement complexe.

Avec Java, le ramasse-miettes est conçu pour s'occuper du problème de la libération de la mémoire (bien que cela n'inclut pas les autres aspects du nettoyage de l'objet). Le ramasse-miettes « sait » quand un objet n'est plus utilisé, et il libère automatiquement la mémoire utilisée par cet objet. Ceci (associé avec le fait que tous les objets sont dérivés de la classe de base fondamentale Object et que les objets sont créés dans le segment) rend la programmation Java plus simple que la programmation C++. Il y a beaucoup moins de décisions à prendre et d'obstacles à surmonter.



1.7.4.1  Ramasse-miettes vs. efficacité et flexibilité

Si cette idée est si bonne, pourquoi le C++ n'intègre-t-il pas ce mécanisme ? Bien sûr car il y a un prix à payer pour cette facilité de programmation, et ce surcoût se traduit par du temps système. Comme on l'a vu, en C++ on peut créer des objets dans la pile et dans ce cas ils sont automatiquement nettoyés (mais dans ce cas on n'a pas la flexibilité de créer autant d'objets que voulu lors de l'exécution). Créer des objets dans la pile est la façon la plus efficace d'allouer de l'espace pour des objets et de libérer cet espace. Créer des objets dans le segment est bien plus coûteux. Hériter de la même classe de base et rendre tous les appels de fonctions polymorphes prélèvent aussi un tribut. Mais le ramasse-miettes est un problème à part car on ne sait pas quand il va démarrer ou combien de temps il va prendre. Cela veut dire qu'il y a une inconsistance dans le temps d'exécution de programmes Java ; on ne peut donc l'utiliser dans certaines situations, comme celles où le temps d'exécution d'un programme est critique (appelés programmes en temps réel, bien que tous les problèmes de programmation en temps réel ne soient pas aussi astreignants).

Les concepteurs du langage C++, en voulant amadouer les programmeurs C, ne voulurent pas ajouter de nouvelles fonctionnalités au langage qui puissent impacter la vitesse ou défavoriser l'utilisation du C++ dans des situations où le C se serait révélé acceptable. Cet objectif a été atteint, mais au prix d'une plus grande complexité lorsqu'on programme en C++. Java est plus simple que le C++, mais la contrepartie en est l'efficacité et quelquefois son champ d'applications. Pour un grand nombre de problèmes de programmation cependant, Java constitue le meilleur choix.

1.8          Traitement des exceptions : gérer les erreurs

Depuis les débuts des langages de programmation, le traitement des erreurs s'est révélé l'un des problèmes les plus ardus. Parce qu'il est difficile de concevoir un bon mécanisme de gestion des erreurs, beaucoup de langages ignorent ce problème et le déléguent aux concepteurs de bibliothèques qui fournissent des mécanismes qui fonctionnent dans beaucoup de situations mais peuvent être facilement contournés, généralement en les ignorant. L'une des faiblesses de la plupart des mécanismes d'erreur est qu'ils reposent sur la vigilance du programmeur à suivre des conventions non imposées par le langage. Si le programmeur n'est pas assez vigilant - ce qui est souvent le cas s'il est pressé - ces mécanismes peuvent facilement être oubliés.

Le système des exceptions pour gérer les erreurs se situe au niveau du langage de programmation et parfois même au niveau du système d'exploitation. Une exception est un objet qui est « émis » depuis l'endroit où l'erreur est apparue et peut être intercepté par un gestionnaire d'exception conçu pour gérer ce type particulier d'erreur. C'est comme si la gestion des exceptions était un chemin d'exécution parallèle à suivre quand les choses se gâtent. Et parce qu'elle utilise un chemin d'exécution séparé, elle n'interfère pas avec le code s'exécutant normalement. Cela rend le code plus simple à écrire car on n'a pas à vérifier constamment si des erreurs sont survenues. De plus, une exception émise n'est pas comme une valeur de retour d'une fonction signalant une erreur ou un drapeau positionné par une fonction pour indiquer une erreur - ils peuvent être ignorés. Une exception ne peut pas être ignorée, on a donc l'assurance qu'elle sera traitée quelque part. Enfin, les exceptions permettent de revenir d'une mauvaise situation assez facilement. Plutôt que terminer un programme, il est souvent possible de remettre les choses en place et de restaurer son exécution, ce qui produit des programmes plus robustes.

Le traitement des exceptions de Java se distingue parmi les langages de programmes, car en Java le traitement des exceptions a été intégré depuis le début et on est forcé de l'utiliser. Si le code produit ne gère pas correctement les exceptions, le compilateur générera des messages d'erreur. Cette consistance rend la gestion des erreurs bien plus aisée.

Il est bon de noter que le traitement des exceptions n'est pas une caractéristique orientée objet, bien que dans les langages OO une exception soit normalement représentée par un objet. Le traitement des exceptions existait avant les langages orientés objet.

1.9          Multithreading

L'un des concepts fondamentaux dans la programmation des ordinateurs est l'idée de traiter plus d'une tâche à la fois. Beaucoup de problèmes requièrent que le programme soit capable de stopper ce qu'il est en train de faire, traite un autre problème puis retourne à sa tâche principale. Le problème a été abordé de beaucoup de manières différentes. Au début, les programmeurs ayant des connaissances sur le fonctionnement de bas niveau de la machine écrivaient des routines d'interruption de service et l'interruption de la tâche principale était initiée par une interruption matérielle. Bien que cela fonctionne correctement, c'était difficile et non portable, et cela rendait le portage d'un programme sur un nouveau type de machine lent et cher.

Quelquefois les interruptions sont nécessaires pour gérer les tâches critiques, mais il existe une large classe de problèmes dans lesquels on tente juste de partitionner le problème en parties séparées et indépendantes afin que le programme soit plus réactif. Dans un programme, ces parties séparées sont appelés threads et le concept général est appelé multithreading. Un exemple classique de multithreading est l'interface utilisateur. En utilisant les threads, un utilisateur peur appuyer sur un bouton et obtenir une réponse plus rapide que s'il devait attendre que le programme termine sa tâche courante.

Généralement, les threads sont juste une façon d'allouer le temps d'un seul processeur. Mais si le système d'exploitation supporte les multi-processeurs, chaque thread peut être assigné à un processeur différent et ils peuvent réellement s'exécuter en parallèle. L'une des caractéristiques intéressantes du multithreading au niveau du langage est que le programmeur n'a pas besoin de se préoccuper du nombre de processeurs. Le programme est divisé logiquement en threads et si la machine dispose de plusieurs processeurs, le programme tourne plus vite, sans aucun ajustement.

Tout ceci pourrait faire croire que le multithreading est simple. Il y a toutefois un point d'achoppement : les ressources partagées. Un problème se pose si plus d'un thread s'exécutant veulent accéder à la même ressource. Par exemple, deux tâches ne peuvent envoyer simultanément de l'information à une imprimante. Pour résoudre ce problème, les ressources pouvant être partagées, comme l'imprimante, doivent être verrouillées avant d'être utilisées. Un thread verrouille donc une ressource, accomplit ce qu'il a à faire, et ensuite relâche le verrou posé afin que quelqu'un d'autre puisse utiliser cette ressource.

Le multithreading Java est intégré au langage, ce qui simplifie grandement ce sujet compliqué. Le multithreading est supporté au niveau de l'objet, donc un thread d'exécution est représenté par un objet. Java fournit aussi des mécanismes limités de verrouillage de ressources. Il peut verrouiller la mémoire de n'importe quel objet (qui n'est, après tout, qu'un type de ressource partagée) de manière à ce qu'un seul thread puisse l'utiliser à un moment donné. Ceci est réalisé grâce au mot clef synchronized.  D'autres types de ressources doivent être verrouillé explicitement par le programmeur, typiquement en créant un objet qui représente un verrou que tous les threads doivent vérifier avant d'accéder à cette ressource.

1.10       Persistance

Quand un  objet est créé, il existe aussi longtemps qu'on en a besoin, mais en aucun cas il continue d'exister après que le programme se termine. Bien que cela semble logique de prime abord, il existe des situations où il serait très pratique si un objet pouvait continuer d'exister et garder son information même quand le programme ne tourne plus. A l'exécution suivante du programme, l'objet serait toujours là et il aurait la même information dont il disposait dans la session précédente. Bien sûr, on peut obtenir ce comportement en écrivant l'information dans un fichier ou une base de données, mais dans l'esprit du tout objet, il serait pratique d'être capable de déclarer un objet persistant et que tous les détails soient pris en charge.

Java fournit un support pour la « persistance légère », qui signifie qu'on peut facilement stocker des objets sur disque et les récupérer plus tard. La raison pour laquelle on parle de « persistance légère » est qu'on est toujours obligé de faire des appels explicites pour le stockage et la récupération. De plus, JavaSpaces (décrit au Chapitre 15) fournit un type de stockage persistant des objets. Un support plus complet de la persistance peut être amené à apparaître dans de futures versions.

1.11       Java et l'Internet

Java n'étant qu'un autre langage de programmation, on peut se demander pourquoi il est si important et pourquoi il est présenté comme une étape révolutionnaire de la programmation. La réponse n'est pas immédiate si on se place du point de vue de la programmation classique. Bien que Java soit très pratique pour résoudre des problèmes traditionnels, il est aussi important car il permet de résoudre des problèmes liés au web.

1.11.1    Qu'est-ce que le Web ?

Le Web peut sembler un peu mystérieux au début, avec tout ce vocabulaire de « surf », « page personnelles », etc... Il y a même eu une réaction importante contre cette « Internet-mania », se questionnant sur la valeur économique et les revenus d'un tel engouement. Il peut être utile de revenir en arrière et voir de quoi il retourne, mais auparavant il faut comprendre le modèle client/serveur, un autre aspect de l'informatique relativement confus.

1.11.1.1                Le concept Client/Serveur

L'idée primitive d'un système client/serveur est qu'on dispose d'un dépôt centralisé de l'information - souvent dans une base de données - qu'on veut distribuer à un ensemble de personnes ou de machines. L'un des concepts majeurs du client/serveur est que le dépôt d'informations est centralisé afin qu'elles puissent être changées facilement et que ces changements se propagent jusqu'aux consommateurs de ces informations. Pris ensemble, le dépôt d'informations, le programme qui distribue l'information et la (les) machine(s) où résident informations et programme sont appelés le serveur. Le programme qui réside sur la machine distante, communique avec le serveur, récupère l'information, la traite et l'affiche sur la machine distante est appelé le client.

Le concept de base du client/serveur n'est donc pas si compliqué. Les problèmes surviennent car un seul serveur doit servir de nombreux clients à la fois. Généralement, un système de gestion de base de données est impliqué afin que le concepteur répartisse les informations dans des tables pour une utilisation optimale. De plus, les systèmes permettent généralement aux utilisateurs d'ajouter de nouvelles informations au serveur. Cela veut dire qu'ils doivent s'assurer que les nouvelles données d'un client ne contredisent pas les nouvelles données d'un autre client, ou que ces nouvelles données ne soient pas perdues pendant leur inscription dans la base de données (cela fait appel aux transactions). Comme les programmes d'application client changent, ils doivent être créés, débuggués, et installés sur les machines clientes, ce qui se révèle plus compliqué et onéreux que ce à quoi on pourrait s'attendre. C'est particulièrement problématique dans les environnements hétérogènes comprenant différents types de matériels et de systèmes d'exploitation. Enfin, se pose toujours le problème des performances : on peut se retrouver avec des centaines de clients exécutant des requêtes en même temps, et donc un petit délai multiplié par le nombre de clients peut être particulièrement pénalisant. Pour minimiser les temps de latence, les programmeurs travaillent dur pour réduire la charge de travail des tâches incriminées, parfois jusque sur les machines clientes, mais aussi sur d'autres machines du site serveur, utilisant ce qu'on appelle le middleware (le middleware est aussi utilisé pour améliorer la maintenabilité).

L'idée relativement simple de distribuer de l'information aux gens a de si nombreux niveaux de complexité dans son implémentation que le problème peut sembler désespérément insoluble. Et pourtant il est crucial : le client/serveur compte pour environ la moitié des activités de programmation. On le retrouve pour tout ce qui va des transactions de cartes de crédit à la distribution de n'importe quel type de données - économique, scientifique, gouvernementale, il suffit de choisir. Dans le passé, on en est arrivé à des solutions particulières aux problèmes particuliers, obligeant à réinventer une solution à chaque fois. Elles étaient difficiles à créer et à utiliser, et l'utilisateur devait apprendre une nouvelle interface pour chacune. Le problème devait être repris dans son ensemble.

1.11.1.2                Le Web en tant que serveur géant

Le Web est en fait un système client/serveur géant. C'est encore pire que ça, puisque tous les serveurs et les clients coexistent en même temps sur un seul réseau. Vous n'avez pas besoin de le savoir d'ailleurs, car tout ce dont vous vous souciez est de vous connecter et d'interagir avec un serveur à la fois (même si vous devez parcourir le monde pour trouver le bon serveur).

Initialement, c'était un processus à une étape. On faisait une requête à un serveur qui renvoyait un fichier, que le logiciel (ie, le client) interprétait en le formatant sur la machine locale. Mais les gens en voulurent plus : afficher des pages d'un serveur ne leur suffisait plus. Ils voulaient bénéficier de toutes les fonctionnalités du client/serveur afin que le client puisse renvoyer des informations au serveur, par exemple pour faire des requêtes précises dans la base de données, ajouter de nouvelles informations au serveur, ou passer des commandes (ce qui nécessitait plus de sécurité que ce que le système primitif offrait). Nous assistons à ces changements dans le développement du Web.

Le browser Web fut un grand bond en avant en avançant le concept qu'une information pouvait être affichée indifféremment sur n'importe quel ordinateur. Cependant, les browsers étaient relativement primitifs et ne remplissaient pas toutes les demandes des utilisateurs. Ils n'étaient pas particulièrement interactifs, et avaient tendance à saturer le serveur et l'Internet car à chaque fois qu'on avait besoin de faire quelque chose qui requérait un traitement il fallait renvoyer les informations au serveur pour que celui-ci les traite. Cela pouvait prendre plusieurs secondes ou minutes pour finalement se rendre compte qu'on avait fait une faute de frappe dans la requête. Comme le browser n'était qu'un afficheur, il ne pouvait pas effectuer le moindre traitement (d'un autre côté, cela était beaucoup plus sûr, car il ne pouvait pas de ce fait exécuter sur la machine locale de programmes contenant des bugs ou des virus).

Pour résoudre ce problème, différentes solutions ont été approchées. En commençant par les standards graphiques qui ont été améliorés pour mettre des animations et de la vidéo dans les browsers. Le reste du problème ne peut être solutionné qu'en incorporant la possibilité d'exécuter des programmes sur le client, dans le browser. C'est ce qu'on appelle la programmation côté client.

1.11.2    La programmation côté client

Le concept initial du Web (serveur-browser) fournissait un contenu interactif, mais l'interactivité était uniquement le fait du serveur. Le serveur produisait des pages statiques pour le browser client, qui ne faisait que les interpréter et les afficher. Le HTML de base contient des mécanismes simples pour récupérer de l'information : des boîtes de texte, des cases à cocher, des listes à options et des listes de choix, ainsi qu'un bouton permettant de réinitialiser le contenu du formulaire ou d'envoyer les données du formulaire au serveur. Cette soumission de données passe par CGI (Common Gateway Interface), un mécanisme fourni par tous les serveurs web. Le texte de la soumission indique à CGI quoi faire avec ces données. L'action la plus commune consiste à exécuter un programme situé sur le serveur dans un répertoire typiquement appelé « cgi-bin » (si vous regardez l'adresse en haut de votre browser quand vous appuyez sur un bouton dans une page web, vous verrez certainement « cgi-bin » quelque part au milieu de tous les caractères). Ces programmes peuvent être écrits dans la plupart des langages. Perl est souvent préféré car il a été conçu pour la manipulation de texte et est interprété, et peut donc être installé sur n'importe quel serveur sans tenir compte du matériel ou du système d'exploitation.

Beaucoup de sites Web populaires sont aujourd'hui bâtis strictement sur CGI, et de fait on peut faire ce qu'on veut avec. Cependant, les sites web bâtis sur des programmes CGI peuvent rapidement devenir ingérables et trop compliqués à maintenir, et se pose de plus le problème du temps de réponse. La réponse d'un programme CGI dépend de la quantité de données à envoyer, ainsi que de la charge du serveur et de l'Internet (de plus, le démarrage d'un programme CGI a tendance à être long). Les concepteurs du Web n'avaient pas prévu que la bande passante serait trop petite pour le type d'applications que les gens développèrent. Par exemple, tout type de graphisme dynamique est impossible à réaliser correctement car un fichier GIF doit être créé et transféré du serveur au client pour chaque version de ce graphe. Et vous avez certainement déjà fait l'expérience de quelque chose d'aussi simple que la validation de données sur un formulaire. Vous appuyez sur le bouton « submit » d'une page, les données sont envoyées sur le serveur, le serveur démarre le programme CGI qui y découvre une erreur, formate une page HTML vous informant de votre erreur et vous la renvoie ; vous devez alors revenir en arrière d'une page et réessayer. Non seulement c'est lent et frustrant, mais c'est inélégant.

La solution est la programmation côté client. La plupart des machines qui font tourner des browsers Web sont des ordinateurs puissants capables de beaucoup de choses, et avec l'approche originale du HTML statique, elles ne font qu'attendre que le serveur leur envoie parcimonieusement la page suivante. La programmation côté client implique que le browser Web prenne en charge la partie du travail qu'il est capable de traiter, avec comme résultat pour l'utilisateur une interactivité et une vitesse inégalées.



Les problèmes de la programmation côté client ne sont guère différents des discussions de la programmation en général. Les paramètres sont quasiment les mêmes, mais la plateforme est différente : un browser Web est comme un système d'exploitation limité. Au final, on est toujours obligé de programmer, et ceci explique le nombre de problèmes rencontrés dans la programmation côté client. Le reste de cette section présente les différentes approches de la programmation côté client.

1.11.2.1                Les plug-ins

L'une des plus importantes avancées en terme de programmation orientée client a été le développement du plug-in. C'est une façon pour le programmeur d'ajouter de nouvelles fonctionnalités au browser en téléchargeant une partie de logiciel qui s'enfiche à l'endroit adéquat dans le  browser. Il indique au browser : « à partir de maintenant tu peux réaliser cette nouvelle opération » (on n'a besoin de télécharger le plug-in qu'une seule fois). De nouveaux comportements puissants sont donc ajoutés au browser via les plug-ins, mais écrire un plug-in n'est pas une tâche facile, et ce n'est pas quelque chose qu'on souhaiterait faire juste pour bâtir un site Web particulier. La valeur d'un plug-in pour la programmation côté client est qu'il permet à un programmeur expérimenté de développer un nouveau langage et d'intégrer ce langage au browser sans la permission du distributeur du browser. Ainsi, les plug-ins fournissent une « porte dérobée » qui permet la création de nouveaux langages de programmation côté client (bien que tous les langages ne soient pas implémentés en tant que plug-ins).

1.11.2.2                Les langages de script

Les plug-ins ont déclenché une explosion de langages de script. Avec un langage de script, du code source est intégré directement dans la page HTML, et le plug-in qui interprète ce langage est automatiquement activé quand le page HTML est affichée. Les langages de script tendent à être relativement aisés à comprendre ; et parce qu'ils sont du texte faisant partie de la page HTML, ils se chargent très rapidement dans la même requête nécessaire pour se procurer la page auprès du serveur. La contrepartie en est que le code est visible pour tout le monde (qui peut donc le voler). Mais généralement on ne fait pas des choses extraordinairement sophistiquées avec les langages de script et ce n'est donc pas trop pénalisant.

Ceci montre que les langages de script utilisés dans les browsers Web sont vraiment spécifiques à certains types de problèmes, principalement la création d'interfaces utilisateurs plus riches et attrayantes. Cependant, un langage de script permet de résoudre 80 pour cent des problèmes rencontrés dans la programmation côté client. La plupart de vos problèmes devraient se trouver parmi ces 80 pour cent, et puisque les langages de script sont plus faciles à mettre en oeuvre et permettent un développement plus rapide, vous devriez d'abord vérifier si un langage de script ne vous suffirait pas avant de vous lancer dans une solution plus complexe telle que la programmation Java ou ActiveX.

Les langages de script les plus répandus parmi les browsers sont JavaScript (qui n'a rien à voir avec Java ; le nom a été choisi uniquement pour bénéficier de l'engouement marketing pour Java du moment), VBScript (qui ressemble à Visual Basic), et Tcl/Tk, qui vient du fameux langage de construction d'interfaces graphiques. Il y en a d'autres bien sûr, et sans doute encore plus en développement.

JavaScript est certainement le plus commun d'entre eux. Il est présent dès l'origine dans Netscape Navigator et Microsoft Internet Explorer (IE). De plus, il y a certainement plus de livres disponibles sur JavaScript que sur les autres langages, et certains outils créent automatiquement des pages utilisant JavaScript. Cependant, si vous parlez couramment Visual Basic ou Tcl/Tk, vous serez certainement plus productif en utilisant ces langages qu'en en apprenant un nouveau (vous aurez déjà largement de quoi faire avec les problèmes soulevés par le Web).

1.11.2.3                Java

Si un langage de script peut résoudre 80 pour cent des problèmes de la programmation côté client, qu'en est-il des 20 pour cent restants ? La solution la plus populaire aujourd'hui est Java. C'est non seulement un langage de programmation puissant conçu pour être sur, interplateformes et international, mais Java est continuellement étendu pour fournir un langage proposant des caractéristiques et des bibliothèques permettant de gérer de manière élégante des problèmes traditionnellement complexes dans les langages de programmation classiques, tels que le multithreading, les accès aux bases de données, la programmation réseau, l'informatique répartie Java permet la programmation côté client via les applets.

Une applet est un mini-programme qui ne tourne que dans un browser Web. L'applet est téléchargée automatiquement en temps que partie d'une page Web (comme, par exemple, une image est automatiquement téléchargée). Quand l'applet est activée elle exécute un programme. Là se trouve la beauté de la chose : cela vous permet de distribuer le logiciel client à partir du serveur au moment où l'utilisateur en a besoin et pas avant. L'utilisateur récupère toujours la dernière version en date du logiciel et sans avoir besoin de le réinstaller. De par la conception même de Java, le programmeur n'a besoin de créer qu'un seul programme, et ce programme tournera automatiquement sur tous les browsers disposant d'un interpréteur interne (ce qui inclut la grande majorité des machines). Puisque Java est un langage de programmation complet, on peut déporter une grande partie des traitements sur le client avant et après avoir envoyé les requêtes au serveur. Par exemple, une requête comportant une erreur dans une date ou utilisant un mauvais paramètre n'a plus besoin d'être envoyée sur Internet pour se voir refusée par le serveur ; un client peut très bien aussi s'occuper de gérer l'affichage d'un nouveau point sur un graphe plutôt que d'attendre que le serveur ne s'en occupe, crée une nouvelle image et l'envoie au client. On gagne ainsi non seulement en vitesse et confort d'utilisation, mais le trafic engendré sur le réseau et la charge résultante sur les serveurs peut être réduite, ce qui est bénéfique pour l'Internet dans son ensemble.

L'un des avantages qu'une applet Java a sur un programme écrit dans un langage de script est qu'il est fourni sous une forme précompilée, et donc le code source n'est pas disponible pour le client. D'un autre côté, une applet Java peut être rétroingéniérée sans trop de problèmes, mais cacher son code n'est pas souvent une priorité absolue. Deux autres facteurs peuvent être importants. Comme vous le verrez plus tard dans ce livre, une applet Java compilée peut comprendre plusieurs modules et nécessiter plusieurs accès au serveur pour être téléchargée (à partir de Java 1.1 cela est minimisé par les archives Java ou fichiers JAR, qui permettent de paqueter tous les modules requis ensemble dans un format compressé pour un téléchargement unique). Un programme scripté sera juste inséré dans le texte de la page Web (et sera généralement plus petit et réduira le nombre d'accès au serveur). Cela peut être important pour votre site Web. Un autre facteur à prendre en compte est la courbe d'apprentissage. Indépendamment de tout ce que vous avez pu entendre, Java n'est pas un langage trivial et facile à apprendre. Si vous avez l'habitude de programmer en Visual Basic, passer à VBScript sera beaucoup plus rapide et comme cela permet de résoudre la majorité des problèmes typiques du client/serveur, il pourrait être difficile de justifier l'investissement de l'apprentissage de Java. Si vous êtes familier avec un langage de script, vous serez certainement gagnant en vous tournant vers JavaScript ou VBScript avant Java, car ils peuvent certainement combler vos besoins et vous serez plus productif plus tôt.

1.11.2.4                ActiveX

A un certain degré, le compétiteur de Java est ActiveX de Microsoft, bien qu'il s'appuie sur une approche radicalement différente. ActiveX était au départ une solution disponible uniquement sur Windows, bien qu'il soit actuellement développé par un consortium indépendant pour devenir interplateformes. Le concept d'ActiveX clame : « si votre programme se connecte à son environnement de cette façon, il peut être inséré dans une page Web et exécuté par un browser qui supporte ActiveX » (IE supporte ActiveX en natif et Netscape utilise un plug-in). Ainsi, ActiveX ne vous restreint pas à un langage particulier. Si par exemple vous êtes un programmeur Windows expérimenté utilisant un langage tel que le C++, Visual Basic ou Delphi de Borland, vous pouvez créer des composants ActiveX sans avoir à tout réapprendre. ActiveX fournit aussi un mécanisme pour la réutilisation de code dans les pages Web.

1.11.2.5                La sécurité

Le téléchargement automatique et l'exécution de programmes sur Internet ressemble au rêve d'un concepteur de virus. ActiveX en particulier pose la question épineuse de la sécurité dans la programmation côté client. Un simple click sur un site Web peut déclencher le téléchargement d'un certain nombre de fichiers en même temps que la page HTML : des images, du code scripté, du code Java compilé, et des composants ActiveX. Certains d'entre eux sont sans importance : les images ne peuvent causer de dommages, et les langages de script sont généralement limités dans les opérations qu'ils sont capables de faire. Java a été conçu pour exécuter ses applets à l'intérieur d'un périmètre de sécurité (appelé bac à sable), qui l'empêche d'aller écrire sur le disque ou d'accéder à la mémoire en dehors de ce périmètre de sécurité.

ActiveX se trouve à l'opposé du spectre. Programmer avec ActiveX est comme programmer sous Windows - on peut faire ce qu'on veut. Donc un composant ActiveX téléchargé en même temps qu'une page Web peut causer bien des dégâts aux fichiers du disque. Bien sûr, les programmes téléchargés et exécutés en dehors du browser présentent les mêmes risques. Les virus téléchargés depuis les BBS ont pendant longtemps été un problème, mais la vitesse obtenue sur Internet accroît encore les difficultés.

La solution semble être les « signatures digitales », où le code est vérifié pour montrer qui est l'auteur. L'idée est basée sur le fait qu'un virus se propage parce que son concepteur peut rester anonyme, et si cet anonymat lui est retiré, l'individu devient responsable de ses actions. Mais si le programme contient un bug fatal bien que non intentionnel cela pose toujours problème.

L'approche de Java est de prévenir ces problèmes via le périmètre de sécurité. L'interpréteur Java du browser Web examine l'applet pendant son chargement pour y détecter les instructions interdites. En particulier, les applets ne peuvent écrire sur le disque ou effacer des fichiers (ce que font la plupart des virus). Les applets sont généralement considérées comme sûres ; et comme ceci est essentiel pour bénéficier d'un système client/serveur fiable, les bugs découverts dans Java permettant la création de virus sont très rapidement corrigés (il est bon de noter que le browser renforce ces restrictions de sécurité, et que certains d'entre eux permettent de sélectionner différents niveaux de sécurité afin de permettre différents niveaux d'accès au système).

On pourrait s'interroger sur cette restriction draconienne contre les accès en écriture sur le disque local. Par exemple, on pourrait vouloir construire une base de données locale ou sauvegarder des données pour un usage futur en mode déconnecté. La vision initiale obligeait tout le monde à se connecter pour réaliser la moindre opération, mais on s'est vite rendu compte que cela était impraticable. La solution a été trouvée avec les « applets signées » qui utilisent le chiffrement avec une clef publique pour vérifier qu'une applet provient bien de là où elle dit venir. Une applet signée peut toujours endommager vos données et votre ordinateur, mais on en revient à la théorie selon laquelle la perte de l'anonymat rend le créateur de l'applet responsable de ses actes, il ne fera donc rien de préjudiciable. Java fournit un environnement pour les signatures digitales afin de permettre à une applet de sortir du périmètre de sécurité si besoin est.

Les signatures digitales ont toutefois oublié un paramètre important, qui est la vitesse à laquelle les gens bougent sur Internet. Si on télécharge un programme buggué et qu'il accomplisse ses méfaits, combien de temps se passera-t-il avant que les dommages ne soient découverts ? Cela peut se compter en jours ou en semaines. Et alors, comment retrouver le programme qui les a causés ? Et en quoi cela vous aidera à ce point ?

1.11.2.6                Internet vs. intranet

Le Web est la solution la plus générique au problème du client/serveur, il est donc logique de vouloir appliquer la même technologie pour résoudre ceux liés aux client/serveur à l'intérieur d'une entreprise. Avec l'approche traditionnelle du client/serveur se pose le problème de l'hétérogénéité du parc de clients, de même que les difficultés pour installer les nouveaux logiciels clients. Ces problèmes sont résolus par les browsers Web et la programmation côté client. Quand la technologie du Web est utilisée dans le système d'information d'une entreprise, on s'y réfère en tant qu'intranet. Les intranets fournissent une plus grande sécurité qu'Internet puisqu'on peut contrôler les accès physiques aux serveurs à l'intérieur de l'entreprise. En terme d'apprentissage, il semblerait qu'une fois que les utilisateurs se sont familiarisés avec le concept d'un browser il leur est plus facile de gérer les différences de conception entre les pages et applets, et le coût d'apprentissage de nouveaux systèmes semble se réduire.

Le problème de la sécurité nous mène à l'une des divisions qui semble se former automatiquement dans le monde de la programmation côté client. Si un programme est distribué sur Internet, on ne sait pas sur quelle plateforme il va tourner et il faut être particulièrement vigilant pour ne pas diffuser du code buggué. Il faut une solution multiplateformes et sûre, comme un langage de script ou Java.

Dans le cas d'un intranet, les contraintes peuvent être complètement différentes. Le temps passé à installer les nouvelles versions est la raison principale qui pousse à passer aux browsers, car les mises à jours sont invisibles et automatiques. Il n'est pas rare que tous les clients soient des plateformes Wintel (Intel / Windows). Dans un intranet, on est responsable de la qualité du code produit et on peut corriger les bugs quand ils sont découverts. De plus, on dispose du code utilisé dans une approche plus traditionnelle du client/serveur où il fallait installer physiquement le client à chaque mise à jour du logiciel.  Dans le cas d'un tel intranet, l'approche la plus raisonnable consiste à choisir la solution qui permettra de réutiliser le code existant plutôt que de repartir de zéro dans un nouveau langage.

Quand on est confronté au problème de la programmation côté client, la meilleure chose à faire est une analyse coûts-bénéfices. Il faut prendre en compte les contraintes du problème, et la solution qui serait la plus rapide à mettre en oeuvre. En effet, comme la programmation côté client reste de la programmation, c'est toujours une bonne idée de choisir l'approche permettant un développement rapide.

1.11.3    La programmation côté serveur

Toute cette discussion a passé sous silence la programmation côté serveur. Que se passe-t-il lorsqu'un serveur reçoit une requête ? La plupart du temps cette requête est simplement « envoie-moi ce fichier ». Le browser interprète alors ce fichier d'une manière particulière : comme une page HTML, une image graphique, une applet Java, un programme script, etc... Une requête plus compliquée à un serveur implique généralement une transaction vers une base de données. Un scénario classique consiste en une requête complexe de recherche d'enregistrements dans une base de données que le serveur formate dans une page HTML et renvoie comme résultat (bien sûr, si le client est « intelligent » via Java ou un langage de script, la phase de formatage peut être réalisée par le client et le serveur n'aura qu'a envoyer les données brutes, ce qui allégera la charge et le trafic engendré). Ou alors un client peut vouloir s'enregistrer dans une base de données quand il joint un groupe ou envoie une commande, ce qui implique des changements dans la base de données. Ces requêtes doivent être traitées par du code s'exécutant sur le serveur, c'est ce qu'on appelle la programmation côté serveur. Traditionnellement, la programmation côté serveur s'est appuyée sur Perl et les scripts CGI, mais des systèmes plus sophistiqués sont en train de voir le jour. Cela inclut des serveurs Web écrits en Java qui permettent de réaliser toute la programmation côté serveur en Java en écrivant ce qu'on appelle des servlets. Les servlets et leur progéniture, les JSPs, sont les deux raisons principales qui poussent les entreprises qui développent un site Web à se tourner vers Java, en particulier parce qu'ils éliminent les problèmes liés aux différences de capacité des browsers.

1.11.4    Une scène séparée : les applications

La plupart de la publicité faite autour de Java se référait aux applets. Mais Java est aussi un langage de programmation à vocation plus générale qui peut résoudre n'importe quel type de problème - du moins en théorie. Et comme précisé plus haut, il peut y avoir des moyens plus efficaces de résoudre la plupart des problèmes client/serveur. Quand on quitte la scène des applets (et les restrictions qui y sont liées telles que les accès en écriture sur le disque), on entre dans le monde des applications qui s'exécutent sans le soutien d'un browser, comme n'importe quel programme. Les atouts de Java sont là encore non seulement sa portabilité, mais aussi sa facilité de programmation. Comme vous allez le voir tout au long de ce livre, Java possède beaucoup de fonctionnalités qui permettent de créer des programmes robustes dans un laps de temps plus court qu'avec d'autres langages de programmation.

Mais il faut bien être conscient qu'il s'agit d'un compromis. Le prix à payer pour ces améliorations est une vitesse d'exécution réduite (bien que des efforts significatifs soient mis en oeuvre dans ce domaine - JDK 1.3, en particulier, introduit le concept d'amélioration de performances « hotspot »). Comme tout langage de programmation, Java a des limitations internes qui peuvent le rendre inadéquat pour résoudre certains types de problèmes. Java évolue rapidement cependant, et à chaque nouvelle version il permet d'adresser un spectre de plus en plus large de problèmes.



1.12       Analyse et conception

Le paradigme de la POO constitue une approche nouvelle et différente de la programmation. Beaucoup de personnes rencontrent des difficultés pour appréhender leur premier projet orienté objet. Une fois compris que tout est supposé être un objet, et au fur et à mesure qu'on se met à penser dans un style plus orienté objet, on commence à créer de « bonnes » conceptions qui s'appuient sur tous les avantages que la POO offre.

Une méthode (ou méthodologie) est un ensemble de processus et d'heuristiques utilisés pour réduire la complexité d'un problème. Beaucoup de méthodes orientées objet ont été formulées depuis l'apparition de la POO. Cette section vous donne un aperçu de ce que vous essayez d'accomplir en utilisant une méthode.

Une méthodologie s'appuie sur un certain nombre d'expériences, il est donc important de comprendre quel problème la méthode tente de résoudre avant d'en adopter une. Ceci est particulièrement vrai avec Java, qui a été conçu pour réduire la complexité (comparé au C) dans l'expression d'un programme. Cette philosophie supprime le besoin de méthodologies toujours plus complexes. Des méthodologies simples peuvent se révéler tout à fait suffisantes avec Java pour une classe de problèmes plus large que ce qu'elles ne pourraient traiter avec des langages procéduraux.

Il est important de réaliser que le terme «> méthodologie » est trompeur et promet trop de choses. Tout ce qui est mis en oeuvre quand on conçoit et réalise un programme est une méthode. Ca peut être une méthode personnelle, et on peut ne pas en être conscient, mais c'est une démarche qu'on suit au fur et à mesure de l'avancement du projet. Si cette méthode est efficace, elle ne nécessitera sans doute que quelques petites adaptations pour fonctionner avec Java. Si vous n'êtes pas satisfait de votre productivité ou du résultat obtenu, vous serez peut-être tenté d'adopter une méthode plus formelle, ou d'en composer une à partir de plusieurs méthodes formelles.

Au fur et à mesure que le projet avance, le plus important est de ne pas se perdre, ce qui est malheureusement très facile. La plupart des méthodes d'analyse et de conception sont conçues pour résoudre même les problèmes les plus gros. Il faut donc bien être conscient que la plupart des projets ne rentrant pas dans cette catégorie, on peut arriver à une bonne analyse et conception avec juste une petite partie de ce qu'une méthode recommande . Une méthode de conception, même limitée, met sur la voie bien mieux que si on commence à coder directement.

Il est aussi facile de rester coincé et tomber dans la « paralysie analytique » où on se dit qu'on ne peut passer à la phase suivante car on n'a pas traqué le moindre petit détail de la phase courante. Il faut bien se dire que quelle que soit la profondeur de l'analyse, certains aspects d'un problème ne se révéleront qu'en phase de conception, et d'autres en phase de réalisation, voire même pas avant que le programme ne soit achevé et exécuté. A cause de ceci, il est crucial d'avancer relativement rapidement dans l'analyse et la conception, et d'implémenter un test du système proposé.

Il est bon de développer un peu ce point. A cause des déboires rencontrés avec les langages procéduraux, il est louable qu'une équipe veuille avancer avec précautions et comprendre tous les détails avant de passer à la conception et l'implémentation. Il est certain que lors de la création d'une base de données, il est capital de comprendre à fond les besoins du client. Mais la conception d'une base de données fait partie d'une classe de problèmes bien définie et bien comprise ; dans ce genre de programmes, la structure de la base de données est le problème à résoudre. Les problèmes traités dans ce chapitre font partie de la classe de problèmes « joker » (invention personnelle), dans laquelle la solution n'est pas une simple reformulation d'une solution déjà éprouvée de nombreuses fois, mais implique un ou plusieurs « facteurs joker » - des éléments pour lesquels il n'existe aucune solution préétablie connue, et qui nécessitent de pousser les recherches . Tenter d'analyser à fond un problème joker avant de passer à la conception et l'implémentation mène à la paralysie analytique parce qu'on ne dispose pas d'assez d'informations pour résoudre ce type de problèmes durant la phase d'analyse. Résoudre ce genre de problèmes requiert de répéter le cycle complet, et cela demande de prendre certains risques (ce qui est sensé, car on essaie de faire quelque chose de nouveau et les revenus potentiels en sont plus élevés). On pourrait croire que le risque est augmenté par cette ruée vers une première implémentation, mais elle peut réduire le risque dans un projet joker car on peut tout de suite se rendre compte si telle approche du problème est viable ou non. Le développement d'un produit s'apparente à de la gestion de risque.

Souvent cela se traduit par « construire un prototype qu'il va falloir jeter ». Avec la POO, on peut encore avoir à en jeter une partie, mais comme le code est encapsulé dans des classes, on aura inévitablement produit durant la première passe quelques classes qui valent la peine d'être conservées et développé des idées sur la conception du système. Ainsi, une première passe rapide sur un problème non seulement fournit des informations critiques pour la prochaine passe d'analyse, de conception et d'implémentation, mais elle produit aussi une base du code.

Ceci dit, si on cherche une méthode qui contient de nombreux détails et suggère de nombreuses étapes et documents, il est toujours difficile de savoir où s'arrêter. Il faut garder à l'esprit ce qu'on essaye de découvrir :

  1. Quels sont les objets ? (Comment partitionner le projet en ses composants élémentaires ?)
  2. Quelles en sont leur interface ? (Quels sont les messages qu'on a besoin d'envoyer à chaque objet ?)

Si on arrive à trouver quels sont les objets et leur interface, alors on peut commencer à coder. On pourra avoir besoin d'autres descriptions et documents, mais on ne peut pas faire avec moins que ça.

Le développement peut être décomposé en cinq phases, et une Phase 0 qui est juste l'engagement initial à respecter une structure de base.

1.12.1    Phase 0 : Faire un plan

Il faut d'abord décider quelles étapes on va suivre dans le développement. Cela semble simple (en fait, tout semble simple), et malgré tout les gens ne prennent cette décision qu'après avoir commencé à coder. Si le plan se résume à « retroussons nos manches et codons », alors ça ne pose pas de problèmes (quelquefois c'est une approche valable quand on a affaire à un problème bien connu). Mais il faut néanmoins accepter que c'est le plan.

On peut aussi décider dans cette phase qu'une structure additionnelle est nécessaire. Certains programmeurs aiment travailler en « mode vacances », sans structure imposée sur le processus de développement de leur travail : « Ce sera fait lorsque ce sera fait ». Cela peut être séduisant un moment, mais disposer de quelques jalons aide à se concentrer et focalise les efforts sur ces jalons au lieu d'être obnubilé par le but unique de « finir le projet ». De plus, cela divise le projet en parties plus petites, ce qui le rend moins redoutable (sans compter que les jalons offrent des opportunités de fête).

Quand j'ai commencé à étudier la structure des histoires (afin de pouvoir un jour écrire un roman), j'étais réticent au début à l'idée de structure, trouvant que j'écrivais mieux quand je laissais juste la plume courir sur le papier. Mais j'ai réalisé plus tard que quand j'écris à propos des ordinateurs, la structure est suffisamment claire pour moi pour que je n'ai pas besoin de trop y réfléchir. Mais je structure tout de même mon travail, bien que ce soit inconsciemment dans ma tête. Même si on pense que le plan est juste de commencer à coder, on passe tout de même par les phases successives en se posant certaines questions et en y répondant.

1.12.1.1                L'exposé de la mission

Tout système qu'on construit, quelle que soit sa complexité, a un but, un besoin fondamental qu'il satisfait. Si on peut voir au delà de l'interface utilisateur, des détails spécifiques au matériel - ou au système -, des algorithmes de codage et des problèmes d'efficacité, on arrive finalement au coeur du problème - simple et nu. Comme le soi-disant concept fondamental d'un film hollywoodien, on peut le décrire en une ou deux phrases. Cette description pure est le point de départ.

Le concept fondamental est assez important car il donne le ton du projet ; c'est l'exposé de la mission. Ce ne sera pas nécessairement le premier jet qui sera le bon (on peut être dans une phase ultérieure du projet avant qu'il ne soit complètement clair), mais il faut continuer d'essayer jusqu'à ce que ça sonne bien. Par exemple, dans un système de contrôle de trafic aérien, on peut commencer avec un concept fondamental basé sur le système qu'on construit : « Le programme tour de contrôle garde la trace d'un avion ». Mais cela n'est plus valable quand le système se réduit à un petit aérodrome, avec un seul contrôleur, ou même aucun. Un modèle plus utile ne décrira pas tant la solution qu'on crée que le problème : « Des avions arrivent, déchargent, partent en révision, rechargent et repartent ».

1.12.2    Phase 1 : Que construit-on ?

Dans la génération précédente de conception de programmes (conception procédurale), cela s'appelait « l'analyse des besoins et les spécifications du système ». C'étaient des endroits où on se perdait facilement, avec des documents au nom intimidant qui pouvaient  occulter le projet. Leurs intentions étaient bonnes, pourtant. L'analyse des besoins consiste à « faire une liste des indicateurs qu'on utilisera pour savoir quand le travail sera terminé et le client satisfait ». Les spécifications du système consistent en « une description de ce que le programme fera (sans ce préoccuper du comment) pour satisfaire les besoins ». L'analyse des besoins est un contrat entre le développeur et le client (même si le client travaille dans la même entreprise, ou se trouve être un objet ou un autre système). Les spécifications du système sont une exploration générale du problème, et en un sens permettent de savoir s'il peut être résolu et en combien de temps. Comme ils requièrent des consensus entre les intervenants sur le projet (et parce qu'ils changent au cours du temps), il vaut mieux les garder aussi  bruts que possible - idéalement en tant que listes et diagrammes - pour ne pas perdre de temps. Il peut y avoir d'autres contraintes qui demandent de produire de gros documents, mais en gardant les documents initiaux petits et concis, cela permet de les créer en quelques sessions de brainstorming avec un leader qui affine la description dynamiquement. Cela permet d'impliquer tous les acteurs du projet, et encourage la participation de toute l'équipe. Plus important encore, cela permet de lancer un projet dans l'enthousiasme.

Il est nécessaire de rester concentré sur ce qu'on essaye d'accomplir dans cette phase : déterminer ce que le système est supposé faire. L'outil le plus utile pour cela est une collection de ce qu'on appelle « cas d'utilisation ». Les cas d'utilisation identifient les caractéristiques clefs du système qui vont révéler certaines des classes fondamentales qu'on utilisera. Ce sont essentiellement des réponses descriptives à des questions comme  :

  • « Qui utilisera le système ? »
  • « Que peuvent faire ces personnes avec le système ? »
  • « Comment tel acteur fait-il cela avec le système ? »
  • « Comment cela pourrait-il fonctionner si quelqu'un d'autre faisait cela, ou si le même acteur avait un objectif différent ? » (pour trouver les variations)
  • « Quels problèmes peuvent apparaître quand on fait cela avec le système ? » (pour trouver les exceptions)

Si on conçoit un guichet automatique, par exemple, le cas d'utilisation pour un aspect particulier des fonctionnalités du système est capable de décrire ce que le guichet fait dans chaque situation possible. Chacune de ces situations est appelée un scénario, et un cas d'utilisation peut être considéré comme une collection de scénarios. On peut penser à un scénario comme à une question qui commence par « Qu'est-ce que le système fait si... ? ». Par exemple, « Qu'est que le guichet fait si un client vient de déposer un chèque dans les dernières 24 heures, et qu'il n'y a pas assez dans le compte sans que le chèque soit encaissé pour fournir le retrait demandé ? ».

Les diagrammes de cas d'utilisations sont voulus simples pour ne pas se perdre prématurément dans les détails de l'implémentation du système :

Chaque bonhomme représente un « acteur », typiquement une personne ou une autre sorte d'agent (cela peut même être un autre système informatique, comme c'est le cas avec « ATM »). La boîte représente les limites du système. Les ellipses représentent les cas d'utilisation, qui sont les descriptions des actions qui peuvent être réalisées avec le système. Les lignes entre les acteurs et les cas d'utilisation représentent les interactions.

Tant que le système est perçu ainsi par l'utilisateur, son implémentation n'est pas importante.

Un cas d'utilisation n'a pas besoin d'être complexe, même si le système sous-jacent l'est. Il est seulement destiné à montrer le système tel qu'il apparaît à l'utilisateur. Par exemple :

Les cas d'utilisation produisent les spécifications des besoins en déterminant toutes les interactions que l'utilisateur peut avoir avec le système. Il faut trouver un ensemble complet de cas d'utilisations du système, et cela terminé on se retrouve avec le coeur de ce que le système est censé faire. La beauté des cas d'utilisation est qu'ils ramènent toujours aux points essentiels et empêchent de se disperser dans des discussions non essentielles à la réalisation du travail à faire. Autrement dit, si on dispose d'un ensemble complet de cas d'utilisation, on peut décrire le système et passer à la phase suivante. Tout ne sera pas parfaitement clair dès le premier jet, mais ça ne fait rien. Tout se décantera avec le temps, et si on cherche à obtenir des spécifications du système parfaites à ce point on se retrouvera coincé.

Si on est bloqué, on peut lancer cette phase en utilisant un outil d'approximation grossier : décrire le système en quelques paragraphes et chercher les noms et les verbes. Les noms suggèrent les acteurs, le contexte des cas d'utilisation ou les objets manipulés dans les cas d'utilisation. Les verbes suggèrent les interactions entre les acteurs et les cas d'utilisation, et spécifient les étapes à l'intérieur des cas d'utilisation. On verra aussi que les noms et les verbes produisent des objets et des messages durant la phase de design (on peut noter que les cas d'utilisation décrivent les interactions entre les sous-systèmes, donc la technique « des noms et des verbes » ne peut être utilisée qu'en tant qu'outil de brainstorming car il ne fournit pas les cas d'utilisation) .

La frontière entre un cas d'utilisation et un acteur peut révéler l'existence d'une interface utilisateur, mais ne définit pas cette interface utilisateur. Pour une méthode de définition et de création d'interfaces utilisateur, se référer à Software for Use de Larry Constantine et Lucy Lockwood, (Addison-Wesley Longman, 1999) ou sur www.ForUse.com.

Bien que cela tienne plus de l'art obscur, à ce point un calendrier de base est important. On dispose maintenant d'une vue d'ensemble de ce qu'on construit, on peut donc se faire une idée du temps nécessaire à sa réalisation. Un grand nombre de facteurs entre en jeu ici. Si on estime un calendrier trop important, l'entreprise peut décider d'abandonner le projet (et utiliser leurs ressources sur quelque chose de plus raisonnable - ce qui est une bonne chose). Ou un directeur peut avoir déjà décidé du temps que le projet devrait prendre et voudra influencer les estimations. Mais il vaut mieux proposer un calendrier honnête et prendre les décisions importantes au début. Beaucoup de techniques pour obtenir des calendriers précis ont été proposées (de même que pour prédire l'évolution de la bourse), mais la meilleure approche est probablement de se baser sur son expérience et son intuition. Proposer une estimation du temps nécessaire pour réaliser le système, puis doubler cette estimation et ajouter 10 pour cent. L'estimation initiale est probablement correcte, on peut obtenir un système fonctionnel avec ce temps. Le doublement transforme le délai en quelque chose de décent, et les 10 pour cent permettront de poser le vernis final et de traiter les détails . Peu importe comment on l'explique et les gémissements obtenus quand on révèle un tel planning, il semble juste que ça fonctionne de cette façon.



1661