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 :
Il s’agit du même diaporama, mais avec une taille de fenêtre différente. Le calcul du nombre de lignes, de colonnes et de la taille de chaque slide ainsi que leur positionnement est géré dynamiquement.
Il est enfin à noter que j’ai réalisé l’application à l’aide du plugin Eclipse ASDT (ActionScript Development Tools), disponible à l’adresse http://sourceforge.net/projects/aseclipseplugin/, mais que le code est utilisable avec d’autres outils.
Préparation et configuration
Nous allons écrire une unique classe SlideShow qui contiendra toute la logique de calcul et de l’affichage des diapositives.
class SlideShow {
...
}
Le programme se déroule en 2 phases. La première consiste à récupérer les images qui vont être utilisées dans le diaporama. La deuxième est l’affichage optimal de ces images, selon la configuration de la fenêtre (largeur et hauteur), le nombre de diapositives et d’autres variables. Cette dernière phase doit intervenir à chaque modification de la configuration, en l’occurrence quand un slide est rajouté ou quand la taille de la fenêtre change.
Chacune de ces phases fait l’objet d’une méthode de la classe SlideShow :
private function loadSlides(slideDirectory: String):Void {
...
}
private function updateSlides():Void {
...
}
Une variable désigne l’ensemble des diapositives. Elle est utilisée par chacune des méthodes précédentes pour, dans le cas de loadSlides(), charger les images, et dans le cas d’updateSlides(), les afficher dans la fenêtre de l’application :
// Contient les slides chargés (MovieClip)
var slides:Array;
On rajoute une méthode pour initialiser la configuration de l’environnement et un constructeur pour lancer les calculs et l’affichage :
public function SlideShow() {
slides = new Array();
initEnv();
// Diapositives dans le dossier "slides/"
loadSlides("slides");
}
// Initialisation de l'environnement
private function initEnv():Void {
// Permet de gérer le redimensionnement de la fenêtre manuellement
Stage.scaleMode = "noScale";
// Alignement des objets vis-à-vis de la fenêtre principale
Stage.align = "TL";
// Instanciation d'un observateur de changement de taille
// de la fenêtre principale
var refreshStageListener:Object = new Object();
Stage.addListener(refreshStageListener);
// Indication de l'élément SlideShow
refreshStageListener.slideshow = this;
// Mise à jour de l'affichage des slides en cas de redimensionnement
refreshStageListener.onResize = function() {
this.slideshow.updateSlides();
};
}
Le constructeur initialise le tableau slides, appelle la méthode de configuration de l’environnement puis la méthode de chargement des images du dossier “slides/”.
Dans l’initialisation de l’environnement, on spécifie tout d’abord que l’application, désignée par la variable globale Stage ne gère pas son redimensionnement seule ("noScale"). Cela nous autorise à créer un observateur du redimensionnement, refreshStageListener, qui appelle la méthode d’actualisation de l’affichage à chaque fois que l’application est redimensionnée (refreshStageListener.onResize). Enfin, on spécifie un alignement de type "TL" (Top Left) pour les objets, ce qui nous permet de gérer manuellement le centrage des objets dans la fenêtre.
Dans ce tutoriel, seules deux variables “utilisateur” sont exploitées : la distance à respecter entre chaque diapositive (espace entre les lignes et les colonnes, et avec le bord de la fenêtre) et le ratio des diapositives, c’est-à-dire le rapport largeur/hauteur. Cette dernière variable rend possible l’affichage à taille maximale des slides sur le support désiré (écran, vidéoprojecteur…). Nous créons 2 attributs dans la classe SlideShow :
// Espace entre les slides
var SLIDE_SPACE:Number = 20;
// Ratio de taille (largeur / hauteur) des slides
// (ratio type Powerpoint par défaut)
var SLIDE_RATIO:Number = 4 / 3;
Chargement des images
Il n’existe pas à ma connaissance de manière simple pour récupérer une liste de fichiers d’un dossier local en ActionScript. À la place, nous effectuons une recherche (très sale) qui suppose l’existence d’un fichier avec un nom particulier. En l’occurrence, nous essayons de charger tous les fichiers appelés “Diapositive###.PNG” d’un dossier donné (avec ### un numéro) à l’aide d’une simple boucle sur une variable entière. Le choix de ce format de nom de fichier n’est pas anodin : en effet, l’exportation d’un diaporama Microsoft PowerPoint en format PNG a pour conséquence la création d’un ensemble d’images PNG dont le nom est “Diapositive1.PNG”, “Diapositive2.PNG”, … Nous pouvons donc passer très facilement d’un diaporama PowerPoint classique à un affichage par notre programme ActionScript, et ainsi créer le diaporama à l’aide de PowerPoint mais l’afficher grâce à notre application.
Chaque image est chargée à l’aide d’un objet MovieClipLoader et est placée à l’intérieur d’un objet de type MovieClip qui représente le slide. Il y a une différence entre le slide et l’image qu’il contient, distinction justifiée car permettant d’ajuster la taille de l’image à la taille du slide.
// Chargement de l'ensemble des slides dans un tableau
private function loadSlides(slideDirectory: String):Void {
// "Chargeur" des images
var imageLoader:MovieClipLoader = new MovieClipLoader();
// Instanciation d'un observateur de chargement des images des slides
var loadImageListener:Object = new Object();
imageLoader.addListener(loadImageListener);
// Indication de l'élément SlideShow
loadImageListener.slideshow = this;
// Enregistrement au chargement de l'image
loadImageListener.onLoadInit = function(image:MovieClip) {
// Récupération du slide contenant l'image
var slide:MovieClip = image._parent;
// Sauvegarde du ratio initial de l'image
image.ratio = image._width / image._height;
// Sauvegarde du slide dans le tableau, à la bonne position
this.slideshow.slides[slide.slidePosition - 1] = slide;
// Redimensionnement de l’image pour rentrer dans le slide sans déformation
if (image.ratio < slide._width / slide._height) {
image._width = slide._width * image.ratio;
image._height = slide._width;
} else {
image._width = slide._height;
image._height = slide._height / image.ratio;
}
// Mise à jour de l’affichage
this.slideshow.updateSlides();
};
// Récupération des images
for (var i:Number = 100; i > 0; i–) {
// Url de l’image
var urlImage:String = slideDirectory + “/Diapositive” + i + “.PNG”;
// Création du slide contenant l’image
var slide:MovieClip = _root.createEmptyMovieClip(”slide” + i, _root.getNextHighestDepth());
// Spécification de la position du slide
slide.slidePosition = i;
// Création de l’image par chargement par un objet MovieClipLoader
slide.image = slide.createEmptyMovieClip(”slide” + i + “_image”, slide.getNextHighestDepth());
imageLoader.loadClip(urlImage, slide.image);
}
}
On initialise une variable imageLoader qui sert à charger les images des diapositives. Sur cette variable on enregistre un observateur, loadImageListener, qui remplit le tableau slides (attribut de la classe SlideShow) à chaque image chargée (méthode onLoadInit()).
La méthode onLoadInit(), appelée dès que le chargement est terminé, s’occupe de stocker dans le tableau slides l’objet MovieClip qui représente la diapositive contenant l’image qui vient d’être chargée. L’image est ensuite redimensionnée pour rentrer dans le cadre de son slide, sans déformation. L’algorithme est relativement simple : si le ratio largeur/hauteur de l’image est inférieur au ratio de la diapositive, cela signifie que, si on ramène l’image à la taille de la diapositive, sa largeur est inférieure à celle de la diapositive. On est donc sûr qu’à même hauteur l’image rentre dans la diapositive. On redéfinit la hauteur comme étant égale à celle de la diapositive et on réajuste la largeur de l’image en conservant le ratio largeur/hauteur pour qu’il n’y ait pas de déformation. De manière opposée, si le ratio de l’image est supérieur au ratio de la diapositive, on réajuste la taille de l’image.
Pour charger les images, on utilise une boucle qui construit un objet slide pour chaque image et qui charge cette dernière grâce à l’url déduite (slideDirectory + "/Diapositive" + i + ".PNG"). La spécification de la position du slide par la propriété slidePosition permet de compléter le tableau slides en conservant le bon ordre.
Affichage des diapositives
Cette méthode a pour objectif de positionner et dimensionner les diapositives selon la configuration actuelle, soit : la taille de la fenêtre, le nombre de slides et les variables utilisateurs (SLIDE_RATIO et SLIDE_SPACE).
Pour déterminer sur combien de colonnes et de lignes doivent s’afficher les diapositives, on utilise un algorithme qui s’appuie sur la recherche de l’aire maximale d’une diapositive : on parcourt toutes les configurations (nombre de colonnes, nombre de lignes) possibles et on choisit celle qui implique des slides d’aire maximale. Pour cela, on itère sur le nombre de colonnes possible - soit entre 1 et le nombre de diapositives - et on calcule le nombre de lignes nécessaire pour afficher l’ensemble des diapositives. Ce nombre est obtenu en divisant le nombre de slides total par le nombre de colonnes, et en arrondissant à l’entier supérieur. On peut ainsi déterminer l’espace disponible pour chaque diapositive suivant la taille de la fenêtre, Stage.width et Stage.height, et l’espace entre chaque diapositive SLIDE_SPACE. On calcule la taille de chaque slide en respectant le ratio (SLIDE_RATIO) donné par l’utilisateur - on utilise pour cela l’algorithme expliqué dans la partie précédente - et on déduit donc l’aire des diapositives. Si cette aire est supérieure à l’aire maximale courante, alors les données correspondantes (taille et nombre de lignes et colonnes) sont stockées et l’aire maximale mise à jour.
// Affichage des slides selon leur taille, nombre, ...
private function updateSlides():Void {
// Récupération de l'espace "utilisable" dans la fenêtre
var stageWidth:Number = Stage.width - SLIDE_SPACE * 2;
var stageHeight:Number = Stage.height - SLIDE_SPACE * 2;
// Aire maximum trouvée
var areaMax:Number = 0;
// Nombre de colonnes de slides correspondant à l'aire maximum
var nbCols:Number = slides.length;
// Nombre de lignes de slides pour l'aire maximum
var nbRows:Number = 1;
// Taille correspondante pour les slides
var slideWidth:Number = 0;
var slideHeight:Number = 0;
// Boucle de "contrainte" :
// On doit trouver quelle est la configuration en termes
// de lignes et de colonnes pour laquelle l'aire des slides
// est maximum (plus "grand" possible).
// Pour tous les nombres de slides possibles dans une colonne [1 .. nombre de slides]
for (var col:Number = this.slides.length; col > 0; col–) {
// Détermination du nombre de lignes correspondant
var row:Number = Math.ceil(slides.length / col);
// Calcul de la taille d’un slide sous cette configuration
var sWidth:Number = Math.max(0, (stageWidth - ((col - 1) * SLIDE_SPACE)) / col);
var sHeight:Number = Math.max(0, (stageHeight - ((row - 1) * SLIDE_SPACE)) / row);
// Réajustement de la taille selon le ratio de taille des slides
if (sWidth / sHeight > SLIDE_RATIO) {
sWidth = sHeight * SLIDE_RATIO;
sHeight = sHeight;
} else {
sWidth = sWidth;
sHeight = sWidth / SLIDE_RATIO;
}
// Calcul de l’aire correspondante
var area:Number = sWidth * sHeight;
// Détermination de l’aire maximum et sauvegarde de
// la configuration (lignes, colonnes, taille) associée
if (area > areaMax)
{
areaMax = area;
nbCols = col;
nbRows = row;
slideWidth = sWidth;
slideHeight = sHeight;
}
}
Une fois ces données disponibles, on peut parcourir le tableau des slides en redimensionnant et positionnant correctement chaque diapositive selon la ligne et la colonne dans laquelle elle doit se situer. On introduit en plus un décalage en X et Y (offsetX et offsetY) qui correspond au recentrage de l’ensemble des diapositives dans la fenêtre de l’application :
// Décalages à effectuer à chaque slide pour obtenir un ensemble centré
// dans la fenêtre principale
var offsetX:Number = SLIDE_SPACE + (Stage.width - nbCols * (slideWidth + SLIDE_SPACE) - SLIDE_SPACE) / 2;
var offsetY:Number = SLIDE_SPACE + (Stage.height - nbRows * (slideHeight + SLIDE_SPACE) - SLIDE_SPACE) / 2;
// Affichage des slides selon le nombre de lignes et colonnes
for (var i:Number = slides.length - 1; i >= 0; i--) {
// Récupération du slide à afficher
var slide:MovieClip = slides[i];
// Calcul de la ligne et colonne du slide selon sa position dans la liste
var currCol:Number = i % nbCols;
var currRow:Number = Math.floor(i / nbCols);
// Spécification de la taille
slide._width = slideWidth;
slide._height = slideHeight;
// Spécification de la position
slide._x = offsetX + currCol * (slideWidth + SLIDE_SPACE);
slide._y = offsetY + currRow * (slideHeight + SLIDE_SPACE);
}
}
Conclusion
Pour un bel effet graphique, il est relativement aisé d’ajouter une légère ombre autour des diapositives ainsi que des animations de repositionnement et redimensionnement. De plus, il est évidemment indispensable de rajouter un mode “plein écran” où les diapositives sont affichées sur l’ensemble de la fenêtre et où il est possible de naviguer de l’une à l’autre, ce qui est l’intérêt d’un vrai diaporama.
Le fichier du tutoriel peut être téléchargé ici. Une version animée est visible à cette adresse, et le fichier source correspondant est téléchargeable là.
S’il vous prend l’envie d’essayer ce tutoriel et si vous ajoutez d’autres fonctionnalités, n’hésitez pas à poster dans les commentaires des liens vers vos applications de diaporamas.
Cet article peut être modifié ou approfondi ultérieurement. Il ne se veut pas complet et n’est pas exempt d’erreurs. N’hésitez pas à les signaler.
Vous pouvez m’adresser vos remarques ou suggestions à enisseo@hotmail.com. Je ne manquerai pas de vous répondre.
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 :
Nous allons pour cela créer tout d’abord un clip animé qui représente un “éclat”, puis nous allons simuler une source d’éclats sur l’écran.
Création du clip de base
Création de l’éclat
Commençons par créer un document Flash de 400 pixels par 400 pixels par exemple. Pour y voir plus clair, spécifions un arrière-plan noir.
Sur ce fond noir, nous allons créer un cercle blanc d’une petite taille, sans bordure. Faire ce cercle en petite taille permet de s’approcher par la suite au mieux du rendu final. Ici, il fait 5 fois 5 pixels. Convertissons maintenant ce cercle en clip animé (touche F8 par défaut sous Windows), et appelons-le “point” histoire de ne pas se mélanger les pinceaux dans le futur.
Nous allons maintenant spécifier quelques filtres à ce cercle pour lui donner un aspect luisant. Un clic simple sur l’objet “point” permet de spécifier des filtres sur cet objet. Tout d’abord, appliquons un premier rayonnement blanc, assez proche du centre (flou de 4 sur les directions X et Y), et d’une intensité modérée : juste ce qu’il faut pour avoir l’impression que le point est une source de lumière. Il ne faut donc pas que la limite entre le cercle et le rayonnement soit trop visible. J’utilise une qualité de rayonnement élevée afin que celui-ci soit bien circulaire (avec une qualité inférieure il a tendance à avoir une forme carrée). N’hésitez pas à zoomer (400%) pour ajuster au mieux la configuration de ce filtre.
Pour donner l’impression que la source a une couleur, nous allons créer un deuxième rayonnement. Il doit sembler “décorer” l’environnement, il faut donc lui donner une plus grande ampleur : un flou sur une distance de 15 en X et Y est correct. Ici, j’ai choisi un bleu, couleur qui a tendance a moins frapper nos yeux, surtout sur du noir. J’ai donc dû spécifier une intensité assez élevée : 650% pour l’exemple. Qualité élevée, pour les mêmes raisons que précédemment.
Sortons de la modification de l’objet “point”. Nous avons crée la base de notre éclat, nous allons maintenant l’animer.
Animation de l’éclat
Nous allons transformer l’objet “point” et ses filtres en un clip animé. De la même façon que tout à l’heure, F8 permet de convertir l’objet “point” en symbole. Appelons celui-ci “eclat”. Dès la création, on peut cocher dans les options avancées “Exporter pour ActionScript”. L’objet de type “eclat” sera alors accessible dans les scripts que nous écrirons.
Ajoutons maintenant l’animation à cet éclat. Pour cela, modifions l’objet “en place” en double-cliquant dessus et insérons une image-clef à l’image 20 (touche F6). Déplaçons l’objet “point” de cette image sur un côté (à droite par exemple), en appuyant sur la touche MAJ pour garder la même ordonnée (y). Ici, j’ai activé l’affichage en pelure d’oignons pour donner une idée de la distance de déplacement : à gauche de l’image on distingue l’objet tel qu’il est positionné sur l’image 1, à droite l’objet déplacé de l’image 20.
L’étape suivante est bien évidemment la création d’une interpolation de mouvement entre l’image 1 et l’image 20. Mettons une accélération importante (ici 70) afin de donner un effet de jaillissement à cet éclat.
Nous allons maintenant faire disparaître l’éclat qui a jailli. J’ai activé ici les contours en pelures d’oignon pour voir l’ensemble de l’animation en contour. Comme l’interpolation est accélérée, vers l’image 15 les déplacements de l’objet “point” se font très courts. C’est donc à partir de là que nous allons faire disparaître l’éclat. À l’image 15, insérons une image-clef (touche F6) afin de découper l’animation en 2 : le jaillissement puis la disparition.
Sélectionnons à nouveau l’image 20 et l’objet “point” à cette image. Nous allons appliquer un flou assez large (distance de 10 en X et Y), de qualité élevée. Il va permettre de faire disparaître l’éclat par un effet proche de l’évaporation. Notez que ce filtre va apparaître automatiquement sur tous les objets “point” de l’animation “eclat” (en l’occurrence les images 1 et 15), mais que ses propriétés le rendent inactif.
Pour compléter notre disparition, appliquons un filtre de transparence complète à notre objet “point”. Dans les propriétés de l’objet, il suffit de sélectionner comme couleur “Alpha” et de la spécifier à 0%.
L’animation est terminée : vous pouvez déplacer le curseur pour constater que Flash s’est occupé de tout. Il reste cependant à indiquer au clip de s’arrêter à la dernière image pour ne pas boucler indéfiniment. Toujours à l’image 20, dans les “Actions”, écrivons stop();. Cela évitera au clip de recommencer à l’image 1 après l’image 20.
Vous pouvez à ce stade tester l’animation : l’éclat jaillit une fois et disparaît. Nous pouvons désormais passer au code ActionScript.
Écriture du script
Revenons à la scène principale. Supprimons l’objet “eclat” que nous venons de créer de la première image de la scène, tout en le laissant dans la bibliothèque. Nous allons l’insérer par script.
Nous écrirons le script à l’image 1 de la scène. Nous voulons donc que différents éclats jaillissent d’un point de l’écran, éclats ayant des tailles et des orientations différentes. Tout d’abord, créons la source des éclats, arbitrairement au point (200, 200) de l’écran :
// Création de la source des éclats
var src:MovieClip = _root.createEmptyMovieClip("src", _root.getNextHighestDepth());
src._x = 200;
src._y = 200;
Pour nommer les éclats que nous créerons, nous aurons besoin d’un compteur. Les classes ActionScript sont, pour la plupart tout du moins, spécifiées “dynamiques” (dynamic class). Cela signifie qu’on peut dynamiquement ajouter des propriétés. Profitons-en pour ajouter le compteur en tant que propriété de la source :
// Compteur d'éclats
src.i = 1;
La scène affiche par défaut 12 images par seconde (modifiable dans les propriétés). Il s’agit plus ou moins de la fréquence d’actualisation des clips. Comme on ne peut aller plus vite que la machine, nous allons créer un éclat par image. Pour cela, spécifions une fonction à appeler à chaque fois qu’une image du clip source est affichée :
// Création d'un éclat par image (12 images/sec par défaut)
src.onEnterFrame = function()
{
[CONTENU DE LA FONCTION]
};
Remplissons maintenant le contenu de la fonction. Nous voulons que la taille des éclats soit variable, que les éclats partent de la source et enfin que leur direction soit aléatoire :
var size:Number = Math.random() * 5; // Taille aléatoire
// Copie d'un clip de la bibliothèque dans un clip (ici _root)
var eclat:MovieClip = _root.attachMovie(
"eclat", // Nom du clip à copier
"eclat" + (this.i++), // Nom du clip créé
_root.getNextHighestDepth(), // Profondeur du clip
{ // Propriétés de l'objet
_x: this._x, _y: this._y, // Sur la source
_width: size, _height: size // Taille
}
);
// Important ! Spécifier _rotation dans les propriétés
// de l'objet à la création a une fâcheuse tendance à le déformer
eclat._rotation = Math.random() * 360; // Direction de l'éclat
On a donc créé une variable size aléatoire entre 0 et 5 permettant de déterminer la largeur (_width) et hauteur (_height) de l’objet “eclat” copié. Cet objet est nommé en fonction du compteur, incrémenté au passage ("eclat" + (this.i++)). Comme nous créons les éclats dans la scène principale (_root.attachMovie), la profondeur et la position sont spécifiées selon la scène principale : _root.getNextHighestDepth() pour la profondeur, this._x et this._y pour récupérer les coordonnées de la source, elle-même positionnée dans la scène principale. Enfin, nous indiquons la rotation (_rotation) après la création pour éviter une déformation de l’objet.
C’est terminé ! Il ne reste plus qu’à essayer ! Si vous vous êtes perdu dans la rédaction du script, vous pouvez le télécharger et le copier-coller au bon endroit.
Pour obtenir d’autres fontaines du plus bel effet, vous pouvez utiliser dans le script un filtre travaillant sur les couleurs : ColorMatrixFilter. Je vous joins un deuxième exemple où les couleurs sont très variées et un troisième, dont les couleurs de l’éclat varient autour de sa teinte originale. Pour cela, il suffit de modifier les teintes rouge, vert et bleu du clip de façon légère : j’additionne à la teinte originale un nombre entre -16 et 16. Voici le résultat :
Edit : les utilisateurs de Flash CS3 devront certainement enregistrer les clips en compatibilité ActionScript 2.0 et les fichiers en compatibilité Flash 8. Si vous parvenez à faire fonctionner le tutoriel en utilisant d’autres versions d’ActionScript et Flash, n’hésitez pas à m’envoyer les modifications par mail ou dans les commentaires.
Conclusion
Ce tutoriel présente une technique simple mais efficace. En modifiant le clip animé, quelques paramètres ou l’affichage il est possible de réaliser de multiples effets : fumée, feu d’artifice, traînées… Attention toutefois aux performances qui tombent rapidement si les animations dupliquées sont trop complexes, trop longues ou trop nombreuses.
Cet article peut être modifié ou approfondi ultérieurement. Il ne se veut pas complet et n’est pas exempt d’erreurs. N’hésitez pas à les signaler.
Vous pouvez m’adresser vos remarques ou suggestions à enisseo@hotmail.com. Je ne manquerai pas de vous répondre.
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.
Hard link sous Windows
Microsoft propose, en cherchant un peu, un système de hard link (lien hard) sous le système de fichier NTFS. Pour les fichiers, ils sont désignés comme tels (”Hard links”), mais pour les dossiers il s’agit des “Junction Points”. Pour les utiliser, deux solutions sont possibles. Très brutalement, à l’aide des scripts du système (programmes fsutil ou linkd) ou bien en utilisant des applications qui permettent d’abstraire ces commandes. J’ai testé plus particulièrement ntfslink, qui s’intègre dans l’explorer et qui permet de créer ou supprimer ces hard links ou junction points en quelques clics.
Cependant, tout comme pour les hard link proposés par Unix, la prudence est de mise : pour éviter les incohérences ou suppression non voulues, il est conseillé d’utiliser les outils du système de fichiers et non pas les commandes classiques de l’explorer Windows.
Synchronisation et porte-documents Windows
Ça fait maintenant quelques mois que je me plains ou déplore tout du moins l’absence d’outils de synchronisation de données efficaces pour d’une part sauvegarder les informations en plusieurs copies, et d’autre part travailler le plus possible avec des versions à jour de mes fichiers. Après un nombre incroyable de logiciels testés, j’ai finalement décidé de créer ma propre application de synchronisation. Seulement, ma maudite curiosité et un entêtement hors du commun m’ont fait reprendre la piste de la synchronisation de fichiers réseau par mon bon vieux Windows (menu “Outils” > “Synchroniser…”). J’ai donc commencé à fouiller sur l’aide pour (enfin) comprendre comme fonctionne cet outil. Et voilà qu’après quelques minutes je tombe sur un bout d’article de la documentation qui me parle de Porte-documents.
Il s’avère que Microsoft a déjà un peu pensé à un cas courant : travailler sur un autre ordinateur en apportant son travail sur clef USB (ou disque dur externe, c’était un exemple), et mettre à jour ses fichiers de retour à la maison. Pour cela, on créé un porte-document sur la clef (menu “Fichier” > “Nouveau” > “Porte-documents”). On copie ensuite tout un tas de cochonneries à synchroniser (le dernier projet sur lequel vous travaillez, vos documents personnels…) dans ce dossier spécial qui va stocker vos fichiers et le chemin absolu vers le fichier source sur votre ordinateur principal. Ensuite, il suffit de cliquer sur “Mettre à jour” dans le dossier (ou dans le menu contextuel) pour que chaque modification soit reportée de votre ordinateur principal aux fichiers du porte-document, même les suppressions de fichiers. On peut donc se balader de PC/Windows à PC/Windows avec sa clef et travailler dessus, quand on rentrera chez soi il suffira de mettre à jour le tout !
Il est cependant dommage que cet outil ne soit pas plus mis en valeur, et également plus abouti. Il manque en effet tout un tas d’options, notamment pour automatiser et rendre transparente la mise à jour (aux premiers essais il ne semble pas très autonome en matière de décisions, et il faut toujours cliquer sur mettre à jour : la documentation ne fournit pas apparemment de solution pour lancer la mise à jour dès insertion de la clef). Enfin, la synchronisation se fait sur un seul dossier : il n’y a pas moyen d’avoir une version du projet en local sur plusieurs ordinateurs.
J’ai appris que ce système existe depuis un moment sous Windows. Cependant, a-t-il été plus développé sous Windows Vista ? J’ai enfin pu constater que les dossiers semblaient lisibles sous Mac. Apparemment il s’agirait seulement d’un dossier classique avec un fichier spécial contenant les informations pour la synchronisation. C’est donc un point positif puisque ça rendrait le système utilisable sous Linux ou MacOS. À voir donc…














