Double initialisation Java
J’ai découvert il y a peu de temps une subtilité dans le fonctionnement du langage Java. Il s’agit du déroulement de l’étape de construction d’une nouvelle instance d’une classe.
Saurez-vous deviner le résultat de l’exécution du programme suivant :
public abstract class ClasseA { private String parent = "A"; public ClasseA() { System.out.println(parent); System.out.println(enfant()); } public abstract String enfant(); private static class ClasseB extends ClasseA { private String enfant = "B"; public ClasseB() { System.out.println(enfant()); } @Override public String enfant() { return this.enfant; } } public static void main(String[] args) { new ClasseB(); } }
Tutoriel : Paysage sous la pluie
Dans ce tutoriel graphique, nous allons voir comment il est possible de modifier les conditions météorologiques d’une photo pour transformer un beau soleil en une pluie grisâtre.
Vous avez dit bizarre…
Aujourd’hui, je me suis vu écrire en Java une méthode très pratique (si, si) qui consiste à retourner le paramètre d’entrée. method(a) { return a; }. À quoi pouvait donc servir cette méthode qui apparaît a priori bien inutile ?
(plus…)
Opérateurs ++ et —
Ce court article a pour objectif de répondre à une question fondamentale et dont la plupart des programmeurs ont une réponse erronée : Quelle est la différence entre i++ et ++i ?
Avant de donner la réponse, une petite question pour se mettre en situation : Quelles vont être les valeurs de j et de k à la fin de l’exécution de ce code ?
int i = 5; int j = ++i + i--; int k = i++ + --i;
Tutoriel : Présentation “PowerPoint” en ActionScript
Introduction
Contrairement à ce que le titre peut signifier, ce tutoriel va se limiter à une partie moins évidente pour l’affichage d’un diaporama (slideshow) en langage ActionScript : la présentation des diapositives (slides) sur plusieurs lignes et plusieurs colonnes de manière intelligente et dynamique. Le résultat de ce tutoriel est la disposition d’un ensemble d’images de manière à ce qu’elles occupent le maximum d’espace, en fonction de la taille de la fenêtre principale et de la taille de chaque image.
Ce tutoriel est destiné à des programmeurs ayant tout de même des notions d’ActionScript - notamment au niveau de la manipulation d’objets MovieClip, des observateurs (listeners) et du chargement de clips (typiquement MovieClipLoader) - et de programmation orientée objet. Je ne m’attarderai que sur les parties un peu plus orientées “algorithmes”.
Voici le résultat auquel nous parviendrons à la fin de ce tutoriel :
Concevoir une application en programmation orientée objet
Cet article fait suite à une série d’expériences que j’ai pu avoir dans la gestion de projets de petite taille en informatique. Il présente les quelques points importants qui dirigent actuellement ma vision de la conception d’une application, notamment en programmation orientée objet, et qui me permettent d’appréhender les problèmes qui peuvent être rencontrés. Ces règles de “bonne conception” sont les résultats d’analyses personnelles sur les raisons des succès ou des échecs de certaines solutions conceptuelles que j’ai été amené à utiliser et sont également le reflet de ma vision actuelle de la programmation orientée objet.
1. Prototyper dans le but de réajuster la conception.
Même avec plusieurs années d’expériences, il est impossible de concevoir efficacement une application du premier coup, sans tester le modèle par un prototype. À l’inverse, il ne faut pas non plus se lancer dans le prototypage sans avoir réfléchi suffisamment à la solution. Un équilibre est donc à trouver.
On effectue tout d’abord une phase de conception relativement conséquente qui a pour buts de décomposer l’application en sous-ensembles et de dégager les problèmes principaux en termes d’intégration et de communication entre ces sous-ensembles. Cette phase entraîne la réalisation d’un prototype à l’échelle de la conception, c’est-à-dire qui met en œuvre les différentes parties de l’application et la communication entre ces parties, sans rentrer davantage dans le détail. Ce prototype permet de valider ou de remettre en question la première solution trouvée. S’enchaînent ensuite une série de raffinements de conception et de prototypes qui amène à une conception valide, qui répond au problème. Les différents prototypes réalisés permettent de surcroît de tester une première fois l’intégration de chaque sous-ensemble et peuvent servir de base pour l’élaboration de procédures de test plus spécifiques.
2. Ne pas perdre de vue que le langage orientée objet est avant tout un modèle.
Le langage objet s’apparente au langage machine mais y ajoute des structures abstraites compréhensibles par l’Homme. Il s’apparente donc plutôt à un modèle qu’à une mise en œuvre du programme, même si ce modèle a une granularité suffisamment fine pour que sa traduction en langage machine soit complètement définie. Lors de la conception d’une application, il faut utiliser le plus possible ces spécificités du modèle (typage, héritage, …) pour décrire de la manière la plus précise et la plus cohérente possible l’application.
3. Utiliser au maximum les notions de programmation orientée objet.
Si l’application doit être réalisée en programmation orientée objet, autant que la conception utilise ce modèle au maximum ! Sans tomber dans l’excès, il ne faut pas hésiter à définir des classes propres à l’application plutôt que d’utiliser les structures génériques du langage. Classiquement, si le nom d’un article d’un magasin peut être représenté par une simple chaîne de caractères (String), sa référence est plus abstraite : il peut s’agir d’un numéro, d’une série de caractères alphabétiques ou encore d’un mélange de caractères de toutes sortes. Une classe spéciale pour désigner cet attribut est alors tout à fait justifiée.
La programmation orientée objet apporte en plus des solutions pour résoudre des problèmes de conception. Les modèles de conception existants (Design Patterns) permettent de se rendre compte de l’étendue des possibilités. L’héritage permet notamment d’ajouter des attributs et méthodes spécifiques à une classe, et donc de définir un ensemble de types qui correspondent précisément aux besoins. La surcharge peut quant à elle ajouter des comportements ou des fonctionnalités de manière transparente.
4. Donner du sens à chaque structure et chaque choix conceptuel.
Une classe possède des propriétés et des fonctionnalités qui permettent d’agir sur son comportement interne. De la même façon qu’une voiture ne sait pas où et comment se garer sans conducteur, un objet n’a pas à savoir comment s’enregistrer dans une base de données ou un fichier. De plus, les propriétés d’une classe sont supposées définir les objets complètement : un attribut qui n’est jamais instancié ou utilisé dans un certain cas d’utilisation doit être remis en question. Aussi, la décomposition en classes et méthodes doit éviter de donner des rôles superflus à des classes.
Enfin, si une classe a pour objectif de n’être instanciée qu’une seule fois, autant définir ses attributs et méthodes de manière statique ! Cela évite les erreurs de multiple instanciation et les lourdeurs d’utilisation.
5. Considérer et utiliser les interfaces comme des contrats.
En programmation orientée objet une interface a pour objectif d’indiquer qu’une classe respecte un certain contrat et peut être utilisée de la manière prévue. La définition d’une interface permet donc d’envisager d’autres implémentations pour une certaine fonctionnalité, sans modifier les signatures des méthodes. On peut ainsi utiliser une interface plutôt qu’une classe pour définir une entrée ou sortie d’une méthode si on envisage à terme la généricité de cette dernière. De plus, l’utilisation d’une interface a peu d’impact en terme de performance puisque la vérification du respect du contrat qu’elle définit est effectuée à la compilation.
6. Choisir les structures de collections génériques en leur donnant un sens.
Seule la méthode ou la classe qui gère un groupe de données de même type est supposée connaître la structure précise de ce groupe. Du point de vue du modèle, et donc des entrées et sorties de cette méthode ou cette classe, seules les propriétés de ce groupe doivent être indiquées : est-il ordonné ? Y a-t-il possibilité de doublons ? Comment s’effectue l’accès aux données ? De cette manière, il n’y a que l’information pertinente qui est indiquée. De plus, s’il y a changement de l’implémentation pour une quelconque raison, celle-ci est transparente pour l’extérieur.
Le langage Java possède par exemple une multitude d’interfaces permettant de décrire des propriétés spécifiques pour un groupe de données, sans dévoiler la classe spécifique qui l’implémente. Pour indiquer qu’une méthode renvoie un groupe d’objets de manière générale on utilisera l’interface Collection. Si on veut préciser que chaque objet n’apparaît qu’une fois dans le groupe, on écrira Set, et si la collection est ordonnée alors on pourra employer List.
7. Décomposer l’application en modules aux rôles clairement définis et aux entrées et sorties génériques.
Une application met généralement en jeu plusieurs sous-ensembles de fonctionnalités. La décomposition de cette application par ces sous-ensembles a plusieurs conséquences bénéfiques :
- parallélisation du travail par séparation de l’équipe en petites groupes ;
- planification, conception et développement indépendants ;
- développements autonomes en matière de technologies.
Cependant, pour pouvoir réaliser cette décomposition, la définition précise des rôles et limites de chaque module ainsi que les structures de données qui permettent la communication entre les modules est nécessaire. Il faut notamment veiller à ce que les entrées et les sorties soient, dans la mesure du possible et selon les contraintes d’intégration et de coût (ressources, performances…), les plus génériques possibles, c’est-à-dire les plus indépendantes du langage de programmation. Cela permet alors de modifier éventuellement la partie communication entre les modules en réalisant par exemple une partie de l’application en code natif, ou une autre disponible sous forme de service Web, sans avoir à redéfinir un ensemble de classes ayant pour objectif d’adapter les structures choisies au protocole de communication. On préfèrera donc l’utilisation des types classiques tels que les entiers ou les chaînes de caractères et les classes qui ne sont que des agrégations de ces types. On évitera plus particulièrement l’utilisation de structures de collections propres au langage. On utilisera donc plutôt des échanges de données sous forme de tableaux. On peut cependant s’autoriser quelques libertés dans des cas spécifiques.
8. Ne pas résoudre un besoin technique ou un problème par un biais conceptuel.
Le cas est typique : lors de la phase de développement on s’aperçoit qu’il manque une certaine fonctionnalité pour une classe ou un module. Il faut alors chercher une solution pour répondre à ce manque. Si la solution trouvée est une “astuce”, c’est-à-dire l’utilisation d’une partie de l’application (module, classe, méthode, attribut…) d’une manière différente (même légèrement) de ce pourquoi elle a été pensée et conçue, alors il faut très certainement continuer à chercher ! En effet, très généralement une “astuce” en entraîne une autre et la solution finale n’a plus aucun sens au niveau du modèle. Reprendre la conception, éventuellement le prototypage, et modifier le code existant pour suivre la nouvelle conception n’est pas du temps de perdu, à plus forte raison pour des applications de grande taille ou qui exigent une certaine robustesse.
9. Ne pas se contenter d’une solution à moitié convaincante et ne pas mélanger deux solutions fonctionnelles.
Une solution qui n’est pas formalisée complètement, donc une solution partielle, ne doit en aucun cas servir de base pour le développement de l’application. À coup sûr des problèmes apparaîtront et l’invalideront. Elle servira éventuellement – et seulement – de support pour la réalisation d’un premier prototype.
Si deux solutions pour un même problème sont valides, il faut éviter de prendre les avantages de chacune et en faire une combinaison. Le risque est de rendre deux sous-parties incompatibles et de trouver alors des solutions conceptuelles qui finalement rendent la qualité du modèle final inférieure à celle des modèles initiaux. En somme, lorsque plusieurs solutions sont possibles, on évitera de les mélanger sans précaution.
10. Ce qui a fonctionné pour une application ne fonctionnera pas pour une autre.
C’est la règle de base. Il n’y a pas de solution conceptuelle magique qui fonctionne pour tout les cas de figure. Même s’il existe des architectures qui peuvent être appliquées à beaucoup de cas, il subsiste toujours des exceptions. Il faut donc à chaque nouveau projet passer par une phase de conception conséquente pour éviter de remettre en cause la solution à chaque problème rencontré.
Les défauts de la Programmation Orientée Objet
La programmation orientée objet a été la grande évolution dans le domaine de la programmation informatique et des langages. Elle a introduit des concepts innovants et faciles d’accès qui ont séduit de nombreux développeurs. Cependant, avec un peu d’expérience, il en ressort des difficultés qui font que la programmation objet en tant que telle peut être un frein dans la construction d’applications, et risque éventuellement dans les années à venir de laisser sa place à d’autres techniques.
On peut facilement concéder à la programmation orientée objet plusieurs atouts. Le premier est marketing : modéliser l’ensemble d’un programme par des objets tels que ceux qui nous entourent est un concept attirant, à portée d’esprit voire même palpable. Cela permet normalement un apprentissage plus aisé de la programmation, en comparaison avec d’autres types de programmation comme la fonctionnelle, pour ne citer qu’elle. Un deuxième atout est la grande part de détection des erreurs à la compilation : les langages Java et C++, par exemple, se sont dotés de compilateurs performants qui, grâce à un typage fort des objets et à tout un tas de notions (héritage, visibilité…), permettent au développeur de corriger une partie du code très en amont. Le dernier atout est une certaine proximité avec les langages bas niveau : l’appel d’une méthode en programmation objet peut se traduire assez intuitivement en C ou Assembler. La réécriture et l’utilisation d’un code bas niveau en code objet se réalisent plus simplement qu’avec d’autres types de programmation. Je passerai sous silence les avantages procurés par le succès de la programmation orientée objet et du langage C++ en particulier, ce qui a amené au fil du temps un certain nombre de bibliothèques et de solutions conceptuelles.
Venons-en aux problèmes que l’on peut rencontrer lorsque l’on programme en objet. Je l’ai dit plus haut : le concept est pour moi purement marketing, et n’apporte pas de vraie solution pratique pour la programmation. En effet ce qui caractérise le monde, ce qui le fait vivre et évoluer, ce n’est pas tant la description des objets qui y habitent mais plutôt - et la notion est importante - les interactions entre ces objets. Depuis des années les modèles objet essaient de proposer des solutions plus ou moins bonnes pour répondre à ce manque.
Lorsque l’on travaille avec des bases de données, par exemple, un schéma souvent rencontré consiste en 3 niveaux : des objets qui représentent les informations et qui ne sont décrits que par des attributs, des objets permettant de communiquer avec la base de données, et enfin - et c’est là où je veux en venir - une couche d’objets qui permettent de faire la liaison entre la base de données et les objets du langage. On a besoin ici de traducteurs qui permettent de faire passer les informations du langage vers les informations de la base de données (classiquement de traduire un objet en langage SQL, et inversement). Ces objets-traducteurs n’ont pas d’existence réelle, ils n’ont pas de légitimité en tant qu’objets mais plutôt en tant que “méthodes”. Ils représentent ainsi en quelque sorte une limite à la programmation orientée objet.
Un modèle de conception fréquemment utilisé est celui de l’Observateur. Il permet à un objet observé de notifier un évènement à des objets observateurs, pas forcément connus de l’objet observé (dans le sens où l’observé ne sait pas qui l’observe et quand) ; il s’agit typiquement de provoquer des actions asynchrones : appel à une méthode lorsqu’un utilisateur clique sur un bouton, lorsque des données arrivent sur un flux, lorsqu’un attribut spécial est modifié… Ce modèle est implémenté de manière très sommaire dans les langages orienté objet : l’objet observé enregistre des objets observateurs par le biais de deux méthodes (ajout et suppression), puis quand une action spéciale est effectuée il parcourt la liste des observateurs et appelle une méthode particulière, généralement définie dans une interface spécifique à chaque type d’observateur. On ne peut s’y tromper : ce système est particulièrement lourd à mettre en place et comporte de nombreux défauts. Pour pouvoir observer les modifications sur un attribut par exemple il est obligatoire que l’objet qui possède les fonctions de modification de cet attribut mette en place tout un système d’observation (gestion de la liste et appel de la méthode sur chaque observateur à chaque modification de l’attribut). Il est donc limité à ce que le développeur d’un objet a bien voulu écrire. Toutes les difficultés de mise en place et d’utilisation de ce système peuvent être constatées lors du développement d’une application avec interface graphique en utilisant le modèle Modèle-Vue-Contrôleur (MVC) : un contrôleur est observateur des actions utilisateur sur la vue, elle-même observateur des modifications sur les données du modèle, modifications initiées par les contrôleurs.
Un autre problème important de la programmation orientée objet se dégage lors du développement d’applications qui se doivent robustes et ayant le moins de bugs possible. Il s’agit de logiciels utilisés dans des domaines critiques : santé, aviation, gestion de centrales éléctriques, etc. Le concept objet ne donne pas en soi de solutions qui permettent de spécifier des contrats que le programme doit respecter. Les contrats déterminent les règles que doit respecter un programme pour qu’il soit jugé correct en spécifiant pour les méthodes dans quels domaines ils ont été écrits et les invariants qui doivent subsister entre les attributs. On va par exemple pouvoir indiquer qu’une méthode n’est valide que pour un certain domaine d’entrée (les entiers positifs inférieurs à 100) et qu’un attribut doit toujours être supérieur à un autre. Le programme va se charger de vérifier que les règles sont respectées à la compilation et pendant l’exécution, et ceci quel que soit le contexte d’exécution (classiquement en multi-threads). La spécification de ces règles est actuellement très complexe en programmation orientée objet et la diffusion d’objets respectant des contrats n’est pas facilitée par des automatismes en matière de compilation et de documentation.
Dans le même genre, la programmation orientée objet souffre, malgré les bibliothèques existantes en la matière (nUnit, jUnit…), d’un manque d’assistance lors des phases de test. La spécification de tests de manière plus abstraite permettrait de gagner du temps dans leur rédaction et également d’augmenter de manière significative leur qualité. Actuellement, la rédaction de jeux de tests valides et fiables s’avère parfois plus longue et complexe que la conception de l’application en elle-même. De plus, le code d’un jeu de test n’est en général pas exempt de bugs. Il devient donc difficile d’être sûr à 100% qu’un programme répond à sa spécification. Par extension, on peut également reprocher aux langages orientés objet un manque d’abstraction dans le débuggage. Il s’agit en général d’afficher les variables ou d’exécuter le programme pas à pas, ce qui rend la détection de la source de l’erreur longue et fastidieuse.
Un aspect à la popularité grimpante semble également faire défaut à la programmation orientée objet dans le sens où elle ne fournit pas de solutions simples à comprendre et à utiliser : la synchronisation. Cette idée met en évidence les difficultés immenses qui apparaissent obligatoirement à terme dans le développement d’une application complexe : la gestion des exécutions en parallèles de parties de l’application. Si les langages objets sont pour la plupart dotés de systèmes de synchronisation (création de threads, gestion des files d’attente, sémaphores, verrous…), il n’en reste pas moins que ceux-ci sont bien souvent un casse-tête pour les concepteurs : quelles opérations peuvent être parallélisées ? quelles variables peuvent être modifiées simultanément ? comment assurer que telle ou telle opération va être exécutée avant une autre ? y’a-t-il un risque d’interblocage ? Pour l’instant, en programmation orientée objet, le concepteur doit mettre la main à la pâte en ayant en tête que le débuggage d’applications multi-threadées n’est pas une mince affaire.
Pour terminer, un dernier concept difficile à mettre en œuvre en programmation orientée objet est celui de l’extensibilité. De nombreuses applications ont vu le jour et dans lesquels la notion d’extensions (plugins) est très présente : les produits Mozilla, Eclipse… C’est à coup sûr une des raisons du succès de ces logiciels. Cependant, la programmation orientée objet n’offre pas de solution clef en main pour construire des applications extensibles. Il faut alors recourir à tout un système plus ou moins complexe qui fait souvent intervenir des opérations de réflexion au sens programmation, c’est-à-dire d’analyse de la structure des classes.
Quelques pistes de solutions commencent à émerger pour contrer ces manques majeurs de la programmation orientée objet. Le langage C# fournit par exemple des réponses syntaxiques à qui permettent de gérer plus facilement un système d’observateurs. Les concepts de programmation par aspect et de programmation par contrat font leur entrée et peuvent être intégrés à des langages objets. Java bénéficie par exemple des bibliothèques AspectJ et jContract. Cependant, il s’agit principalement d’astuces syntaxiques ou conceptuelles, voire de surcouches du langage, et il subsiste donc des soucis en terme d’efficacité mémoire et de performances.
La programmation orientée objet a donc un côté très attrayant pour le développeur : elle bénificie d’une popularité gigantesque et d’un concept simple et accrocheur. Malgré tout, elle doit évoluer vers de nouvelles formes de programmation afin de répondre à des exigences de plus en plus fortes, notamment au niveau de la création d’interfaces graphiques et d’applications multi-threadées ou distribuées.
Feux d’artifices
Voici un petit exemple de la technique du tutorial précédent : une fois pour les traînées de feu au lancement de la fusée et une autre évidemment lors de l’éclat des feux. J’utilise également la classe mx.transitions.Tween pour générer aléatoirement les déplacements des fusées en l’air.
Tutoriel : Fontaine aux lucioles avec Flash
Présentation
Un peu de script et de graphisme avec un tutoriel Flash sur la réalisation d’une sorte de fontaine à lucioles multicolores ! Rien de compliqué, il faut cependant avoir quelques bases en Flash, notamment dans la création de clips, et quelques notions d’ActionScript. J’utilise personnellement la version 8 de Flash, mais je pense que l’animation est réalisable avec des versions antérieures.
Voici le résultat auquel nous allons parvenir :
Applications de poche
Chez des amis ou sur des ordinateurs publics, un des principaux problèmes est la confrontation à un environnement inconnu : logiciels absents ou mal configurés, disque dur en vrac, etc. Avec l’augmentation de l’espace mémoire des clefs USB et leur encombrement, plusieurs solutions ont émergé pour permettre à tout un chacun de voyager avec son espace de travail dans la poche.
Je vais développer ici dans un premier temps ce que j’utilise actuellement : les applications portables. Il existe de nombreux logiciels qui ne nécessitent pas de s’infiltrer dans le système d’exploitation pour être utilisés, et qui peuvent donc être installés sur un disque amovible. On trouve à peu près tout ce dont on a besoin en matière de fonctionnalités : navigateur internet, client mail, traitement de texte… Wikipédia en possède une liste très complète et organisée, avec en bonus-track une liste de jeux portables.
Si vous vous sentez un peu perdu dans tout ça, John Haller a prévu de quoi vous simplifier la vie : PortableApps.com. Ce site très bien fait fournit une sélection d’applications portables (Firefox, Thunderbird, OpenOffice…) ainsi qu’un menu qui permet de lister les logiciels installés et des dossiers personnels (”Musique”, “Vidéos”…) pour donner à la clef un aspect plus “Mes Documents”. L’installation s’effectue sans souci (je conseille d’ailleurs également de l’effectuer à la racine de la clef pour une question de clarté) et l’utilisation est simple. Pour installer des applications non proposées sur le site c’est tout aussi facile : il suffit de l’installer dans le dossier prévu à cet effet sur la clef et le menu se charge d’explorer ce dossier pour trouver les fichiers exécutables et les rajouter dans le menu.
Un environnement de travail assez complet prend entre 100 Mo et 300Mo - OpenOffice en utilise déjà plus de 100 !. Il ne reste ensuite qu’à configurer les applications et à synchroniser les documents (en utilisant les porte-documents de Windows ou un logiciel comme Toucan, proposé par PortableApps) : on se sent enfin chez soi !
J’ai eu l’occasion de rencontrer d’autres solutions encore plus intéressantes, mais pas de les tester moi-même (pour le moment). Il s’agit d’environnement virtuels complets, détectés automatiquements à l’insertion de la clef et qui modifient jusqu’au bureau Windows : U3, Ceedo, MojoPac… Les vidéos de démonstrations sont explicites, et ces logiciels seront peut-être l’objet d’un prochain article.
En attendant, on peut donc, pour une somme dérisoire - les clefs USB 2Go sont maintenant disponibles à moins de 20 € et la plupart des logiciels portables (tous ?) sont gratuits - et pour un effort assez réduit, gagner sensiblement en confort et productivité loin de son ordinateur personnel.

